diff --git a/src/main/java/emu/grasscutter/command/commands/EntityCommand.java b/src/main/java/emu/grasscutter/command/commands/EntityCommand.java index 4ed18b21a..38b9c397e 100644 --- a/src/main/java/emu/grasscutter/command/commands/EntityCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/EntityCommand.java @@ -1,139 +1,139 @@ -package emu.grasscutter.command.commands; - -import static emu.grasscutter.command.CommandHelpers.*; -import static emu.grasscutter.utils.Language.translate; - -import emu.grasscutter.command.Command; -import emu.grasscutter.command.CommandHandler; -import emu.grasscutter.game.entity.*; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ElementType; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.server.event.entity.EntityDamageEvent; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.regex.Pattern; -import lombok.Setter; - -@Command( - label = "entity", - usage = { - " [state] [maxhp] [hp(0 for infinite)] [atk] [def]", - " [ai] [maxhp] [hp(0 for infinite)] [atk] [def]" - }, - permission = "server.entity") -public final class EntityCommand implements CommandHandler { - private static final Map> intCommandHandlers = - Map.ofEntries( - Map.entry(stateRegex, EntityParameters::setState), - Map.entry(maxHPRegex, EntityParameters::setMaxHP), - Map.entry(hpRegex, EntityParameters::setHp), - Map.entry(defRegex, EntityParameters::setDef), - Map.entry(atkRegex, EntityParameters::setAtk), - Map.entry(aiRegex, EntityParameters::setAi)); - - @Override - public void execute(Player sender, Player targetPlayer, List args) { - EntityParameters param = new EntityParameters(); - - parseIntParameters(args, param, intCommandHandlers); - - // At this point, first remaining argument MUST be the id and the rest the pos - if (args.size() != 1) { - sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar - throw new IllegalArgumentException(); - } - - try { - param.configId = Integer.parseInt(args.get(0)); - } catch (NumberFormatException ignored) { - CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.cfgId")); - } - - param.scene = targetPlayer.getScene(); - var entity = param.scene.getEntityByConfigId(param.configId); - - if (entity == null) { - CommandHandler.sendMessage(sender, translate(sender, "commands.entity.not_found_error")); - return; - } - applyFightProps(entity, param); - applyGadgetParams(entity, param); - applyMonsterParams(entity, param); - - CommandHandler.sendMessage(sender, translate(sender, "commands.status.success")); - } - - private void applyGadgetParams(GameEntity entity, EntityParameters param) { - if (!(entity instanceof EntityGadget)) { - return; - } - if (param.state != -1) { - ((EntityGadget) entity).updateState(param.state); - } - } - - private void applyMonsterParams(GameEntity entity, EntityParameters param) { - if (!(entity instanceof EntityMonster)) { - return; - } - - if (param.ai != -1) { - ((EntityMonster) entity).setAiId(param.ai); - // TODO notify - } - } - - private void applyFightProps(GameEntity entity, EntityParameters param) { - var changedFields = new ArrayList(); - if (param.maxHP != -1) { - setFightProperty(entity, FightProperty.FIGHT_PROP_MAX_HP, param.maxHP, changedFields); - } - if (param.hp != -1) { - float targetHp = param.hp == 0 ? Float.MAX_VALUE : param.hp; - float oldHp = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - setFightProperty(entity, FightProperty.FIGHT_PROP_CUR_HP, targetHp, changedFields); - EntityDamageEvent event = - new EntityDamageEvent(entity, oldHp - targetHp, ElementType.None, null); - callHPEvents(entity, event); - } - if (param.atk != -1) { - setFightProperty(entity, FightProperty.FIGHT_PROP_ATTACK, param.atk, changedFields); - setFightProperty(entity, FightProperty.FIGHT_PROP_CUR_ATTACK, param.atk, changedFields); - } - if (param.def != -1) { - setFightProperty(entity, FightProperty.FIGHT_PROP_DEFENSE, param.def, changedFields); - setFightProperty(entity, FightProperty.FIGHT_PROP_CUR_DEFENSE, param.def, changedFields); - } - if (!changedFields.isEmpty()) { - entity - .getScene() - .broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, changedFields)); - } - } - - private void callHPEvents(GameEntity entity, EntityDamageEvent event) { - entity.callLuaHPEvent(event); - } - - private void setFightProperty( - GameEntity entity, FightProperty property, float value, List modifiedProps) { - entity.setFightProperty(property, value); - modifiedProps.add(property); - } - - private static class EntityParameters { - @Setter public int configId = -1; - @Setter public int state = -1; - @Setter public int hp = -1; - @Setter public int maxHP = -1; - @Setter public int atk = -1; - @Setter public int def = -1; - @Setter public int ai = -1; - public Scene scene = null; - } -} +package emu.grasscutter.command.commands; + +import static emu.grasscutter.command.CommandHelpers.*; +import static emu.grasscutter.utils.Language.translate; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.entity.*; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ElementType; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.server.event.entity.EntityDamageEvent; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.regex.Pattern; +import lombok.Setter; + +@Command( + label = "entity", + usage = { + " [state] [maxhp] [hp(0 for infinite)] [atk] [def]", + " [ai] [maxhp] [hp(0 for infinite)] [atk] [def]" + }, + permission = "server.entity") +public final class EntityCommand implements CommandHandler { + private static final Map> intCommandHandlers = + Map.ofEntries( + Map.entry(stateRegex, EntityParameters::setState), + Map.entry(maxHPRegex, EntityParameters::setMaxHP), + Map.entry(hpRegex, EntityParameters::setHp), + Map.entry(defRegex, EntityParameters::setDef), + Map.entry(atkRegex, EntityParameters::setAtk), + Map.entry(aiRegex, EntityParameters::setAi)); + + @Override + public void execute(Player sender, Player targetPlayer, List args) { + EntityParameters param = new EntityParameters(); + + parseIntParameters(args, param, intCommandHandlers); + + // At this point, first remaining argument MUST be the id and the rest the pos + if (args.size() != 1) { + sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar + throw new IllegalArgumentException(); + } + + try { + param.configId = Integer.parseInt(args.get(0)); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.cfgId")); + } + + param.scene = targetPlayer.getScene(); + var entity = param.scene.getEntityByConfigId(param.configId); + + if (entity == null) { + CommandHandler.sendMessage(sender, translate(sender, "commands.entity.not_found_error")); + return; + } + applyFightProps(entity, param); + applyGadgetParams(entity, param); + applyMonsterParams(entity, param); + + CommandHandler.sendMessage(sender, translate(sender, "commands.status.success")); + } + + private void applyGadgetParams(GameEntity entity, EntityParameters param) { + if (!(entity instanceof EntityGadget)) { + return; + } + if (param.state != -1) { + ((EntityGadget) entity).updateState(param.state); + } + } + + private void applyMonsterParams(GameEntity entity, EntityParameters param) { + if (!(entity instanceof EntityMonster)) { + return; + } + + if (param.ai != -1) { + ((EntityMonster) entity).setAiId(param.ai); + // TODO notify + } + } + + private void applyFightProps(GameEntity entity, EntityParameters param) { + var changedFields = new ArrayList(); + if (param.maxHP != -1) { + setFightProperty(entity, FightProperty.FIGHT_PROP_MAX_HP, param.maxHP, changedFields); + } + if (param.hp != -1) { + float targetHp = param.hp == 0 ? Float.MAX_VALUE : param.hp; + float oldHp = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + setFightProperty(entity, FightProperty.FIGHT_PROP_CUR_HP, targetHp, changedFields); + EntityDamageEvent event = + new EntityDamageEvent(entity, oldHp - targetHp, ElementType.None, null); + callHPEvents(entity, event); + } + if (param.atk != -1) { + setFightProperty(entity, FightProperty.FIGHT_PROP_ATTACK, param.atk, changedFields); + setFightProperty(entity, FightProperty.FIGHT_PROP_CUR_ATTACK, param.atk, changedFields); + } + if (param.def != -1) { + setFightProperty(entity, FightProperty.FIGHT_PROP_DEFENSE, param.def, changedFields); + setFightProperty(entity, FightProperty.FIGHT_PROP_CUR_DEFENSE, param.def, changedFields); + } + if (!changedFields.isEmpty()) { + entity + .getScene() + .broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, changedFields)); + } + } + + private void callHPEvents(GameEntity entity, EntityDamageEvent event) { + entity.runLuaCallbacks(event); + } + + private void setFightProperty( + GameEntity entity, FightProperty property, float value, List modifiedProps) { + entity.setFightProperty(property, value); + modifiedProps.add(property); + } + + private static class EntityParameters { + @Setter public int configId = -1; + @Setter public int state = -1; + @Setter public int hp = -1; + @Setter public int maxHP = -1; + @Setter public int atk = -1; + @Setter public int def = -1; + @Setter public int ai = -1; + public Scene scene = null; + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/SetPropCommand.java b/src/main/java/emu/grasscutter/command/commands/SetPropCommand.java index 1eef5c47e..2609e3677 100644 --- a/src/main/java/emu/grasscutter/command/commands/SetPropCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/SetPropCommand.java @@ -1,279 +1,279 @@ -package emu.grasscutter.command.commands; - -import emu.grasscutter.command.Command; -import emu.grasscutter.command.CommandHandler; -import emu.grasscutter.data.GameData; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.game.tower.TowerLevelRecord; -import emu.grasscutter.server.packet.send.PacketOpenStateChangeNotify; -import emu.grasscutter.server.packet.send.PacketSceneAreaUnlockNotify; -import emu.grasscutter.server.packet.send.PacketScenePointUnlockNotify; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Command( - label = "setProp", - aliases = {"prop"}, - usage = {" "}, - permission = "player.setprop", - permissionTargeted = "player.setprop.others") -public final class SetPropCommand implements CommandHandler { - // List of map areas. Unfortunately, there is no readily available source for them in excels or - // bins. - private static final List sceneAreas = - List.of( - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, - 28, 29, 32, 100, 101, 102, 103, 200, 210, 300, 400, 401, 402, 403); - Map props; - - public SetPropCommand() { - this.props = new HashMap<>(); - // Full PlayerProperty enum that won't be advertised but can be used by devs - for (PlayerProperty prop : PlayerProperty.values()) { - String name = prop.toString().substring(5); // PROP_EXP -> EXP - String key = name.toLowerCase(); // EXP -> exp - this.props.put(key, new Prop(name, prop)); - } - // Add special props - Prop worldlevel = - new Prop("World Level", PlayerProperty.PROP_PLAYER_WORLD_LEVEL, PseudoProp.WORLD_LEVEL); - this.props.put("worldlevel", worldlevel); - this.props.put("wl", worldlevel); - - Prop abyss = new Prop("Tower Level", PseudoProp.TOWER_LEVEL); - this.props.put("abyss", abyss); - this.props.put("abyssfloor", abyss); - this.props.put("ut", abyss); - this.props.put("tower", abyss); - this.props.put("towerlevel", abyss); - this.props.put("unlocktower", abyss); - - Prop bplevel = new Prop("BP Level", PseudoProp.BP_LEVEL); - this.props.put("bplevel", bplevel); - this.props.put("bp", bplevel); - this.props.put("battlepass", bplevel); - - Prop godmode = new Prop("GodMode", PseudoProp.GOD_MODE); - this.props.put("godmode", godmode); - this.props.put("god", godmode); - - Prop nostamina = new Prop("UnlimitedStamina", PseudoProp.UNLIMITED_STAMINA); - this.props.put("unlimitedstamina", nostamina); - this.props.put("us", nostamina); - this.props.put("nostamina", nostamina); - this.props.put("nostam", nostamina); - this.props.put("ns", nostamina); - - Prop unlimitedenergy = new Prop("UnlimitedEnergy", PseudoProp.UNLIMITED_ENERGY); - this.props.put("unlimitedenergy", unlimitedenergy); - this.props.put("ue", unlimitedenergy); - - Prop setopenstate = new Prop("SetOpenstate", PseudoProp.SET_OPENSTATE); - this.props.put("setopenstate", setopenstate); - this.props.put("so", setopenstate); - - Prop unsetopenstate = new Prop("UnsetOpenstate", PseudoProp.UNSET_OPENSTATE); - this.props.put("unsetopenstate", unsetopenstate); - this.props.put("uo", unsetopenstate); - - Prop unlockmap = new Prop("UnlockMap", PseudoProp.UNLOCK_MAP); - this.props.put("unlockmap", unlockmap); - this.props.put("um", unlockmap); - } - - @Override - public void execute(Player sender, Player targetPlayer, List args) { - if (args.size() != 2) { - sendUsageMessage(sender); - return; - } - String propStr = args.get(0).toLowerCase(); - String valueStr = args.get(1).toLowerCase(); - int value; - - if (!props.containsKey(propStr)) { - sendUsageMessage(sender); - return; - } - try { - value = - switch (valueStr.toLowerCase()) { - case "on", "true" -> 1; - case "off", "false" -> 0; - case "toggle" -> -1; - default -> Integer.parseInt(valueStr); - }; - } catch (NumberFormatException ignored) { - CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error"); - return; - } - - boolean success = false; - Prop prop = props.get(propStr); - - success = - switch (prop.pseudoProp) { - case WORLD_LEVEL -> targetPlayer.setWorldLevel(value); - case BP_LEVEL -> targetPlayer.getBattlePassManager().setLevel(value); - case TOWER_LEVEL -> this.setTowerLevel(sender, targetPlayer, value); - case GOD_MODE, UNLIMITED_STAMINA, UNLIMITED_ENERGY -> this.setBool( - sender, targetPlayer, prop.pseudoProp, value); - case SET_OPENSTATE -> this.setOpenState(targetPlayer, value, 1); - case UNSET_OPENSTATE -> this.setOpenState(targetPlayer, value, 0); - case UNLOCK_MAP -> unlockMap(targetPlayer); - default -> targetPlayer.setProperty(prop.prop, value); - }; - - if (success) { - if (targetPlayer == sender) { - CommandHandler.sendTranslatedMessage( - sender, "commands.generic.set_to", prop.name, valueStr); - } else { - String uidStr = targetPlayer.getAccount().getId(); - CommandHandler.sendTranslatedMessage( - sender, "commands.generic.set_for_to", prop.name, uidStr, valueStr); - } - } else { - if (prop.prop - != PlayerProperty.PROP_NONE) { // PseudoProps need to do their own error messages - int min = targetPlayer.getPropertyMin(prop.prop); - int max = targetPlayer.getPropertyMax(prop.prop); - CommandHandler.sendTranslatedMessage( - sender, "commands.generic.invalid.value_between", prop.name, min, max); - } - } - } - - private boolean setTowerLevel(Player sender, Player targetPlayer, int topFloor) { - List floorIds = targetPlayer.getServer().getTowerSystem().getAllFloors(); - if (topFloor < 0 || topFloor > floorIds.size()) { - CommandHandler.sendTranslatedMessage( - sender, "commands.generic.invalid.value_between", "Tower Level", 0, floorIds.size()); - return false; - } - - Map recordMap = targetPlayer.getTowerManager().getRecordMap(); - // Add records for each unlocked floor - for (int floor : floorIds.subList(0, topFloor)) { - if (!recordMap.containsKey(floor)) { - recordMap.put(floor, new TowerLevelRecord(floor)); - } - } - // Remove records for each floor past our target - for (int floor : floorIds.subList(topFloor, floorIds.size())) { - recordMap.remove(floor); - } - // Six stars required on Floor 8 to unlock Floor 9+ - if (topFloor > 8) { - recordMap - .get(floorIds.get(7)) - .setLevelStars( - 0, - 6); // levelIds seem to start at 1 for Floor 1 Chamber 1, so this doesn't get shown at - // all - } - return true; - } - - private boolean setBool(Player sender, Player targetPlayer, PseudoProp pseudoProp, int value) { - boolean enabled = - switch (pseudoProp) { - case GOD_MODE -> targetPlayer.inGodmode(); - case UNLIMITED_STAMINA -> targetPlayer.getUnlimitedStamina(); - case UNLIMITED_ENERGY -> !targetPlayer.getEnergyManager().getEnergyUsage(); - default -> false; - }; - enabled = - switch (value) { - case -1 -> !enabled; - case 0 -> false; - default -> true; - }; - - switch (pseudoProp) { - case GOD_MODE: - targetPlayer.setGodmode(enabled); - break; - case UNLIMITED_STAMINA: - targetPlayer.setUnlimitedStamina(enabled); - break; - case UNLIMITED_ENERGY: - targetPlayer.getEnergyManager().setEnergyUsage(!enabled); - break; - default: - return false; - } - return true; - } - - private boolean setOpenState(Player targetPlayer, int state, int value) { - targetPlayer.sendPacket(new PacketOpenStateChangeNotify(state, value)); - return true; - } - - private boolean unlockMap(Player targetPlayer) { - // Unlock. - GameData.getScenePointsPerScene() - .forEach( - (sceneId, scenePoints) -> { - // Unlock trans points. - targetPlayer.getUnlockedScenePoints(sceneId).addAll(scenePoints); - - // Unlock map areas. - targetPlayer.getUnlockedSceneAreas(sceneId).addAll(sceneAreas); - }); - - // Send notify. - int playerScene = targetPlayer.getSceneId(); - targetPlayer.sendPacket( - new PacketScenePointUnlockNotify( - playerScene, targetPlayer.getUnlockedScenePoints(playerScene))); - targetPlayer.sendPacket( - new PacketSceneAreaUnlockNotify( - playerScene, targetPlayer.getUnlockedSceneAreas(playerScene))); - return true; - } - - enum PseudoProp { - NONE, - WORLD_LEVEL, - TOWER_LEVEL, - BP_LEVEL, - GOD_MODE, - UNLIMITED_STAMINA, - UNLIMITED_ENERGY, - SET_OPENSTATE, - UNSET_OPENSTATE, - UNLOCK_MAP - } - - static class Prop { - String name; - PlayerProperty prop; - PseudoProp pseudoProp; - - public Prop(PlayerProperty prop) { - this(prop.toString(), prop, PseudoProp.NONE); - } - - public Prop(String name) { - this(name, PlayerProperty.PROP_NONE, PseudoProp.NONE); - } - - public Prop(String name, PseudoProp pseudoProp) { - this(name, PlayerProperty.PROP_NONE, pseudoProp); - } - - public Prop(String name, PlayerProperty prop) { - this(name, prop, PseudoProp.NONE); - } - - public Prop(String name, PlayerProperty prop, PseudoProp pseudoProp) { - this.name = name; - this.prop = prop; - this.pseudoProp = pseudoProp; - } - } -} +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.tower.TowerLevelRecord; +import emu.grasscutter.server.packet.send.PacketOpenStateChangeNotify; +import emu.grasscutter.server.packet.send.PacketSceneAreaUnlockNotify; +import emu.grasscutter.server.packet.send.PacketScenePointUnlockNotify; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Command( + label = "setProp", + aliases = {"prop"}, + usage = {" "}, + permission = "player.setprop", + permissionTargeted = "player.setprop.others") +public final class SetPropCommand implements CommandHandler { + // List of map areas. Unfortunately, there is no readily available source for them in excels or + // bins. + private static final List sceneAreas = + List.of( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 32, 100, 101, 102, 103, 200, 210, 300, 400, 401, 402, 403); + Map props; + + public SetPropCommand() { + this.props = new HashMap<>(); + // Full PlayerProperty enum that won't be advertised but can be used by devs + for (PlayerProperty prop : PlayerProperty.values()) { + String name = prop.toString().substring(5); // PROP_EXP -> EXP + String key = name.toLowerCase(); // EXP -> exp + this.props.put(key, new Prop(name, prop)); + } + // Add special props + Prop worldlevel = + new Prop("World Level", PlayerProperty.PROP_PLAYER_WORLD_LEVEL, PseudoProp.WORLD_LEVEL); + this.props.put("worldlevel", worldlevel); + this.props.put("wl", worldlevel); + + Prop abyss = new Prop("Tower Level", PseudoProp.TOWER_LEVEL); + this.props.put("abyss", abyss); + this.props.put("abyssfloor", abyss); + this.props.put("ut", abyss); + this.props.put("tower", abyss); + this.props.put("towerlevel", abyss); + this.props.put("unlocktower", abyss); + + Prop bplevel = new Prop("BP Level", PseudoProp.BP_LEVEL); + this.props.put("bplevel", bplevel); + this.props.put("bp", bplevel); + this.props.put("battlepass", bplevel); + + Prop godmode = new Prop("GodMode", PseudoProp.GOD_MODE); + this.props.put("godmode", godmode); + this.props.put("god", godmode); + + Prop nostamina = new Prop("UnlimitedStamina", PseudoProp.UNLIMITED_STAMINA); + this.props.put("unlimitedstamina", nostamina); + this.props.put("us", nostamina); + this.props.put("nostamina", nostamina); + this.props.put("nostam", nostamina); + this.props.put("ns", nostamina); + + Prop unlimitedenergy = new Prop("UnlimitedEnergy", PseudoProp.UNLIMITED_ENERGY); + this.props.put("unlimitedenergy", unlimitedenergy); + this.props.put("ue", unlimitedenergy); + + Prop setopenstate = new Prop("SetOpenstate", PseudoProp.SET_OPENSTATE); + this.props.put("setopenstate", setopenstate); + this.props.put("so", setopenstate); + + Prop unsetopenstate = new Prop("UnsetOpenstate", PseudoProp.UNSET_OPENSTATE); + this.props.put("unsetopenstate", unsetopenstate); + this.props.put("uo", unsetopenstate); + + Prop unlockmap = new Prop("UnlockMap", PseudoProp.UNLOCK_MAP); + this.props.put("unlockmap", unlockmap); + this.props.put("um", unlockmap); + } + + @Override + public void execute(Player sender, Player targetPlayer, List args) { + if (args.size() != 2) { + sendUsageMessage(sender); + return; + } + String propStr = args.get(0).toLowerCase(); + String valueStr = args.get(1).toLowerCase(); + int value; + + if (!props.containsKey(propStr)) { + sendUsageMessage(sender); + return; + } + try { + value = + switch (valueStr.toLowerCase()) { + case "on", "true" -> 1; + case "off", "false" -> 0; + case "toggle" -> -1; + default -> Integer.parseInt(valueStr); + }; + } catch (NumberFormatException ignored) { + CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error"); + return; + } + + boolean success = false; + Prop prop = props.get(propStr); + + success = + switch (prop.pseudoProp) { + case WORLD_LEVEL -> targetPlayer.setWorldLevel(value); + case BP_LEVEL -> targetPlayer.getBattlePassManager().setLevel(value); + case TOWER_LEVEL -> this.setTowerLevel(sender, targetPlayer, value); + case GOD_MODE, UNLIMITED_STAMINA, UNLIMITED_ENERGY -> this.setBool( + sender, targetPlayer, prop.pseudoProp, value); + case SET_OPENSTATE -> this.setOpenState(targetPlayer, value, 1); + case UNSET_OPENSTATE -> this.setOpenState(targetPlayer, value, 0); + case UNLOCK_MAP -> unlockMap(targetPlayer); + default -> targetPlayer.setProperty(prop.prop, value); + }; + + if (success) { + if (targetPlayer == sender) { + CommandHandler.sendTranslatedMessage( + sender, "commands.generic.set_to", prop.name, valueStr); + } else { + String uidStr = targetPlayer.getAccount().getId(); + CommandHandler.sendTranslatedMessage( + sender, "commands.generic.set_for_to", prop.name, uidStr, valueStr); + } + } else { + if (prop.prop + != PlayerProperty.PROP_NONE) { // PseudoProps need to do their own error messages + int min = targetPlayer.getPropertyMin(prop.prop); + int max = targetPlayer.getPropertyMax(prop.prop); + CommandHandler.sendTranslatedMessage( + sender, "commands.generic.invalid.value_between", prop.name, min, max); + } + } + } + + private boolean setTowerLevel(Player sender, Player targetPlayer, int topFloor) { + List floorIds = targetPlayer.getServer().getTowerSystem().getAllFloors(); + if (topFloor < 0 || topFloor > floorIds.size()) { + CommandHandler.sendTranslatedMessage( + sender, "commands.generic.invalid.value_between", "Tower Level", 0, floorIds.size()); + return false; + } + + Map recordMap = targetPlayer.getTowerManager().getRecordMap(); + // Add records for each unlocked floor + for (int floor : floorIds.subList(0, topFloor)) { + if (!recordMap.containsKey(floor)) { + recordMap.put(floor, new TowerLevelRecord(floor)); + } + } + // Remove records for each floor past our target + for (int floor : floorIds.subList(topFloor, floorIds.size())) { + recordMap.remove(floor); + } + // Six stars required on Floor 8 to unlock Floor 9+ + if (topFloor > 8) { + recordMap + .get(floorIds.get(7)) + .setLevelStars( + 0, + 6); // levelIds seem to start at 1 for Floor 1 Chamber 1, so this doesn't get shown at + // all + } + return true; + } + + private boolean setBool(Player sender, Player targetPlayer, PseudoProp pseudoProp, int value) { + boolean enabled = + switch (pseudoProp) { + case GOD_MODE -> targetPlayer.isInGodMode(); + case UNLIMITED_STAMINA -> targetPlayer.isUnlimitedStamina(); + case UNLIMITED_ENERGY -> !targetPlayer.getEnergyManager().isEnergyUsage(); + default -> false; + }; + enabled = + switch (value) { + case -1 -> !enabled; + case 0 -> false; + default -> true; + }; + + switch (pseudoProp) { + case GOD_MODE: + targetPlayer.setInGodMode(enabled); + break; + case UNLIMITED_STAMINA: + targetPlayer.setUnlimitedStamina(enabled); + break; + case UNLIMITED_ENERGY: + targetPlayer.getEnergyManager().setEnergyUsage(!enabled); + break; + default: + return false; + } + return true; + } + + private boolean setOpenState(Player targetPlayer, int state, int value) { + targetPlayer.sendPacket(new PacketOpenStateChangeNotify(state, value)); + return true; + } + + private boolean unlockMap(Player targetPlayer) { + // Unlock. + GameData.getScenePointsPerScene() + .forEach( + (sceneId, scenePoints) -> { + // Unlock trans points. + targetPlayer.getUnlockedScenePoints(sceneId).addAll(scenePoints); + + // Unlock map areas. + targetPlayer.getUnlockedSceneAreas(sceneId).addAll(sceneAreas); + }); + + // Send notify. + int playerScene = targetPlayer.getSceneId(); + targetPlayer.sendPacket( + new PacketScenePointUnlockNotify( + playerScene, targetPlayer.getUnlockedScenePoints(playerScene))); + targetPlayer.sendPacket( + new PacketSceneAreaUnlockNotify( + playerScene, targetPlayer.getUnlockedSceneAreas(playerScene))); + return true; + } + + enum PseudoProp { + NONE, + WORLD_LEVEL, + TOWER_LEVEL, + BP_LEVEL, + GOD_MODE, + UNLIMITED_STAMINA, + UNLIMITED_ENERGY, + SET_OPENSTATE, + UNSET_OPENSTATE, + UNLOCK_MAP + } + + static class Prop { + String name; + PlayerProperty prop; + PseudoProp pseudoProp; + + public Prop(PlayerProperty prop) { + this(prop.toString(), prop, PseudoProp.NONE); + } + + public Prop(String name) { + this(name, PlayerProperty.PROP_NONE, PseudoProp.NONE); + } + + public Prop(String name, PseudoProp pseudoProp) { + this(name, PlayerProperty.PROP_NONE, pseudoProp); + } + + public Prop(String name, PlayerProperty prop) { + this(name, prop, PseudoProp.NONE); + } + + public Prop(String name, PlayerProperty prop, PseudoProp pseudoProp) { + this.name = name; + this.prop = prop; + this.pseudoProp = pseudoProp; + } + } +} diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index a989b3a5f..9ad159200 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -6,11 +6,12 @@ import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.utils.JsonUtils; +import lombok.NoArgsConstructor; +import java.util.Set; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Locale; -import java.util.Set; import static emu.grasscutter.Grasscutter.config; @@ -18,14 +19,6 @@ import static emu.grasscutter.Grasscutter.config; * *when your JVM fails* */ public class ConfigContainer { - public Structure folderStructure = new Structure(); - public Database databaseInfo = new Database(); - public Language language = new Language(); - public Account account = new Account(); - public Server server = new Server(); - // DO NOT. TOUCH. THE VERSION NUMBER. - public int version = version(); - private static int version() { return 4; } @@ -40,8 +33,7 @@ public class ConfigContainer { Grasscutter.getLogger().info("Updating legacy .."); Grasscutter.saveConfig(null); } - } catch (Exception ignored) { - } + } catch (Exception ignored) { } var existing = config.version; var latest = version(); @@ -59,8 +51,7 @@ public class ConfigContainer { } catch (Exception exception) { Grasscutter.getLogger().error("Failed to update a configuration field.", exception); } - }); - updated.version = version(); + }); updated.version = version(); try { // Save configuration & reload. Grasscutter.saveConfig(updated); @@ -70,6 +61,15 @@ public class ConfigContainer { } } + public Structure folderStructure = new Structure(); + public Database databaseInfo = new Database(); + public Language language = new Language(); + public Account account = new Account(); + public Server server = new Server(); + + // DO NOT. TOUCH. THE VERSION NUMBER. + public int version = version(); + /* Option containers. */ public static class Database { @@ -145,7 +145,7 @@ public class ConfigContainer { public int accessPort = 0; /* Entities within a certain range will be loaded for the player */ - public int loadEntitiesForPlayerRange = 100; + public int loadEntitiesForPlayerRange = 300; public boolean enableScriptInBigWorld = false; public boolean enableConsole = true; @@ -154,13 +154,24 @@ public class ConfigContainer { /* Controls whether packets should be logged in console or not */ public ServerDebugMode logPackets = ServerDebugMode.NONE; /* Show packet payload in console or no (in any case the payload is shown in encrypted view) */ - public Boolean isShowPacketPayload = false; + public boolean isShowPacketPayload = false; /* Show annoying loop packets or no */ - public Boolean isShowLoopPackets = false; + public boolean isShowLoopPackets = false; + + public boolean cacheSceneEntitiesEveryRun = false; public GameOptions gameOptions = new GameOptions(); public JoinOptions joinOptions = new JoinOptions(); public ConsoleAccount serverAccount = new ConsoleAccount(); + + public VisionOptions[] visionOptions = new VisionOptions[] { + new VisionOptions("VISION_LEVEL_NORMAL" , 80 , 20), + new VisionOptions("VISION_LEVEL_LITTLE_REMOTE" , 16 , 40), + new VisionOptions("VISION_LEVEL_REMOTE" , 1000 , 250), + new VisionOptions("VISION_LEVEL_SUPER" , 4000 , 1000), + new VisionOptions("VISION_LEVEL_NEARBY" , 40 , 20), + new VisionOptions("VISION_LEVEL_SUPER_NEARBY" , 20 , 20) + }; } /* Data containers. */ @@ -188,10 +199,10 @@ public class ConfigContainer { public ServerDebugMode logPackets = ServerDebugMode.ALL; /* Show packet payload in console or no (in any case the payload is shown in encrypted view) */ - public Boolean isShowPacketPayload = false; + public boolean isShowPacketPayload = false; /* Show annoying loop packets or no */ - public Boolean isShowLoopPackets = false; + public boolean isShowLoopPackets = false; /* Controls whether http requests should be logged in console or not */ public ServerDebugMode logRequests = ServerDebugMode.ALL; @@ -224,6 +235,7 @@ public class ConfigContainer { public boolean staminaUsage = true; public boolean energyUsage = true; public boolean fishhookTeleport = true; + public boolean questing = false; public ResinOptions resinOptions = new ResinOptions(); public Rates rates = new Rates(); @@ -253,6 +265,18 @@ public class ConfigContainer { } } + public static class VisionOptions { + public String name; + public int visionRange; + public int gridWidth; + + public VisionOptions(String name, int visionRange, int gridWidth) { + this.name = name; + this.visionRange = visionRange; + this.gridWidth = gridWidth; + } + } + public static class JoinOptions { public int[] welcomeEmotes = {2007, 1002, 4010}; public String welcomeMessage = "Welcome to a Grasscutter server."; @@ -261,12 +285,12 @@ public class ConfigContainer { public static class Mail { public String title = "Welcome to Grasscutter!"; public String content = """ - Hi there!\r - First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r - \r - Check out our:\r - - """; + Hi there!\r + First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! \r + \r + Check out our:\r + + """; public String sender = "Lawnmower"; public emu.grasscutter.game.mail.Mail.MailItem[] items = { new emu.grasscutter.game.mail.Mail.MailItem(13509, 1, 1), @@ -292,13 +316,13 @@ public class ConfigContainer { /* Objects. */ + @NoArgsConstructor public static class Region { public String Name = "os_usa"; public String Title = "Grasscutter"; public String Ip = "127.0.0.1"; public int Port = 22102; - public Region() { - } + public Region( String name, String title, String address, int port @@ -306,7 +330,7 @@ public class ConfigContainer { this.Name = name; this.Title = title; this.Ip = address; - this.Port = port; + this.Port = port; } } } diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index f409fc706..e35cd33da 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -1,598 +1,606 @@ -package emu.grasscutter.data; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.binout.*; -import emu.grasscutter.data.binout.config.*; -import emu.grasscutter.data.binout.routes.*; -import emu.grasscutter.data.custom.*; -import emu.grasscutter.data.excels.*; -import emu.grasscutter.data.excels.achievement.*; -import emu.grasscutter.data.excels.activity.*; -import emu.grasscutter.data.excels.avatar.*; -import emu.grasscutter.data.excels.codex.*; -import emu.grasscutter.data.excels.dungeon.*; -import emu.grasscutter.data.excels.monster.*; -import emu.grasscutter.data.excels.reliquary.*; -import emu.grasscutter.data.excels.tower.*; -import emu.grasscutter.data.excels.trial.*; -import emu.grasscutter.data.excels.weapon.*; -import emu.grasscutter.data.excels.world.*; -import emu.grasscutter.data.server.*; -import emu.grasscutter.game.dungeons.*; -import emu.grasscutter.game.quest.*; -import emu.grasscutter.game.world.*; -import emu.grasscutter.utils.Utils; -import it.unimi.dsi.fastutil.ints.*; -import java.lang.reflect.Field; -import java.util.*; -import javax.annotation.Nullable; -import lombok.Getter; -import lombok.val; - -@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection"}) -public final class GameData { - @Getter private static final Map abilityDataMap = new HashMap<>(); - - @Getter - private static final Int2ObjectMap scenePointEntryMap = - new Int2ObjectOpenHashMap<>(); - - // BinOutputs - @Getter - private static final Int2ObjectMap homeworldDefaultSaveData = - new Int2ObjectOpenHashMap<>(); - - @Getter private static final Int2ObjectMap abilityHashes = new Int2ObjectOpenHashMap<>(); - - @Deprecated(forRemoval = true) - @Getter - private static final Map abilityModifiers = new HashMap<>(); - - @Getter private static final Map gadgetConfigData = new HashMap<>(); - @Getter private static final Map openConfigEntries = new HashMap<>(); - - private static final Int2ObjectMap mainQuestData = new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap questsKeys = new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap npcBornData = new Int2ObjectOpenHashMap<>(); - private static final Map abilityEmbryos = new HashMap<>(); - - // ExcelConfigs - @Getter - private static final Int2ObjectMap activityCondExcelConfigDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap dungeonPassConfigDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap dungeonChallengeConfigDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap> sceneRouteData = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final ArrayList codexReliquaryArrayList = new ArrayList<>(); - - private static final Int2ObjectMap achievementDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap achievementGoalDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap activityDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap activityShopDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap activityWatcherDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarCostumeDataItemIdMap = - new Int2ObjectLinkedOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarCostumeDataMap = - new Int2ObjectLinkedOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarCurveDataMap = - new Int2ObjectLinkedOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarFetterLevelDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarFlycloakDataMap = - new Int2ObjectLinkedOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarLevelDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarSkillDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarSkillDepotDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap avatarTalentDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap battlePassMissionDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap battlePassRewardDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap - blossomRefreshExcelConfigDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter private static final Int2ObjectMap buffDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap chapterDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter private static final Int2ObjectMap cityDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap codexAnimalDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap codexMaterialDataIdMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap codexQuestDataIdMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap codexReliquaryDataIdMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap codexWeaponDataIdMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap combineDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap cookBonusDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap cookRecipeDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap compoundDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap dailyDungeonDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap dungeonDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap dungeonEntryDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap envAnimalGatherConfigDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap equipAffixDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap fetterCharacterCardDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap forgeDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap furnitureMakeConfigDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap gadgetDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap gatherDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap homeWorldBgmDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap homeWorldLevelDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap investigationMonsterDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter private static final Int2ObjectMap itemDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap monsterCurveDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap monsterDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap monsterDescribeDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap musicGameBasicDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter private static final Int2ObjectMap npcDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap openStateDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap personalLineDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap playerLevelDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap proudSkillDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap questDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap reliquaryAffixDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap reliquaryMainPropDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap reliquarySetDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap rewardDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap rewardPreviewDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap sceneDataMap = new Int2ObjectLinkedOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap towerFloorDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap towerLevelDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap towerScheduleDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap triggerExcelConfigDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap trialAvatarDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap trialAvatarActivityDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap trialAvatarActivityDataDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap trialAvatarTemplateDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap trialReliquaryDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap weaponCurveDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap weaponLevelDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap weaponPromoteDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap weatherDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap worldAreaDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap worldLevelDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap rewindDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap teleportDataMap = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap refreshPolicyExcelConfigDataMap = - new Int2ObjectOpenHashMap<>(); - - private static final Int2ObjectMap avatarPromoteDataMap = - new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap fetterDataMap = new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap reliquaryLevelDataMap = - new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap shopGoodsDataMap = - new Int2ObjectOpenHashMap<>(); - - // The following are accessed via getMapByResourceDef, and will show as unused - private static final Int2ObjectMap codexMaterialDataMap = - new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap codexQuestDataMap = - new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap codexReliquaryDataMap = - new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap codexWeaponDataMap = - new Int2ObjectOpenHashMap<>(); - - // Custom community server resources - @Getter - private static final Int2ObjectMap> dungeonDropDataMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap gadgetMappingMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap activityCondGroupMap = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Int2ObjectMap groupReplacements = - new Int2ObjectOpenHashMap<>(); - - // Cache - @Getter private static final IntList scenePointIdList = new IntArrayList(); - @Getter private static final List openStateList = new ArrayList<>(); - @Getter private static final Map> scenePointsPerScene = new HashMap<>(); - @Getter private static final Map scriptSceneDataMap = new HashMap<>(); - - @Getter - private static final Map configLevelEntityDataMap = new HashMap<>(); - - @Getter - private static final Int2ObjectMap proudSkillGroupLevels = new Int2ObjectOpenHashMap<>(); - - @Getter private static final Int2IntMap proudSkillGroupMaxLevels = new Int2IntOpenHashMap(); - - @Getter - private static final Int2ObjectMap avatarSkillLevels = new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Map> beginCondQuestMap = - new HashMap<>(); // cache filled by QuestData - - @Getter private static final Map questTalkMap = new HashMap<>(); - - @Getter - private static final Int2ObjectMap trialAvatarCustomData = - new Int2ObjectOpenHashMap<>(); - - @Getter - private static final Map trialAvatarActivityCustomData = - new HashMap<>(); - - @Getter - private static final Map trialAvatarActivityDataCustomData = - new HashMap<>(); - - @Getter - private static final Int2IntMap trialAvatarIndexIdTrialActivityDataDataMap = - new Int2IntOpenHashMap(); - - private static final Map> fetters = new HashMap<>(); - private static final Map> shopGoods = new HashMap<>(); - - // Getters with different names that stay for now - public static Int2ObjectMap getMainQuestDataMap() { - return mainQuestData; - } - - public static Int2ObjectMap getMainQuestEncryptionMap() { - return questsKeys; - } - - public static Int2ObjectMap getSceneNpcBornData() { - return npcBornData; - } - - public static Map getAbilityEmbryoInfo() { - return abilityEmbryos; - } - - // Getters that get values rather than containers. If Lombok ever gets syntactic sugar for this, - // we should adopt that. - public static AbilityData getAbilityData(String abilityName) { - return abilityDataMap.get(abilityName); - } - - public static IntSet getAvatarSkillLevels(int avatarSkillId) { - return avatarSkillLevels.get(avatarSkillId); - } - - public static IntSet getProudSkillGroupLevels(int proudSkillGroupId) { - return proudSkillGroupLevels.get(proudSkillGroupId); - } - - public static int getProudSkillGroupMaxLevel(int proudSkillGroupId) { - return proudSkillGroupMaxLevels.getOrDefault(proudSkillGroupId, 0); - } - - // Multi-keyed getters - public static AvatarPromoteData getAvatarPromoteData(int promoteId, int promoteLevel) { - return avatarPromoteDataMap.get((promoteId << 8) + promoteLevel); - } - - public static WeaponPromoteData getWeaponPromoteData(int promoteId, int promoteLevel) { - return weaponPromoteDataMap.get((promoteId << 8) + promoteLevel); - } - - public static ReliquaryLevelData getRelicLevelData(int rankLevel, int level) { - return reliquaryLevelDataMap.get((rankLevel << 8) + level); - } - - public static ScenePointEntry getScenePointEntryById(int sceneId, int pointId) { - return scenePointEntryMap.get((sceneId << 16) + pointId); - } - - // Non-nullable value getters - public static int getAvatarLevelExpRequired(int level) { - return Optional.ofNullable(avatarLevelDataMap.get(level)).map(d -> d.getExp()).orElse(0); - } - - public static int getAvatarFetterLevelExpRequired(int level) { - return Optional.ofNullable(avatarFetterLevelDataMap.get(level)).map(d -> d.getExp()).orElse(0); - } - - public static int getRelicExpRequired(int rankLevel, int level) { - return Optional.ofNullable(getRelicLevelData(rankLevel, level)).map(d -> d.getExp()).orElse(0); - } - - // Generic getter - public static Int2ObjectMap getMapByResourceDef(Class resourceDefinition) { - Int2ObjectMap map = null; - - try { - Field field = - GameData.class.getDeclaredField( - Utils.lowerCaseFirstChar(resourceDefinition.getSimpleName()) + "Map"); - - field.setAccessible(true); - map = (Int2ObjectMap) field.get(null); - field.setAccessible(false); - } catch (Exception e) { - Grasscutter.getLogger() - .error("Error fetching resource map for " + resourceDefinition.getSimpleName(), e); - } - - return map; - } - - public static int getWeaponExpRequired(int rankLevel, int level) { - WeaponLevelData levelData = weaponLevelDataMap.get(level); - if (levelData == null) { - return 0; - } - try { - return levelData.getRequiredExps()[rankLevel - 1]; - } catch (Exception e) { - return 0; - } - } - - public static Map> getFetterDataEntries() { - if (fetters.isEmpty()) { - fetterDataMap.forEach( - (k, v) -> { - if (!fetters.containsKey(v.getAvatarId())) { - fetters.put(v.getAvatarId(), new ArrayList<>()); - } - fetters.get(v.getAvatarId()).add(k); - }); - } - - return fetters; - } - - public static Map> getShopGoodsDataEntries() { - if (shopGoods.isEmpty()) { - shopGoodsDataMap.forEach( - (k, v) -> { - if (!shopGoods.containsKey(v.getShopType())) - shopGoods.put(v.getShopType(), new ArrayList<>()); - shopGoods.get(v.getShopType()).add(v); - }); - } - - return shopGoods; - } - - /** - * Fetches the route data for a scene by ID. - * - * @param sceneId The ID of the scene to fetch the route data for. - * @return The route data for the scene, or an empty map if the scene has no route data. - */ - public static Int2ObjectMap getSceneRoutes(int sceneId) { - return sceneRouteData.computeIfAbsent(sceneId, k -> new Int2ObjectOpenHashMap<>()); - } - - /** - * Fetches the trial data - * - * @param trialAvatarIndexId - * @return - */ - @Nullable public static TrialAvatarActivityDataData getTrialAvatarActivityDataByAvatarIndex( - int trialAvatarIndexId) { - // prefer custom data over official data - val dataId = trialAvatarIndexIdTrialActivityDataDataMap.get(trialAvatarIndexId); - val datamap = - GameData.getTrialAvatarActivityDataCustomData().isEmpty() - ? GameData.getTrialAvatarActivityDataDataMap() - : GameData.getTrialAvatarActivityDataCustomData(); - return datamap.get(dataId); - } - - public static Int2ObjectMap getAchievementDataMap() { - AchievementData.divideIntoGroups(); - return achievementDataMap; - } -} +package emu.grasscutter.data; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.binout.*; +import emu.grasscutter.data.binout.config.*; +import emu.grasscutter.data.binout.routes.*; +import emu.grasscutter.data.custom.*; +import emu.grasscutter.data.excels.*; +import emu.grasscutter.data.excels.achievement.*; +import emu.grasscutter.data.excels.activity.*; +import emu.grasscutter.data.excels.avatar.*; +import emu.grasscutter.data.excels.codex.*; +import emu.grasscutter.data.excels.dungeon.*; +import emu.grasscutter.data.excels.monster.*; +import emu.grasscutter.data.excels.reliquary.*; +import emu.grasscutter.data.excels.tower.*; +import emu.grasscutter.data.excels.trial.*; +import emu.grasscutter.data.excels.weapon.*; +import emu.grasscutter.data.excels.world.*; +import emu.grasscutter.data.server.*; +import emu.grasscutter.game.dungeons.*; +import emu.grasscutter.game.quest.*; +import emu.grasscutter.game.world.*; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.*; +import java.lang.reflect.Field; +import java.util.*; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.val; + +@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection"}) +public final class GameData { + @Getter private static final Map abilityDataMap = new HashMap<>(); + + @Getter + private static final Int2ObjectMap scenePointEntryMap = + new Int2ObjectOpenHashMap<>(); + + // BinOutputs + @Getter + private static final Int2ObjectMap homeworldDefaultSaveData = + new Int2ObjectOpenHashMap<>(); + + @Getter private static final Int2ObjectMap abilityHashes = new Int2ObjectOpenHashMap<>(); + + @Deprecated(forRemoval = true) + @Getter + private static final Map abilityModifiers = new HashMap<>(); + + @Getter private static final Map avatarConfigData = new HashMap<>(); + @Getter private static final Map gadgetConfigData = new HashMap<>(); + @Getter private static final Map monsterConfigData = new HashMap<>(); + + @Getter private static final Map openConfigEntries = new HashMap<>(); + + private static final Int2ObjectMap mainQuestData = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap questsKeys = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap npcBornData = new Int2ObjectOpenHashMap<>(); + private static final Map abilityEmbryos = new HashMap<>(); + + // ExcelConfigs + @Getter + private static final Int2ObjectMap activityCondExcelConfigDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap dungeonPassConfigDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap dungeonChallengeConfigDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap> sceneRouteData = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final ArrayList codexReliquaryArrayList = new ArrayList<>(); + + private static final Int2ObjectMap achievementDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap achievementGoalDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap activityDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap activityShopDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap activityWatcherDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarCostumeDataItemIdMap = + new Int2ObjectLinkedOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarCostumeDataMap = + new Int2ObjectLinkedOpenHashMap<>(); + + @Getter private static final Int2ObjectMap avatarReplaceCostumeDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarCurveDataMap = + new Int2ObjectLinkedOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarFetterLevelDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarFlycloakDataMap = + new Int2ObjectLinkedOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarLevelDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarSkillDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarSkillDepotDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap avatarTalentDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap battlePassMissionDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap battlePassRewardDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap + blossomRefreshExcelConfigDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter private static final Int2ObjectMap buffDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap chapterDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter private static final Int2ObjectMap cityDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap codexAnimalDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap codexMaterialDataIdMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap codexQuestDataIdMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap codexReliquaryDataIdMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap codexWeaponDataIdMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap combineDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap cookBonusDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap cookRecipeDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap compoundDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap dailyDungeonDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap dungeonDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap dungeonEntryDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap envAnimalGatherConfigDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap equipAffixDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap fetterCharacterCardDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap forgeDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap furnitureMakeConfigDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap gadgetDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap gatherDataMap = new Int2ObjectOpenHashMap<>(); + @Getter @Deprecated // This is to prevent people from using this map. This is for the resource loader only! + private static final Int2ObjectMap guideTriggerDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap homeWorldBgmDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap homeWorldLevelDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap investigationMonsterDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter private static final Int2ObjectMap itemDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap monsterCurveDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap monsterDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap monsterDescribeDataMap = + new Int2ObjectOpenHashMap<>(); + @Getter private static final Int2ObjectMap monsterSpecialNameDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap musicGameBasicDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter private static final Int2ObjectMap npcDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap openStateDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap personalLineDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap playerLevelDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap proudSkillDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap questDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap reliquaryAffixDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap reliquaryMainPropDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap reliquarySetDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap rewardDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap rewardPreviewDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap sceneDataMap = new Int2ObjectLinkedOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap towerFloorDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap towerLevelDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap towerScheduleDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap triggerExcelConfigDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap trialAvatarDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap trialAvatarActivityDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap trialAvatarActivityDataDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap trialAvatarTemplateDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap trialReliquaryDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap weaponCurveDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap weaponLevelDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap weaponPromoteDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap weatherDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap worldAreaDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap worldLevelDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap rewindDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap teleportDataMap = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap refreshPolicyExcelConfigDataMap = + new Int2ObjectOpenHashMap<>(); + + private static final Int2ObjectMap avatarPromoteDataMap = + new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap fetterDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap reliquaryLevelDataMap = + new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap shopGoodsDataMap = + new Int2ObjectOpenHashMap<>(); + + // The following are accessed via getMapByResourceDef, and will show as unused + private static final Int2ObjectMap codexMaterialDataMap = + new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap codexQuestDataMap = + new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap codexReliquaryDataMap = + new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap codexWeaponDataMap = + new Int2ObjectOpenHashMap<>(); + + // Custom community server resources + @Getter + private static final Int2ObjectMap> dungeonDropDataMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap gadgetMappingMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap activityCondGroupMap = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Int2ObjectMap groupReplacements = + new Int2ObjectOpenHashMap<>(); + + // Cache + @Getter private static final IntList scenePointIdList = new IntArrayList(); + @Getter private static final List openStateList = new ArrayList<>(); + @Getter private static final Map> scenePointsPerScene = new HashMap<>(); + @Getter private static final Map scriptSceneDataMap = new HashMap<>(); + @Getter private static final Map guideTriggerDataStringMap = new HashMap<>(); + @Getter private static final Map configLevelEntityDataMap = new HashMap<>(); + + @Getter + private static final Int2ObjectMap proudSkillGroupLevels = new Int2ObjectOpenHashMap<>(); + + @Getter private static final Int2IntMap proudSkillGroupMaxLevels = new Int2IntOpenHashMap(); + + @Getter + private static final Int2ObjectMap avatarSkillLevels = new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Map> beginCondQuestMap = + new HashMap<>(); // cache filled by QuestData + + @Getter private static final Map questTalkMap = new HashMap<>(); + + @Getter + private static final Int2ObjectMap trialAvatarCustomData = + new Int2ObjectOpenHashMap<>(); + + @Getter + private static final Map trialAvatarActivityCustomData = + new HashMap<>(); + + @Getter + private static final Map trialAvatarActivityDataCustomData = + new HashMap<>(); + + @Getter + private static final Int2IntMap trialAvatarIndexIdTrialActivityDataDataMap = + new Int2IntOpenHashMap(); + + private static final Map> fetters = new HashMap<>(); + private static final Map> shopGoods = new HashMap<>(); + + // Getters with different names that stay for now + public static Int2ObjectMap getMainQuestDataMap() { + return mainQuestData; + } + + public static Int2ObjectMap getMainQuestEncryptionMap() { + return questsKeys; + } + + public static Int2ObjectMap getSceneNpcBornData() { + return npcBornData; + } + + public static Map getAbilityEmbryoInfo() { + return abilityEmbryos; + } + + // Getters that get values rather than containers. If Lombok ever gets syntactic sugar for this, + // we should adopt that. + public static AbilityData getAbilityData(String abilityName) { + return abilityDataMap.get(abilityName); + } + + public static IntSet getAvatarSkillLevels(int avatarSkillId) { + return avatarSkillLevels.get(avatarSkillId); + } + + public static IntSet getProudSkillGroupLevels(int proudSkillGroupId) { + return proudSkillGroupLevels.get(proudSkillGroupId); + } + + public static int getProudSkillGroupMaxLevel(int proudSkillGroupId) { + return proudSkillGroupMaxLevels.getOrDefault(proudSkillGroupId, 0); + } + + // Multi-keyed getters + public static AvatarPromoteData getAvatarPromoteData(int promoteId, int promoteLevel) { + return avatarPromoteDataMap.get((promoteId << 8) + promoteLevel); + } + + public static WeaponPromoteData getWeaponPromoteData(int promoteId, int promoteLevel) { + return weaponPromoteDataMap.get((promoteId << 8) + promoteLevel); + } + + public static ReliquaryLevelData getRelicLevelData(int rankLevel, int level) { + return reliquaryLevelDataMap.get((rankLevel << 8) + level); + } + + public static ScenePointEntry getScenePointEntryById(int sceneId, int pointId) { + return scenePointEntryMap.get((sceneId << 16) + pointId); + } + + // Non-nullable value getters + public static int getAvatarLevelExpRequired(int level) { + return Optional.ofNullable(avatarLevelDataMap.get(level)).map(d -> d.getExp()).orElse(0); + } + + public static int getAvatarFetterLevelExpRequired(int level) { + return Optional.ofNullable(avatarFetterLevelDataMap.get(level)).map(d -> d.getExp()).orElse(0); + } + + public static int getRelicExpRequired(int rankLevel, int level) { + return Optional.ofNullable(getRelicLevelData(rankLevel, level)).map(d -> d.getExp()).orElse(0); + } + + // Generic getter + public static Int2ObjectMap getMapByResourceDef(Class resourceDefinition) { + Int2ObjectMap map = null; + + try { + Field field = + GameData.class.getDeclaredField( + Utils.lowerCaseFirstChar(resourceDefinition.getSimpleName()) + "Map"); + + field.setAccessible(true); + map = (Int2ObjectMap) field.get(null); + field.setAccessible(false); + } catch (Exception e) { + Grasscutter.getLogger() + .error("Error fetching resource map for " + resourceDefinition.getSimpleName(), e); + } + + return map; + } + + public static int getWeaponExpRequired(int rankLevel, int level) { + WeaponLevelData levelData = weaponLevelDataMap.get(level); + if (levelData == null) { + return 0; + } + try { + return levelData.getRequiredExps()[rankLevel - 1]; + } catch (Exception e) { + return 0; + } + } + + public static Map> getFetterDataEntries() { + if (fetters.isEmpty()) { + fetterDataMap.forEach( + (k, v) -> { + if (!fetters.containsKey(v.getAvatarId())) { + fetters.put(v.getAvatarId(), new ArrayList<>()); + } + fetters.get(v.getAvatarId()).add(k); + }); + } + + return fetters; + } + + public static Map> getShopGoodsDataEntries() { + if (shopGoods.isEmpty()) { + shopGoodsDataMap.forEach( + (k, v) -> { + if (!shopGoods.containsKey(v.getShopType())) + shopGoods.put(v.getShopType(), new ArrayList<>()); + shopGoods.get(v.getShopType()).add(v); + }); + } + + return shopGoods; + } + + /** + * Fetches the route data for a scene by ID. + * + * @param sceneId The ID of the scene to fetch the route data for. + * @return The route data for the scene, or an empty map if the scene has no route data. + */ + public static Int2ObjectMap getSceneRoutes(int sceneId) { + return sceneRouteData.computeIfAbsent(sceneId, k -> new Int2ObjectOpenHashMap<>()); + } + + /** + * Fetches the trial data + * + * @param trialAvatarIndexId + * @return + */ + @Nullable public static TrialAvatarActivityDataData getTrialAvatarActivityDataByAvatarIndex( + int trialAvatarIndexId) { + // prefer custom data over official data + val dataId = trialAvatarIndexIdTrialActivityDataDataMap.get(trialAvatarIndexId); + val datamap = + GameData.getTrialAvatarActivityDataCustomData().isEmpty() + ? GameData.getTrialAvatarActivityDataDataMap() + : GameData.getTrialAvatarActivityDataCustomData(); + return datamap.get(dataId); + } + + public static Int2ObjectMap getAchievementDataMap() { + AchievementData.divideIntoGroups(); + return achievementDataMap; + } +} diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index 768eedb3b..837793bad 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -1,657 +1,681 @@ -package emu.grasscutter.data; - -import static emu.grasscutter.utils.FileUtils.getDataPath; -import static emu.grasscutter.utils.FileUtils.getResourcePath; -import static emu.grasscutter.utils.Language.translate; - -import com.google.gson.annotations.SerializedName; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.binout.*; -import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; -import emu.grasscutter.data.common.PointData; -import emu.grasscutter.game.managers.blossom.BlossomConfig; -import emu.grasscutter.game.quest.QuestEncryptionKey; -import emu.grasscutter.game.world.SpawnDataEntry; -import emu.grasscutter.game.world.SpawnDataEntry.GridBlockId; -import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry; -import emu.grasscutter.scripts.SceneIndexManager; -import emu.grasscutter.utils.FileUtils; -import emu.grasscutter.utils.JsonUtils; -import emu.grasscutter.utils.TsvUtils; -import it.unimi.dsi.fastutil.Pair; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntArraySet; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.regex.Pattern; -import java.util.stream.Stream; -import lombok.val; -import org.reflections.Reflections; - -public class ResourceLoader { - - private static final Set loadedResources = new CopyOnWriteArraySet<>(); - private static boolean loadedAll = false; - - // Get a list of all resource classes, sorted by loadPriority - public static List> getResourceDefClasses() { - Reflections reflections = new Reflections(ResourceLoader.class.getPackage().getName()); - Set classes = reflections.getSubTypesOf(GameResource.class); - - List> classList = new ArrayList<>(classes.size()); - classes.forEach( - o -> { - Class c = (Class) o; - if (c.getAnnotation(ResourceType.class) != null) { - classList.add(c); - } - }); - - classList.sort( - (a, b) -> - b.getAnnotation(ResourceType.class).loadPriority().value() - - a.getAnnotation(ResourceType.class).loadPriority().value()); - - return classList; - } - - // Get a list containing sets of all resource classes, sorted by loadPriority - protected static List>> getResourceDefClassesPrioritySets() { - val reflections = new Reflections(ResourceLoader.class.getPackage().getName()); - val classes = reflections.getSubTypesOf(GameResource.class); - val priorities = ResourceType.LoadPriority.getInOrder(); - Grasscutter.getLogger().debug("Priorities are " + priorities); - val map = new LinkedHashMap>>(priorities.size()); - priorities.forEach(p -> map.put(p, new HashSet<>())); - - classes.forEach( - c -> { - // val c = (Class) o; - val annotation = c.getAnnotation(ResourceType.class); - if (annotation != null) { - map.get(annotation.loadPriority()).add(c); - } - }); - return List.copyOf(map.values()); - } - - public static void loadAll() { - if (loadedAll) return; - Grasscutter.getLogger().info(translate("messages.status.resources.loading")); - - // Load ability lists - loadAbilityEmbryos(); - loadOpenConfig(); - loadAbilityModifiers(); - // Load resources - loadResources(true); - // Process into depots - GameDepot.load(); - // Load spawn data and quests - loadGadgetConfigData(); - loadSpawnData(); - loadQuests(); - loadScriptSceneData(); - // Load scene points - must be done AFTER resources are loaded - loadScenePoints(); - // Load default home layout - loadHomeworldDefaultSaveData(); - loadNpcBornData(); - loadBlossomResources(); - cacheTalentLevelSets(); - - Grasscutter.getLogger().info(translate("messages.status.resources.finish")); - loadedAll = true; - } - - public static void loadResources() { - loadResources(false); - } - - public static void loadResources(boolean doReload) { - long startTime = System.nanoTime(); - val errors = - new ConcurrentLinkedQueue< - Pair>(); // Logger in a parallel stream will deadlock - - getResourceDefClassesPrioritySets() - .forEach( - classes -> { - classes.stream() - .parallel() - .unordered() - .forEach( - c -> { - val type = c.getAnnotation(ResourceType.class); - if (type == null) return; - - val map = GameData.getMapByResourceDef(c); - if (map == null) return; - - try { - loadFromResource(c, type, map, doReload); - } catch (Exception e) { - errors.add(Pair.of(Arrays.toString(type.name()), e)); - } - }); - }); - errors.forEach( - pair -> - Grasscutter.getLogger() - .error("Error loading resource file: " + pair.left(), pair.right())); - long endTime = System.nanoTime(); - long ns = (endTime - startTime); // divide by 1000000 to get milliseconds. - Grasscutter.getLogger().debug("Loading resources took " + ns + "ns == " + ns / 1000000 + "ms"); - } - - @SuppressWarnings("rawtypes") - protected static void loadFromResource( - Class c, ResourceType type, Int2ObjectMap map, boolean doReload) throws Exception { - val simpleName = c.getSimpleName(); - if (doReload || !loadedResources.contains(simpleName)) { - for (String name : type.name()) { - loadFromResource(c, FileUtils.getExcelPath(name), map); - } - loadedResources.add(simpleName); - } - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - protected static void loadFromResource(Class c, Path filename, Int2ObjectMap map) - throws Exception { - val results = - switch (FileUtils.getFileExtension(filename)) { - case "json" -> JsonUtils.loadToList(filename, c); - case "tsj" -> TsvUtils.loadTsjToListSetField(filename, c); - case "tsv" -> TsvUtils.loadTsvToListSetField(filename, c); - default -> null; - }; - if (results == null) return; - results.forEach( - o -> { - GameResource res = (GameResource) o; - res.onLoad(); - map.put(res.getId(), res); - }); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - protected static void loadFromResource(Class c, String fileName, Int2ObjectMap map) - throws Exception { - JsonUtils.loadToList(getResourcePath("ExcelBinOutput/" + fileName), c) - .forEach( - o -> { - GameResource res = (GameResource) o; - res.onLoad(); - map.put(res.getId(), res); - }); - } - - private static void loadScenePoints() { - val pattern = Pattern.compile("scene([0-9]+)_point\\.json"); - try { - Files.newDirectoryStream(getResourcePath("BinOutput/Scene/Point"), "scene*_point.json") - .forEach( - path -> { - val matcher = pattern.matcher(path.getFileName().toString()); - if (!matcher.find()) return; - int sceneId = Integer.parseInt(matcher.group(1)); - - ScenePointConfig config; - try { - config = JsonUtils.loadToClass(path, ScenePointConfig.class); - } catch (Exception e) { - e.printStackTrace(); - return; - } - - if (config.points == null) return; - - val scenePoints = new IntArrayList(); - config.points.forEach( - (pointId, pointData) -> { - val scenePoint = new ScenePointEntry(sceneId, pointData); - scenePoints.add(pointId); - pointData.setId(pointId); - - GameData.getScenePointIdList().add(pointId); - GameData.getScenePointEntryMap().put((sceneId << 16) + pointId, scenePoint); - - pointData.updateDailyDungeon(); - }); - GameData.getScenePointsPerScene().put(sceneId, scenePoints); - }); - } catch (IOException ignored) { - Grasscutter.getLogger() - .error("Scene point files cannot be found, you cannot use teleport waypoints!"); - } - } - - private static void cacheTalentLevelSets() { - // All known levels, keyed by proudSkillGroupId - GameData.getProudSkillDataMap() - .forEach( - (id, data) -> - GameData.getProudSkillGroupLevels() - .computeIfAbsent(data.getProudSkillGroupId(), i -> new IntArraySet()) - .add(data.getLevel())); - // All known levels, keyed by avatarSkillId - GameData.getAvatarSkillDataMap() - .forEach( - (id, data) -> - GameData.getAvatarSkillLevels() - .put( - (int) id, - GameData.getProudSkillGroupLevels().get(data.getProudSkillGroupId()))); - // Maximum known levels, keyed by proudSkillGroupId - GameData.getProudSkillGroupLevels() - .forEach( - (id, set) -> - GameData.getProudSkillGroupMaxLevels() - .put((int) id, set.intStream().max().orElse(-1))); - } - - private static void loadAbilityEmbryos() { - List embryoList = null; - - // Read from cached file if exists - try { - embryoList = - JsonUtils.loadToList(getDataPath("AbilityEmbryos.json"), AbilityEmbryoEntry.class); - } catch (Exception ignored) { - } - - if (embryoList == null) { - // Load from BinOutput - var pattern = Pattern.compile("ConfigAvatar_(.+?)\\.json"); - - var entries = new ArrayList(); - try (var stream = - Files.newDirectoryStream(getResourcePath("BinOutput/Avatar/"), "ConfigAvatar_*.json")) { - - stream.forEach( - path -> { - var matcher = pattern.matcher(path.getFileName().toString()); - if (!matcher.find()) return; - - var avatarName = matcher.group(1); - AvatarConfig config; - try { - config = JsonUtils.loadToClass(path, AvatarConfig.class); - } catch (Exception e) { - Grasscutter.getLogger().error("Error loading player ability embryos:", e); - return; - } - - if (config.abilities == null) return; - - entries.add( - new AbilityEmbryoEntry( - avatarName, - config.abilities.stream() - .map(Object::toString) - .toArray(size -> new String[config.abilities.size()]))); - }); - } catch (IOException e) { - Grasscutter.getLogger().error("Error loading ability embryos: no files found"); - return; - } - - embryoList = entries; - - try { - GameDepot.setPlayerAbilities( - JsonUtils.loadToMap( - getResourcePath( - "BinOutput/AbilityGroup/AbilityGroup_Other_PlayerElementAbility.json"), - String.class, - AvatarConfig.class)); - } catch (IOException e) { - Grasscutter.getLogger().error("Error loading player abilities:", e); - } - } - - if (embryoList == null || embryoList.isEmpty()) { - Grasscutter.getLogger().error("No embryos loaded!"); - return; - } - - for (AbilityEmbryoEntry entry : embryoList) { - GameData.getAbilityEmbryoInfo().put(entry.getName(), entry); - } - } - - private static void loadAbilityModifiers() { - // Load from BinOutput - try (Stream paths = Files.walk(getResourcePath("BinOutput/Ability/Temp/"))) { - paths - .filter(Files::isRegularFile) - .filter(path -> path.toString().endsWith(".json")) - .forEach(ResourceLoader::loadAbilityModifiers); - } catch (IOException e) { - Grasscutter.getLogger().error("Error loading ability modifiers: ", e); - } - // System.out.println("Loaded modifiers, found types:"); - // modifierActionTypes.stream().sorted().forEach(s -> System.out.printf("%s, ", s)); - // System.out.println("[End]"); - } - - private static void loadAbilityModifiers(Path path) { - try { - JsonUtils.loadToList(path, AbilityConfigData.class) - .forEach(data -> loadAbilityData(data.Default)); - } catch (IOException e) { - Grasscutter.getLogger() - .error("Error loading ability modifiers from path " + path.toString() + ": ", e); - } - } - - private static void loadAbilityData(AbilityData data) { - GameData.getAbilityDataMap().put(data.abilityName, data); - - val modifiers = data.modifiers; - if (modifiers == null || modifiers.size() == 0) return; - - String name = data.abilityName; - AbilityModifierEntry modifierEntry = new AbilityModifierEntry(name); - modifiers.forEach( - (key, modifier) -> { - Stream.ofNullable(modifier.onAdded) - .flatMap(Stream::of) - // .map(action -> {modifierActionTypes.add(action.$type); return action;}) - .filter(action -> action.type == AbilityModifierAction.Type.HealHP) - .forEach(action -> modifierEntry.getOnAdded().add(action)); - Stream.ofNullable(modifier.onThinkInterval) - .flatMap(Stream::of) - // .map(action -> {modifierActionTypes.add(action.$type); return action;}) - .filter(action -> action.type == AbilityModifierAction.Type.HealHP) - .forEach(action -> modifierEntry.getOnThinkInterval().add(action)); - Stream.ofNullable(modifier.onRemoved) - .flatMap(Stream::of) - // .map(action -> {modifierActionTypes.add(action.$type); return action;}) - .filter(action -> action.type == AbilityModifierAction.Type.HealHP) - .forEach(action -> modifierEntry.getOnRemoved().add(action)); - }); - - GameData.getAbilityModifiers().put(name, modifierEntry); - } - - private static void loadSpawnData() { - String[] spawnDataNames = {"Spawns.json", "GadgetSpawns.json"}; - ArrayList spawnEntryMap = new ArrayList<>(); - - for (String name : spawnDataNames) { - // Load spawn entries from file - try (InputStreamReader reader = DataLoader.loadReader(name)) { - // Add spawns to group if it already exists in our spawn group map - spawnEntryMap.addAll(JsonUtils.loadToList(reader, SpawnGroupEntry.class)); - } catch (Exception ignored) { - } - } - - if (spawnEntryMap.isEmpty()) { - Grasscutter.getLogger().error("No spawn data loaded!"); - return; - } - - HashMap> areaSort = new HashMap<>(); - // key = sceneId,x,z , value = ArrayList - for (SpawnGroupEntry entry : spawnEntryMap) { - entry - .getSpawns() - .forEach( - s -> { - s.setGroup(entry); - GridBlockId point = s.getBlockId(); - if (!areaSort.containsKey(point)) { - areaSort.put(point, new ArrayList<>()); - } - areaSort.get(point).add(s); - }); - } - GameDepot.addSpawnListById(areaSort); - } - - private static void loadOpenConfig() { - // Read from cached file if exists - List list = null; - - try { - list = JsonUtils.loadToList(getDataPath("OpenConfig.json"), OpenConfigEntry.class); - } catch (Exception ignored) { - } - - if (list == null) { - Map map = new TreeMap<>(); - String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"}; - - for (String folderName : folderNames) { - try { - Files.newDirectoryStream(getResourcePath(folderName), "*.json") - .forEach( - path -> { - try { - JsonUtils.loadToMap(path, String.class, OpenConfigData[].class) - .forEach((name, data) -> map.put(name, new OpenConfigEntry(name, data))); - } catch (Exception e) { - e.printStackTrace(); - } - }); - } catch (IOException e) { - Grasscutter.getLogger() - .error("Error loading open config: no files found in " + folderName); - return; - } - } - - list = new ArrayList<>(map.values()); - } - - if (list == null || list.isEmpty()) { - Grasscutter.getLogger().error("No openconfig entries loaded!"); - return; - } - - for (OpenConfigEntry entry : list) { - GameData.getOpenConfigEntries().put(entry.getName(), entry); - } - } - - private static void loadQuests() { - try { - Files.list(getResourcePath("BinOutput/Quest/")) - .forEach( - path -> { - try { - val mainQuest = JsonUtils.loadToClass(path, MainQuestData.class); - GameData.getMainQuestDataMap().put(mainQuest.getId(), mainQuest); - } catch (IOException e) { - - } - }); - } catch (IOException e) { - Grasscutter.getLogger().error("Quest data missing"); - return; - } - - try { - val questEncryptionMap = GameData.getMainQuestEncryptionMap(); - String path = "QuestEncryptionKeys.json"; - try { - JsonUtils.loadToList(getResourcePath(path), QuestEncryptionKey.class) - .forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key)); - } catch (IOException | NullPointerException ignored) { - } - try { - DataLoader.loadList(path, QuestEncryptionKey.class) - .forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key)); - } catch (IOException | NullPointerException ignored) { - } - Grasscutter.getLogger().debug("Loaded {} quest keys.", questEncryptionMap.size()); - } catch (Exception e) { - Grasscutter.getLogger().error("Unable to load quest keys.", e); - } - - Grasscutter.getLogger() - .debug("Loaded " + GameData.getMainQuestDataMap().size() + " MainQuestDatas."); - } - - public static void loadScriptSceneData() { - try { - Files.list(getResourcePath("ScriptSceneData/")) - .forEach( - path -> { - try { - GameData.getScriptSceneDataMap() - .put( - path.getFileName().toString(), - JsonUtils.loadToClass(path, ScriptSceneData.class)); - } catch (IOException e) { - e.printStackTrace(); - } - }); - Grasscutter.getLogger() - .debug("Loaded " + GameData.getScriptSceneDataMap().size() + " ScriptSceneDatas."); - } catch (IOException e) { - Grasscutter.getLogger().debug("ScriptSceneData folder missing or empty."); - } - } - - private static void loadHomeworldDefaultSaveData() { - val pattern = Pattern.compile("scene([0-9]+)_home_config\\.json"); - try { - Files.newDirectoryStream( - getResourcePath("BinOutput/HomeworldDefaultSave"), "scene*_home_config.json") - .forEach( - path -> { - val matcher = pattern.matcher(path.getFileName().toString()); - if (!matcher.find()) return; - - try { - val sceneId = Integer.parseInt(matcher.group(1)); - val data = JsonUtils.loadToClass(path, HomeworldDefaultSaveData.class); - GameData.getHomeworldDefaultSaveData().put(sceneId, data); - } catch (Exception ignored) { - } - }); - Grasscutter.getLogger() - .debug( - "Loaded " - + GameData.getHomeworldDefaultSaveData().size() - + " HomeworldDefaultSaveDatas."); - } catch (IOException e) { - Grasscutter.getLogger().error("Failed to load HomeworldDefaultSave folder."); - } - } - - private static void loadNpcBornData() { - try { - Files.newDirectoryStream(getResourcePath("BinOutput/Scene/SceneNpcBorn/"), "*.json") - .forEach( - path -> { - try { - val data = JsonUtils.loadToClass(path, SceneNpcBornData.class); - if (data.getBornPosList() == null || data.getBornPosList().size() == 0) { - return; - } - - data.setIndex( - SceneIndexManager.buildIndex( - 3, data.getBornPosList(), item -> item.getPos().toPoint())); - GameData.getSceneNpcBornData().put(data.getSceneId(), data); - } catch (IOException ignored) { - } - }); - Grasscutter.getLogger() - .debug("Loaded " + GameData.getSceneNpcBornData().size() + " SceneNpcBornDatas."); - } catch (IOException e) { - Grasscutter.getLogger().error("Failed to load SceneNpcBorn folder."); - } - } - - private static void loadGadgetConfigData() { - try { - Files.newDirectoryStream(getResourcePath("BinOutput/Gadget/"), "*.json") - .forEach( - path -> { - try { - GameData.getGadgetConfigData() - .putAll(JsonUtils.loadToMap(path, String.class, ConfigGadget.class)); - } catch (Exception e) { - Grasscutter.getLogger() - .error("failed to load ConfigGadget entries for " + path.toString(), e); - } - }); - - Grasscutter.getLogger() - .debug("Loaded {} ConfigGadget entries.", GameData.getGadgetConfigData().size()); - } catch (IOException e) { - Grasscutter.getLogger().error("Failed to load ConfigGadget folder."); - } - } - - private static void loadBlossomResources() { - try { - GameDepot.setBlossomConfig(DataLoader.loadClass("BlossomConfig.json", BlossomConfig.class)); - Grasscutter.getLogger().debug("Loaded BlossomConfig."); - } catch (IOException e) { - Grasscutter.getLogger().warn("Failed to load BlossomConfig."); - } - } - - // private static HashSet modifierActionTypes = new HashSet<>(); - public static class AbilityConfigData { - public AbilityData Default; - } - - public static class AvatarConfig { - @SerializedName( - value = "abilities", - alternate = {"targetAbilities"}) - public ArrayList abilities; - } - - // BinOutput configs - - public static class AvatarConfigAbility { - public String abilityName; - - public String toString() { - return abilityName; - } - } - - private static class OpenConfig { - public OpenConfigData[] data; - } - - public static class OpenConfigData { - public String $type; - public String abilityName; - - @SerializedName( - value = "talentIndex", - alternate = {"OJOFFKLNAHN"}) - public int talentIndex; - - @SerializedName( - value = "skillID", - alternate = {"overtime"}) - public int skillID; - - @SerializedName( - value = "pointDelta", - alternate = {"IGEBKIHPOIF"}) - public int pointDelta; - } - - public class ScenePointConfig { // Sadly this doesn't work as a local class in loadScenePoints() - public Map points; - } -} +package emu.grasscutter.data; + +import static emu.grasscutter.utils.FileUtils.getDataPath; +import static emu.grasscutter.utils.FileUtils.getResourcePath; +import static emu.grasscutter.utils.Language.translate; + +import com.google.gson.annotations.SerializedName; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.binout.*; +import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; +import emu.grasscutter.data.binout.config.ConfigEntityAvatar; +import emu.grasscutter.data.binout.config.ConfigEntityBase; +import emu.grasscutter.data.binout.config.ConfigEntityGadget; +import emu.grasscutter.data.binout.config.ConfigEntityMonster; +import emu.grasscutter.data.common.PointData; +import emu.grasscutter.game.managers.blossom.BlossomConfig; +import emu.grasscutter.game.quest.QuestEncryptionKey; +import emu.grasscutter.game.world.SpawnDataEntry; +import emu.grasscutter.game.world.SpawnDataEntry.GridBlockId; +import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry; +import emu.grasscutter.scripts.SceneIndexManager; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.JsonUtils; +import emu.grasscutter.utils.TsvUtils; +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntArraySet; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import lombok.val; +import org.reflections.Reflections; + +public class ResourceLoader { + + private static final Set loadedResources = new CopyOnWriteArraySet<>(); + private static boolean loadedAll = false; + + // Get a list of all resource classes, sorted by loadPriority + public static List> getResourceDefClasses() { + Reflections reflections = new Reflections(ResourceLoader.class.getPackage().getName()); + Set classes = reflections.getSubTypesOf(GameResource.class); + + List> classList = new ArrayList<>(classes.size()); + classes.forEach( + o -> { + Class c = (Class) o; + if (c.getAnnotation(ResourceType.class) != null) { + classList.add(c); + } + }); + + classList.sort( + (a, b) -> + b.getAnnotation(ResourceType.class).loadPriority().value() + - a.getAnnotation(ResourceType.class).loadPriority().value()); + + return classList; + } + + // Get a list containing sets of all resource classes, sorted by loadPriority + protected static List>> getResourceDefClassesPrioritySets() { + val reflections = new Reflections(ResourceLoader.class.getPackage().getName()); + val classes = reflections.getSubTypesOf(GameResource.class); + val priorities = ResourceType.LoadPriority.getInOrder(); + Grasscutter.getLogger().debug("Priorities are " + priorities); + val map = new LinkedHashMap>>(priorities.size()); + priorities.forEach(p -> map.put(p, new HashSet<>())); + + classes.forEach( + c -> { + // val c = (Class) o; + val annotation = c.getAnnotation(ResourceType.class); + if (annotation != null) { + map.get(annotation.loadPriority()).add(c); + } + }); + return List.copyOf(map.values()); + } + + public static void loadAll() { + if (loadedAll) return; + Grasscutter.getLogger().info(translate("messages.status.resources.loading")); + + loadConfigData(); + // Load ability lists + loadAbilityEmbryos(); + loadOpenConfig(); + loadAbilityModifiers(); + // Load resources + loadResources(true); + // Process into depots + GameDepot.load(); + // Load spawn data and quests + loadSpawnData(); + loadQuests(); + loadScriptSceneData(); + // Load scene points - must be done AFTER resources are loaded + loadScenePoints(); + // Load default home layout + loadHomeworldDefaultSaveData(); + loadNpcBornData(); + loadBlossomResources(); + cacheTalentLevelSets(); + + Grasscutter.getLogger().info(translate("messages.status.resources.finish")); + loadedAll = true; + } + + public static void loadResources() { + loadResources(false); + } + + public static void loadResources(boolean doReload) { + long startTime = System.nanoTime(); + val errors = + new ConcurrentLinkedQueue< + Pair>(); // Logger in a parallel stream will deadlock + + getResourceDefClassesPrioritySets() + .forEach( + classes -> { + classes.stream() + .parallel() + .unordered() + .forEach( + c -> { + val type = c.getAnnotation(ResourceType.class); + if (type == null) return; + + val map = GameData.getMapByResourceDef(c); + if (map == null) return; + + try { + loadFromResource(c, type, map, doReload); + } catch (Exception e) { + errors.add(Pair.of(Arrays.toString(type.name()), e)); + } + }); + }); + errors.forEach( + pair -> + Grasscutter.getLogger() + .error("Error loading resource file: " + pair.left(), pair.right())); + long endTime = System.nanoTime(); + long ns = (endTime - startTime); // divide by 1000000 to get milliseconds. + Grasscutter.getLogger().debug("Loading resources took " + ns + "ns == " + ns / 1000000 + "ms"); + } + + @SuppressWarnings("rawtypes") + protected static void loadFromResource( + Class c, ResourceType type, Int2ObjectMap map, boolean doReload) throws Exception { + val simpleName = c.getSimpleName(); + if (doReload || !loadedResources.contains(simpleName)) { + for (String name : type.name()) { + loadFromResource(c, FileUtils.getExcelPath(name), map); + } + loadedResources.add(simpleName); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected static void loadFromResource(Class c, Path filename, Int2ObjectMap map) + throws Exception { + val results = + switch (FileUtils.getFileExtension(filename)) { + case "json" -> JsonUtils.loadToList(filename, c); + case "tsj" -> TsvUtils.loadTsjToListSetField(filename, c); + case "tsv" -> TsvUtils.loadTsvToListSetField(filename, c); + default -> null; + }; + if (results == null) return; + results.forEach( + o -> { + GameResource res = (GameResource) o; + res.onLoad(); + map.put(res.getId(), res); + }); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected static void loadFromResource(Class c, String fileName, Int2ObjectMap map) + throws Exception { + JsonUtils.loadToList(getResourcePath("ExcelBinOutput/" + fileName), c) + .forEach( + o -> { + GameResource res = (GameResource) o; + res.onLoad(); + map.put(res.getId(), res); + }); + } + + private static void loadScenePoints() { + val pattern = Pattern.compile("scene([0-9]+)_point\\.json"); + try { + Files.newDirectoryStream(getResourcePath("BinOutput/Scene/Point"), "scene*_point.json") + .forEach( + path -> { + val matcher = pattern.matcher(path.getFileName().toString()); + if (!matcher.find()) return; + int sceneId = Integer.parseInt(matcher.group(1)); + + ScenePointConfig config; + try { + config = JsonUtils.loadToClass(path, ScenePointConfig.class); + } catch (Exception e) { + e.printStackTrace(); + return; + } + + if (config.points == null) return; + + val scenePoints = new IntArrayList(); + config.points.forEach( + (pointId, pointData) -> { + val scenePoint = new ScenePointEntry(sceneId, pointData); + scenePoints.add(pointId); + pointData.setId(pointId); + + GameData.getScenePointIdList().add(pointId); + GameData.getScenePointEntryMap().put((sceneId << 16) + pointId, scenePoint); + + pointData.updateDailyDungeon(); + }); + GameData.getScenePointsPerScene().put(sceneId, scenePoints); + }); + } catch (IOException ignored) { + Grasscutter.getLogger() + .error("Scene point files cannot be found, you cannot use teleport waypoints!"); + } + } + + private static void cacheTalentLevelSets() { + // All known levels, keyed by proudSkillGroupId + GameData.getProudSkillDataMap() + .forEach( + (id, data) -> + GameData.getProudSkillGroupLevels() + .computeIfAbsent(data.getProudSkillGroupId(), i -> new IntArraySet()) + .add(data.getLevel())); + // All known levels, keyed by avatarSkillId + GameData.getAvatarSkillDataMap() + .forEach( + (id, data) -> + GameData.getAvatarSkillLevels() + .put( + (int) id, + GameData.getProudSkillGroupLevels().get(data.getProudSkillGroupId()))); + // Maximum known levels, keyed by proudSkillGroupId + GameData.getProudSkillGroupLevels() + .forEach( + (id, set) -> + GameData.getProudSkillGroupMaxLevels() + .put((int) id, set.intStream().max().orElse(-1))); + } + + private static void loadAbilityEmbryos() { + List embryoList = null; + + // Read from cached file if exists + try { + embryoList = + JsonUtils.loadToList(getDataPath("AbilityEmbryos.json"), AbilityEmbryoEntry.class); + } catch (Exception ignored) { + } + + if (embryoList == null) { + // Load from BinOutput + var pattern = Pattern.compile("ConfigAvatar_(.+?)\\.json"); + + var entries = new ArrayList(); + try (var stream = + Files.newDirectoryStream(getResourcePath("BinOutput/Avatar/"), "ConfigAvatar_*.json")) { + + stream.forEach( + path -> { + var matcher = pattern.matcher(path.getFileName().toString()); + if (!matcher.find()) return; + + var avatarName = matcher.group(1); + AvatarConfig config; + try { + config = JsonUtils.loadToClass(path, AvatarConfig.class); + } catch (Exception e) { + Grasscutter.getLogger().error("Error loading player ability embryos:", e); + return; + } + + if (config.abilities == null) return; + + entries.add( + new AbilityEmbryoEntry( + avatarName, + config.abilities.stream() + .map(Object::toString) + .toArray(size -> new String[config.abilities.size()]))); + }); + } catch (IOException e) { + Grasscutter.getLogger().error("Error loading ability embryos: no files found"); + return; + } + + embryoList = entries; + + try { + GameDepot.setPlayerAbilities( + JsonUtils.loadToMap( + getResourcePath( + "BinOutput/AbilityGroup/AbilityGroup_Other_PlayerElementAbility.json"), + String.class, + AvatarConfig.class)); + } catch (IOException e) { + Grasscutter.getLogger().error("Error loading player abilities:", e); + } + } + + if (embryoList == null || embryoList.isEmpty()) { + Grasscutter.getLogger().error("No embryos loaded!"); + return; + } + + for (AbilityEmbryoEntry entry : embryoList) { + GameData.getAbilityEmbryoInfo().put(entry.getName(), entry); + } + } + + private static void loadAbilityModifiers() { + // Load from BinOutput + try (Stream paths = Files.walk(getResourcePath("BinOutput/Ability/Temp/"))) { + paths + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".json")) + .forEach(ResourceLoader::loadAbilityModifiers); + } catch (IOException e) { + Grasscutter.getLogger().error("Error loading ability modifiers: ", e); + } + // System.out.println("Loaded modifiers, found types:"); + // modifierActionTypes.stream().sorted().forEach(s -> System.out.printf("%s, ", s)); + // System.out.println("[End]"); + } + + private static void loadAbilityModifiers(Path path) { + try { + JsonUtils.loadToList(path, AbilityConfigData.class) + .forEach(data -> loadAbilityData(data.Default)); + } catch (IOException e) { + Grasscutter.getLogger() + .error("Error loading ability modifiers from path " + path.toString() + ": ", e); + } + } + + private static void loadAbilityData(AbilityData data) { + GameData.getAbilityDataMap().put(data.abilityName, data); + + val modifiers = data.modifiers; + if (modifiers == null || modifiers.size() == 0) return; + + String name = data.abilityName; + AbilityModifierEntry modifierEntry = new AbilityModifierEntry(name); + modifiers.forEach( + (key, modifier) -> { + Stream.ofNullable(modifier.onAdded) + .flatMap(Stream::of) + // .map(action -> {modifierActionTypes.add(action.$type); return action;}) + .filter(action -> action.type == AbilityModifierAction.Type.HealHP) + .forEach(action -> modifierEntry.getOnAdded().add(action)); + Stream.ofNullable(modifier.onThinkInterval) + .flatMap(Stream::of) + // .map(action -> {modifierActionTypes.add(action.$type); return action;}) + .filter(action -> action.type == AbilityModifierAction.Type.HealHP) + .forEach(action -> modifierEntry.getOnThinkInterval().add(action)); + Stream.ofNullable(modifier.onRemoved) + .flatMap(Stream::of) + // .map(action -> {modifierActionTypes.add(action.$type); return action;}) + .filter(action -> action.type == AbilityModifierAction.Type.HealHP) + .forEach(action -> modifierEntry.getOnRemoved().add(action)); + }); + + GameData.getAbilityModifiers().put(name, modifierEntry); + } + + private static void loadSpawnData() { + String[] spawnDataNames = {"Spawns.json", "GadgetSpawns.json"}; + ArrayList spawnEntryMap = new ArrayList<>(); + + for (String name : spawnDataNames) { + // Load spawn entries from file + try (InputStreamReader reader = DataLoader.loadReader(name)) { + // Add spawns to group if it already exists in our spawn group map + spawnEntryMap.addAll(JsonUtils.loadToList(reader, SpawnGroupEntry.class)); + } catch (Exception ignored) { + } + } + + if (spawnEntryMap.isEmpty()) { + Grasscutter.getLogger().error("No spawn data loaded!"); + return; + } + + HashMap> areaSort = new HashMap<>(); + // key = sceneId,x,z , value = ArrayList + for (SpawnGroupEntry entry : spawnEntryMap) { + entry + .getSpawns() + .forEach( + s -> { + s.setGroup(entry); + GridBlockId point = s.getBlockId(); + if (!areaSort.containsKey(point)) { + areaSort.put(point, new ArrayList<>()); + } + areaSort.get(point).add(s); + }); + } + GameDepot.addSpawnListById(areaSort); + } + + private static void loadOpenConfig() { + // Read from cached file if exists + List list = null; + + try { + list = JsonUtils.loadToList(getDataPath("OpenConfig.json"), OpenConfigEntry.class); + } catch (Exception ignored) { + } + + if (list == null) { + Map map = new TreeMap<>(); + String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"}; + + for (String folderName : folderNames) { + try { + Files.newDirectoryStream(getResourcePath(folderName), "*.json") + .forEach( + path -> { + try { + JsonUtils.loadToMap(path, String.class, OpenConfigData[].class) + .forEach((name, data) -> map.put(name, new OpenConfigEntry(name, data))); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } catch (IOException e) { + Grasscutter.getLogger() + .error("Error loading open config: no files found in " + folderName); + return; + } + } + + list = new ArrayList<>(map.values()); + } + + if (list == null || list.isEmpty()) { + Grasscutter.getLogger().error("No openconfig entries loaded!"); + return; + } + + for (OpenConfigEntry entry : list) { + GameData.getOpenConfigEntries().put(entry.getName(), entry); + } + } + + private static void loadQuests() { + try { + Files.list(getResourcePath("BinOutput/Quest/")) + .forEach( + path -> { + try { + val mainQuest = JsonUtils.loadToClass(path, MainQuestData.class); + GameData.getMainQuestDataMap().put(mainQuest.getId(), mainQuest); + } catch (IOException e) { + + } + }); + } catch (IOException e) { + Grasscutter.getLogger().error("Quest data missing"); + return; + } + + try { + val questEncryptionMap = GameData.getMainQuestEncryptionMap(); + String path = "QuestEncryptionKeys.json"; + try { + JsonUtils.loadToList(getResourcePath(path), QuestEncryptionKey.class) + .forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key)); + } catch (IOException | NullPointerException ignored) { + } + try { + DataLoader.loadList(path, QuestEncryptionKey.class) + .forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key)); + } catch (IOException | NullPointerException ignored) { + } + Grasscutter.getLogger().debug("Loaded {} quest keys.", questEncryptionMap.size()); + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to load quest keys.", e); + } + + Grasscutter.getLogger() + .debug("Loaded " + GameData.getMainQuestDataMap().size() + " MainQuestDatas."); + } + + public static void loadScriptSceneData() { + try { + Files.list(getResourcePath("ScriptSceneData/")) + .forEach( + path -> { + try { + GameData.getScriptSceneDataMap() + .put( + path.getFileName().toString(), + JsonUtils.loadToClass(path, ScriptSceneData.class)); + } catch (IOException e) { + e.printStackTrace(); + } + }); + Grasscutter.getLogger() + .debug("Loaded " + GameData.getScriptSceneDataMap().size() + " ScriptSceneDatas."); + } catch (IOException e) { + Grasscutter.getLogger().debug("ScriptSceneData folder missing or empty."); + } + } + + private static void loadHomeworldDefaultSaveData() { + val pattern = Pattern.compile("scene([0-9]+)_home_config\\.json"); + try { + Files.newDirectoryStream( + getResourcePath("BinOutput/HomeworldDefaultSave"), "scene*_home_config.json") + .forEach( + path -> { + val matcher = pattern.matcher(path.getFileName().toString()); + if (!matcher.find()) return; + + try { + val sceneId = Integer.parseInt(matcher.group(1)); + val data = JsonUtils.loadToClass(path, HomeworldDefaultSaveData.class); + GameData.getHomeworldDefaultSaveData().put(sceneId, data); + } catch (Exception ignored) { + } + }); + Grasscutter.getLogger() + .debug( + "Loaded " + + GameData.getHomeworldDefaultSaveData().size() + + " HomeworldDefaultSaveDatas."); + } catch (IOException e) { + Grasscutter.getLogger().error("Failed to load HomeworldDefaultSave folder."); + } + } + + private static void loadNpcBornData() { + try { + Files.newDirectoryStream(getResourcePath("BinOutput/Scene/SceneNpcBorn/"), "*.json") + .forEach( + path -> { + try { + val data = JsonUtils.loadToClass(path, SceneNpcBornData.class); + if (data.getBornPosList() == null || data.getBornPosList().size() == 0) { + return; + } + + data.setIndex( + SceneIndexManager.buildIndex( + 3, data.getBornPosList(), item -> item.getPos().toPoint())); + GameData.getSceneNpcBornData().put(data.getSceneId(), data); + } catch (IOException ignored) { + } + }); + Grasscutter.getLogger() + .debug("Loaded " + GameData.getSceneNpcBornData().size() + " SceneNpcBornDatas."); + } catch (IOException e) { + Grasscutter.getLogger().error("Failed to load SceneNpcBorn folder."); + } + } + + private static void loadConfigData(){ + loadConfigData(GameData.getAvatarConfigData(), "BinOutput/Avatar/", ConfigEntityAvatar.class); + loadConfigData(GameData.getMonsterConfigData(), "BinOutput/Monster/", ConfigEntityMonster.class); + loadConfigDataMap(GameData.getGadgetConfigData(), "BinOutput/Gadget/", ConfigEntityGadget.class); + } + + private static void loadConfigData(Map targetMap, String folderPath, Class configClass) { + val className = configClass.getName(); + try(val stream = Files.newDirectoryStream(getResourcePath(folderPath), "*.json")) { + stream.forEach(path -> { + try { + val name = path.getFileName().toString().replace(".json", ""); + targetMap.put(name, JsonUtils.loadToClass(path, configClass)); + } catch (Exception e) { + Grasscutter.getLogger().error("failed to load {} entries for {}", className, path.toString(), e); + } + }); + + Grasscutter.getLogger().debug("Loaded {} {} entries.", GameData.getMonsterConfigData().size(), className); + } catch (IOException e) { + Grasscutter.getLogger().error("Failed to load {} folder.", className); + } + } + + private static void loadConfigDataMap(Map targetMap, String folderPath, Class configClass) { + val className = configClass.getName(); + try(val stream = Files.newDirectoryStream(getResourcePath(folderPath), "*.json")) { + stream.forEach(path -> { + try { + targetMap.putAll(JsonUtils.loadToMap(path, String.class, configClass)); + } catch (Exception e) { + Grasscutter.getLogger().error("failed to load {} entries for {}", className, path.toString(), e); + } + }); + + Grasscutter.getLogger().debug("Loaded {} {} entries.", GameData.getMonsterConfigData().size(), className); + } catch (IOException e) { + Grasscutter.getLogger().error("Failed to load {} folder.", className); + } + } + + private static void loadBlossomResources() { + try { + GameDepot.setBlossomConfig(DataLoader.loadClass("BlossomConfig.json", BlossomConfig.class)); + Grasscutter.getLogger().debug("Loaded BlossomConfig."); + } catch (IOException e) { + Grasscutter.getLogger().warn("Failed to load BlossomConfig."); + } + } + + // private static HashSet modifierActionTypes = new HashSet<>(); + public static class AbilityConfigData { + public AbilityData Default; + } + + public static class AvatarConfig { + @SerializedName( + value = "abilities", + alternate = {"targetAbilities"}) + public ArrayList abilities; + } + + // BinOutput configs + + public static class AvatarConfigAbility { + public String abilityName; + + public String toString() { + return abilityName; + } + } + + private static class OpenConfig { + public OpenConfigData[] data; + } + + public static class OpenConfigData { + public String $type; + public String abilityName; + + @SerializedName( + value = "talentIndex", + alternate = {"OJOFFKLNAHN"}) + public int talentIndex; + + @SerializedName( + value = "skillID", + alternate = {"overtime"}) + public int skillID; + + @SerializedName( + value = "pointDelta", + alternate = {"IGEBKIHPOIF"}) + public int pointDelta; + } + + public class ScenePointConfig { // Sadly this doesn't work as a local class in loadScenePoints() + public Map points; + } +} diff --git a/src/main/java/emu/grasscutter/data/common/PointData.java b/src/main/java/emu/grasscutter/data/common/PointData.java index 47ba64b45..220ef9d6b 100644 --- a/src/main/java/emu/grasscutter/data/common/PointData.java +++ b/src/main/java/emu/grasscutter/data/common/PointData.java @@ -1,61 +1,58 @@ -package emu.grasscutter.data.common; - -import com.google.gson.annotations.SerializedName; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.dungeon.DailyDungeonData; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntList; -import lombok.Getter; -import lombok.Setter; - -public class PointData { - @Getter @Setter private int id; - private String $type; - @Getter private Position tranPos; - - @SerializedName( - value = "dungeonIds", - alternate = {"JHHFPGJNMIN"}) - @Getter - private int[] dungeonIds; - - @SerializedName( - value = "dungeonRandomList", - alternate = {"OIBKFJNBLHO"}) - @Getter - private int[] dungeonRandomList; - - @SerializedName( - value = "tranSceneId", - alternate = {"JHBICGBAPIH"}) - @Getter - @Setter - private int tranSceneId; - - public String getType() { - return $type; - } - - public void updateDailyDungeon() { - if (this.dungeonRandomList == null || this.dungeonRandomList.length == 0) { - return; - } - - IntList newDungeons = new IntArrayList(); - int day = Grasscutter.getCurrentDayOfWeek(); - - for (int randomId : this.dungeonRandomList) { - DailyDungeonData data = GameData.getDailyDungeonDataMap().get(randomId); - - if (data != null) { - for (int d : data.getDungeonsByDay(day)) { - newDungeons.add(d); - } - } - } - - this.dungeonIds = newDungeons.toIntArray(); - } -} +package emu.grasscutter.data.common; + +import com.google.gson.annotations.SerializedName; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.dungeon.DailyDungeonData; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import lombok.Getter; +import lombok.Setter; + +public final class PointData { + @Getter @Setter private int id; + private String $type; + @Getter private Position tranPos; + @Getter private Position pos; + @Getter private Position rot; + @Getter private Position size; + + @SerializedName(value="dungeonIds", alternate={"JHHFPGJNMIN"}) + @Getter private int[] dungeonIds; + + @SerializedName(value="dungeonRandomList", alternate={"OIBKFJNBLHO"}) + @Getter private int[] dungeonRandomList; + + @SerializedName(value="groupIDs", alternate={"HFOBOOHKBGF"}) + @Getter private int[] groupIDs; + + @SerializedName(value="tranSceneId", alternate={"JHBICGBAPIH"}) + @Getter @Setter private int tranSceneId; + + public String getType() { + return $type; + } + + public void updateDailyDungeon() { + if (this.dungeonRandomList == null || this.dungeonRandomList.length == 0) { + return; + } + + IntList newDungeons = new IntArrayList(); + int day = Grasscutter.getCurrentDayOfWeek(); + + for (int randomId : this.dungeonRandomList) { + DailyDungeonData data = GameData.getDailyDungeonDataMap().get(randomId); + + if (data != null) { + for (int d : data.getDungeonsByDay(day)) { + newDungeons.add(d); + } + } + } + + this.dungeonIds = newDungeons.toIntArray(); + } +} diff --git a/src/main/java/emu/grasscutter/data/excels/GadgetData.java b/src/main/java/emu/grasscutter/data/excels/GadgetData.java index 6d2d4ac8b..33d3c2ee6 100644 --- a/src/main/java/emu/grasscutter/data/excels/GadgetData.java +++ b/src/main/java/emu/grasscutter/data/excels/GadgetData.java @@ -7,7 +7,7 @@ import lombok.Getter; @ResourceType(name = "GadgetExcelConfigData.json") @Getter -public class GadgetData extends GameResource { +public final class GadgetData extends GameResource { @Getter(onMethod_ = @Override) private int id; @@ -17,5 +17,6 @@ public class GadgetData extends GameResource { private String[] tags; private String itemJsonName; private long nameTextMapHash; - private int campID; + private int campId; + private String visionLevel; } diff --git a/src/main/java/emu/grasscutter/data/excels/avatar/AvatarData.java b/src/main/java/emu/grasscutter/data/excels/avatar/AvatarData.java index a4253ff4d..be4bc6c4b 100644 --- a/src/main/java/emu/grasscutter/data/excels/avatar/AvatarData.java +++ b/src/main/java/emu/grasscutter/data/excels/avatar/AvatarData.java @@ -41,7 +41,7 @@ public class AvatarData extends GameResource { @Getter private int staminaRecoverSpeed; @Getter - private List candSkillDepotIds; + private List candSkillDepotIds; @Getter private String avatarIdentityType; @Getter diff --git a/src/main/java/emu/grasscutter/data/excels/monster/MonsterData.java b/src/main/java/emu/grasscutter/data/excels/monster/MonsterData.java index 03dd10682..9945bde0e 100644 --- a/src/main/java/emu/grasscutter/data/excels/monster/MonsterData.java +++ b/src/main/java/emu/grasscutter/data/excels/monster/MonsterData.java @@ -1,117 +1,120 @@ -package emu.grasscutter.data.excels.monster; - -import com.google.gson.annotations.SerializedName; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.GameResource; -import emu.grasscutter.data.ResourceType; -import emu.grasscutter.data.ResourceType.LoadPriority; -import emu.grasscutter.data.common.PropGrowCurve; -import emu.grasscutter.data.excels.GadgetData; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.MonsterType; -import java.util.List; -import java.util.Set; -import lombok.Getter; - -@ResourceType(name = "MonsterExcelConfigData.json", loadPriority = LoadPriority.LOW) -@Getter -public class MonsterData extends GameResource { - public static Set definedFightProperties = - Set.of( - FightProperty.FIGHT_PROP_BASE_HP, - FightProperty.FIGHT_PROP_BASE_ATTACK, - FightProperty.FIGHT_PROP_BASE_DEFENSE, - FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, - FightProperty.FIGHT_PROP_FIRE_SUB_HURT, - FightProperty.FIGHT_PROP_ELEC_SUB_HURT, - FightProperty.FIGHT_PROP_WATER_SUB_HURT, - FightProperty.FIGHT_PROP_GRASS_SUB_HURT, - FightProperty.FIGHT_PROP_WIND_SUB_HURT, - FightProperty.FIGHT_PROP_ROCK_SUB_HURT, - FightProperty.FIGHT_PROP_ICE_SUB_HURT); - - @Getter(onMethod_ = @Override) - private int id; - - private String monsterName; - private MonsterType type; - private String serverScript; - private List affix; - private String ai; - private int[] equips; - private List hpDrops; - private int killDropId; - private String excludeWeathers; - private int featureTagGroupID; - private int mpPropID; - private String skin; - private int describeId; - private int combatBGMLevel; - private int entityBudgetLevel; - - @SerializedName("hpBase") - private float baseHp; - - @SerializedName("attackBase") - private float baseAttack; - - @SerializedName("defenseBase") - private float baseDefense; - - private float fireSubHurt; - private float elecSubHurt; - private float grassSubHurt; - private float waterSubHurt; - private float windSubHurt; - private float rockSubHurt; - private float iceSubHurt; - private float physicalSubHurt; - private List propGrowCurves; - private long nameTextMapHash; - private int campID; - - // Transient - private int weaponId; - private MonsterDescribeData describeData; - - public float getFightProperty(FightProperty prop) { - return switch (prop) { - case FIGHT_PROP_BASE_HP -> this.baseHp; - case FIGHT_PROP_BASE_ATTACK -> this.baseAttack; - case FIGHT_PROP_BASE_DEFENSE -> this.baseDefense; - case FIGHT_PROP_PHYSICAL_SUB_HURT -> this.physicalSubHurt; - case FIGHT_PROP_FIRE_SUB_HURT -> this.fireSubHurt; - case FIGHT_PROP_ELEC_SUB_HURT -> this.elecSubHurt; - case FIGHT_PROP_WATER_SUB_HURT -> this.waterSubHurt; - case FIGHT_PROP_GRASS_SUB_HURT -> this.grassSubHurt; - case FIGHT_PROP_WIND_SUB_HURT -> this.windSubHurt; - case FIGHT_PROP_ROCK_SUB_HURT -> this.rockSubHurt; - case FIGHT_PROP_ICE_SUB_HURT -> this.iceSubHurt; - default -> 0f; - }; - } - - @Override - public void onLoad() { - this.describeData = GameData.getMonsterDescribeDataMap().get(this.getDescribeId()); - - for (int id : this.equips) { - if (id == 0) { - continue; - } - GadgetData gadget = GameData.getGadgetDataMap().get(id); - if (gadget == null) { - continue; - } - if (gadget.getItemJsonName().equals("Default_MonsterWeapon")) { - this.weaponId = id; - } - } - } - - @Getter - public class HpDrops { - private int DropId; - private int HpPercent; - } -} +package emu.grasscutter.data.excels.monster; + +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gson.annotations.SerializedName; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; +import emu.grasscutter.data.ResourceType.LoadPriority; +import emu.grasscutter.data.common.PropGrowCurve; +import emu.grasscutter.data.excels.GadgetData; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.MonsterType; +import lombok.Getter; + +@ResourceType(name = "MonsterExcelConfigData.json", loadPriority = LoadPriority.LOW) +@Getter +public class MonsterData extends GameResource { + static public Set definedFightProperties = Set.of(FightProperty.FIGHT_PROP_BASE_HP, FightProperty.FIGHT_PROP_BASE_ATTACK, FightProperty.FIGHT_PROP_BASE_DEFENSE, FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, FightProperty.FIGHT_PROP_FIRE_SUB_HURT, FightProperty.FIGHT_PROP_ELEC_SUB_HURT, FightProperty.FIGHT_PROP_WATER_SUB_HURT, FightProperty.FIGHT_PROP_GRASS_SUB_HURT, FightProperty.FIGHT_PROP_WIND_SUB_HURT, FightProperty.FIGHT_PROP_ROCK_SUB_HURT, FightProperty.FIGHT_PROP_ICE_SUB_HURT); + + @Getter(onMethod_ = @Override) + private int id; + + private String monsterName; + private MonsterType type; + private String serverScript; + private List affix; + private String ai; + private int[] equips; + private List hpDrops; + private int killDropId; + private String excludeWeathers; + private int featureTagGroupID; + private int mpPropID; + private String skin; + private int describeId; + private int combatBGMLevel; + private int entityBudgetLevel; + + @SerializedName("hpBase") + private float baseHp; + @SerializedName("attackBase") + private float baseAttack; + @SerializedName("defenseBase") + private float baseDefense; + + private float fireSubHurt; + private float elecSubHurt; + private float grassSubHurt; + private float waterSubHurt; + private float windSubHurt; + private float rockSubHurt; + private float iceSubHurt; + private float physicalSubHurt; + private List propGrowCurves; + private long nameTextMapHash; + private int campID; + + // Transient + private int weaponId; + private MonsterDescribeData describeData; + + private int specialNameId; // will only be set if describe data is available + + @Override + public void onLoad() { + for (int id : this.equips) { + if (id == 0) { + continue; + } + + GadgetData gadget = GameData.getGadgetDataMap().get(id); + if (gadget == null) { + continue; + } + + if (gadget.getItemJsonName().equals("Default_MonsterWeapon")) { + this.weaponId = id; + } + } + + this.describeData = GameData.getMonsterDescribeDataMap().get(this.getDescribeId()); + + if (this.describeData == null){ + return; + } + for(Entry entry: GameData.getMonsterSpecialNameDataMap().entrySet()) { + if (entry.getValue().getSpecialNameLabId() == this.getDescribeData().getSpecialNameLabId()){ + this.specialNameId = entry.getKey(); + break; + } + } + } + + public float getFightProperty(FightProperty prop) { + return switch (prop) { + case FIGHT_PROP_BASE_HP -> this.baseHp; + case FIGHT_PROP_BASE_ATTACK -> this.baseAttack; + case FIGHT_PROP_BASE_DEFENSE -> this.baseDefense; + case FIGHT_PROP_PHYSICAL_SUB_HURT -> this.physicalSubHurt; + case FIGHT_PROP_FIRE_SUB_HURT -> this.fireSubHurt; + case FIGHT_PROP_ELEC_SUB_HURT -> this.elecSubHurt; + case FIGHT_PROP_WATER_SUB_HURT -> this.waterSubHurt; + case FIGHT_PROP_GRASS_SUB_HURT -> this.grassSubHurt; + case FIGHT_PROP_WIND_SUB_HURT -> this.windSubHurt; + case FIGHT_PROP_ROCK_SUB_HURT -> this.rockSubHurt; + case FIGHT_PROP_ICE_SUB_HURT -> this.iceSubHurt; + default -> 0f; + }; + } + + @Getter + public class HpDrops { + private int DropId; + private int HpPercent; + } +} diff --git a/src/main/java/emu/grasscutter/data/excels/monster/MonsterDescribeData.java b/src/main/java/emu/grasscutter/data/excels/monster/MonsterDescribeData.java index f20d001ae..9111d1de5 100644 --- a/src/main/java/emu/grasscutter/data/excels/monster/MonsterDescribeData.java +++ b/src/main/java/emu/grasscutter/data/excels/monster/MonsterDescribeData.java @@ -1,17 +1,21 @@ -package emu.grasscutter.data.excels.monster; - -import emu.grasscutter.data.GameResource; -import emu.grasscutter.data.ResourceType; -import emu.grasscutter.data.ResourceType.LoadPriority; -import lombok.Getter; - -@ResourceType(name = "MonsterDescribeExcelConfigData.json", loadPriority = LoadPriority.HIGH) -@Getter -public class MonsterDescribeData extends GameResource { - @Getter(onMethod_ = @Override) - private int id; - - private long nameTextMapHash; - private int titleID; - private int specialNameLabID; -} +package emu.grasscutter.data.excels.monster; + +import com.google.gson.annotations.SerializedName; +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; +import emu.grasscutter.data.ResourceType.LoadPriority; +import lombok.Getter; + +@ResourceType(name = "MonsterDescribeExcelConfigData.json", loadPriority = LoadPriority.HIGH) +@Getter +public class MonsterDescribeData extends GameResource { + @Getter(onMethod_ = @Override) + private int id; + + private long nameTextMapHash; + @SerializedName(value = "titleId", alternate={"titleID"}) + private int titleId; + @SerializedName(value = "specialNameLabId", alternate={"specialNameLabID"}) + private int specialNameLabId; + private MonsterSpecialNameData specialNameData; +} diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index de3cb79e8..e3e965db5 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -1,452 +1,464 @@ -package emu.grasscutter.database; - -import static com.mongodb.client.model.Filters.eq; - -import com.mongodb.client.result.DeleteResult; -import dev.morphia.query.FindOptions; -import dev.morphia.query.Sort; -import dev.morphia.query.experimental.filters.Filters; -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.Account; -import emu.grasscutter.game.achievement.Achievements; -import emu.grasscutter.game.activity.PlayerActivityData; -import emu.grasscutter.game.activity.musicgame.MusicGameBeatmap; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.battlepass.BattlePassManager; -import emu.grasscutter.game.friends.Friendship; -import emu.grasscutter.game.gacha.GachaRecord; -import emu.grasscutter.game.home.GameHome; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.mail.Mail; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.quest.GameMainQuest; -import java.util.List; -import java.util.stream.Stream; - -public final class DatabaseHelper { - public static Account createAccount(String username) { - return createAccountWithUid(username, 0); - } - - public static Account createAccountWithUid(String username, int reservedUid) { - // Unique names only - if (DatabaseHelper.checkIfAccountExists(username)) { - return null; - } - - // Make sure there are no id collisions - if (reservedUid > 0) { - // Cannot make account with the same uid as the server console - if (reservedUid == GameConstants.SERVER_CONSOLE_UID) { - return null; - } - - if (DatabaseHelper.checkIfAccountExists(reservedUid)) { - return null; - } - - // Make sure no existing player already has this id. - if (DatabaseHelper.checkIfPlayerExists(reservedUid)) { - return null; - } - } - - // Account - Account account = new Account(); - account.setUsername(username); - account.setId(Integer.toString(DatabaseManager.getNextId(account))); - - if (reservedUid > 0) { - account.setReservedPlayerUid(reservedUid); - } - - DatabaseHelper.saveAccount(account); - return account; - } - - @Deprecated - public static Account createAccountWithPassword(String username, String password) { - // Unique names only - Account exists = DatabaseHelper.getAccountByName(username); - if (exists != null) { - return null; - } - - // Account - Account account = new Account(); - account.setId(Integer.toString(DatabaseManager.getNextId(account))); - account.setUsername(username); - account.setPassword(password); - DatabaseHelper.saveAccount(account); - return account; - } - - public static void saveAccount(Account account) { - DatabaseManager.getAccountDatastore().save(account); - } - - public static Account getAccountByName(String username) { - return DatabaseManager.getAccountDatastore() - .find(Account.class) - .filter(Filters.eq("username", username)) - .first(); - } - - public static Account getAccountByToken(String token) { - if (token == null) return null; - return DatabaseManager.getAccountDatastore() - .find(Account.class) - .filter(Filters.eq("token", token)) - .first(); - } - - public static Account getAccountBySessionKey(String sessionKey) { - if (sessionKey == null) return null; - return DatabaseManager.getAccountDatastore() - .find(Account.class) - .filter(Filters.eq("sessionKey", sessionKey)) - .first(); - } - - public static Account getAccountById(String uid) { - return DatabaseManager.getAccountDatastore() - .find(Account.class) - .filter(Filters.eq("_id", uid)) - .first(); - } - - public static Account getAccountByPlayerId(int playerId) { - return DatabaseManager.getAccountDatastore() - .find(Account.class) - .filter(Filters.eq("reservedPlayerId", playerId)) - .first(); - } - - public static boolean checkIfAccountExists(String name) { - return DatabaseManager.getAccountDatastore() - .find(Account.class) - .filter(Filters.eq("username", name)) - .count() - > 0; - } - - public static boolean checkIfAccountExists(int reservedUid) { - return DatabaseManager.getAccountDatastore() - .find(Account.class) - .filter(Filters.eq("reservedPlayerId", reservedUid)) - .count() - > 0; - } - - public static synchronized void deleteAccount(Account target) { - // To delete an account, we need to also delete all the other documents in the database that - // reference the account. - // This should optimally be wrapped inside a transaction, to make sure an error thrown mid-way - // does not leave the - // database in an inconsistent state, but unfortunately Mongo only supports that when we have a - // replica set ... - - Player player = Grasscutter.getGameServer().getPlayerByAccountId(target.getId()); - - // Close session first - if (player != null) { - player.getSession().close(); - } else { - player = getPlayerByAccount(target); - if (player == null) return; - } - int uid = player.getUid(); - // Delete data from collections - DatabaseManager.getGameDatabase().getCollection("achievements").deleteMany(eq("uid", uid)); - DatabaseManager.getGameDatabase().getCollection("activities").deleteMany(eq("uid", uid)); - DatabaseManager.getGameDatabase().getCollection("homes").deleteMany(eq("ownerUid", uid)); - DatabaseManager.getGameDatabase().getCollection("mail").deleteMany(eq("ownerUid", uid)); - DatabaseManager.getGameDatabase().getCollection("avatars").deleteMany(eq("ownerId", uid)); - DatabaseManager.getGameDatabase().getCollection("gachas").deleteMany(eq("ownerId", uid)); - DatabaseManager.getGameDatabase().getCollection("items").deleteMany(eq("ownerId", uid)); - DatabaseManager.getGameDatabase().getCollection("quests").deleteMany(eq("ownerUid", uid)); - DatabaseManager.getGameDatabase().getCollection("battlepass").deleteMany(eq("ownerUid", uid)); - - // Delete friendships. - // Here, we need to make sure to not only delete the deleted account's friendships, - // but also all friendship entries for that account's friends. - DatabaseManager.getGameDatabase().getCollection("friendships").deleteMany(eq("ownerId", uid)); - DatabaseManager.getGameDatabase().getCollection("friendships").deleteMany(eq("friendId", uid)); - - // Delete the player last. - DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("id", uid)).delete(); - - // Finally, delete the account itself. - DatabaseManager.getAccountDatastore() - .find(Account.class) - .filter(Filters.eq("id", target.getId())) - .delete(); - } - - public static Stream getByGameClass(Class classType) { - return DatabaseManager.getGameDatastore().find(classType).stream(); - } - - @Deprecated(forRemoval = true) - public static List getAllPlayers() { - return DatabaseManager.getGameDatastore().find(Player.class).stream().toList(); - } - - public static Player getPlayerByUid(int id) { - return DatabaseManager.getGameDatastore() - .find(Player.class) - .filter(Filters.eq("_id", id)) - .first(); - } - - @Deprecated - public static Player getPlayerByAccount(Account account) { - return DatabaseManager.getGameDatastore() - .find(Player.class) - .filter(Filters.eq("accountId", account.getId())) - .first(); - } - - public static Player getPlayerByAccount(Account account, Class playerClass) { - return DatabaseManager.getGameDatastore() - .find(playerClass) - .filter(Filters.eq("accountId", account.getId())) - .first(); - } - - public static boolean checkIfPlayerExists(int uid) { - return DatabaseManager.getGameDatastore() - .find(Player.class) - .filter(Filters.eq("_id", uid)) - .count() - > 0; - } - - public static synchronized Player generatePlayerUid(Player character, int reservedId) { - // Check if reserved id - int id; - if (reservedId > 0 && !checkIfPlayerExists(reservedId)) { - id = reservedId; - character.setUid(id); - } else { - do { - id = DatabaseManager.getNextId(character); - } while (checkIfPlayerExists(id)); - character.setUid(id); - } - // Save to database - DatabaseManager.getGameDatastore().save(character); - return character; - } - - public static synchronized int getNextPlayerId(int reservedId) { - // Check if reserved id - int id; - if (reservedId > 0 && !checkIfPlayerExists(reservedId)) { - id = reservedId; - } else { - do { - id = DatabaseManager.getNextId(Player.class); - } while (checkIfPlayerExists(id)); - } - return id; - } - - public static void savePlayer(Player character) { - DatabaseManager.getGameDatastore().save(character); - } - - public static void saveAvatar(Avatar avatar) { - DatabaseManager.getGameDatastore().save(avatar); - } - - public static List getAvatars(Player player) { - return DatabaseManager.getGameDatastore() - .find(Avatar.class) - .filter(Filters.eq("ownerId", player.getUid())) - .stream() - .toList(); - } - - public static void saveItem(GameItem item) { - DatabaseManager.getGameDatastore().save(item); - } - - public static boolean deleteItem(GameItem item) { - DeleteResult result = DatabaseManager.getGameDatastore().delete(item); - return result.wasAcknowledged(); - } - - public static List getInventoryItems(Player player) { - return DatabaseManager.getGameDatastore() - .find(GameItem.class) - .filter(Filters.eq("ownerId", player.getUid())) - .stream() - .toList(); - } - - public static List getFriends(Player player) { - return DatabaseManager.getGameDatastore() - .find(Friendship.class) - .filter(Filters.eq("ownerId", player.getUid())) - .stream() - .toList(); - } - - public static List getReverseFriends(Player player) { - return DatabaseManager.getGameDatastore() - .find(Friendship.class) - .filter(Filters.eq("friendId", player.getUid())) - .stream() - .toList(); - } - - public static void saveFriendship(Friendship friendship) { - DatabaseManager.getGameDatastore().save(friendship); - } - - public static void deleteFriendship(Friendship friendship) { - DatabaseManager.getGameDatastore().delete(friendship); - } - - public static Friendship getReverseFriendship(Friendship friendship) { - return DatabaseManager.getGameDatastore() - .find(Friendship.class) - .filter( - Filters.and( - Filters.eq("ownerId", friendship.getFriendId()), - Filters.eq("friendId", friendship.getOwnerId()))) - .first(); - } - - public static List getGachaRecords(int ownerId, int page, int gachaType) { - return getGachaRecords(ownerId, page, gachaType, 10); - } - - public static List getGachaRecords( - int ownerId, int page, int gachaType, int pageSize) { - return DatabaseManager.getGameDatastore() - .find(GachaRecord.class) - .filter(Filters.eq("ownerId", ownerId), Filters.eq("gachaType", gachaType)) - .iterator( - new FindOptions() - .sort(Sort.descending("transactionDate")) - .skip(pageSize * page) - .limit(pageSize)) - .toList(); - } - - public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType) { - return getGachaRecordsMaxPage(ownerId, page, gachaType, 10); - } - - public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType, int pageSize) { - long count = - DatabaseManager.getGameDatastore() - .find(GachaRecord.class) - .filter(Filters.eq("ownerId", ownerId), Filters.eq("gachaType", gachaType)) - .count(); - return count / 10 + (count % 10 > 0 ? 1 : 0); - } - - public static void saveGachaRecord(GachaRecord gachaRecord) { - DatabaseManager.getGameDatastore().save(gachaRecord); - } - - public static List getAllMail(Player player) { - return DatabaseManager.getGameDatastore() - .find(Mail.class) - .filter(Filters.eq("ownerUid", player.getUid())) - .stream() - .toList(); - } - - public static void saveMail(Mail mail) { - DatabaseManager.getGameDatastore().save(mail); - } - - public static boolean deleteMail(Mail mail) { - DeleteResult result = DatabaseManager.getGameDatastore().delete(mail); - return result.wasAcknowledged(); - } - - public static List getAllQuests(Player player) { - return DatabaseManager.getGameDatastore() - .find(GameMainQuest.class) - .filter(Filters.eq("ownerUid", player.getUid())) - .stream() - .toList(); - } - - public static void saveQuest(GameMainQuest quest) { - DatabaseManager.getGameDatastore().save(quest); - } - - public static boolean deleteQuest(GameMainQuest quest) { - return DatabaseManager.getGameDatastore().delete(quest).wasAcknowledged(); - } - - public static GameHome getHomeByUid(int id) { - return DatabaseManager.getGameDatastore() - .find(GameHome.class) - .filter(Filters.eq("ownerUid", id)) - .first(); - } - - public static void saveHome(GameHome gameHome) { - DatabaseManager.getGameDatastore().save(gameHome); - } - - public static BattlePassManager loadBattlePass(Player player) { - BattlePassManager manager = - DatabaseManager.getGameDatastore() - .find(BattlePassManager.class) - .filter(Filters.eq("ownerUid", player.getUid())) - .first(); - if (manager == null) { - manager = new BattlePassManager(player); - manager.save(); - } else { - manager.setPlayer(player); - } - return manager; - } - - public static void saveBattlePass(BattlePassManager manager) { - DatabaseManager.getGameDatastore().save(manager); - } - - public static PlayerActivityData getPlayerActivityData(int uid, int activityId) { - return DatabaseManager.getGameDatastore() - .find(PlayerActivityData.class) - .filter(Filters.and(Filters.eq("uid", uid), Filters.eq("activityId", activityId))) - .first(); - } - - public static void savePlayerActivityData(PlayerActivityData playerActivityData) { - DatabaseManager.getGameDatastore().save(playerActivityData); - } - - public static MusicGameBeatmap getMusicGameBeatmap(long musicShareId) { - return DatabaseManager.getGameDatastore() - .find(MusicGameBeatmap.class) - .filter(Filters.eq("musicShareId", musicShareId)) - .first(); - } - - public static void saveMusicGameBeatmap(MusicGameBeatmap musicGameBeatmap) { - DatabaseManager.getGameDatastore().save(musicGameBeatmap); - } - - public static Achievements getAchievementData(int uid) { - return DatabaseManager.getGameDatastore() - .find(Achievements.class) - .filter(Filters.and(Filters.eq("uid", uid))) - .first(); - } - - public static void saveAchievementData(Achievements achievements) { - DatabaseManager.getGameDatastore().save(achievements); - } -} +package emu.grasscutter.database; + +import static com.mongodb.client.model.Filters.eq; + +import com.mongodb.client.result.DeleteResult; +import dev.morphia.query.FindOptions; +import dev.morphia.query.Sort; +import dev.morphia.query.experimental.filters.Filters; +import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.Account; +import emu.grasscutter.game.achievement.Achievements; +import emu.grasscutter.game.activity.PlayerActivityData; +import emu.grasscutter.game.activity.musicgame.MusicGameBeatmap; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.battlepass.BattlePassManager; +import emu.grasscutter.game.friends.Friendship; +import emu.grasscutter.game.gacha.GachaRecord; +import emu.grasscutter.game.home.GameHome; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.mail.Mail; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.game.world.SceneGroupInstance; + +import java.util.List; +import java.util.stream.Stream; + +public final class DatabaseHelper { + public static Account createAccount(String username) { + return createAccountWithUid(username, 0); + } + + public static Account createAccountWithUid(String username, int reservedUid) { + // Unique names only + if (DatabaseHelper.checkIfAccountExists(username)) { + return null; + } + + // Make sure there are no id collisions + if (reservedUid > 0) { + // Cannot make account with the same uid as the server console + if (reservedUid == GameConstants.SERVER_CONSOLE_UID) { + return null; + } + + if (DatabaseHelper.checkIfAccountExists(reservedUid)) { + return null; + } + + // Make sure no existing player already has this id. + if (DatabaseHelper.checkIfPlayerExists(reservedUid)) { + return null; + } + } + + // Account + Account account = new Account(); + account.setUsername(username); + account.setId(Integer.toString(DatabaseManager.getNextId(account))); + + if (reservedUid > 0) { + account.setReservedPlayerUid(reservedUid); + } + + DatabaseHelper.saveAccount(account); + return account; + } + + @Deprecated + public static Account createAccountWithPassword(String username, String password) { + // Unique names only + Account exists = DatabaseHelper.getAccountByName(username); + if (exists != null) { + return null; + } + + // Account + Account account = new Account(); + account.setId(Integer.toString(DatabaseManager.getNextId(account))); + account.setUsername(username); + account.setPassword(password); + DatabaseHelper.saveAccount(account); + return account; + } + + public static void saveAccount(Account account) { + DatabaseManager.getAccountDatastore().save(account); + } + + public static Account getAccountByName(String username) { + return DatabaseManager.getAccountDatastore() + .find(Account.class) + .filter(Filters.eq("username", username)) + .first(); + } + + public static Account getAccountByToken(String token) { + if (token == null) return null; + return DatabaseManager.getAccountDatastore() + .find(Account.class) + .filter(Filters.eq("token", token)) + .first(); + } + + public static Account getAccountBySessionKey(String sessionKey) { + if (sessionKey == null) return null; + return DatabaseManager.getAccountDatastore() + .find(Account.class) + .filter(Filters.eq("sessionKey", sessionKey)) + .first(); + } + + public static Account getAccountById(String uid) { + return DatabaseManager.getAccountDatastore() + .find(Account.class) + .filter(Filters.eq("_id", uid)) + .first(); + } + + public static Account getAccountByPlayerId(int playerId) { + return DatabaseManager.getAccountDatastore() + .find(Account.class) + .filter(Filters.eq("reservedPlayerId", playerId)) + .first(); + } + + public static boolean checkIfAccountExists(String name) { + return DatabaseManager.getAccountDatastore() + .find(Account.class) + .filter(Filters.eq("username", name)) + .count() + > 0; + } + + public static boolean checkIfAccountExists(int reservedUid) { + return DatabaseManager.getAccountDatastore() + .find(Account.class) + .filter(Filters.eq("reservedPlayerId", reservedUid)) + .count() + > 0; + } + + public static synchronized void deleteAccount(Account target) { + // To delete an account, we need to also delete all the other documents in the database that + // reference the account. + // This should optimally be wrapped inside a transaction, to make sure an error thrown mid-way + // does not leave the + // database in an inconsistent state, but unfortunately Mongo only supports that when we have a + // replica set ... + + Player player = Grasscutter.getGameServer().getPlayerByAccountId(target.getId()); + + // Close session first + if (player != null) { + player.getSession().close(); + } else { + player = getPlayerByAccount(target); + if (player == null) return; + } + int uid = player.getUid(); + // Delete data from collections + DatabaseManager.getGameDatabase().getCollection("achievements").deleteMany(eq("uid", uid)); + DatabaseManager.getGameDatabase().getCollection("activities").deleteMany(eq("uid", uid)); + DatabaseManager.getGameDatabase().getCollection("homes").deleteMany(eq("ownerUid", uid)); + DatabaseManager.getGameDatabase().getCollection("mail").deleteMany(eq("ownerUid", uid)); + DatabaseManager.getGameDatabase().getCollection("avatars").deleteMany(eq("ownerId", uid)); + DatabaseManager.getGameDatabase().getCollection("gachas").deleteMany(eq("ownerId", uid)); + DatabaseManager.getGameDatabase().getCollection("items").deleteMany(eq("ownerId", uid)); + DatabaseManager.getGameDatabase().getCollection("quests").deleteMany(eq("ownerUid", uid)); + DatabaseManager.getGameDatabase().getCollection("battlepass").deleteMany(eq("ownerUid", uid)); + + // Delete friendships. + // Here, we need to make sure to not only delete the deleted account's friendships, + // but also all friendship entries for that account's friends. + DatabaseManager.getGameDatabase().getCollection("friendships").deleteMany(eq("ownerId", uid)); + DatabaseManager.getGameDatabase().getCollection("friendships").deleteMany(eq("friendId", uid)); + + // Delete the player last. + DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("id", uid)).delete(); + + // Finally, delete the account itself. + DatabaseManager.getAccountDatastore() + .find(Account.class) + .filter(Filters.eq("id", target.getId())) + .delete(); + } + + public static Stream getByGameClass(Class classType) { + return DatabaseManager.getGameDatastore().find(classType).stream(); + } + + @Deprecated(forRemoval = true) + public static List getAllPlayers() { + return DatabaseManager.getGameDatastore().find(Player.class).stream().toList(); + } + + public static Player getPlayerByUid(int id) { + return DatabaseManager.getGameDatastore() + .find(Player.class) + .filter(Filters.eq("_id", id)) + .first(); + } + + @Deprecated + public static Player getPlayerByAccount(Account account) { + return DatabaseManager.getGameDatastore() + .find(Player.class) + .filter(Filters.eq("accountId", account.getId())) + .first(); + } + + public static Player getPlayerByAccount(Account account, Class playerClass) { + return DatabaseManager.getGameDatastore() + .find(playerClass) + .filter(Filters.eq("accountId", account.getId())) + .first(); + } + + public static boolean checkIfPlayerExists(int uid) { + return DatabaseManager.getGameDatastore() + .find(Player.class) + .filter(Filters.eq("_id", uid)) + .count() + > 0; + } + + public static synchronized Player generatePlayerUid(Player character, int reservedId) { + // Check if reserved id + int id; + if (reservedId > 0 && !checkIfPlayerExists(reservedId)) { + id = reservedId; + character.setUid(id); + } else { + do { + id = DatabaseManager.getNextId(character); + } while (checkIfPlayerExists(id)); + character.setUid(id); + } + // Save to database + DatabaseManager.getGameDatastore().save(character); + return character; + } + + public static synchronized int getNextPlayerId(int reservedId) { + // Check if reserved id + int id; + if (reservedId > 0 && !checkIfPlayerExists(reservedId)) { + id = reservedId; + } else { + do { + id = DatabaseManager.getNextId(Player.class); + } while (checkIfPlayerExists(id)); + } + return id; + } + + public static void savePlayer(Player character) { + DatabaseManager.getGameDatastore().save(character); + } + + public static void saveAvatar(Avatar avatar) { + DatabaseManager.getGameDatastore().save(avatar); + } + + public static List getAvatars(Player player) { + return DatabaseManager.getGameDatastore() + .find(Avatar.class) + .filter(Filters.eq("ownerId", player.getUid())) + .stream() + .toList(); + } + + public static void saveItem(GameItem item) { + DatabaseManager.getGameDatastore().save(item); + } + + public static boolean deleteItem(GameItem item) { + DeleteResult result = DatabaseManager.getGameDatastore().delete(item); + return result.wasAcknowledged(); + } + + public static List getInventoryItems(Player player) { + return DatabaseManager.getGameDatastore() + .find(GameItem.class) + .filter(Filters.eq("ownerId", player.getUid())) + .stream() + .toList(); + } + + public static List getFriends(Player player) { + return DatabaseManager.getGameDatastore() + .find(Friendship.class) + .filter(Filters.eq("ownerId", player.getUid())) + .stream() + .toList(); + } + + public static List getReverseFriends(Player player) { + return DatabaseManager.getGameDatastore() + .find(Friendship.class) + .filter(Filters.eq("friendId", player.getUid())) + .stream() + .toList(); + } + + public static void saveFriendship(Friendship friendship) { + DatabaseManager.getGameDatastore().save(friendship); + } + + public static void deleteFriendship(Friendship friendship) { + DatabaseManager.getGameDatastore().delete(friendship); + } + + public static Friendship getReverseFriendship(Friendship friendship) { + return DatabaseManager.getGameDatastore() + .find(Friendship.class) + .filter( + Filters.and( + Filters.eq("ownerId", friendship.getFriendId()), + Filters.eq("friendId", friendship.getOwnerId()))) + .first(); + } + + public static List getGachaRecords(int ownerId, int page, int gachaType) { + return getGachaRecords(ownerId, page, gachaType, 10); + } + + public static List getGachaRecords( + int ownerId, int page, int gachaType, int pageSize) { + return DatabaseManager.getGameDatastore() + .find(GachaRecord.class) + .filter(Filters.eq("ownerId", ownerId), Filters.eq("gachaType", gachaType)) + .iterator( + new FindOptions() + .sort(Sort.descending("transactionDate")) + .skip(pageSize * page) + .limit(pageSize)) + .toList(); + } + + public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType) { + return getGachaRecordsMaxPage(ownerId, page, gachaType, 10); + } + + public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType, int pageSize) { + long count = + DatabaseManager.getGameDatastore() + .find(GachaRecord.class) + .filter(Filters.eq("ownerId", ownerId), Filters.eq("gachaType", gachaType)) + .count(); + return count / 10 + (count % 10 > 0 ? 1 : 0); + } + + public static void saveGachaRecord(GachaRecord gachaRecord) { + DatabaseManager.getGameDatastore().save(gachaRecord); + } + + public static List getAllMail(Player player) { + return DatabaseManager.getGameDatastore() + .find(Mail.class) + .filter(Filters.eq("ownerUid", player.getUid())) + .stream() + .toList(); + } + + public static void saveMail(Mail mail) { + DatabaseManager.getGameDatastore().save(mail); + } + + public static boolean deleteMail(Mail mail) { + DeleteResult result = DatabaseManager.getGameDatastore().delete(mail); + return result.wasAcknowledged(); + } + + public static List getAllQuests(Player player) { + return DatabaseManager.getGameDatastore() + .find(GameMainQuest.class) + .filter(Filters.eq("ownerUid", player.getUid())) + .stream() + .toList(); + } + + public static void saveQuest(GameMainQuest quest) { + DatabaseManager.getGameDatastore().save(quest); + } + + public static boolean deleteQuest(GameMainQuest quest) { + return DatabaseManager.getGameDatastore().delete(quest).wasAcknowledged(); + } + + public static GameHome getHomeByUid(int id) { + return DatabaseManager.getGameDatastore() + .find(GameHome.class) + .filter(Filters.eq("ownerUid", id)) + .first(); + } + + public static void saveHome(GameHome gameHome) { + DatabaseManager.getGameDatastore().save(gameHome); + } + + public static BattlePassManager loadBattlePass(Player player) { + BattlePassManager manager = + DatabaseManager.getGameDatastore() + .find(BattlePassManager.class) + .filter(Filters.eq("ownerUid", player.getUid())) + .first(); + if (manager == null) { + manager = new BattlePassManager(player); + manager.save(); + } else { + manager.setPlayer(player); + } + return manager; + } + + public static void saveBattlePass(BattlePassManager manager) { + DatabaseManager.getGameDatastore().save(manager); + } + + public static PlayerActivityData getPlayerActivityData(int uid, int activityId) { + return DatabaseManager.getGameDatastore() + .find(PlayerActivityData.class) + .filter(Filters.and(Filters.eq("uid", uid), Filters.eq("activityId", activityId))) + .first(); + } + + public static void savePlayerActivityData(PlayerActivityData playerActivityData) { + DatabaseManager.getGameDatastore().save(playerActivityData); + } + + public static MusicGameBeatmap getMusicGameBeatmap(long musicShareId) { + return DatabaseManager.getGameDatastore() + .find(MusicGameBeatmap.class) + .filter(Filters.eq("musicShareId", musicShareId)) + .first(); + } + + public static void saveMusicGameBeatmap(MusicGameBeatmap musicGameBeatmap) { + DatabaseManager.getGameDatastore().save(musicGameBeatmap); + } + + public static Achievements getAchievementData(int uid) { + return DatabaseManager.getGameDatastore() + .find(Achievements.class) + .filter(Filters.and(Filters.eq("uid", uid))) + .first(); + } + + public static void saveAchievementData(Achievements achievements) { + DatabaseManager.getGameDatastore().save(achievements); + } + + public static void saveGroupInstance(SceneGroupInstance instance) { + DatabaseManager.getGameDatastore().save(instance); + } + + public static SceneGroupInstance loadGroupInstance(int groupId, Player owner) { + return DatabaseManager.getGameDatastore().find(SceneGroupInstance.class) + .filter(Filters.and(Filters.eq("ownerUid", owner.getUid()), + Filters.eq("groupId", groupId))).first(); + } +} diff --git a/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java b/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java index 8fdbd7370..ba82bb696 100644 --- a/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java +++ b/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java @@ -1,127 +1,134 @@ -package emu.grasscutter.game.activity; - -import dev.morphia.annotations.Entity; -import dev.morphia.annotations.Id; -import dev.morphia.annotations.Transient; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.common.ItemParamData; -import emu.grasscutter.data.excels.activity.ActivityWatcherData; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass; -import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify; -import emu.grasscutter.utils.JsonUtils; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Data; -import lombok.experimental.FieldDefaults; - -@Entity("activities") -@Data -@FieldDefaults(level = AccessLevel.PRIVATE) -@Builder(builderMethodName = "of") -public class PlayerActivityData { - @Id String id; - int uid; - int activityId; - Map watcherInfoMap; - /** the detail data of each type of activity (Json format) */ - String detail; - - @Transient Player player; - @Transient ActivityHandler activityHandler; - - public static PlayerActivityData getByPlayer(Player player, int activityId) { - return DatabaseHelper.getPlayerActivityData(player.getUid(), activityId); - } - - public void save() { - DatabaseHelper.savePlayerActivityData(this); - } - - public synchronized void addWatcherProgress(int watcherId) { - var watcherInfo = watcherInfoMap.get(watcherId); - if (watcherInfo == null) { - return; - } - - if (watcherInfo.curProgress >= watcherInfo.totalProgress) { - return; - } - - watcherInfo.curProgress++; - getPlayer().sendPacket(new PacketActivityUpdateWatcherNotify(activityId, watcherInfo)); - } - - public List getAllWatcherInfoList() { - return watcherInfoMap.values().stream().map(WatcherInfo::toProto).toList(); - } - - public void setDetail(Object detail) { - this.detail = JsonUtils.encode(detail); - } - - public void takeWatcherReward(int watcherId) { - var watcher = watcherInfoMap.get(watcherId); - if (watcher == null || watcher.isTakenReward()) { - return; - } - - var reward = - Optional.of(watcher) - .map(WatcherInfo::getMetadata) - .map(ActivityWatcherData::getRewardID) - .map(id -> GameData.getRewardDataMap().get(id.intValue())); - - if (reward.isEmpty()) { - return; - } - - List rewards = new ArrayList<>(); - for (ItemParamData param : reward.get().getRewardItemList()) { - rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1))); - } - - player.getInventory().addItems(rewards, ActionReason.ActivityWatcher); - watcher.setTakenReward(true); - save(); - } - - @Entity - @Data - @FieldDefaults(level = AccessLevel.PRIVATE) - @Builder(builderMethodName = "of") - public static class WatcherInfo { - int watcherId; - int totalProgress; - int curProgress; - boolean isTakenReward; - - public static WatcherInfo init(ActivityWatcher watcher) { - return WatcherInfo.of() - .watcherId(watcher.getWatcherId()) - .totalProgress(watcher.getActivityWatcherData().getProgress()) - .isTakenReward(false) - .build(); - } - - public ActivityWatcherData getMetadata() { - return GameData.getActivityWatcherDataMap().get(watcherId); - } - - public ActivityWatcherInfoOuterClass.ActivityWatcherInfo toProto() { - return ActivityWatcherInfoOuterClass.ActivityWatcherInfo.newBuilder() - .setWatcherId(watcherId) - .setCurProgress(curProgress) - .setTotalProgress(totalProgress) - .setIsTakenReward(isTakenReward) - .build(); - } - } -} +package emu.grasscutter.game.activity; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Id; +import dev.morphia.annotations.Transient; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.data.excels.activity.ActivityWatcherData; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass; +import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify; +import emu.grasscutter.utils.JsonUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Entity("activities") +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +@Builder(builderMethodName = "of") +public class PlayerActivityData { + @Id String id; + int uid; + int activityId; + Map watcherInfoMap; + /** the detail data of each type of activity (Json format) */ + String detail; + + @Transient Player player; + @Transient ActivityHandler activityHandler; + + public static PlayerActivityData getByPlayer(Player player, int activityId) { + return DatabaseHelper.getPlayerActivityData(player.getUid(), activityId); + } + + public void save() { + DatabaseHelper.savePlayerActivityData(this); + } + + public synchronized void addWatcherProgress(int watcherId) { + var watcherInfo = watcherInfoMap.get(watcherId); + if (watcherInfo == null) { + return; + } + + if (watcherInfo.curProgress >= watcherInfo.totalProgress) { + return; + } + + watcherInfo.curProgress++; + getPlayer().sendPacket(new PacketActivityUpdateWatcherNotify(activityId, watcherInfo)); + } + + public List getAllWatcherInfoList() { + return watcherInfoMap.values().stream().map(WatcherInfo::toProto).toList(); + } + + public void setDetail(Object detail) { + this.detail = JsonUtils.encode(detail); + } + + public void takeWatcherReward(int watcherId) { + var watcher = watcherInfoMap.get(watcherId); + if (watcher == null || watcher.isTakenReward()) { + return; + } + + var reward = + Optional.of(watcher) + .map(WatcherInfo::getMetadata) + .map(ActivityWatcherData::getRewardID) + .map(id -> GameData.getRewardDataMap().get(id.intValue())); + + if (reward.isEmpty()) { + return; + } + + List rewards = new ArrayList<>(); + for (ItemParamData param : reward.get().getRewardItemList()) { + rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1))); + } + + player.getInventory().addItems(rewards, ActionReason.ActivityWatcher); + watcher.setTakenReward(true); + save(); + } + + @Entity + @Data + @FieldDefaults(level = AccessLevel.PRIVATE) + @Builder(builderMethodName = "of") + public static class WatcherInfo { + int watcherId; + int totalProgress; + int curProgress; + boolean isTakenReward; + + /** + * @return True when the progress of this watcher has reached the total progress. + */ + public boolean isFinished(){ + return this.curProgress >= this.totalProgress; + } + + public static WatcherInfo init(ActivityWatcher watcher) { + return WatcherInfo.of() + .watcherId(watcher.getWatcherId()) + .totalProgress(watcher.getActivityWatcherData().getProgress()) + .isTakenReward(false) + .build(); + } + + public ActivityWatcherData getMetadata() { + return GameData.getActivityWatcherDataMap().get(watcherId); + } + + public ActivityWatcherInfoOuterClass.ActivityWatcherInfo toProto() { + return ActivityWatcherInfoOuterClass.ActivityWatcherInfo.newBuilder() + .setWatcherId(watcherId) + .setCurProgress(curProgress) + .setTotalProgress(totalProgress) + .setIsTakenReward(isTakenReward) + .build(); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/avatar/Avatar.java b/src/main/java/emu/grasscutter/game/avatar/Avatar.java index 5ad30bf92..2597b1ae3 100644 --- a/src/main/java/emu/grasscutter/game/avatar/Avatar.java +++ b/src/main/java/emu/grasscutter/game/avatar/Avatar.java @@ -1,1221 +1,1271 @@ -package emu.grasscutter.game.avatar; - -import static emu.grasscutter.config.Configuration.GAME_OPTIONS; - -import dev.morphia.annotations.*; -import emu.grasscutter.GameConstants; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.OpenConfigEntry; -import emu.grasscutter.data.binout.OpenConfigEntry.SkillPointModifier; -import emu.grasscutter.data.common.FightPropData; -import emu.grasscutter.data.excels.*; -import emu.grasscutter.data.excels.ItemData.WeaponProperty; -import emu.grasscutter.data.excels.avatar.AvatarData; -import emu.grasscutter.data.excels.avatar.AvatarSkillData; -import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; -import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData.InherentProudSkillOpens; -import emu.grasscutter.data.excels.avatar.AvatarTalentData; -import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData; -import emu.grasscutter.data.excels.reliquary.ReliquaryLevelData; -import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData; -import emu.grasscutter.data.excels.reliquary.ReliquarySetData; -import emu.grasscutter.data.excels.trial.TrialAvatarTemplateData; -import emu.grasscutter.data.excels.weapon.WeaponCurveData; -import emu.grasscutter.data.excels.weapon.WeaponPromoteData; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.game.inventory.EquipType; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.inventory.ItemType; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.*; -import emu.grasscutter.net.proto.AvatarFetterInfoOuterClass.AvatarFetterInfo; -import emu.grasscutter.net.proto.AvatarInfoOuterClass.AvatarInfo; -import emu.grasscutter.net.proto.AvatarSkillInfoOuterClass.AvatarSkillInfo; -import emu.grasscutter.net.proto.FetterDataOuterClass.FetterData; -import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass; -import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass.ShowAvatarInfo; -import emu.grasscutter.net.proto.ShowEquipOuterClass.ShowEquip; -import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGrantRecord; -import emu.grasscutter.net.proto.TrialAvatarInfoOuterClass.TrialAvatarInfo; -import emu.grasscutter.server.packet.send.*; -import emu.grasscutter.utils.ProtoHelper; -import it.unimi.dsi.fastutil.ints.*; -import java.util.*; -import java.util.stream.Stream; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import lombok.val; -import org.bson.types.ObjectId; - -@Entity(value = "avatars", useDiscriminator = false) -public class Avatar { - @Transient @Getter private final Int2ObjectMap equips; - @Transient @Getter private final Int2FloatOpenHashMap fightProperties; - @Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides; - @Id private ObjectId id; - @Indexed @Getter private int ownerId; // Id of player that this avatar belongs to - @Transient private Player owner; - @Transient @Getter private AvatarData avatarData; - @Transient @Getter private AvatarSkillDepotData skillDepot; - @Transient @Getter private long guid; // Player unique id - @Getter private int avatarId; // Id of avatar - @Getter @Setter private int level = 1; - @Getter @Setter private int exp; - @Getter @Setter private int promoteLevel; - @Getter @Setter private int satiation; // Fullness - @Getter @Setter private int satiationPenalty; // When eating too much - @Getter @Setter private float currentHp; - private float currentEnergy; - @Transient @Getter private Set extraAbilityEmbryos; - - private List fetters; - - private final Map skillLevelMap = new Int2IntArrayMap(7); // Talent levels - - @Transient @Getter - private final Map skillExtraChargeMap = new Int2IntArrayMap(2); // Charges - - @Transient - private final Map proudSkillBonusMap = - new Int2IntArrayMap(2); // Talent bonus levels (from const) - - @Getter private int skillDepotId; - private Set talentIdList; // Constellation id list - @Getter private Set proudSkillList; // Character passives - - @Getter @Setter private int flyCloak; - @Getter @Setter private int costume; - @Getter private int bornTime; - - @Getter @Setter private int fetterLevel = 1; - @Getter @Setter private int fetterExp; - - @Getter @Setter private int nameCardRewardId; - @Getter @Setter private int nameCardId; - - // trial avatar property - @Getter @Setter private int trialAvatarId = 0; - // cannot store to db if grant reason is not integer - @Getter @Setter - private int grantReason = TrialAvatarGrantRecord.GrantReason.GRANT_REASON_INVALID.getNumber(); - - @Getter @Setter private int fromParentQuestId = 0; - // so far no outer class or prop value has information of this, but from packet: - // 1 = normal, 2 = trial avatar - @Getter @Setter private int avatarType = Type.NORMAL.getNumber(); - - @Deprecated // Do not use. Morhpia only! - public Avatar() { - this.equips = new Int2ObjectOpenHashMap<>(); - this.fightProperties = new Int2FloatOpenHashMap(); - this.fightPropOverrides = new Int2FloatOpenHashMap(); - this.extraAbilityEmbryos = new HashSet<>(); - this.fetters = new ArrayList<>(); // TODO Move to avatar - } - - // On creation - public Avatar(int avatarId) { - this(GameData.getAvatarDataMap().get(avatarId)); - } - - public Avatar(AvatarData data) { - this(); - this.avatarId = data.getId(); - this.nameCardRewardId = data.getNameCardRewardId(); - this.nameCardId = data.getNameCardId(); - this.avatarData = data; - this.bornTime = (int) (System.currentTimeMillis() / 1000); - this.flyCloak = 140001; - - this.talentIdList = new HashSet<>(); - this.proudSkillList = new HashSet<>(); - - // Combat properties - Stream.of(FightProperty.values()) - .map(FightProperty::getId) - .filter(id -> (id > 0) && (id < 3000)) - .forEach(id -> this.setFightProperty(id, 0f)); - - // Skill depot - this.setSkillDepotData( - switch (this.avatarId) { - case GameConstants.MAIN_CHARACTER_MALE -> GameData.getAvatarSkillDepotDataMap() - .get(504); // Hack to start with anemo skills - case GameConstants.MAIN_CHARACTER_FEMALE -> GameData.getAvatarSkillDepotDataMap() - .get(704); - default -> data.getSkillDepot(); - }); - - // Set stats - this.recalcStats(); - this.currentHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.currentHp); - this.currentEnergy = 0f; - // Load handler - this.onLoad(); - } - - public static int getMinPromoteLevel(int level) { - if (level > 80) { - return 6; - } else if (level > 70) { - return 5; - } else if (level > 60) { - return 4; - } else if (level > 50) { - return 3; - } else if (level > 40) { - return 2; - } else if (level > 20) { - return 1; - } - return 0; - } - - public Player getPlayer() { - return this.owner; - } - - public ObjectId getObjectId() { - return this.id; - } - - protected void setAvatarData(AvatarData data) { - if (this.avatarData != null) return; - this.avatarData = data; // Used while loading this from the database - } - - public void setOwner(Player player) { - this.owner = player; - this.ownerId = player.getUid(); - this.guid = player.getNextGameGuid(); - } - - public boolean addSatiation(int value) { - if (this.satiation >= 10000) return false; - this.satiation += value; - return true; - } - - public float reduceSatiation(int value) { - if (this.satiation == 0) return 0; - this.satiation -= value; - if (this.satiation < 0) { - this.satiation = 0; - } - return this.satiation; - } - - public float reduceSatiationPenalty(int value) { - if (this.satiationPenalty == 0) return 0; - this.satiationPenalty -= value; - if (this.satiationPenalty < 0) { - this.satiationPenalty = 0; - } - return this.satiationPenalty; - } - - public GameItem getEquipBySlot(EquipType slot) { - return this.getEquips().get(slot.getValue()); - } - - private GameItem getEquipBySlot(int slotId) { - return this.getEquips().get(slotId); - } - - public GameItem getWeapon() { - return this.getEquipBySlot(EquipType.EQUIP_WEAPON); - } - - protected void setSkillDepot(AvatarSkillDepotData skillDepot) { - if (this.skillDepot != null) return; - this.skillDepot = skillDepot; // Used while loading this from the database - } - - public void setSkillDepotData(AvatarSkillDepotData skillDepot) { - // Set id and depot - this.skillDepotId = skillDepot.getId(); - this.skillDepot = skillDepot; - // Add any missing skills - this.skillDepot - .getSkillsAndEnergySkill() - .forEach(skillId -> this.skillLevelMap.putIfAbsent(skillId, 1)); - // Add proud skills - this.proudSkillList.clear(); - skillDepot.getInherentProudSkillOpens().stream() - .filter(openData -> openData.getProudSkillGroupId() > 0) - .filter(openData -> openData.getNeedAvatarPromoteLevel() <= this.getPromoteLevel()) - .mapToInt(openData -> (openData.getProudSkillGroupId() * 100) + 1) - .filter(proudSkillId -> GameData.getProudSkillDataMap().containsKey(proudSkillId)) - .forEach(proudSkillId -> this.proudSkillList.add(proudSkillId)); - this.recalcStats(); - } - - public List getFetterList() { - return fetters; - } - - public void setFetterList(List fetterList) { - this.fetters = fetterList; - } - - public void setCurrentEnergy() { - if (GAME_OPTIONS.energyUsage) { - this.setCurrentEnergy(this.currentEnergy); - } - } - - public void setCurrentEnergy(float currentEnergy) { - var depot = this.skillDepot; - if (depot != null && depot.getEnergySkillData() != null) { - ElementType element = depot.getElementType(); - var maxEnergy = depot.getEnergySkillData().getCostElemVal(); - this.setFightProperty(element.getMaxEnergyProp(), maxEnergy); - this.setFightProperty( - element.getCurEnergyProp(), GAME_OPTIONS.energyUsage ? currentEnergy : maxEnergy); - } - } - - public void setCurrentEnergy(FightProperty curEnergyProp, float currentEnergy) { - if (GAME_OPTIONS.energyUsage) { - this.setFightProperty(curEnergyProp, currentEnergy); - this.currentEnergy = currentEnergy; - this.save(); - } - } - - public void setFightProperty(FightProperty prop, float value) { - this.getFightProperties().put(prop.getId(), value); - } - - private void setFightProperty(int id, float value) { - this.getFightProperties().put(id, value); - } - - public void addFightProperty(FightProperty prop, float value) { - this.getFightProperties().put(prop.getId(), getFightProperty(prop) + value); - } - - public float getFightProperty(FightProperty prop) { - return getFightProperties().getOrDefault(prop.getId(), 0f); - } - - public Map - getSkillLevelMap() { // Returns a copy of the skill levels for the current skillDepot. - var map = new Int2IntOpenHashMap(); - this.skillDepot - .getSkillsAndEnergySkill() - .forEach( - skillId -> map.put(skillId, this.skillLevelMap.putIfAbsent(skillId, 1).intValue())); - return map; - } - - // Returns a copy of the skill bonus levels for the current skillDepot, capped to avoid invalid - // levels. - public Map getProudSkillBonusMap() { - var map = new Int2IntArrayMap(); - this.skillDepot - .getSkillsAndEnergySkill() - .forEach( - skillId -> { - val skillData = GameData.getAvatarSkillDataMap().get(skillId); - if (skillData == null) return; - int proudSkillGroupId = skillData.getProudSkillGroupId(); - int bonus = this.proudSkillBonusMap.getOrDefault(proudSkillGroupId, 0); - int maxLevel = GameData.getProudSkillGroupMaxLevel(proudSkillGroupId); - int curLevel = this.skillLevelMap.getOrDefault(skillId, 0); - if (maxLevel > 0) { - bonus = Math.min(bonus, maxLevel - curLevel); - } - map.put(proudSkillGroupId, bonus); - }); - return map; - } - - public IntSet getTalentIdList() { // Returns a copy of the unlocked constellations for the current - // skillDepot. - var talents = new IntOpenHashSet(this.getSkillDepot().getTalents()); - talents.removeIf(id -> !this.talentIdList.contains(id)); - return talents; - } - - public int getCoreProudSkillLevel() { - var lockedTalents = new IntOpenHashSet(this.getSkillDepot().getTalents()); - lockedTalents.removeAll(this.getTalentIdList()); - // One below the lowest locked talent, or 6 if there are no locked talents. - return lockedTalents.intStream().map(i -> i % 10).min().orElse(7) - 1; - } - - public boolean equipItem(GameItem item, boolean shouldRecalc) { - // Sanity check equip type - EquipType itemEquipType = item.getItemData().getEquipType(); - if (itemEquipType == EquipType.EQUIP_NONE) { - return false; - } - - // Check if other avatars have this item equipped - Avatar otherAvatar = getPlayer().getAvatars().getAvatarById(item.getEquipCharacter()); - if (otherAvatar != null) { - // Unequip other avatar's item - if (otherAvatar.unequipItem(item.getItemData().getEquipType())) { - getPlayer() - .sendPacket( - new PacketAvatarEquipChangeNotify(otherAvatar, item.getItemData().getEquipType())); - } - // Swap with other avatar - if (getEquips().containsKey(itemEquipType.getValue())) { - GameItem toSwap = this.getEquipBySlot(itemEquipType); - otherAvatar.equipItem(toSwap, false); - } - // Recalc - otherAvatar.recalcStats(); - } else if (getEquips().containsKey(itemEquipType.getValue())) { - // Unequip item in current slot if it exists - unequipItem(itemEquipType); - } - - // Set equip - getEquips().put(itemEquipType.getValue(), item); - - if (itemEquipType == EquipType.EQUIP_WEAPON && getPlayer().getWorld() != null) { - item.setWeaponEntityId(this.getPlayer().getWorld().getNextEntityId(EntityIdType.WEAPON)); - } - - item.setEquipCharacter(this.getAvatarId()); - item.save(); - - if (this.getPlayer().hasSentLoginPackets()) { - this.getPlayer().sendPacket(new PacketAvatarEquipChangeNotify(this, item)); - } - - if (shouldRecalc) { - this.recalcStats(); - } - - return true; - } - - public boolean unequipItem(EquipType slot) { - GameItem item = getEquips().remove(slot.getValue()); - - if (item != null) { - item.setEquipCharacter(0); - item.save(); - return true; - } - - return false; - } - - public void recalcStats() { - recalcStats(false); - } - - public void recalcStats(boolean forceSendAbilityChange) { - // Setup - var data = this.getAvatarData(); - var promoteData = - GameData.getAvatarPromoteData(data.getAvatarPromoteId(), this.getPromoteLevel()); - var setMap = new Int2IntOpenHashMap(); - - // Extra ability embryos - Set prevExtraAbilityEmbryos = this.getExtraAbilityEmbryos(); - this.extraAbilityEmbryos = new HashSet<>(); - - // Fetters - this.setFetterList(data.getFetters()); - this.setNameCardRewardId(data.getNameCardRewardId()); - this.setNameCardId(data.getNameCardId()); - - // Get hp percent, set to 100% if none - float hpPercent = - this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 - ? 1f - : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) - / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - - // Store current energy value for later - float currentEnergy = - (this.getSkillDepot() != null) - ? this.getFightProperty(this.getSkillDepot().getElementType().getCurEnergyProp()) - : 0f; - - // Clear properties - this.getFightProperties().clear(); - - // Base stats - this.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, data.getBaseHp(this.getLevel())); - this.setFightProperty( - FightProperty.FIGHT_PROP_BASE_ATTACK, data.getBaseAttack(this.getLevel())); - this.setFightProperty( - FightProperty.FIGHT_PROP_BASE_DEFENSE, data.getBaseDefense(this.getLevel())); - this.setFightProperty(FightProperty.FIGHT_PROP_CRITICAL, data.getBaseCritical()); - this.setFightProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, data.getBaseCriticalHurt()); - this.setFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, 1f); - - if (promoteData != null) { - for (FightPropData fightPropData : promoteData.getAddProps()) { - this.addFightProperty(fightPropData.getProp(), fightPropData.getValue()); - } - } - - // Set energy usage - setCurrentEnergy(currentEnergy); - - // Artifacts - for (int slotId = 1; slotId <= 5; slotId++) { - // Get artifact - GameItem equip = this.getEquipBySlot(slotId); - if (equip == null) { - continue; - } - // Artifact main stat - ReliquaryMainPropData mainPropData = - GameData.getReliquaryMainPropDataMap().get(equip.getMainPropId()); - if (mainPropData != null) { - ReliquaryLevelData levelData = - GameData.getRelicLevelData(equip.getItemData().getRankLevel(), equip.getLevel()); - if (levelData != null) { - this.addFightProperty( - mainPropData.getFightProp(), levelData.getPropValue(mainPropData.getFightProp())); - } - } - // Artifact sub stats - for (int appendPropId : equip.getAppendPropIdList()) { - ReliquaryAffixData affixData = GameData.getReliquaryAffixDataMap().get(appendPropId); - if (affixData != null) { - this.addFightProperty(affixData.getFightProp(), affixData.getPropValue()); - } - } - // Set bonus - if (equip.getItemData().getSetId() > 0) { - setMap.addTo(equip.getItemData().getSetId(), 1); - } - } - - // Set stuff - setMap.forEach( - (setId, amount) -> { - ReliquarySetData setData = GameData.getReliquarySetDataMap().get((int) setId); - if (setData == null) return; - - // Calculate how many items are from the set - // Add affix data from set bonus - val setNeedNum = setData.getSetNeedNum(); - for (int setIndex = 0; setIndex < setNeedNum.length; setIndex++) { - if (amount < setNeedNum[setIndex]) break; - - int affixId = (setData.getEquipAffixId() * 10) + setIndex; - EquipAffixData affix = GameData.getEquipAffixDataMap().get(affixId); - if (affix == null) { - continue; - } - - // Add properties from this affix to our avatar - for (FightPropData prop : affix.getAddProps()) { - this.addFightProperty(prop.getProp(), prop.getValue()); - } - - // Add any skill strings from this affix - this.addToExtraAbilityEmbryos(affix.getOpenConfig(), true); - } - }); - - // Weapon - GameItem weapon = this.getWeapon(); - if (weapon != null) { - // Add stats - WeaponCurveData curveData = GameData.getWeaponCurveDataMap().get(weapon.getLevel()); - if (curveData != null) { - for (WeaponProperty weaponProperty : weapon.getItemData().getWeaponProperties()) { - this.addFightProperty( - weaponProperty.getPropType(), - weaponProperty.getInitValue() * curveData.getMultByProp(weaponProperty.getType())); - } - } - // Weapon promotion stats - WeaponPromoteData wepPromoteData = - GameData.getWeaponPromoteData( - weapon.getItemData().getWeaponPromoteId(), weapon.getPromoteLevel()); - if (wepPromoteData != null) { - for (FightPropData prop : wepPromoteData.getAddProps()) { - if (prop.getValue() == 0f || prop.getProp() == null) { - continue; - } - this.addFightProperty(prop.getProp(), prop.getValue()); - } - } - // Add weapon skill from affixes - if (weapon.getAffixes() != null && weapon.getAffixes().size() > 0) { - // Weapons usually dont have more than one affix but just in case... - for (int af : weapon.getAffixes()) { - if (af == 0) { - continue; - } - // Calculate affix id - int affixId = (af * 10) + weapon.getRefinement(); - EquipAffixData affix = GameData.getEquipAffixDataMap().get(affixId); - if (affix == null) { - continue; - } - - // Add properties from this affix to our avatar - for (FightPropData prop : affix.getAddProps()) { - this.addFightProperty(prop.getProp(), prop.getValue()); - } - - // Add any skill strings from this affix - this.addToExtraAbilityEmbryos(affix.getOpenConfig(), true); - } - } - } - - // Add proud skills and unlock them if needed - AvatarSkillDepotData skillDepot = - GameData.getAvatarSkillDepotDataMap().get(this.getSkillDepotId()); - this.getProudSkillList().clear(); - for (InherentProudSkillOpens openData : skillDepot.getInherentProudSkillOpens()) { - if (openData.getProudSkillGroupId() == 0) { - continue; - } - if (openData.getNeedAvatarPromoteLevel() <= this.getPromoteLevel()) { - int proudSkillId = (openData.getProudSkillGroupId() * 100) + 1; - if (GameData.getProudSkillDataMap().containsKey(proudSkillId)) { - this.getProudSkillList().add(proudSkillId); - } - } - } - - // Proud skills - for (int proudSkillId : this.getProudSkillList()) { - ProudSkillData proudSkillData = GameData.getProudSkillDataMap().get(proudSkillId); - if (proudSkillData == null) { - continue; - } - - // Add properties from this proud skill to our avatar - for (FightPropData prop : proudSkillData.getAddProps()) { - this.addFightProperty(prop.getProp(), prop.getValue()); - } - - // Add any embryos from this proud skill - this.addToExtraAbilityEmbryos(proudSkillData.getOpenConfig()); - } - - // Constellations - this.getTalentIdList() - .intStream() - .mapToObj(GameData.getAvatarTalentDataMap()::get) - .filter(Objects::nonNull) - .map(AvatarTalentData::getOpenConfig) - .filter(Objects::nonNull) - .forEach(this::addToExtraAbilityEmbryos); - // Add any skill strings from this constellation - - // Set % stats - FightProperty.forEachCompoundProperty( - c -> - this.setFightProperty( - c.getResult(), - this.getFightProperty(c.getFlat()) - + (this.getFightProperty(c.getBase()) - * (1f + this.getFightProperty(c.getPercent()))))); - - // Reapply all overrides - this.fightProperties.putAll(this.fightPropOverrides); - - // Set current hp - this.setFightProperty( - FightProperty.FIGHT_PROP_CUR_HP, - this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent); - - // Packet - if (getPlayer() != null && getPlayer().hasSentLoginPackets()) { - // Update stats for client - getPlayer().sendPacket(new PacketAvatarFightPropNotify(this)); - // Update client abilities - EntityAvatar entity = this.getAsEntity(); - if (entity != null - && (!this.getExtraAbilityEmbryos().equals(prevExtraAbilityEmbryos) - || forceSendAbilityChange)) { - getPlayer().sendPacket(new PacketAbilityChangeNotify(entity)); - } - } - } - - public void addToExtraAbilityEmbryos(String openConfig) { - this.addToExtraAbilityEmbryos(openConfig, false); - } - - public void addToExtraAbilityEmbryos(String openConfig, boolean forceAdd) { - if (openConfig == null || openConfig.length() == 0) { - return; - } - - OpenConfigEntry entry = GameData.getOpenConfigEntries().get(openConfig); - if (entry == null) { - if (forceAdd) { - // Add config string to ability skill list anyways - this.getExtraAbilityEmbryos().add(openConfig); - } - return; - } - - if (entry.getAddAbilities() != null) { - for (String ability : entry.getAddAbilities()) { - this.getExtraAbilityEmbryos().add(ability); - } - } - } - - public void calcConstellation(OpenConfigEntry entry, boolean notifyClient) { - if (entry == null) return; - if (this.getPlayer() == null) notifyClient = false; - - // Check if new constellation adds +3 to a skill level - if (this.calcConstellationExtraLevels(entry) && notifyClient) { - // Packet - this.getPlayer() - .sendPacket(new PacketProudSkillExtraLevelNotify(this, entry.getExtraTalentIndex())); - } - // Check if new constellation adds skill charges - if (this.calcConstellationExtraCharges(entry) && notifyClient) { - // Packet - Stream.of(entry.getSkillPointModifiers()) - .mapToInt(SkillPointModifier::getSkillId) - .forEach( - skillId -> { - this.getPlayer() - .sendPacket( - new PacketAvatarSkillMaxChargeCountNotify( - this, skillId, this.getSkillExtraChargeMap().getOrDefault(skillId, 0))); - }); - } - } - - public void recalcConstellations() { - // Clear first - this.proudSkillBonusMap.clear(); - this.skillExtraChargeMap.clear(); - - // Sanity checks - if (this.avatarData == null || this.skillDepot == null) { - return; - } - - this.getTalentIdList() - .intStream() - .mapToObj(GameData.getAvatarTalentDataMap()::get) - .filter(Objects::nonNull) - .map(AvatarTalentData::getOpenConfig) - .filter(Objects::nonNull) - .filter(openConfig -> openConfig.length() > 0) - .map(GameData.getOpenConfigEntries()::get) - .filter(Objects::nonNull) - .forEach(e -> this.calcConstellation(e, false)); - } - - private boolean calcConstellationExtraCharges(OpenConfigEntry entry) { - var skillPointModifiers = entry.getSkillPointModifiers(); - if (skillPointModifiers == null) return false; - - for (var mod : skillPointModifiers) { - AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(mod.getSkillId()); - - if (skillData == null) continue; - - int charges = skillData.getMaxChargeNum() + mod.getDelta(); - - this.getSkillExtraChargeMap().put(mod.getSkillId(), charges); - } - return true; - } - - private boolean calcConstellationExtraLevels(OpenConfigEntry entry) { - int skillId = - switch (entry.getExtraTalentIndex()) { - case 9 -> this.skillDepot.getEnergySkill(); // Ult skill - case 2 -> (this.skillDepot.getSkills().size() >= 2) - ? this.skillDepot.getSkills().get(1) - : 0; // E skill - default -> 0; - }; - // Sanity check - if (skillId == 0) { - return false; - } - - // Get proud skill group id - AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(skillId); - - if (skillData == null) { - return false; - } - - // Add to bonus list - this.addProudSkillLevelBonus(skillData.getProudSkillGroupId(), 3); - return true; - } - - private int addProudSkillLevelBonus(int proudSkillGroupId, int bonus) { - return this.proudSkillBonusMap.compute( - proudSkillGroupId, (k, v) -> (v == null) ? bonus : v + bonus); - } - - public boolean upgradeSkill(int skillId) { - AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(skillId); - if (skillData == null) return false; - - // Get data for next skill level - int newLevel = this.skillLevelMap.getOrDefault(skillId, 0) + 1; - if (newLevel > 10) return false; - - // Proud skill data - int proudSkillId = (skillData.getProudSkillGroupId() * 100) + newLevel; - ProudSkillData proudSkill = GameData.getProudSkillDataMap().get(proudSkillId); - if (proudSkill == null) return false; - - // Make sure break level is correct - if (this.getPromoteLevel() < proudSkill.getBreakLevel()) return false; - - // Pay materials and mora if possible - if (!this.getPlayer().getInventory().payItems(proudSkill.getTotalCostItems())) return false; - - // Upgrade skill - this.setSkillLevel(skillId, newLevel); - return true; - } - - public boolean setSkillLevel(int skillId, int level) { - if (level < 0 || level > 15) return false; - var validLevels = GameData.getAvatarSkillLevels(skillId); - if (validLevels != null && !validLevels.contains(level)) return false; - int oldLevel = - this.skillLevelMap.getOrDefault( - skillId, 0); // just taking the return value of put would have null concerns - this.skillLevelMap.put(skillId, level); - this.save(); - - // Packet - val player = this.getPlayer(); - if (player != null) { - player.sendPacket(new PacketAvatarSkillChangeNotify(this, skillId, oldLevel, level)); - player.sendPacket(new PacketAvatarSkillUpgradeRsp(this, skillId, oldLevel, level)); - } - return true; - } - - public boolean unlockConstellation() { - return this.unlockConstellation(false); - } - - public boolean unlockConstellation(boolean skipPayment) { - int currentTalentLevel = this.getCoreProudSkillLevel(); - int talentId = this.skillDepot.getTalents().get(currentTalentLevel); - return this.unlockConstellation(talentId, skipPayment); - } - - public boolean unlockConstellation(int talentId) { - return unlockConstellation(talentId, false); - } - - public boolean unlockConstellation(int talentId, boolean skipPayment) { - // Get talent - AvatarTalentData talentData = GameData.getAvatarTalentDataMap().get(talentId); - if (talentData == null) return false; - var player = this.getPlayer(); - - // Pay constellation item if possible - if (!skipPayment - && (player != null) - && !player.getInventory().payItem(talentData.getMainCostItemId(), 1)) { - return false; - } - - // Apply + recalc - this.talentIdList.add(talentData.getId()); - - // Packet - if (player != null) { - player.sendPacket(new PacketAvatarUnlockTalentNotify(this, talentId)); - player.sendPacket(new PacketUnlockAvatarTalentRsp(this, talentId)); - } - - // Proud skill bonus map (Extra skills) - this.calcConstellation(GameData.getOpenConfigEntries().get(talentData.getOpenConfig()), true); - - // Recalc + save avatar - this.recalcStats(true); - this.save(); - return true; - } - - public void forceConstellationLevel(int level) { - if (level > 6) return; // Sanity check - - if (level < 0) { // Special case for resetConst to remove inactive depots too - this.talentIdList.clear(); - this.recalcStats(); - this.save(); - return; - } - this.talentIdList.removeAll( - this.getTalentIdList()); // Only remove constellations from active depot - for (int i = 0; i < level; i++) this.unlockConstellation(true); - this.recalcStats(); - this.save(); - } - - public boolean sendSkillExtraChargeMap() { - val map = this.getSkillExtraChargeMap(); - if (map.isEmpty()) return false; - this.getPlayer() - .sendPacket( - new PacketAvatarSkillInfoNotify( - this.guid, - new Int2IntArrayMap( - map))); // TODO: Remove this allocation when updating interfaces to FastUtils - // later - return true; - } - - public EntityAvatar getAsEntity() { - for (EntityAvatar entity : getPlayer().getTeamManager().getActiveTeam()) { - if (entity.getAvatar() == this) { - return entity; - } - } - return null; - } - - public int getEntityId() { - EntityAvatar entity = getAsEntity(); - return entity != null ? entity.getId() : 0; - } - - public void save() { - DatabaseHelper.saveAvatar(this); - } - - public AvatarInfo toProto() { - int fetterLevel = this.getFetterLevel(); - AvatarFetterInfo.Builder avatarFetter = AvatarFetterInfo.newBuilder().setExpLevel(fetterLevel); - - if (fetterLevel != 10) { - avatarFetter.setExpNumber(this.getFetterExp()); - } - - if (this.fetters != null) { - this.fetters.forEach( - fetterId -> - avatarFetter.addFetterList( - FetterData.newBuilder() - .setFetterId(fetterId) - .setFetterState(FetterState.FINISH.getValue()))); - } - - int cardId = this.getNameCardId(); - - if (this.getPlayer().getNameCardList().contains(cardId)) { - avatarFetter.addRewardedFetterLevelList(10); - } - - AvatarInfo.Builder avatarInfo = - AvatarInfo.newBuilder() - .setAvatarId(this.getAvatarId()) - .setGuid(this.getGuid()) - .setLifeState(1) - .addAllTalentIdList(this.getTalentIdList()) - .putAllFightPropMap(this.getFightProperties()) - .setSkillDepotId(this.getSkillDepotId()) - .setCoreProudSkillLevel(this.getCoreProudSkillLevel()) - .putAllSkillLevelMap(this.getSkillLevelMap()) - .addAllInherentProudSkillList(this.getProudSkillList()) - .putAllProudSkillExtraLevelMap(this.getProudSkillBonusMap()) - .setAvatarType(1) - .setBornTime(this.getBornTime()) - .setFetterInfo(avatarFetter) - .setWearingFlycloakId(this.getFlyCloak()) - .setCostumeId(this.getCostume()); - - this.getSkillExtraChargeMap() - .forEach( - (skillId, count) -> - avatarInfo.putSkillMap( - skillId, AvatarSkillInfo.newBuilder().setMaxChargeCount(count).build())); - - this.getEquips().forEach((k, item) -> avatarInfo.addEquipGuidList(item.getGuid())); - - avatarInfo.putPropMap( - PlayerProperty.PROP_LEVEL.getId(), - ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel())); - avatarInfo.putPropMap( - PlayerProperty.PROP_EXP.getId(), - ProtoHelper.newPropValue(PlayerProperty.PROP_EXP, this.getExp())); - avatarInfo.putPropMap( - PlayerProperty.PROP_BREAK_LEVEL.getId(), - ProtoHelper.newPropValue(PlayerProperty.PROP_BREAK_LEVEL, this.getPromoteLevel())); - avatarInfo.putPropMap( - PlayerProperty.PROP_SATIATION_VAL.getId(), - ProtoHelper.newPropValue(PlayerProperty.PROP_SATIATION_VAL, this.getSatiation())); - avatarInfo.putPropMap( - PlayerProperty.PROP_SATIATION_PENALTY_TIME.getId(), - ProtoHelper.newPropValue( - PlayerProperty.PROP_SATIATION_PENALTY_TIME, this.getSatiationPenalty())); - - return avatarInfo.build(); - } - - // used only in character showcase - public ShowAvatarInfo toShowAvatarInfoProto() { - AvatarFetterInfo.Builder avatarFetter = - AvatarFetterInfo.newBuilder().setExpLevel(this.getFetterLevel()); - - ShowAvatarInfo.Builder showAvatarInfo = - ShowAvatarInfoOuterClass.ShowAvatarInfo.newBuilder() - .setAvatarId(avatarId) - .addAllTalentIdList(this.getTalentIdList()) - .putAllFightPropMap(this.getFightProperties()) - .setSkillDepotId(this.getSkillDepotId()) - .setCoreProudSkillLevel(this.getCoreProudSkillLevel()) - .addAllInherentProudSkillList(this.getProudSkillList()) - .putAllSkillLevelMap(this.getSkillLevelMap()) - .putAllProudSkillExtraLevelMap(this.getProudSkillBonusMap()) - .setFetterInfo(avatarFetter) - .setCostumeId(this.getCostume()); - - showAvatarInfo.putPropMap( - PlayerProperty.PROP_LEVEL.getId(), - ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel())); - showAvatarInfo.putPropMap( - PlayerProperty.PROP_EXP.getId(), - ProtoHelper.newPropValue(PlayerProperty.PROP_EXP, this.getExp())); - showAvatarInfo.putPropMap( - PlayerProperty.PROP_BREAK_LEVEL.getId(), - ProtoHelper.newPropValue(PlayerProperty.PROP_BREAK_LEVEL, this.getPromoteLevel())); - showAvatarInfo.putPropMap( - PlayerProperty.PROP_SATIATION_VAL.getId(), - ProtoHelper.newPropValue(PlayerProperty.PROP_SATIATION_VAL, this.getSatiation())); - showAvatarInfo.putPropMap( - PlayerProperty.PROP_SATIATION_PENALTY_TIME.getId(), - ProtoHelper.newPropValue( - PlayerProperty.PROP_SATIATION_PENALTY_TIME, this.getSatiationPenalty())); - int maxStamina = this.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); - showAvatarInfo.putPropMap( - PlayerProperty.PROP_MAX_STAMINA.getId(), - ProtoHelper.newPropValue(PlayerProperty.PROP_MAX_STAMINA, maxStamina)); - - for (GameItem item : this.getEquips().values()) { - if (item.getItemType() == ItemType.ITEM_RELIQUARY) { - showAvatarInfo.addEquipList( - ShowEquip.newBuilder() - .setItemId(item.getItemId()) - .setReliquary(item.toReliquaryProto())); - } else if (item.getItemType() == ItemType.ITEM_WEAPON) { - showAvatarInfo.addEquipList( - ShowEquip.newBuilder().setItemId(item.getItemId()).setWeapon(item.toWeaponProto())); - } - } - - return showAvatarInfo.build(); - } - - /** - * Converts this avatar into a trial avatar. - * - * @param level The avatar's level. - * @param avatarId The ID of the avatar. - * @param grantReason The reason for granting the avatar. - * @param questId The ID of the quest that granted the avatar. - */ - public void setTrialAvatarInfo( - int level, int avatarId, TrialAvatarGrantRecord.GrantReason grantReason, int questId) { - this.setLevel(level); - this.setPromoteLevel(getMinPromoteLevel(level)); - this.setTrialAvatarId(avatarId); - this.setGrantReason(grantReason.getNumber()); - this.setFromParentQuestId(questId); - this.setAvatarType(Type.TRIAL.getNumber()); - this.applyTrialSkillLevels(); - this.applyTrialItems(); - } - - /** - * Gets the gear template based on the avatar's level. - * - * @return The avatar's template. - */ - private int getTrialTemplate() { - return this.getLevel() <= 9 - ? 1 - : (int) - (Math.floor(this.getLevel() / 10f) * 10); // round trial level to fit template levels - } - - /** - * @return The level to be used for the avatar's skills (talents). - */ - public int getTrialSkillLevel() { - // Use default data if custom data not available. - if (GameData.getTrialAvatarCustomData().isEmpty()) { - var template = getTrialTemplate(); // round trial level to fit template levels - - var templateData = GameData.getTrialAvatarTemplateDataMap().get(template); - return templateData == null ? 1 : templateData.getTrialAvatarSkillLevel(); - } - - // Use custom data. - var trialData = GameData.getTrialAvatarCustomData().get(this.getTrialAvatarId()); - if (trialData == null) return 1; - - return trialData.getCoreProudSkillLevel(); // enhanced version of weapon - } - - /** Applies the correct skill level for the trial avatar. */ - public void applyTrialSkillLevels() { - this.getSkillLevelMap() - .keySet() - .forEach(skill -> this.setSkillLevel(skill, this.getTrialSkillLevel())); - } - - /** - * @return The weapon to use with the avatar. - */ - public int getTrialWeaponId() { - // Use default data if custom data not available. - if (GameData.getTrialAvatarCustomData().isEmpty()) { - if (GameData.getTrialAvatarDataMap().get(this.getTrialAvatarId()) == null) - return this.getAvatarData().getInitialWeapon(); - - return GameData.getItemDataMap().get(this.getAvatarData().getInitialWeapon() + 100) == null - ? getAvatarData().getInitialWeapon() - : getAvatarData().getInitialWeapon() + 100; // enhanced version of weapon - } - - // Use custom data. - var trialData = GameData.getTrialAvatarCustomData().get(this.getTrialAvatarId()); - if (trialData == null) return 0; - - var trialCustomParams = trialData.getTrialAvatarParamList(); - return trialCustomParams.size() < 2 - ? getAvatarData().getInitialWeapon() - : Integer.parseInt(trialCustomParams.get(1).split(";")[0]); - } - - /** - * @return A list of artifact IDs to use with the avatar. - */ - public List getTrialReliquary() { - // Use default data if custom data not available. - if (GameData.getTrialAvatarCustomData().isEmpty()) { - int trialAvatarTemplateLevel = getTrialTemplate(); - - TrialAvatarTemplateData templateData = - GameData.getTrialAvatarTemplateDataMap().get(trialAvatarTemplateLevel); - return templateData == null ? List.of() : templateData.getTrialReliquaryList(); - } - - // Use custom data. - var trialData = GameData.getTrialAvatarCustomData().get(this.getTrialAvatarId()); - if (trialData == null) return List.of(); - - var trialCustomParams = - GameData.getTrialAvatarCustomData().get(getTrialAvatarId()).getTrialAvatarParamList(); - return trialCustomParams.size() < 3 - ? List.of() - : Stream.of(trialCustomParams.get(2).split(";")).map(Integer::parseInt).toList(); - } - - /** Applies the correct items for the trial avatar. */ - public void applyTrialItems() { - // Use an enhanced version of the weapon if available. - var weapon = new GameItem(this.getTrialWeaponId()); - weapon.setLevel(this.getLevel()); - weapon.setExp(0); - weapon.setPromoteLevel(getMinPromoteLevel(this.getLevel())); - this.getEquips().put(weapon.getEquipSlot(), weapon); - - // Add artifacts for the trial avatar. - this.getTrialReliquary() - .forEach( - id -> { - var reliquaryData = GameData.getTrialReliquaryDataMap().get((int) id); - if (reliquaryData == null) return; - - var relic = new GameItem(reliquaryData.getReliquaryId()); - relic.setLevel(reliquaryData.getLevel()); - relic.setMainPropId(reliquaryData.getMainPropId()); - relic.getAppendPropIdList().addAll(reliquaryData.getAppendPropList()); - this.getEquips().put(relic.getEquipSlot(), relic); - }); - - // Add costume if avatar has a costume. - GameData.getAvatarCostumeDataItemIdMap() - .values() - .forEach( - costumeData -> { - if (costumeData.getCharacterId() != this.getAvatarId()) return; - this.setCostume(costumeData.getId()); - }); - } - - /** Equips the items applied from {@link Avatar#applyTrialItems()}. */ - public void equipTrialItems() { - var player = this.getPlayer(); - - this.getEquips() - .values() - .forEach( - item -> { - item.setEquipCharacter(this.getAvatarId()); - item.setOwner(player); - if (item.getItemData().getEquipType() == EquipType.EQUIP_WEAPON) { - item.setWeaponEntityId(player.getWorld().getNextEntityId(EntityIdType.WEAPON)); - player.sendPacket(new PacketAvatarEquipChangeNotify(this, item)); - } - }); - } - - /** - * Converts this (trial) avatar into a trial info protocol buffer. - * - * @return The trial info protocol buffer. - */ - public TrialAvatarInfo toTrialInfo() { - var trialAvatar = - TrialAvatarInfo.newBuilder() - .setTrialAvatarId(this.getTrialAvatarId()) - .setGrantRecord( - TrialAvatarGrantRecord.newBuilder() - .setGrantReason(this.getGrantReason()) - .setFromParentQuestId(this.getFromParentQuestId())); - - // Check if the avatar is a trial avatar. - if (this.getTrialAvatarId() > 0) { - // Add the artifacts & weapons for the avatar. - trialAvatar.addAllTrialEquipList( - this.getEquips().values().stream().map(GameItem::toProto).toList()); - } - - return trialAvatar.build(); - } - - @PostLoad - private void onLoad() {} - - @PrePersist - private void prePersist() { - this.currentHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - } - - @AllArgsConstructor - @Getter - enum Type { - NORMAL(0), - TRIAL(1); - - final int number; - } -} +package emu.grasscutter.game.avatar; + +import static emu.grasscutter.config.Configuration.GAME_OPTIONS; + +import dev.morphia.annotations.*; +import emu.grasscutter.GameConstants; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.OpenConfigEntry; +import emu.grasscutter.data.binout.OpenConfigEntry.SkillPointModifier; +import emu.grasscutter.data.common.FightPropData; +import emu.grasscutter.data.excels.*; +import emu.grasscutter.data.excels.ItemData.WeaponProperty; +import emu.grasscutter.data.excels.avatar.AvatarData; +import emu.grasscutter.data.excels.avatar.AvatarSkillData; +import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; +import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData.InherentProudSkillOpens; +import emu.grasscutter.data.excels.avatar.AvatarTalentData; +import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData; +import emu.grasscutter.data.excels.reliquary.ReliquaryLevelData; +import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData; +import emu.grasscutter.data.excels.reliquary.ReliquarySetData; +import emu.grasscutter.data.excels.trial.TrialAvatarTemplateData; +import emu.grasscutter.data.excels.weapon.WeaponCurveData; +import emu.grasscutter.data.excels.weapon.WeaponPromoteData; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.entity.EntityAvatar; +import emu.grasscutter.game.inventory.EquipType; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.inventory.ItemType; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.*; +import emu.grasscutter.net.proto.AvatarFetterInfoOuterClass.AvatarFetterInfo; +import emu.grasscutter.net.proto.AvatarInfoOuterClass.AvatarInfo; +import emu.grasscutter.net.proto.AvatarSkillInfoOuterClass.AvatarSkillInfo; +import emu.grasscutter.net.proto.FetterDataOuterClass.FetterData; +import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass; +import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass.ShowAvatarInfo; +import emu.grasscutter.net.proto.ShowEquipOuterClass.ShowEquip; +import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGrantRecord; +import emu.grasscutter.net.proto.TrialAvatarInfoOuterClass.TrialAvatarInfo; +import emu.grasscutter.server.packet.send.*; +import emu.grasscutter.utils.ProtoHelper; +import it.unimi.dsi.fastutil.ints.*; +import java.util.*; +import java.util.stream.Stream; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.val; +import org.bson.types.ObjectId; + +import javax.annotation.Nonnull; + +@Entity(value = "avatars", useDiscriminator = false) +public class Avatar { + @Transient @Getter private final Int2ObjectMap equips; + @Transient @Getter private final Int2FloatOpenHashMap fightProperties; + @Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides; + @Id private ObjectId id; + @Indexed @Getter private int ownerId; // Id of player that this avatar belongs to + @Transient private Player owner; + @Transient @Getter private AvatarData avatarData; + @Transient @Getter private AvatarSkillDepotData skillDepot; + @Transient @Getter private long guid; // Player unique id + @Getter private int avatarId; // Id of avatar + @Getter @Setter private int level = 1; + @Getter @Setter private int exp; + @Getter @Setter private int promoteLevel; + @Getter @Setter private int satiation; // Fullness + @Getter @Setter private int satiationPenalty; // When eating too much + @Getter @Setter private float currentHp; + private float currentEnergy; + @Transient @Getter private Set extraAbilityEmbryos; + + private List fetters; + + private final Map skillLevelMap = new Int2IntArrayMap(7); // Talent levels + + @Transient @Getter + private final Map skillExtraChargeMap = new Int2IntArrayMap(2); // Charges + + @Transient + private final Map proudSkillBonusMap = + new Int2IntArrayMap(2); // Talent bonus levels (from const) + + @Getter private int skillDepotId; + private Set talentIdList; // Constellation id list + @Getter private Set proudSkillList; // Character passives + + @Getter @Setter private int flyCloak; + @Getter @Setter private int costume; + @Getter private int bornTime; + + @Getter @Setter private int fetterLevel = 1; + @Getter @Setter private int fetterExp; + + @Getter @Setter private int nameCardRewardId; + @Getter @Setter private int nameCardId; + + // trial avatar property + @Getter @Setter private int trialAvatarId = 0; + // cannot store to db if grant reason is not integer + @Getter @Setter + private int grantReason = TrialAvatarGrantRecord.GrantReason.GRANT_REASON_INVALID.getNumber(); + + @Getter @Setter private int fromParentQuestId = 0; + // so far no outer class or prop value has information of this, but from packet: + // 1 = normal, 2 = trial avatar + @Getter @Setter private int avatarType = Type.NORMAL.getNumber(); + + @Deprecated // Do not use. Morhpia only! + public Avatar() { + this.equips = new Int2ObjectOpenHashMap<>(); + this.fightProperties = new Int2FloatOpenHashMap(); + this.fightPropOverrides = new Int2FloatOpenHashMap(); + this.extraAbilityEmbryos = new HashSet<>(); + this.fetters = new ArrayList<>(); // TODO Move to avatar + } + + // On creation + public Avatar(int avatarId) { + this(GameData.getAvatarDataMap().get(avatarId)); + } + + public Avatar(AvatarData data) { + this(); + this.avatarId = data.getId(); + this.nameCardRewardId = data.getNameCardRewardId(); + this.nameCardId = data.getNameCardId(); + this.avatarData = data; + this.bornTime = (int) (System.currentTimeMillis() / 1000); + this.flyCloak = 140001; + + this.talentIdList = new HashSet<>(); + this.proudSkillList = new HashSet<>(); + + // Combat properties + Stream.of(FightProperty.values()) + .map(FightProperty::getId) + .filter(id -> (id > 0) && (id < 3000)) + .forEach(id -> this.setFightProperty(id, 0f)); + + // Skill depot + this.setSkillDepotData( + switch (this.avatarId) { + case GameConstants.MAIN_CHARACTER_MALE -> GameData.getAvatarSkillDepotDataMap() + .get(504); // Hack to start with anemo skills + case GameConstants.MAIN_CHARACTER_FEMALE -> GameData.getAvatarSkillDepotDataMap() + .get(704); + default -> data.getSkillDepot(); + }); + + // Set stats + this.recalcStats(); + this.currentHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.currentHp); + this.currentEnergy = 0f; + // Load handler + this.onLoad(); + } + + public static int getMinPromoteLevel(int level) { + if (level > 80) { + return 6; + } else if (level > 70) { + return 5; + } else if (level > 60) { + return 4; + } else if (level > 50) { + return 3; + } else if (level > 40) { + return 2; + } else if (level > 20) { + return 1; + } + return 0; + } + + public Player getPlayer() { + return this.owner; + } + + public ObjectId getObjectId() { + return this.id; + } + + protected void setAvatarData(AvatarData data) { + if (this.avatarData != null) return; + this.avatarData = data; // Used while loading this from the database + } + + public void setOwner(Player player) { + this.owner = player; + this.ownerId = player.getUid(); + this.guid = player.getNextGameGuid(); + } + + public boolean addSatiation(int value) { + if (this.satiation >= 10000) return false; + this.satiation += value; + return true; + } + + public float reduceSatiation(int value) { + if (this.satiation == 0) return 0; + this.satiation -= value; + if (this.satiation < 0) { + this.satiation = 0; + } + return this.satiation; + } + + public float reduceSatiationPenalty(int value) { + if (this.satiationPenalty == 0) return 0; + this.satiationPenalty -= value; + if (this.satiationPenalty < 0) { + this.satiationPenalty = 0; + } + return this.satiationPenalty; + } + + public GameItem getEquipBySlot(EquipType slot) { + return this.getEquips().get(slot.getValue()); + } + + private GameItem getEquipBySlot(int slotId) { + return this.getEquips().get(slotId); + } + + public GameItem getWeapon() { + return this.getEquipBySlot(EquipType.EQUIP_WEAPON); + } + + protected void setSkillDepot(AvatarSkillDepotData skillDepot) { + if (this.skillDepot != null) return; + this.skillDepot = skillDepot; // Used while loading this from the database + } + + /** + * Changes this avatar's skill depot. + * Does not notify the player of the change. + * + * @param skillDepot The new skill depot. + */ + public void setSkillDepotData(AvatarSkillDepotData skillDepot) { + this.setSkillDepotData(skillDepot, false); + } + + /** + * Changes this avatar's skill depot. + * + * @param skillDepot The new skill depot. + * @param notify Whether to notify the player of the change. + */ + public void setSkillDepotData(AvatarSkillDepotData skillDepot, boolean notify) { + // Set id and depot + this.skillDepotId = skillDepot.getId(); + this.skillDepot = skillDepot; + // Add any missing skills + this.skillDepot + .getSkillsAndEnergySkill() + .forEach(skillId -> this.skillLevelMap.putIfAbsent(skillId, 1)); + // Add proud skills + this.proudSkillList.clear(); + skillDepot.getInherentProudSkillOpens().stream() + .filter(openData -> openData.getProudSkillGroupId() > 0) + .filter(openData -> openData.getNeedAvatarPromoteLevel() <= this.getPromoteLevel()) + .mapToInt(openData -> (openData.getProudSkillGroupId() * 100) + 1) + .filter(proudSkillId -> GameData.getProudSkillDataMap().containsKey(proudSkillId)) + .forEach(proudSkillId -> this.proudSkillList.add(proudSkillId)); + this.recalcStats(); + + // Send the depot change notification. + if (notify) this.owner.sendPacket(new PacketAvatarSkillDepotChangeNotify(this)); + } + + /** + * Changes the avatar's element to the target element, if the character has values for it set in the candSkillDepot + * + * @param elementTypeToChange element to change to + * @return false if failed or already using that element, true if it actually changed + */ + public boolean changeElement(@Nonnull ElementType elementTypeToChange) { + var candSkillDepotIdsList = this.avatarData.getCandSkillDepotIds(); + var candSkillDepotIndex = elementTypeToChange.getDepotIndex(); + + // if no candidate skill to change or index out of bound + if (candSkillDepotIdsList == null || + candSkillDepotIndex >= candSkillDepotIdsList.size()) { + return false; + } + + var candSkillDepotId = candSkillDepotIdsList.get(candSkillDepotIndex); + + // Sanity checks for skill depots + val skillDepot = GameData.getAvatarSkillDepotDataMap().get(candSkillDepotId); + if (skillDepot == null || skillDepot.getId() == skillDepotId) { + return false; + } + + // Set skill depot + setSkillDepotData(skillDepot, true); + return true; + } + + public List getFetterList() { + return fetters; + } + + public void setFetterList(List fetterList) { + this.fetters = fetterList; + } + + public void setCurrentEnergy() { + if (GAME_OPTIONS.energyUsage) { + this.setCurrentEnergy(this.currentEnergy); + } + } + + public void setCurrentEnergy(float currentEnergy) { + var depot = this.skillDepot; + if (depot != null && depot.getEnergySkillData() != null) { + ElementType element = depot.getElementType(); + var maxEnergy = depot.getEnergySkillData().getCostElemVal(); + this.setFightProperty(element.getMaxEnergyProp(), maxEnergy); + this.setFightProperty( + element.getCurEnergyProp(), GAME_OPTIONS.energyUsage ? currentEnergy : maxEnergy); + } + } + + public void setCurrentEnergy(FightProperty curEnergyProp, float currentEnergy) { + if (GAME_OPTIONS.energyUsage) { + this.setFightProperty(curEnergyProp, currentEnergy); + this.currentEnergy = currentEnergy; + this.save(); + } + } + + public void setFightProperty(FightProperty prop, float value) { + this.getFightProperties().put(prop.getId(), value); + } + + private void setFightProperty(int id, float value) { + this.getFightProperties().put(id, value); + } + + public void addFightProperty(FightProperty prop, float value) { + this.getFightProperties().put(prop.getId(), getFightProperty(prop) + value); + } + + public float getFightProperty(FightProperty prop) { + return getFightProperties().getOrDefault(prop.getId(), 0f); + } + + public Map + getSkillLevelMap() { // Returns a copy of the skill levels for the current skillDepot. + var map = new Int2IntOpenHashMap(); + this.skillDepot + .getSkillsAndEnergySkill() + .forEach( + skillId -> map.put(skillId, this.skillLevelMap.putIfAbsent(skillId, 1).intValue())); + return map; + } + + // Returns a copy of the skill bonus levels for the current skillDepot, capped to avoid invalid + // levels. + public Map getProudSkillBonusMap() { + var map = new Int2IntArrayMap(); + this.skillDepot + .getSkillsAndEnergySkill() + .forEach( + skillId -> { + val skillData = GameData.getAvatarSkillDataMap().get(skillId); + if (skillData == null) return; + int proudSkillGroupId = skillData.getProudSkillGroupId(); + int bonus = this.proudSkillBonusMap.getOrDefault(proudSkillGroupId, 0); + int maxLevel = GameData.getProudSkillGroupMaxLevel(proudSkillGroupId); + int curLevel = this.skillLevelMap.getOrDefault(skillId, 0); + if (maxLevel > 0) { + bonus = Math.min(bonus, maxLevel - curLevel); + } + map.put(proudSkillGroupId, bonus); + }); + return map; + } + + public IntSet getTalentIdList() { // Returns a copy of the unlocked constellations for the current + // skillDepot. + var talents = new IntOpenHashSet(this.getSkillDepot().getTalents()); + talents.removeIf(id -> !this.talentIdList.contains(id)); + return talents; + } + + public int getCoreProudSkillLevel() { + var lockedTalents = new IntOpenHashSet(this.getSkillDepot().getTalents()); + lockedTalents.removeAll(this.getTalentIdList()); + // One below the lowest locked talent, or 6 if there are no locked talents. + return lockedTalents.intStream().map(i -> i % 10).min().orElse(7) - 1; + } + + public boolean equipItem(GameItem item, boolean shouldRecalc) { + // Sanity check equip type + EquipType itemEquipType = item.getItemData().getEquipType(); + if (itemEquipType == EquipType.EQUIP_NONE) { + return false; + } + + // Check if other avatars have this item equipped + Avatar otherAvatar = getPlayer().getAvatars().getAvatarById(item.getEquipCharacter()); + if (otherAvatar != null) { + // Unequip other avatar's item + if (otherAvatar.unequipItem(item.getItemData().getEquipType())) { + getPlayer() + .sendPacket( + new PacketAvatarEquipChangeNotify(otherAvatar, item.getItemData().getEquipType())); + } + // Swap with other avatar + if (getEquips().containsKey(itemEquipType.getValue())) { + GameItem toSwap = this.getEquipBySlot(itemEquipType); + otherAvatar.equipItem(toSwap, false); + } + // Recalc + otherAvatar.recalcStats(); + } else if (getEquips().containsKey(itemEquipType.getValue())) { + // Unequip item in current slot if it exists + unequipItem(itemEquipType); + } + + // Set equip + getEquips().put(itemEquipType.getValue(), item); + + if (itemEquipType == EquipType.EQUIP_WEAPON && getPlayer().getWorld() != null) { + item.setWeaponEntityId(this.getPlayer().getWorld().getNextEntityId(EntityIdType.WEAPON)); + } + + item.setEquipCharacter(this.getAvatarId()); + item.save(); + + if (this.getPlayer().hasSentLoginPackets()) { + this.getPlayer().sendPacket(new PacketAvatarEquipChangeNotify(this, item)); + } + + if (shouldRecalc) { + this.recalcStats(); + } + + return true; + } + + public boolean unequipItem(EquipType slot) { + GameItem item = getEquips().remove(slot.getValue()); + + if (item != null) { + item.setEquipCharacter(0); + item.save(); + return true; + } + + return false; + } + + public void recalcStats() { + recalcStats(false); + } + + public void recalcStats(boolean forceSendAbilityChange) { + // Setup + var data = this.getAvatarData(); + var promoteData = + GameData.getAvatarPromoteData(data.getAvatarPromoteId(), this.getPromoteLevel()); + var setMap = new Int2IntOpenHashMap(); + + // Extra ability embryos + Set prevExtraAbilityEmbryos = this.getExtraAbilityEmbryos(); + this.extraAbilityEmbryos = new HashSet<>(); + + // Fetters + this.setFetterList(data.getFetters()); + this.setNameCardRewardId(data.getNameCardRewardId()); + this.setNameCardId(data.getNameCardId()); + + // Get hp percent, set to 100% if none + float hpPercent = + this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 + ? 1f + : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) + / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + + // Store current energy value for later + float currentEnergy = + (this.getSkillDepot() != null) + ? this.getFightProperty(this.getSkillDepot().getElementType().getCurEnergyProp()) + : 0f; + + // Clear properties + this.getFightProperties().clear(); + + // Base stats + this.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, data.getBaseHp(this.getLevel())); + this.setFightProperty( + FightProperty.FIGHT_PROP_BASE_ATTACK, data.getBaseAttack(this.getLevel())); + this.setFightProperty( + FightProperty.FIGHT_PROP_BASE_DEFENSE, data.getBaseDefense(this.getLevel())); + this.setFightProperty(FightProperty.FIGHT_PROP_CRITICAL, data.getBaseCritical()); + this.setFightProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, data.getBaseCriticalHurt()); + this.setFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, 1f); + + if (promoteData != null) { + for (FightPropData fightPropData : promoteData.getAddProps()) { + this.addFightProperty(fightPropData.getProp(), fightPropData.getValue()); + } + } + + // Set energy usage + setCurrentEnergy(currentEnergy); + + // Artifacts + for (int slotId = 1; slotId <= 5; slotId++) { + // Get artifact + GameItem equip = this.getEquipBySlot(slotId); + if (equip == null) { + continue; + } + // Artifact main stat + ReliquaryMainPropData mainPropData = + GameData.getReliquaryMainPropDataMap().get(equip.getMainPropId()); + if (mainPropData != null) { + ReliquaryLevelData levelData = + GameData.getRelicLevelData(equip.getItemData().getRankLevel(), equip.getLevel()); + if (levelData != null) { + this.addFightProperty( + mainPropData.getFightProp(), levelData.getPropValue(mainPropData.getFightProp())); + } + } + // Artifact sub stats + for (int appendPropId : equip.getAppendPropIdList()) { + ReliquaryAffixData affixData = GameData.getReliquaryAffixDataMap().get(appendPropId); + if (affixData != null) { + this.addFightProperty(affixData.getFightProp(), affixData.getPropValue()); + } + } + // Set bonus + if (equip.getItemData().getSetId() > 0) { + setMap.addTo(equip.getItemData().getSetId(), 1); + } + } + + // Set stuff + setMap.forEach( + (setId, amount) -> { + ReliquarySetData setData = GameData.getReliquarySetDataMap().get((int) setId); + if (setData == null) return; + + // Calculate how many items are from the set + // Add affix data from set bonus + val setNeedNum = setData.getSetNeedNum(); + for (int setIndex = 0; setIndex < setNeedNum.length; setIndex++) { + if (amount < setNeedNum[setIndex]) break; + + int affixId = (setData.getEquipAffixId() * 10) + setIndex; + EquipAffixData affix = GameData.getEquipAffixDataMap().get(affixId); + if (affix == null) { + continue; + } + + // Add properties from this affix to our avatar + for (FightPropData prop : affix.getAddProps()) { + this.addFightProperty(prop.getProp(), prop.getValue()); + } + + // Add any skill strings from this affix + this.addToExtraAbilityEmbryos(affix.getOpenConfig(), true); + } + }); + + // Weapon + GameItem weapon = this.getWeapon(); + if (weapon != null) { + // Add stats + WeaponCurveData curveData = GameData.getWeaponCurveDataMap().get(weapon.getLevel()); + if (curveData != null) { + for (WeaponProperty weaponProperty : weapon.getItemData().getWeaponProperties()) { + this.addFightProperty( + weaponProperty.getPropType(), + weaponProperty.getInitValue() * curveData.getMultByProp(weaponProperty.getType())); + } + } + // Weapon promotion stats + WeaponPromoteData wepPromoteData = + GameData.getWeaponPromoteData( + weapon.getItemData().getWeaponPromoteId(), weapon.getPromoteLevel()); + if (wepPromoteData != null) { + for (FightPropData prop : wepPromoteData.getAddProps()) { + if (prop.getValue() == 0f || prop.getProp() == null) { + continue; + } + this.addFightProperty(prop.getProp(), prop.getValue()); + } + } + // Add weapon skill from affixes + if (weapon.getAffixes() != null && weapon.getAffixes().size() > 0) { + // Weapons usually dont have more than one affix but just in case... + for (int af : weapon.getAffixes()) { + if (af == 0) { + continue; + } + // Calculate affix id + int affixId = (af * 10) + weapon.getRefinement(); + EquipAffixData affix = GameData.getEquipAffixDataMap().get(affixId); + if (affix == null) { + continue; + } + + // Add properties from this affix to our avatar + for (FightPropData prop : affix.getAddProps()) { + this.addFightProperty(prop.getProp(), prop.getValue()); + } + + // Add any skill strings from this affix + this.addToExtraAbilityEmbryos(affix.getOpenConfig(), true); + } + } + } + + // Add proud skills and unlock them if needed + AvatarSkillDepotData skillDepot = + GameData.getAvatarSkillDepotDataMap().get(this.getSkillDepotId()); + this.getProudSkillList().clear(); + for (InherentProudSkillOpens openData : skillDepot.getInherentProudSkillOpens()) { + if (openData.getProudSkillGroupId() == 0) { + continue; + } + if (openData.getNeedAvatarPromoteLevel() <= this.getPromoteLevel()) { + int proudSkillId = (openData.getProudSkillGroupId() * 100) + 1; + if (GameData.getProudSkillDataMap().containsKey(proudSkillId)) { + this.getProudSkillList().add(proudSkillId); + } + } + } + + // Proud skills + for (int proudSkillId : this.getProudSkillList()) { + ProudSkillData proudSkillData = GameData.getProudSkillDataMap().get(proudSkillId); + if (proudSkillData == null) { + continue; + } + + // Add properties from this proud skill to our avatar + for (FightPropData prop : proudSkillData.getAddProps()) { + this.addFightProperty(prop.getProp(), prop.getValue()); + } + + // Add any embryos from this proud skill + this.addToExtraAbilityEmbryos(proudSkillData.getOpenConfig()); + } + + // Constellations + this.getTalentIdList() + .intStream() + .mapToObj(GameData.getAvatarTalentDataMap()::get) + .filter(Objects::nonNull) + .map(AvatarTalentData::getOpenConfig) + .filter(Objects::nonNull) + .forEach(this::addToExtraAbilityEmbryos); + // Add any skill strings from this constellation + + // Set % stats + FightProperty.forEachCompoundProperty( + c -> + this.setFightProperty( + c.getResult(), + this.getFightProperty(c.getFlat()) + + (this.getFightProperty(c.getBase()) + * (1f + this.getFightProperty(c.getPercent()))))); + + // Reapply all overrides + this.fightProperties.putAll(this.fightPropOverrides); + + // Set current hp + this.setFightProperty( + FightProperty.FIGHT_PROP_CUR_HP, + this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent); + + // Packet + if (getPlayer() != null && getPlayer().hasSentLoginPackets()) { + // Update stats for client + getPlayer().sendPacket(new PacketAvatarFightPropNotify(this)); + // Update client abilities + EntityAvatar entity = this.getAsEntity(); + if (entity != null + && (!this.getExtraAbilityEmbryos().equals(prevExtraAbilityEmbryos) + || forceSendAbilityChange)) { + getPlayer().sendPacket(new PacketAbilityChangeNotify(entity)); + } + } + } + + public void addToExtraAbilityEmbryos(String openConfig) { + this.addToExtraAbilityEmbryos(openConfig, false); + } + + public void addToExtraAbilityEmbryos(String openConfig, boolean forceAdd) { + if (openConfig == null || openConfig.length() == 0) { + return; + } + + OpenConfigEntry entry = GameData.getOpenConfigEntries().get(openConfig); + if (entry == null) { + if (forceAdd) { + // Add config string to ability skill list anyways + this.getExtraAbilityEmbryos().add(openConfig); + } + return; + } + + if (entry.getAddAbilities() != null) { + for (String ability : entry.getAddAbilities()) { + this.getExtraAbilityEmbryos().add(ability); + } + } + } + + public void calcConstellation(OpenConfigEntry entry, boolean notifyClient) { + if (entry == null) return; + if (this.getPlayer() == null) notifyClient = false; + + // Check if new constellation adds +3 to a skill level + if (this.calcConstellationExtraLevels(entry) && notifyClient) { + // Packet + this.getPlayer() + .sendPacket(new PacketProudSkillExtraLevelNotify(this, entry.getExtraTalentIndex())); + } + // Check if new constellation adds skill charges + if (this.calcConstellationExtraCharges(entry) && notifyClient) { + // Packet + Stream.of(entry.getSkillPointModifiers()) + .mapToInt(SkillPointModifier::getSkillId) + .forEach( + skillId -> { + this.getPlayer() + .sendPacket( + new PacketAvatarSkillMaxChargeCountNotify( + this, skillId, this.getSkillExtraChargeMap().getOrDefault(skillId, 0))); + }); + } + } + + public void recalcConstellations() { + // Clear first + this.proudSkillBonusMap.clear(); + this.skillExtraChargeMap.clear(); + + // Sanity checks + if (this.avatarData == null || this.skillDepot == null) { + return; + } + + this.getTalentIdList() + .intStream() + .mapToObj(GameData.getAvatarTalentDataMap()::get) + .filter(Objects::nonNull) + .map(AvatarTalentData::getOpenConfig) + .filter(Objects::nonNull) + .filter(openConfig -> openConfig.length() > 0) + .map(GameData.getOpenConfigEntries()::get) + .filter(Objects::nonNull) + .forEach(e -> this.calcConstellation(e, false)); + } + + private boolean calcConstellationExtraCharges(OpenConfigEntry entry) { + var skillPointModifiers = entry.getSkillPointModifiers(); + if (skillPointModifiers == null) return false; + + for (var mod : skillPointModifiers) { + AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(mod.getSkillId()); + + if (skillData == null) continue; + + int charges = skillData.getMaxChargeNum() + mod.getDelta(); + + this.getSkillExtraChargeMap().put(mod.getSkillId(), charges); + } + return true; + } + + private boolean calcConstellationExtraLevels(OpenConfigEntry entry) { + int skillId = + switch (entry.getExtraTalentIndex()) { + case 9 -> this.skillDepot.getEnergySkill(); // Ult skill + case 2 -> (this.skillDepot.getSkills().size() >= 2) + ? this.skillDepot.getSkills().get(1) + : 0; // E skill + default -> 0; + }; + // Sanity check + if (skillId == 0) { + return false; + } + + // Get proud skill group id + AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(skillId); + + if (skillData == null) { + return false; + } + + // Add to bonus list + this.addProudSkillLevelBonus(skillData.getProudSkillGroupId(), 3); + return true; + } + + private int addProudSkillLevelBonus(int proudSkillGroupId, int bonus) { + return this.proudSkillBonusMap.compute( + proudSkillGroupId, (k, v) -> (v == null) ? bonus : v + bonus); + } + + public boolean upgradeSkill(int skillId) { + AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(skillId); + if (skillData == null) return false; + + // Get data for next skill level + int newLevel = this.skillLevelMap.getOrDefault(skillId, 0) + 1; + if (newLevel > 10) return false; + + // Proud skill data + int proudSkillId = (skillData.getProudSkillGroupId() * 100) + newLevel; + ProudSkillData proudSkill = GameData.getProudSkillDataMap().get(proudSkillId); + if (proudSkill == null) return false; + + // Make sure break level is correct + if (this.getPromoteLevel() < proudSkill.getBreakLevel()) return false; + + // Pay materials and mora if possible + if (!this.getPlayer().getInventory().payItems(proudSkill.getTotalCostItems())) return false; + + // Upgrade skill + this.setSkillLevel(skillId, newLevel); + return true; + } + + public boolean setSkillLevel(int skillId, int level) { + if (level < 0 || level > 15) return false; + var validLevels = GameData.getAvatarSkillLevels(skillId); + if (validLevels != null && !validLevels.contains(level)) return false; + int oldLevel = + this.skillLevelMap.getOrDefault( + skillId, 0); // just taking the return value of put would have null concerns + this.skillLevelMap.put(skillId, level); + this.save(); + + // Packet + val player = this.getPlayer(); + if (player != null) { + player.sendPacket(new PacketAvatarSkillChangeNotify(this, skillId, oldLevel, level)); + player.sendPacket(new PacketAvatarSkillUpgradeRsp(this, skillId, oldLevel, level)); + } + return true; + } + + public boolean unlockConstellation() { + return this.unlockConstellation(false); + } + + public boolean unlockConstellation(boolean skipPayment) { + int currentTalentLevel = this.getCoreProudSkillLevel(); + int talentId = this.skillDepot.getTalents().get(currentTalentLevel); + return this.unlockConstellation(talentId, skipPayment); + } + + public boolean unlockConstellation(int talentId) { + return unlockConstellation(talentId, false); + } + + public boolean unlockConstellation(int talentId, boolean skipPayment) { + // Get talent + AvatarTalentData talentData = GameData.getAvatarTalentDataMap().get(talentId); + if (talentData == null) return false; + var player = this.getPlayer(); + + // Pay constellation item if possible + if (!skipPayment + && (player != null) + && !player.getInventory().payItem(talentData.getMainCostItemId(), 1)) { + return false; + } + + // Apply + recalc + this.talentIdList.add(talentData.getId()); + + // Packet + if (player != null) { + player.sendPacket(new PacketAvatarUnlockTalentNotify(this, talentId)); + player.sendPacket(new PacketUnlockAvatarTalentRsp(this, talentId)); + } + + // Proud skill bonus map (Extra skills) + this.calcConstellation(GameData.getOpenConfigEntries().get(talentData.getOpenConfig()), true); + + // Recalc + save avatar + this.recalcStats(true); + this.save(); + return true; + } + + public void forceConstellationLevel(int level) { + if (level > 6) return; // Sanity check + + if (level < 0) { // Special case for resetConst to remove inactive depots too + this.talentIdList.clear(); + this.recalcStats(); + this.save(); + return; + } + this.talentIdList.removeAll( + this.getTalentIdList()); // Only remove constellations from active depot + for (int i = 0; i < level; i++) this.unlockConstellation(true); + this.recalcStats(); + this.save(); + } + + public boolean sendSkillExtraChargeMap() { + val map = this.getSkillExtraChargeMap(); + if (map.isEmpty()) return false; + this.getPlayer() + .sendPacket( + new PacketAvatarSkillInfoNotify( + this.guid, + new Int2IntArrayMap( + map))); // TODO: Remove this allocation when updating interfaces to FastUtils + // later + return true; + } + + public EntityAvatar getAsEntity() { + for (EntityAvatar entity : getPlayer().getTeamManager().getActiveTeam()) { + if (entity.getAvatar() == this) { + return entity; + } + } + return null; + } + + public int getEntityId() { + EntityAvatar entity = getAsEntity(); + return entity != null ? entity.getId() : 0; + } + + public void save() { + DatabaseHelper.saveAvatar(this); + } + + public AvatarInfo toProto() { + int fetterLevel = this.getFetterLevel(); + AvatarFetterInfo.Builder avatarFetter = AvatarFetterInfo.newBuilder().setExpLevel(fetterLevel); + + if (fetterLevel != 10) { + avatarFetter.setExpNumber(this.getFetterExp()); + } + + if (this.fetters != null) { + this.fetters.forEach( + fetterId -> + avatarFetter.addFetterList( + FetterData.newBuilder() + .setFetterId(fetterId) + .setFetterState(FetterState.FINISH.getValue()))); + } + + int cardId = this.getNameCardId(); + + if (this.getPlayer().getNameCardList().contains(cardId)) { + avatarFetter.addRewardedFetterLevelList(10); + } + + AvatarInfo.Builder avatarInfo = + AvatarInfo.newBuilder() + .setAvatarId(this.getAvatarId()) + .setGuid(this.getGuid()) + .setLifeState(1) + .addAllTalentIdList(this.getTalentIdList()) + .putAllFightPropMap(this.getFightProperties()) + .setSkillDepotId(this.getSkillDepotId()) + .setCoreProudSkillLevel(this.getCoreProudSkillLevel()) + .putAllSkillLevelMap(this.getSkillLevelMap()) + .addAllInherentProudSkillList(this.getProudSkillList()) + .putAllProudSkillExtraLevelMap(this.getProudSkillBonusMap()) + .setAvatarType(1) + .setBornTime(this.getBornTime()) + .setFetterInfo(avatarFetter) + .setWearingFlycloakId(this.getFlyCloak()) + .setCostumeId(this.getCostume()); + + this.getSkillExtraChargeMap() + .forEach( + (skillId, count) -> + avatarInfo.putSkillMap( + skillId, AvatarSkillInfo.newBuilder().setMaxChargeCount(count).build())); + + this.getEquips().forEach((k, item) -> avatarInfo.addEquipGuidList(item.getGuid())); + + avatarInfo.putPropMap( + PlayerProperty.PROP_LEVEL.getId(), + ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel())); + avatarInfo.putPropMap( + PlayerProperty.PROP_EXP.getId(), + ProtoHelper.newPropValue(PlayerProperty.PROP_EXP, this.getExp())); + avatarInfo.putPropMap( + PlayerProperty.PROP_BREAK_LEVEL.getId(), + ProtoHelper.newPropValue(PlayerProperty.PROP_BREAK_LEVEL, this.getPromoteLevel())); + avatarInfo.putPropMap( + PlayerProperty.PROP_SATIATION_VAL.getId(), + ProtoHelper.newPropValue(PlayerProperty.PROP_SATIATION_VAL, this.getSatiation())); + avatarInfo.putPropMap( + PlayerProperty.PROP_SATIATION_PENALTY_TIME.getId(), + ProtoHelper.newPropValue( + PlayerProperty.PROP_SATIATION_PENALTY_TIME, this.getSatiationPenalty())); + + return avatarInfo.build(); + } + + // used only in character showcase + public ShowAvatarInfo toShowAvatarInfoProto() { + AvatarFetterInfo.Builder avatarFetter = + AvatarFetterInfo.newBuilder().setExpLevel(this.getFetterLevel()); + + ShowAvatarInfo.Builder showAvatarInfo = + ShowAvatarInfoOuterClass.ShowAvatarInfo.newBuilder() + .setAvatarId(avatarId) + .addAllTalentIdList(this.getTalentIdList()) + .putAllFightPropMap(this.getFightProperties()) + .setSkillDepotId(this.getSkillDepotId()) + .setCoreProudSkillLevel(this.getCoreProudSkillLevel()) + .addAllInherentProudSkillList(this.getProudSkillList()) + .putAllSkillLevelMap(this.getSkillLevelMap()) + .putAllProudSkillExtraLevelMap(this.getProudSkillBonusMap()) + .setFetterInfo(avatarFetter) + .setCostumeId(this.getCostume()); + + showAvatarInfo.putPropMap( + PlayerProperty.PROP_LEVEL.getId(), + ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel())); + showAvatarInfo.putPropMap( + PlayerProperty.PROP_EXP.getId(), + ProtoHelper.newPropValue(PlayerProperty.PROP_EXP, this.getExp())); + showAvatarInfo.putPropMap( + PlayerProperty.PROP_BREAK_LEVEL.getId(), + ProtoHelper.newPropValue(PlayerProperty.PROP_BREAK_LEVEL, this.getPromoteLevel())); + showAvatarInfo.putPropMap( + PlayerProperty.PROP_SATIATION_VAL.getId(), + ProtoHelper.newPropValue(PlayerProperty.PROP_SATIATION_VAL, this.getSatiation())); + showAvatarInfo.putPropMap( + PlayerProperty.PROP_SATIATION_PENALTY_TIME.getId(), + ProtoHelper.newPropValue( + PlayerProperty.PROP_SATIATION_PENALTY_TIME, this.getSatiationPenalty())); + int maxStamina = this.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); + showAvatarInfo.putPropMap( + PlayerProperty.PROP_MAX_STAMINA.getId(), + ProtoHelper.newPropValue(PlayerProperty.PROP_MAX_STAMINA, maxStamina)); + + for (GameItem item : this.getEquips().values()) { + if (item.getItemType() == ItemType.ITEM_RELIQUARY) { + showAvatarInfo.addEquipList( + ShowEquip.newBuilder() + .setItemId(item.getItemId()) + .setReliquary(item.toReliquaryProto())); + } else if (item.getItemType() == ItemType.ITEM_WEAPON) { + showAvatarInfo.addEquipList( + ShowEquip.newBuilder().setItemId(item.getItemId()).setWeapon(item.toWeaponProto())); + } + } + + return showAvatarInfo.build(); + } + + /** + * Converts this avatar into a trial avatar. + * + * @param level The avatar's level. + * @param avatarId The ID of the avatar. + * @param grantReason The reason for granting the avatar. + * @param questId The ID of the quest that granted the avatar. + */ + public void setTrialAvatarInfo( + int level, int avatarId, TrialAvatarGrantRecord.GrantReason grantReason, int questId) { + this.setLevel(level); + this.setPromoteLevel(getMinPromoteLevel(level)); + this.setTrialAvatarId(avatarId); + this.setGrantReason(grantReason.getNumber()); + this.setFromParentQuestId(questId); + this.setAvatarType(Type.TRIAL.getNumber()); + this.applyTrialSkillLevels(); + this.applyTrialItems(); + } + + /** + * Gets the gear template based on the avatar's level. + * + * @return The avatar's template. + */ + private int getTrialTemplate() { + return this.getLevel() <= 9 + ? 1 + : (int) + (Math.floor(this.getLevel() / 10f) * 10); // round trial level to fit template levels + } + + /** + * @return The level to be used for the avatar's skills (talents). + */ + public int getTrialSkillLevel() { + // Use default data if custom data not available. + if (GameData.getTrialAvatarCustomData().isEmpty()) { + var template = getTrialTemplate(); // round trial level to fit template levels + + var templateData = GameData.getTrialAvatarTemplateDataMap().get(template); + return templateData == null ? 1 : templateData.getTrialAvatarSkillLevel(); + } + + // Use custom data. + var trialData = GameData.getTrialAvatarCustomData().get(this.getTrialAvatarId()); + if (trialData == null) return 1; + + return trialData.getCoreProudSkillLevel(); // enhanced version of weapon + } + + /** Applies the correct skill level for the trial avatar. */ + public void applyTrialSkillLevels() { + this.getSkillLevelMap() + .keySet() + .forEach(skill -> this.setSkillLevel(skill, this.getTrialSkillLevel())); + } + + /** + * @return The weapon to use with the avatar. + */ + public int getTrialWeaponId() { + // Use default data if custom data not available. + if (GameData.getTrialAvatarCustomData().isEmpty()) { + if (GameData.getTrialAvatarDataMap().get(this.getTrialAvatarId()) == null) + return this.getAvatarData().getInitialWeapon(); + + return GameData.getItemDataMap().get(this.getAvatarData().getInitialWeapon() + 100) == null + ? getAvatarData().getInitialWeapon() + : getAvatarData().getInitialWeapon() + 100; // enhanced version of weapon + } + + // Use custom data. + var trialData = GameData.getTrialAvatarCustomData().get(this.getTrialAvatarId()); + if (trialData == null) return 0; + + var trialCustomParams = trialData.getTrialAvatarParamList(); + return trialCustomParams.size() < 2 + ? getAvatarData().getInitialWeapon() + : Integer.parseInt(trialCustomParams.get(1).split(";")[0]); + } + + /** + * @return A list of artifact IDs to use with the avatar. + */ + public List getTrialReliquary() { + // Use default data if custom data not available. + if (GameData.getTrialAvatarCustomData().isEmpty()) { + int trialAvatarTemplateLevel = getTrialTemplate(); + + TrialAvatarTemplateData templateData = + GameData.getTrialAvatarTemplateDataMap().get(trialAvatarTemplateLevel); + return templateData == null ? List.of() : templateData.getTrialReliquaryList(); + } + + // Use custom data. + var trialData = GameData.getTrialAvatarCustomData().get(this.getTrialAvatarId()); + if (trialData == null) return List.of(); + + var trialCustomParams = + GameData.getTrialAvatarCustomData().get(getTrialAvatarId()).getTrialAvatarParamList(); + return trialCustomParams.size() < 3 + ? List.of() + : Stream.of(trialCustomParams.get(2).split(";")).map(Integer::parseInt).toList(); + } + + /** Applies the correct items for the trial avatar. */ + public void applyTrialItems() { + // Use an enhanced version of the weapon if available. + var weapon = new GameItem(this.getTrialWeaponId()); + weapon.setLevel(this.getLevel()); + weapon.setExp(0); + weapon.setPromoteLevel(getMinPromoteLevel(this.getLevel())); + this.getEquips().put(weapon.getEquipSlot(), weapon); + + // Add artifacts for the trial avatar. + this.getTrialReliquary() + .forEach( + id -> { + var reliquaryData = GameData.getTrialReliquaryDataMap().get((int) id); + if (reliquaryData == null) return; + + var relic = new GameItem(reliquaryData.getReliquaryId()); + relic.setLevel(reliquaryData.getLevel()); + relic.setMainPropId(reliquaryData.getMainPropId()); + relic.getAppendPropIdList().addAll(reliquaryData.getAppendPropList()); + this.getEquips().put(relic.getEquipSlot(), relic); + }); + + // Add costume if avatar has a costume. + GameData.getAvatarCostumeDataItemIdMap() + .values() + .forEach( + costumeData -> { + if (costumeData.getCharacterId() != this.getAvatarId()) return; + this.setCostume(costumeData.getId()); + }); + } + + /** Equips the items applied from {@link Avatar#applyTrialItems()}. */ + public void equipTrialItems() { + var player = this.getPlayer(); + + this.getEquips() + .values() + .forEach( + item -> { + item.setEquipCharacter(this.getAvatarId()); + item.setOwner(player); + if (item.getItemData().getEquipType() == EquipType.EQUIP_WEAPON) { + item.setWeaponEntityId(player.getWorld().getNextEntityId(EntityIdType.WEAPON)); + player.sendPacket(new PacketAvatarEquipChangeNotify(this, item)); + } + }); + } + + /** + * Converts this (trial) avatar into a trial info protocol buffer. + * + * @return The trial info protocol buffer. + */ + public TrialAvatarInfo toTrialInfo() { + var trialAvatar = + TrialAvatarInfo.newBuilder() + .setTrialAvatarId(this.getTrialAvatarId()) + .setGrantRecord( + TrialAvatarGrantRecord.newBuilder() + .setGrantReason(this.getGrantReason()) + .setFromParentQuestId(this.getFromParentQuestId())); + + // Check if the avatar is a trial avatar. + if (this.getTrialAvatarId() > 0) { + // Add the artifacts & weapons for the avatar. + trialAvatar.addAllTrialEquipList( + this.getEquips().values().stream().map(GameItem::toProto).toList()); + } + + return trialAvatar.build(); + } + + @PostLoad + private void onLoad() {} + + @PrePersist + private void prePersist() { + this.currentHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + } + + @AllArgsConstructor + @Getter + enum Type { + NORMAL(0), + TRIAL(1); + + final int number; + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/BasicDungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/BasicDungeonSettleListener.java index 68d2d87c8..24e65d841 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/BasicDungeonSettleListener.java +++ b/src/main/java/emu/grasscutter/game/dungeons/BasicDungeonSettleListener.java @@ -1,14 +1,18 @@ -package emu.grasscutter.game.dungeons; - -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.server.packet.send.PacketDungeonSettleNotify; -import emu.grasscutter.utils.Utils; - -public class BasicDungeonSettleListener implements DungeonSettleListener { - - @Override - public void onDungeonSettle(Scene scene) { - scene.setAutoCloseTime(Utils.getCurrentSeconds() + 1000); - scene.broadcastPacket(new PacketDungeonSettleNotify(scene.getChallenge())); - } -} +package emu.grasscutter.game.dungeons; + +import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult; +import emu.grasscutter.server.packet.send.PacketDungeonSettleNotify; + +public class BasicDungeonSettleListener implements DungeonSettleListener { + + @Override + public void onDungeonSettle(DungeonManager dungeonManager, BaseDungeonResult.DungeonEndReason endReason) { + var scene = dungeonManager.getScene(); + var dungeonData = dungeonManager.getDungeonData(); + var time = scene.getSceneTimeSeconds() - dungeonManager.getStartSceneTime() ; + // TODO time taken and chests handling + DungeonEndStats stats = new DungeonEndStats(scene.getKilledMonsterCount(), time, 0, endReason); + + scene.broadcastPacket(new PacketDungeonSettleNotify(new BaseDungeonResult(dungeonData, stats))); + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java index d97668bbd..96b5e3cef 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java @@ -1,332 +1,314 @@ -package emu.grasscutter.game.dungeons; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.common.ItemParamData; -import emu.grasscutter.data.excels.dungeon.DungeonData; -import emu.grasscutter.data.excels.dungeon.DungeonPassConfigData; -import emu.grasscutter.game.activity.trialavatar.TrialAvatarActivityHandler; -import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult; -import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.props.ActivityType; -import emu.grasscutter.game.props.WatcherTriggerType; -import emu.grasscutter.game.quest.enums.LogicType; -import emu.grasscutter.game.quest.enums.QuestContent; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.data.ScriptArgs; -import emu.grasscutter.server.packet.send.PacketDungeonWayPointNotify; -import emu.grasscutter.server.packet.send.PacketGadgetAutoPickDropInfoNotify; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.Utils; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import javax.annotation.Nullable; -import lombok.Getter; -import lombok.NonNull; -import lombok.val; - -/** - * TODO handle time limits TODO handle respawn points TODO handle team wipes and respawns TODO check - * monster level and levelConfigMap - */ -public class DungeonManager { - - @Getter private final Scene scene; - @Getter private final DungeonData dungeonData; - @Getter private final DungeonPassConfigData passConfigData; - - @Getter private final int[] finishedConditions; - private final IntSet rewardedPlayers = new IntOpenHashSet(); - private final Set activeDungeonWayPoints = new HashSet<>(); - private boolean ended = false; - private int newestWayPoint = 0; - @Getter private int startSceneTime = 0; - - DungeonTrialTeam trialTeam = null; - - public DungeonManager(@NonNull Scene scene, @NonNull DungeonData dungeonData) { - this.scene = scene; - this.dungeonData = dungeonData; - this.passConfigData = GameData.getDungeonPassConfigDataMap().get(dungeonData.getPassCond()); - this.finishedConditions = new int[passConfigData.getConds().size()]; - this.scene.setDungeonManager(this); - } - - public void triggerEvent(DungeonPassConditionType conditionType, int... params) { - if (ended) { - return; - } - for (int i = 0; i < passConfigData.getConds().size(); i++) { - var cond = passConfigData.getConds().get(i); - if (conditionType == cond.getCondType()) { - if (getScene().getWorld().getServer().getDungeonSystem().triggerCondition(cond, params)) { - finishedConditions[i] = 1; - } - } - } - - if (isFinishedSuccessfully()) { - finishDungeon(); - } - } - - public boolean isFinishedSuccessfully() { - return LogicType.calculate(passConfigData.getLogicType(), finishedConditions); - } - - public int getLevelForMonster(int id) { - // TODO should use levelConfigMap? and how? - return dungeonData.getShowLevel(); - } - - public boolean activateRespawnPoint(int pointId) { - val respawnPoint = GameData.getScenePointEntryById(scene.getId(), pointId); - - if (respawnPoint == null) { - Grasscutter.getLogger().warn("trying to activate unknown respawn point {}", pointId); - return false; - } - - scene.broadcastPacket( - new PacketDungeonWayPointNotify( - activeDungeonWayPoints.add(pointId), activeDungeonWayPoints)); - newestWayPoint = pointId; - - Grasscutter.getLogger().debug("[unimplemented respawn] activated respawn point {}", pointId); - return true; - } - - @Nullable public Position getRespawnLocation() { - if (newestWayPoint == 0) { // validity is checked before setting it, so if != 0 its always valid - return null; - } - val pointData = GameData.getScenePointEntryById(scene.getId(), newestWayPoint).getPointData(); - return pointData.getTranPos() != null ? pointData.getTranPos() : pointData.getPos(); - } - - public Position getRespawnRotation() { - if (newestWayPoint == 0) { // validity is checked before setting it, so if != 0 its always valid - return null; - } - val pointData = GameData.getScenePointEntryById(scene.getId(), newestWayPoint).getPointData(); - return pointData.getRot() != null ? pointData.getRot() : null; - } - - public boolean getStatueDrops(Player player, boolean useCondensed, int groupId) { - if (!isFinishedSuccessfully() - || dungeonData.getRewardPreviewData() == null - || dungeonData.getRewardPreviewData().getPreviewItems().length == 0) { - return false; - } - - // Already rewarded - if (rewardedPlayers.contains(player.getUid())) { - return false; - } - - if (!handleCost(player, useCondensed)) { - return false; - } - - // Get and roll rewards. - List rewards = new ArrayList<>(this.rollRewards(useCondensed)); - // Add rewards to player and send notification. - player.getInventory().addItems(rewards, ActionReason.DungeonStatueDrop); - player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(rewards)); - - rewardedPlayers.add(player.getUid()); - - scene.getScriptManager().callEvent(new ScriptArgs(groupId, EventType.EVENT_DUNGEON_REWARD_GET)); - return true; - } - - public boolean handleCost(Player player, boolean useCondensed) { - int resinCost = dungeonData.getStatueCostCount() != 0 ? dungeonData.getStatueCostCount() : 20; - if (resinCost == 0) { - return true; - } - if (useCondensed) { - // Check if condensed resin is usable here. - // For this, we use the following logic for now: - // The normal resin cost of the dungeon has to be 20. - if (resinCost != 20) { - return false; - } - - // Spend the condensed resin and only proceed if the transaction succeeds. - return player.getResinManager().useCondensedResin(1); - } else if (dungeonData.getStatueCostID() == 106) { - // Spend the resin and only proceed if the transaction succeeds. - return player.getResinManager().useResin(resinCost); - } - return true; - } - - private List rollRewards(boolean useCondensed) { - List rewards = new ArrayList<>(); - int dungeonId = this.dungeonData.getId(); - // If we have specific drop data for this dungeon, we use it. - if (GameData.getDungeonDropDataMap().containsKey(dungeonId)) { - List dropEntries = GameData.getDungeonDropDataMap().get(dungeonId); - - // Roll for each drop group. - for (var entry : dropEntries) { - // Determine the number of drops we get for this entry. - int start = entry.getCounts().get(0); - int end = entry.getCounts().get(entry.getCounts().size() - 1); - var candidateAmounts = IntStream.range(start, end + 1).boxed().collect(Collectors.toList()); - - int amount = Utils.drawRandomListElement(candidateAmounts, entry.getProbabilities()); - - if (useCondensed) { - amount += Utils.drawRandomListElement(candidateAmounts, entry.getProbabilities()); - } - - // Double rewards in multiplay mode, if specified. - if (entry.isMpDouble() && this.getScene().getPlayerCount() > 1) { - amount *= 2; - } - - // Roll items for this group. - // Here, we have to handle stacking, or the client will not display results correctly. - // For now, we use the following logic: If the possible drop item are a list of multiple - // items, - // we roll them separately. If not, we stack them. This should work out in practice, at - // least - // for the currently existing set of dungeons. - if (entry.getItems().size() == 1) { - rewards.add(new GameItem(entry.getItems().get(0), amount)); - } else { - for (int i = 0; i < amount; i++) { - // int itemIndex = ThreadLocalRandom.current().nextInt(0, entry.getItems().size()); - // int itemId = entry.getItems().get(itemIndex); - int itemId = - Utils.drawRandomListElement(entry.getItems(), entry.getItemProbabilities()); - rewards.add(new GameItem(itemId, 1)); - } - } - } - } - // Otherwise, we fall back to the preview data. - else { - Grasscutter.getLogger() - .info("No drop data found or dungeon {}, falling back to preview data ...", dungeonId); - for (ItemParamData param : dungeonData.getRewardPreviewData().getPreviewItems()) { - rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1))); - } - } - - return rewards; - } - - public void applyTrialTeam(Player player) { - if (getDungeonData() == null) return; - - switch (getDungeonData().getType()) { - // case DUNGEON_PLOT is handled by quest execs - case DUNGEON_ACTIVITY -> { - switch (getDungeonData().getPlayType()) { - case DUNGEON_PLAY_TYPE_TRIAL_AVATAR -> { - val activityHandler = - player - .getActivityManager() - .getActivityHandlerAs( - ActivityType.NEW_ACTIVITY_TRIAL_AVATAR, TrialAvatarActivityHandler.class); - activityHandler.ifPresent( - trialAvatarActivityHandler -> - this.trialTeam = trialAvatarActivityHandler.getTrialAvatarDungeonTeam()); - } - } - } - case DUNGEON_ELEMENT_CHALLENGE -> {} // TODO - } - if (this.trialTeam != null) { - player.getTeamManager().addTrialAvatars(trialTeam.trialAvatarIds); - } - } - - public void unsetTrialTeam(Player player) { - if (this.trialTeam == null) { - return; - } - player.getTeamManager().removeTrialAvatar(); - this.trialTeam = null; - } - - public void startDungeon() { - this.startSceneTime = scene.getSceneTimeSeconds(); - scene - .getPlayers() - .forEach( - p -> { - p.getQuestManager() - .queueEvent(QuestContent.QUEST_CONTENT_ENTER_DUNGEON, dungeonData.getId()); - applyTrialTeam(p); - }); - } - - public void finishDungeon() { - notifyEndDungeon(true); - endDungeon(BaseDungeonResult.DungeonEndReason.COMPLETED); - } - - public void notifyEndDungeon(boolean successfully) { - scene - .getPlayers() - .forEach( - p -> { - // Quest trigger - p.getQuestManager() - .queueEvent( - successfully - ? QuestContent.QUEST_CONTENT_FINISH_DUNGEON - : QuestContent.QUEST_CONTENT_FAIL_DUNGEON, - dungeonData.getId()); - - // Battle pass trigger - if (dungeonData.getType().isCountsToBattlepass() && successfully) { - p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON); - } - }); - scene - .getScriptManager() - .callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0)); - } - - public void quitDungeon() { - notifyEndDungeon(false); - endDungeon(BaseDungeonResult.DungeonEndReason.QUIT); - } - - public void failDungeon() { - notifyEndDungeon(false); - endDungeon(BaseDungeonResult.DungeonEndReason.FAILED); - } - - public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) { - if (scene.getDungeonSettleListeners() != null) { - scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason)); - } - ended = true; - } - - public void restartDungeon() { - this.scene.setKilledMonsterCount(0); - this.rewardedPlayers.clear(); - Arrays.fill(finishedConditions, 0); - this.ended = false; - this.activeDungeonWayPoints.clear(); - } - - public void cleanUpScene() { - this.scene.setDungeonManager(null); - this.scene.setKilledMonsterCount(0); - } -} +package emu.grasscutter.game.dungeons; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.data.excels.dungeon.DungeonData; +import emu.grasscutter.data.excels.dungeon.DungeonPassConfigData; +import emu.grasscutter.game.activity.trialavatar.TrialAvatarActivityHandler; +import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult; +import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.props.ActivityType; +import emu.grasscutter.game.props.WatcherTriggerType; +import emu.grasscutter.game.quest.enums.LogicType; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.packet.send.PacketDungeonWayPointNotify; +import emu.grasscutter.server.packet.send.PacketGadgetAutoPickDropInfoNotify; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import lombok.Getter; +import lombok.NonNull; +import lombok.val; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * TODO handle time limits + * TODO handle respawn points + * TODO handle team wipes and respawns + * TODO check monster level and levelConfigMap + */ +public final class DungeonManager { + @Getter private final Scene scene; + @Getter private final DungeonData dungeonData; + @Getter private final DungeonPassConfigData passConfigData; + + @Getter private final int[] finishedConditions; + private final IntSet rewardedPlayers = new IntOpenHashSet(); + private final Set activeDungeonWayPoints = new HashSet<>(); + private boolean ended = false; + private int newestWayPoint = 0; + @Getter private int startSceneTime = 0; + + DungeonTrialTeam trialTeam = null; + + public DungeonManager(@NonNull Scene scene, @NonNull DungeonData dungeonData) { + this.scene = scene; + this.dungeonData = dungeonData; + this.passConfigData = GameData.getDungeonPassConfigDataMap().get(dungeonData.getPassCond()); + this.finishedConditions = new int[passConfigData.getConds().size()]; + this.scene.setDungeonManager(this); + } + + public void triggerEvent(DungeonPassConditionType conditionType, int... params) { + if (ended) { + return; + } + for (int i = 0; i < passConfigData.getConds().size(); i++) { + var cond = passConfigData.getConds().get(i); + if (conditionType == cond.getCondType()) { + if (getScene().getWorld().getServer().getDungeonSystem().triggerCondition(cond, params)) { + finishedConditions[i] = 1; + } + + } + } + + if (isFinishedSuccessfully()) { + finishDungeon(); + } + + } + + public boolean isFinishedSuccessfully() { + return LogicType.calculate(passConfigData.getLogicType(), finishedConditions); + } + + public int getLevelForMonster(int id) { + //TODO should use levelConfigMap? and how? + return dungeonData.getShowLevel(); + } + + public boolean activateRespawnPoint(int pointId) { + val respawnPoint = GameData.getScenePointEntryById(scene.getId(), pointId); + + if (respawnPoint == null) { + Grasscutter.getLogger().warn("trying to activate unknown respawn point {}", pointId); + return false; + } + + scene.broadcastPacket(new PacketDungeonWayPointNotify(activeDungeonWayPoints.add(pointId), activeDungeonWayPoints)); + newestWayPoint = pointId; + + Grasscutter.getLogger().debug("[unimplemented respawn] activated respawn point {}", pointId); + return true; + } + + @Nullable + public Position getRespawnLocation() { + if (newestWayPoint == 0) { // validity is checked before setting it, so if != 0 its always valid + return null; + } + var pointData = GameData.getScenePointEntryById(scene.getId(), newestWayPoint).getPointData(); + return pointData.getTranPos() != null ? pointData.getTranPos() : pointData.getPos(); + } + + public Position getRespawnRotation() { + if (newestWayPoint == 0) { // validity is checked before setting it, so if != 0 its always valid + return null; + } + val pointData = GameData.getScenePointEntryById(scene.getId(), newestWayPoint).getPointData(); + return pointData.getRot() != null ? pointData.getRot() : null; + } + + public boolean getStatueDrops(Player player, boolean useCondensed, int groupId) { + if (!isFinishedSuccessfully() || dungeonData.getRewardPreviewData() == null || dungeonData.getRewardPreviewData().getPreviewItems().length == 0) { + return false; + } + + // Already rewarded + if (rewardedPlayers.contains(player.getUid())) { + return false; + } + + + if (!handleCost(player, useCondensed)) { + return false; + } + + // Get and roll rewards. + List rewards = new ArrayList<>(this.rollRewards(useCondensed)); + // Add rewards to player and send notification. + player.getInventory().addItems(rewards, ActionReason.DungeonStatueDrop); + player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(rewards)); + + rewardedPlayers.add(player.getUid()); + + scene.getScriptManager().callEvent(new ScriptArgs(groupId, EventType.EVENT_DUNGEON_REWARD_GET)); + return true; + } + + public boolean handleCost(Player player, boolean useCondensed) { + int resinCost = dungeonData.getStatueCostCount() != 0 ? dungeonData.getStatueCostCount() : 20; + if (resinCost == 0) { + return true; + } + if (useCondensed) { + // Check if condensed resin is usable here. + // For this, we use the following logic for now: + // The normal resin cost of the dungeon has to be 20. + if (resinCost != 20) { + return false; + } + + // Spend the condensed resin and only proceed if the transaction succeeds. + return player.getResinManager().useCondensedResin(1); + } else if (dungeonData.getStatueCostID() == 106) { + // Spend the resin and only proceed if the transaction succeeds. + return player.getResinManager().useResin(resinCost); + } + return true; + } + + private List rollRewards(boolean useCondensed) { + List rewards = new ArrayList<>(); + int dungeonId = this.dungeonData.getId(); + // If we have specific drop data for this dungeon, we use it. + if (GameData.getDungeonDropDataMap().containsKey(dungeonId)) { + List dropEntries = GameData.getDungeonDropDataMap().get(dungeonId); + + // Roll for each drop group. + for (var entry : dropEntries) { + // Determine the number of drops we get for this entry. + int start = entry.getCounts().get(0); + int end = entry.getCounts().get(entry.getCounts().size() - 1); + var candidateAmounts = IntStream.range(start, end + 1).boxed().collect(Collectors.toList()); + + int amount = Utils.drawRandomListElement(candidateAmounts, entry.getProbabilities()); + + if (useCondensed) { + amount += Utils.drawRandomListElement(candidateAmounts, entry.getProbabilities()); + } + + // Double rewards in multiplay mode, if specified. + if (entry.isMpDouble() && this.getScene().getPlayerCount() > 1) { + amount *= 2; + } + + // Roll items for this group. + // Here, we have to handle stacking, or the client will not display results correctly. + // For now, we use the following logic: If the possible drop item are a list of multiple items, + // we roll them separately. If not, we stack them. This should work out in practice, at least + // for the currently existing set of dungeons. + if (entry.getItems().size() == 1) { + rewards.add(new GameItem(entry.getItems().get(0), amount)); + } else { + for (int i = 0; i < amount; i++) { + // int itemIndex = ThreadLocalRandom.current().nextInt(0, entry.getItems().size()); + // int itemId = entry.getItems().get(itemIndex); + int itemId = Utils.drawRandomListElement(entry.getItems(), entry.getItemProbabilities()); + rewards.add(new GameItem(itemId, 1)); + } + } + } + } + // Otherwise, we fall back to the preview data. + else { + Grasscutter.getLogger().info("No drop data found or dungeon {}, falling back to preview data ...", dungeonId); + for (ItemParamData param : dungeonData.getRewardPreviewData().getPreviewItems()) { + rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1))); + } + } + + return rewards; + } + + public void applyTrialTeam(Player player) { + if (getDungeonData() == null) return; + + switch (getDungeonData().getType()) { + // case DUNGEON_PLOT is handled by quest execs + case DUNGEON_ACTIVITY -> { + switch (getDungeonData().getPlayType()) { + case DUNGEON_PLAY_TYPE_TRIAL_AVATAR -> { + val activityHandler = player.getActivityManager() + .getActivityHandlerAs(ActivityType.NEW_ACTIVITY_TRIAL_AVATAR, TrialAvatarActivityHandler.class); + activityHandler.ifPresent(trialAvatarActivityHandler -> + this.trialTeam = trialAvatarActivityHandler.getTrialAvatarDungeonTeam()); + } + } + } + case DUNGEON_ELEMENT_CHALLENGE -> {} // TODO + } + + if (this.trialTeam != null) { + player.getTeamManager().addTrialAvatars(trialTeam.trialAvatarIds); + } + } + + public void unsetTrialTeam(Player player){ + if (this.trialTeam == null) return; + + player.getTeamManager().removeTrialAvatar(); + this.trialTeam = null; + } + + public void startDungeon() { + this.startSceneTime = scene.getSceneTimeSeconds(); + scene.getPlayers().forEach(p -> { + p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_ENTER_DUNGEON, dungeonData.getId()); + applyTrialTeam(p); + }); + } + + public void finishDungeon() { + notifyEndDungeon(true); + endDungeon(BaseDungeonResult.DungeonEndReason.COMPLETED); + } + + public void notifyEndDungeon(boolean successfully) { + scene.getPlayers().forEach(p -> { + // Quest trigger + p.getQuestManager().queueEvent(successfully ? + QuestContent.QUEST_CONTENT_FINISH_DUNGEON : QuestContent.QUEST_CONTENT_FAIL_DUNGEON, + dungeonData.getId()); + + // Battle pass trigger + if (dungeonData.getType().isCountsToBattlepass() && successfully) { + p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON); + } + }); + scene.getScriptManager().callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0)); + } + + public void quitDungeon() { + notifyEndDungeon(false); + endDungeon(BaseDungeonResult.DungeonEndReason.QUIT); + } + + public void failDungeon() { + notifyEndDungeon(false); + endDungeon(BaseDungeonResult.DungeonEndReason.FAILED); + } + + public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) { + if (scene.getDungeonSettleListeners() != null) { + scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason)); + } + ended = true; + } + + public void restartDungeon() { + this.scene.setKilledMonsterCount(0); + this.rewardedPlayers.clear(); + Arrays.fill(finishedConditions, 0); + this.ended = false; + this.activeDungeonWayPoints.clear(); + } + + public void cleanUpScene() { + this.scene.setDungeonManager(null); + this.scene.setKilledMonsterCount(0); + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonSettleListener.java index 45f36a7b4..e49e6f58d 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonSettleListener.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonSettleListener.java @@ -1,7 +1,7 @@ -package emu.grasscutter.game.dungeons; - -import emu.grasscutter.game.world.Scene; - -public interface DungeonSettleListener { - void onDungeonSettle(Scene scene); -} +package emu.grasscutter.game.dungeons; + +import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult; + +public interface DungeonSettleListener { + void onDungeonSettle(DungeonManager dungeonManager, BaseDungeonResult.DungeonEndReason endReason); +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonSystem.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonSystem.java index 67963c90b..ac7076590 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/DungeonSystem.java +++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonSystem.java @@ -1,115 +1,157 @@ -package emu.grasscutter.game.dungeons; - -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.SceneType; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.server.game.BaseGameSystem; -import emu.grasscutter.server.game.GameServer; -import emu.grasscutter.server.packet.send.PacketDungeonEntryInfoRsp; -import emu.grasscutter.server.packet.send.PacketPlayerEnterDungeonRsp; -import emu.grasscutter.utils.Position; -import java.util.List; - -public final class DungeonSystem extends BaseGameSystem { - private static final BasicDungeonSettleListener basicDungeonSettleObserver = - new BasicDungeonSettleListener(); - - public DungeonSystem(GameServer server) { - super(server); - } - - public void getEntryInfo(Player player, int pointId) { - var entry = GameData.getScenePointEntryById(player.getScene().getId(), pointId); - - if (entry == null) { - // Error - player.sendPacket(new PacketDungeonEntryInfoRsp()); - return; - } - - player.sendPacket(new PacketDungeonEntryInfoRsp(player, entry.getPointData())); - } - - public boolean enterDungeon(Player player, int pointId, int dungeonId) { - var data = GameData.getDungeonDataMap().get(dungeonId); - if (data == null) { - return false; - } - - Grasscutter.getLogger() - .debug( - "{}({}) is trying to enter dungeon {}", - player.getNickname(), - player.getUid(), - dungeonId); - - var sceneId = data.getSceneId(); - player.getScene().setPrevScene(sceneId); - - if (player.getWorld().transferPlayerToScene(player, sceneId, data)) { - player.getScene().addDungeonSettleObserver(basicDungeonSettleObserver); - player.getQuestManager().triggerEvent(QuestContent.QUEST_CONTENT_ENTER_DUNGEON, data.getId()); - } - - player.getScene().setPrevScenePoint(pointId); - player.sendPacket(new PacketPlayerEnterDungeonRsp(pointId, dungeonId)); - return true; - } - - /** used in tower dungeons handoff */ - public boolean handoffDungeon( - Player player, int dungeonId, List dungeonSettleListeners) { - var data = GameData.getDungeonDataMap().get(dungeonId); - if (data == null) { - return false; - } - - Grasscutter.getLogger() - .debug( - "{}({}) is trying to enter tower dungeon {}", - player.getNickname(), - player.getUid(), - dungeonId); - - if (player.getWorld().transferPlayerToScene(player, data.getSceneId(), data)) { - dungeonSettleListeners.forEach(player.getScene()::addDungeonSettleObserver); - } - - return true; - } - - public void exitDungeon(Player player) { - var scene = player.getScene(); - - if (scene == null || scene.getSceneType() != SceneType.SCENE_DUNGEON) { - return; - } - - // Get previous scene - var prevScene = scene.getPrevScene() > 0 ? scene.getPrevScene() : 3; - - // Get previous position - var dungeonData = scene.getDungeonData(); - var prevPos = new Position(GameConstants.START_POSITION); - - if (dungeonData != null) { - var entry = GameData.getScenePointEntryById(prevScene, scene.getPrevScenePoint()); - - if (entry != null) { - prevPos.set(entry.getPointData().getTranPos()); - } - } - - // clean temp team if it has - player.getTeamManager().cleanTemporaryTeam(); - player.getTowerManager().clearEntry(); - - // Transfer player back to world - player.getWorld().transferPlayerToScene(player, prevScene, prevPos); - player.sendPacket(new BasePacket(PacketOpcodes.PlayerQuitDungeonRsp)); - } -} +package emu.grasscutter.game.dungeons; + +import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.ScenePointEntry; +import emu.grasscutter.data.excels.dungeon.DungeonData; +import emu.grasscutter.data.excels.dungeon.DungeonPassConfigData; +import emu.grasscutter.game.dungeons.handlers.DungeonBaseHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.SceneType; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.server.game.BaseGameSystem; +import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.server.packet.send.PacketDungeonEntryInfoRsp; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.val; +import org.reflections.Reflections; + +import java.util.List; + +public class DungeonSystem extends BaseGameSystem { + private static final BasicDungeonSettleListener basicDungeonSettleObserver = new BasicDungeonSettleListener(); + private final Int2ObjectMap passCondHandlers; + + public DungeonSystem(GameServer server) { + super(server); + this.passCondHandlers = new Int2ObjectOpenHashMap<>(); + registerHandlers(); + } + + public void registerHandlers() { + this.registerHandlers(this.passCondHandlers, "emu.grasscutter.game.dungeons.pass_condition", DungeonBaseHandler.class); + } + + public void registerHandlers(Int2ObjectMap map, String packageName, Class clazz) { + Reflections reflections = new Reflections(packageName); + var handlerClasses = reflections.getSubTypesOf(clazz); + + for (var obj : handlerClasses) { + this.registerPacketHandler(map, obj); + } + } + + public void registerPacketHandler(Int2ObjectMap map, Class handlerClass) { + try { + DungeonValue opcode = handlerClass.getAnnotation(DungeonValue.class); + + if (opcode == null || opcode.value() == null) { + return; + } + + map.put(opcode.value().ordinal(), handlerClass.getDeclaredConstructor().newInstance()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void getEntryInfo(Player player, int pointId) { + ScenePointEntry entry = GameData.getScenePointEntryById(player.getScene().getId(), pointId); + + if (entry == null) { + // Error + player.sendPacket(new PacketDungeonEntryInfoRsp()); + return; + } + + player.sendPacket(new PacketDungeonEntryInfoRsp(player, entry.getPointData())); + } + + public boolean triggerCondition(DungeonPassConfigData.DungeonPassCondition condition, int... params) { + var handler = passCondHandlers.get(condition.getCondType().ordinal()); + + if (handler == null) { + Grasscutter.getLogger().debug("Could not trigger condition {} at {}", condition.getCondType(), params); + return false; + } + + return handler.execute(condition, params); + } + + public boolean enterDungeon(Player player, int pointId, int dungeonId) { + DungeonData data = GameData.getDungeonDataMap().get(dungeonId); + + if (data == null) { + return false; + } + Grasscutter.getLogger().info("{}({}) is trying to enter dungeon {}" ,player.getNickname(),player.getUid(),dungeonId); + + int sceneId = data.getSceneId(); + var scene = player.getScene(); + scene.setPrevScene(sceneId); + + if (player.getWorld().transferPlayerToScene(player, sceneId, data)) { + scene = player.getScene(); + scene.addDungeonSettleObserver(basicDungeonSettleObserver); + } + + scene.setPrevScenePoint(pointId); + return true; + } + + /** + * used in tower dungeons handoff + */ + public boolean handoffDungeon(Player player, int dungeonId, List dungeonSettleListeners) { + DungeonData data = GameData.getDungeonDataMap().get(dungeonId); + + if (data == null) { + return false; + } + Grasscutter.getLogger().info("{}({}) is trying to enter tower dungeon {}" ,player.getNickname(),player.getUid(),dungeonId); + + if (player.getWorld().transferPlayerToScene(player, data.getSceneId(), data)) { + dungeonSettleListeners.forEach(player.getScene()::addDungeonSettleObserver); + } + return true; + } + + public void exitDungeon(Player player) { + Scene scene = player.getScene(); + + if (scene==null || scene.getSceneType() != SceneType.SCENE_DUNGEON) { + return; + } + + // Get previous scene + int prevScene = scene.getPrevScene() > 0 ? scene.getPrevScene() : 3; + + // Get previous position + val dungeonManager = scene.getDungeonManager(); + DungeonData dungeonData = dungeonManager != null ? dungeonManager.getDungeonData() : null; + Position prevPos = new Position(GameConstants.START_POSITION); + + if (dungeonData != null) { + ScenePointEntry entry = GameData.getScenePointEntryById(prevScene, scene.getPrevScenePoint()); + + if (entry != null) { + prevPos.set(entry.getPointData().getTranPos()); + } + if(!dungeonManager.isFinishedSuccessfully()){ + dungeonManager.quitDungeon(); + } + + dungeonManager.unsetTrialTeam(player); + } + // clean temp team if it has + player.getTeamManager().cleanTemporaryTeam(); + player.getTowerManager().clearEntry(); + + + // Transfer player back to world + player.getWorld().transferPlayerToScene(player, prevScene, prevPos); + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java index 7005608dc..9b98311a6 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java +++ b/src/main/java/emu/grasscutter/game/dungeons/TowerDungeonSettleListener.java @@ -1,31 +1,38 @@ -package emu.grasscutter.game.dungeons; - -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.server.packet.send.PacketDungeonSettleNotify; -import emu.grasscutter.server.packet.send.PacketTowerFloorRecordChangeNotify; -import emu.grasscutter.utils.Utils; - -public class TowerDungeonSettleListener implements DungeonSettleListener { - - @Override - public void onDungeonSettle(Scene scene) { - if (scene.getScriptManager().getVariables().containsKey("stage") - && scene.getScriptManager().getVariables().get("stage") == 1) { - return; - } - scene.setAutoCloseTime(Utils.getCurrentSeconds() + 1000); - var towerManager = scene.getPlayers().get(0).getTowerManager(); - - towerManager.notifyCurLevelRecordChangeWhenDone(3); - scene.broadcastPacket( - new PacketTowerFloorRecordChangeNotify( - towerManager.getCurrentFloorId(), 3, towerManager.canEnterScheduleFloor())); - - scene.broadcastPacket( - new PacketDungeonSettleNotify( - scene.getChallenge(), - towerManager.hasNextFloor(), - towerManager.hasNextLevel(), - towerManager.hasNextLevel() ? 0 : towerManager.getNextFloorId())); - } -} +package emu.grasscutter.game.dungeons; + +import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult.DungeonEndReason; +import emu.grasscutter.game.world.SceneGroupInstance; +import emu.grasscutter.game.dungeons.dungeon_results.TowerResult; +import emu.grasscutter.server.packet.send.PacketDungeonSettleNotify; +import emu.grasscutter.server.packet.send.PacketTowerFloorRecordChangeNotify; + +public class TowerDungeonSettleListener implements DungeonSettleListener { + + @Override + public void onDungeonSettle(DungeonManager dungeonManager, DungeonEndReason endReason) { + var scene = dungeonManager.getScene(); + var dungeonData = dungeonManager.getDungeonData(); + if (scene.getLoadedGroups().stream().anyMatch(g -> { + var variables = scene.getScriptManager().getVariables(g.id); + return variables != null && variables.containsKey("stage") && variables.get("stage") == 1; + })) { + return; + } + + var towerManager = scene.getPlayers().get(0).getTowerManager(); + + towerManager.notifyCurLevelRecordChangeWhenDone(3); + scene.broadcastPacket(new PacketTowerFloorRecordChangeNotify( + towerManager.getCurrentFloorId(), + 3, + towerManager.canEnterScheduleFloor() + )); + + var challenge = scene.getChallenge(); + var dungeonStats = new DungeonEndStats(scene.getKilledMonsterCount(), challenge.getFinishedTime(), 0, endReason); + var result = new TowerResult(dungeonData, dungeonStats, towerManager, challenge); + + scene.broadcastPacket(new PacketDungeonSettleNotify(result)); + + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java index 4e304e3d0..26bce996c 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java @@ -1,144 +1,167 @@ -package emu.grasscutter.game.dungeons.challenge; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.scripts.data.ScriptArgs; -import emu.grasscutter.server.packet.send.PacketDungeonChallengeBeginNotify; -import emu.grasscutter.server.packet.send.PacketDungeonChallengeFinishNotify; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class WorldChallenge { - private final Scene scene; - private final SceneGroup group; - private final int challengeId; - private final int challengeIndex; - private final List paramList; - private final int timeLimit; - private final List challengeTriggers; - private final int goal; - private final AtomicInteger score; - private boolean progress; - private boolean success; - private long startedAt; - private int finishedTime; - - public WorldChallenge( - Scene scene, - SceneGroup group, - int challengeId, - int challengeIndex, - List paramList, - int timeLimit, - int goal, - List challengeTriggers) { - this.scene = scene; - this.group = group; - this.challengeId = challengeId; - this.challengeIndex = challengeIndex; - this.paramList = paramList; - this.timeLimit = timeLimit; - this.challengeTriggers = challengeTriggers; - this.goal = goal; - this.score = new AtomicInteger(0); - } - - public boolean inProgress() { - return this.progress; - } - - public void onCheckTimeOut() { - if (!inProgress()) { - return; - } - if (timeLimit <= 0) { - return; - } - challengeTriggers.forEach(t -> t.onCheckTimeout(this)); - } - - public void start() { - if (inProgress()) { - Grasscutter.getLogger().info("Could not start a in progress challenge."); - return; - } - this.progress = true; - this.startedAt = System.currentTimeMillis(); - getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this)); - challengeTriggers.forEach(t -> t.onBegin(this)); - } - - public void done() { - if (!inProgress()) { - return; - } - finish(true); - this.getScene() - .getScriptManager() - .callEvent( - EventType.EVENT_CHALLENGE_SUCCESS, - // TODO record the time in PARAM2 and used in action - new ScriptArgs().setParam2(finishedTime)); - - challengeTriggers.forEach(t -> t.onFinish(this)); - } - - public void fail() { - if (!inProgress()) { - return; - } - finish(false); - this.getScene().getScriptManager().callEvent(EventType.EVENT_CHALLENGE_FAIL, null); - challengeTriggers.forEach(t -> t.onFinish(this)); - } - - private void finish(boolean success) { - this.progress = false; - this.success = success; - this.finishedTime = (int) ((System.currentTimeMillis() - this.startedAt) / 1000L); - getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this)); - } - - public int increaseScore() { - return score.incrementAndGet(); - } - - public void onMonsterDeath(EntityMonster monster) { - if (!inProgress()) { - return; - } - if (monster.getGroupId() != getGroup().id) { - return; - } - this.challengeTriggers.forEach(t -> t.onMonsterDeath(this, monster)); - } - - public void onGadgetDeath(EntityGadget gadget) { - if (!inProgress()) { - return; - } - if (gadget.getGroupId() != getGroup().id) { - return; - } - this.challengeTriggers.forEach(t -> t.onGadgetDeath(this, gadget)); - } - - public void onGadgetDamage(EntityGadget gadget) { - if (!inProgress()) { - return; - } - if (gadget.getGroupId() != getGroup().id) { - return; - } - this.challengeTriggers.forEach(t -> t.onGadgetDamage(this, gadget)); - } -} +package emu.grasscutter.game.dungeons.challenge; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; +import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.game.props.WatcherTriggerType; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.scripts.data.SceneTrigger; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.packet.send.PacketDungeonChallengeBeginNotify; +import emu.grasscutter.server.packet.send.PacketDungeonChallengeFinishNotify; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.Getter; +import lombok.Setter; +import lombok.val; + +@Getter +@Setter +public class WorldChallenge { + private final Scene scene; + private final SceneGroup group; + private final int challengeId; + private final int challengeIndex; + private final List paramList; + private final int timeLimit; + private final List challengeTriggers; + private final int goal; + private final AtomicInteger score; + private boolean progress; + private boolean success; + private long startedAt; + private int finishedTime; + + public WorldChallenge( + Scene scene, + SceneGroup group, + int challengeId, + int challengeIndex, + List paramList, + int timeLimit, + int goal, + List challengeTriggers) { + this.scene = scene; + this.group = group; + this.challengeId = challengeId; + this.challengeIndex = challengeIndex; + this.paramList = paramList; + this.timeLimit = timeLimit; + this.challengeTriggers = challengeTriggers; + this.goal = goal; + this.score = new AtomicInteger(0); + } + + public boolean inProgress() { + return this.progress; + } + + public void onCheckTimeOut() { + if (!inProgress()) { + return; + } + if (timeLimit <= 0) { + return; + } + challengeTriggers.forEach(t -> t.onCheckTimeout(this)); + } + + public void start() { + if (inProgress()) { + Grasscutter.getLogger().info("Could not start a in progress challenge."); + return; + } + this.progress = true; + this.startedAt = System.currentTimeMillis(); + getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this)); + challengeTriggers.forEach(t -> t.onBegin(this)); + } + + public void done() { + if (!this.inProgress()) return; + this.finish(true); + + var scene = this.getScene(); + var dungeonManager = scene.getDungeonManager(); + if (dungeonManager != null && dungeonManager.getDungeonData() != null) { + scene.getPlayers().forEach(p -> p.getActivityManager().triggerWatcher( + WatcherTriggerType.TRIGGER_FINISH_CHALLENGE, + String.valueOf(dungeonManager.getDungeonData().getId()), + String.valueOf(this.getGroup().id), + String.valueOf(this.getChallengeId()) + )); + } + + scene.getScriptManager().callEvent( + // TODO record the time in PARAM2 and used in action + new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_SUCCESS).setParam2(finishedTime)); + this.getScene().triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_FINISH_CHALLENGE, getChallengeId(), getChallengeIndex()); + + this.challengeTriggers.forEach(t -> t.onFinish(this)); + } + + public void fail(){ + if (!this.inProgress()) return; + this.finish(true); + + this.getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_FAIL)); + challengeTriggers.forEach(t -> t.onFinish(this)); + } + + private void finish(boolean success) { + this.progress = false; + this.success = success; + this.finishedTime = (int) ((System.currentTimeMillis() - this.startedAt) / 1000L); + getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this)); + } + + public int increaseScore() { + return score.incrementAndGet(); + } + + public void onMonsterDeath(EntityMonster monster) { + if (!inProgress()) { + return; + } + if (monster.getGroupId() != getGroup().id) { + return; + } + this.challengeTriggers.forEach(t -> t.onMonsterDeath(this, monster)); + } + + public void onGadgetDeath(EntityGadget gadget) { + if (!inProgress()) { + return; + } + if (gadget.getGroupId() != getGroup().id) { + return; + } + this.challengeTriggers.forEach(t -> t.onGadgetDeath(this, gadget)); + } + + public void onGroupTriggerDeath(SceneTrigger trigger) { + if(!this.inProgress()) return; + + var triggerGroup = trigger.getCurrentGroup(); + if (triggerGroup == null || + triggerGroup.id != getGroup().id) { + return; + } + + this.challengeTriggers.forEach(t -> t.onGroupTrigger(this, trigger)); + } + + public void onGadgetDamage(EntityGadget gadget) { + if (!inProgress()) { + return; + } + if (gadget.getGroupId() != getGroup().id) { + return; + } + this.challengeTriggers.forEach(t -> t.onGadgetDamage(this, gadget)); + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/ChallengeFactory.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/ChallengeFactory.java index 8ac893f6a..a5649a334 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/ChallengeFactory.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/ChallengeFactory.java @@ -1,37 +1,36 @@ -package emu.grasscutter.game.dungeons.challenge.factory; - -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.data.SceneGroup; -import java.util.ArrayList; -import java.util.List; - -public class ChallengeFactory { - - private static final List challengeFactoryHandlers = new ArrayList<>(); - - static { - challengeFactoryHandlers.add(new DungeonChallengeFactoryHandler()); - challengeFactoryHandlers.add(new DungeonGuardChallengeFactoryHandler()); - challengeFactoryHandlers.add(new KillGadgetChallengeFactoryHandler()); - challengeFactoryHandlers.add(new KillMonsterChallengeFactoryHandler()); - } - - public static WorldChallenge getChallenge( - int param1, - int param2, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group) { - for (var handler : challengeFactoryHandlers) { - if (!handler.isThisType(param1, param2, param3, param4, param5, param6, scene, group)) { - continue; - } - return handler.build(param1, param2, param3, param4, param5, param6, scene, group); - } - return null; - } -} +package emu.grasscutter.game.dungeons.challenge.factory; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.dungeons.challenge.WorldChallenge; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.scripts.data.SceneGroup; +import lombok.val; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ChallengeFactory { + private static final List challengeFactoryHandlers = new ArrayList<>(); + + static { + challengeFactoryHandlers.add(new KillAndGuardChallengeFactoryHandler()); + challengeFactoryHandlers.add(new KillMonsterCountChallengeFactoryHandler()); + challengeFactoryHandlers.add(new KillMonsterInTimeChallengeFactoryHandler()); + challengeFactoryHandlers.add(new KillMonsterTimeChallengeFactoryHandler()); + challengeFactoryHandlers.add(new SurviveChallengeFactoryHandler()); + challengeFactoryHandlers.add(new TriggerInTimeChallengeFactoryHandler()); + } + + public static WorldChallenge getChallenge(int localChallengeId, int challengeDataId, int param3, int param4, int param5, int param6, Scene scene, SceneGroup group){ + val challengeData = GameData.getDungeonChallengeConfigDataMap().get(challengeDataId); + val challengeType = challengeData.getChallengeType(); + + for(var handler : challengeFactoryHandlers){ + if(!handler.isThisType(challengeType)){ + continue; + } + return handler.build(localChallengeId, challengeDataId, param3, param4, param5, param6, scene, group); + } + return null; + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/ChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/ChallengeFactoryHandler.java index cdf3c6820..ae77fd989 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/ChallengeFactoryHandler.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/ChallengeFactoryHandler.java @@ -1,27 +1,11 @@ package emu.grasscutter.game.dungeons.challenge.factory; import emu.grasscutter.game.dungeons.challenge.WorldChallenge; +import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.data.SceneGroup; public interface ChallengeFactoryHandler { - boolean isThisType( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group); - - WorldChallenge build( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group); + boolean isThisType(ChallengeType challengeType); + WorldChallenge build(int challengeIndex, int challengeId, int param3, int param4, int param5, int param6, Scene scene, SceneGroup group); } diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/DungeonChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/DungeonChallengeFactoryHandler.java deleted file mode 100644 index 72108f3ba..000000000 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/DungeonChallengeFactoryHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -package emu.grasscutter.game.dungeons.challenge.factory; - -import emu.grasscutter.game.dungeons.challenge.DungeonChallenge; -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger; -import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterTrigger; -import emu.grasscutter.game.props.SceneType; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.data.SceneGroup; -import java.util.List; - -public class DungeonChallengeFactoryHandler implements ChallengeFactoryHandler { - @Override - public boolean isThisType( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group) { - // ActiveChallenge with 1,1000,300,233101003,15,0 - return scene.getSceneType() == SceneType.SCENE_DUNGEON && param4 == group.id; - } - - @Override - public WorldChallenge build( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group) { - var realGroup = scene.getScriptManager().getGroupById(param4); - return new DungeonChallenge( - scene, - realGroup, - challengeId, // Id - challengeIndex, // Index - List.of(param5, param3), - param3, // Limit - param5, // Goal - List.of(new InTimeTrigger(), new KillMonsterTrigger())); - } -} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/DungeonGuardChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/DungeonGuardChallengeFactoryHandler.java deleted file mode 100644 index 4456ab759..000000000 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/DungeonGuardChallengeFactoryHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -package emu.grasscutter.game.dungeons.challenge.factory; - -import emu.grasscutter.game.dungeons.challenge.DungeonChallenge; -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.dungeons.challenge.trigger.GuardTrigger; -import emu.grasscutter.game.props.SceneType; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.data.SceneGroup; -import java.util.List; - -public class DungeonGuardChallengeFactoryHandler implements ChallengeFactoryHandler { - @Override - public boolean isThisType( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group) { - // ActiveChallenge with 1,188,234101003,12,3030,0 - return scene.getSceneType() == SceneType.SCENE_DUNGEON && param3 == group.id; - } - - @Override - public WorldChallenge build( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group) { - var realGroup = scene.getScriptManager().getGroupById(param3); - return new DungeonChallenge( - scene, - realGroup, - challengeId, // Id - challengeIndex, // Index - List.of(param4, 0), - 0, // Limit - param4, // Goal - List.of(new GuardTrigger())); - } -} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java index c311128ee..2547c2fa1 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java @@ -1,17 +1,18 @@ package emu.grasscutter.game.dungeons.challenge.factory; -import static emu.grasscutter.game.dungeons.challenge.enums.ChallengeType.CHALLENGE_KILL_COUNT_GUARD_HP; - import emu.grasscutter.game.dungeons.challenge.WorldChallenge; import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType; import emu.grasscutter.game.dungeons.challenge.trigger.GuardTrigger; import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterCountTrigger; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.data.SceneGroup; -import java.util.List; import lombok.val; -public class KillAndGuardChallengeFactoryHandler implements ChallengeFactoryHandler { +import java.util.List; + +import static emu.grasscutter.game.dungeons.challenge.enums.ChallengeType.CHALLENGE_KILL_COUNT_GUARD_HP; + +public class KillAndGuardChallengeFactoryHandler implements ChallengeFactoryHandler{ @Override public boolean isThisType(ChallengeType challengeType) { // ActiveChallenge with 1,188,234101003,12,3030,0 @@ -19,24 +20,15 @@ public class KillAndGuardChallengeFactoryHandler implements ChallengeFactoryHand } @Override /*TODO check param4 == monstesToKill*/ - public WorldChallenge build( - int challengeIndex, - int challengeId, - int groupId, - int monstersToKill, - int gadgetCFGId, - int unused, - Scene scene, - SceneGroup group) { + public WorldChallenge build(int challengeIndex, int challengeId, int groupId, int monstersToKill, int gadgetCFGId, int unused, Scene scene, SceneGroup group) { val realGroup = scene.getScriptManager().getGroupById(groupId); return new WorldChallenge( - scene, - realGroup, + scene, realGroup, challengeId, // Id challengeIndex, // Index List.of(monstersToKill, 0), 0, // Limit - monstersToKill, // Goal + monstersToKill, // Goal List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId))); } } diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillGadgetChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillGadgetChallengeFactoryHandler.java deleted file mode 100644 index 60f1fa07d..000000000 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillGadgetChallengeFactoryHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -package emu.grasscutter.game.dungeons.challenge.factory; - -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger; -import emu.grasscutter.game.dungeons.challenge.trigger.KillGadgetTrigger; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.data.SceneGroup; -import java.util.List; - -public class KillGadgetChallengeFactoryHandler implements ChallengeFactoryHandler { - @Override - public boolean isThisType( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group) { - // kill gadgets(explosive barrel) in time - // ActiveChallenge with 56,201,20,2,201,4 - // open chest in time - // ActiveChallenge with 666,202,30,7,202,1 - return challengeId == 201 || challengeId == 202; - } - - @Override - public WorldChallenge build( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group) { - return new WorldChallenge( - scene, - group, - challengeId, // Id - challengeIndex, // Index - List.of(param3, param6, 0), - param3, // Limit - param6, // Goal - List.of(new InTimeTrigger(), new KillGadgetTrigger())); - } -} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterChallengeFactoryHandler.java deleted file mode 100644 index 98f708e30..000000000 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterChallengeFactoryHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -package emu.grasscutter.game.dungeons.challenge.factory; - -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger; -import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterTrigger; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.data.SceneGroup; -import java.util.List; - -public class KillMonsterChallengeFactoryHandler implements ChallengeFactoryHandler { - @Override - public boolean isThisType( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group) { - // ActiveChallenge with 180,180,45,133108061,1,0 - return challengeId == 180; - } - - @Override - public WorldChallenge build( - int challengeIndex, - int challengeId, - int param3, - int param4, - int param5, - int param6, - Scene scene, - SceneGroup group) { - var realGroup = scene.getScriptManager().getGroupById(param4); - return new WorldChallenge( - scene, - realGroup, - challengeId, // Id - challengeIndex, // Index - List.of(param5, param3), - param3, // Limit - param5, // Goal - List.of(new KillMonsterTrigger(), new InTimeTrigger())); - } -} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterCountChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterCountChallengeFactoryHandler.java index fc5ba5add..a21c42bbc 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterCountChallengeFactoryHandler.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterCountChallengeFactoryHandler.java @@ -5,10 +5,11 @@ import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType; import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterCountTrigger; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.data.SceneGroup; -import java.util.List; import lombok.val; -public class KillMonsterCountChallengeFactoryHandler implements ChallengeFactoryHandler { +import java.util.List; + +public class KillMonsterCountChallengeFactoryHandler implements ChallengeFactoryHandler{ @Override public boolean isThisType(ChallengeType challengeType) { // ActiveChallenge with 1, 1, 241033003, 15, 0, 0 @@ -16,24 +17,16 @@ public class KillMonsterCountChallengeFactoryHandler implements ChallengeFactory } @Override - public WorldChallenge build( - int challengeIndex, - int challengeId, - int groupId, - int goal, - int param5, - int param6, - Scene scene, - SceneGroup group) { + public WorldChallenge build(int challengeIndex, int challengeId, int groupId, int goal, int param5, int param6, Scene scene, SceneGroup group) { val realGroup = scene.getScriptManager().getGroupById(groupId); return new WorldChallenge( - scene, - realGroup, + scene, realGroup, challengeId, // Id challengeIndex, // Index List.of(goal, groupId), 0, // Limit - goal, // Goal - List.of(new KillMonsterCountTrigger())); + goal, // Goal + List.of(new KillMonsterCountTrigger()) + ); } } diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterInTimeChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterInTimeChallengeFactoryHandler.java index 36ebef1af..5adddb0db 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterInTimeChallengeFactoryHandler.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterInTimeChallengeFactoryHandler.java @@ -1,40 +1,33 @@ -package emu.grasscutter.game.dungeons.challenge.factory; - -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType; -import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger; -import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterTrigger; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.data.SceneGroup; -import java.util.List; -import lombok.val; - -public class KillMonsterInTimeChallengeFactoryHandler implements ChallengeFactoryHandler { - @Override - public boolean isThisType(ChallengeType challengeType) { - // ActiveChallenge with 180, 72, 240, 133220161, 133220161, 0 - return challengeType == ChallengeType.CHALLENGE_KILL_MONSTER_IN_TIME; - } - - @Override - public WorldChallenge build( - int challengeIndex, - int challengeId, - int timeLimit, - int groupId, - int targetCfgId, - int param6, - Scene scene, - SceneGroup group) { - val realGroup = scene.getScriptManager().getGroupById(groupId); - return new WorldChallenge( - scene, - realGroup, - challengeId, // Id - challengeIndex, // Index - List.of(timeLimit), - timeLimit, // Limit - 0, // Goal - List.of(new KillMonsterTrigger(targetCfgId), new InTimeTrigger())); - } -} +package emu.grasscutter.game.dungeons.challenge.factory; + +import emu.grasscutter.game.dungeons.challenge.WorldChallenge; +import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType; +import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger; +import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterTrigger; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.scripts.data.SceneGroup; +import lombok.val; + +import java.util.List; + +public class KillMonsterInTimeChallengeFactoryHandler implements ChallengeFactoryHandler{ + @Override + public boolean isThisType(ChallengeType challengeType) { + // ActiveChallenge with 180, 72, 240, 133220161, 133220161, 0 + return challengeType == ChallengeType.CHALLENGE_KILL_MONSTER_IN_TIME; + } + + @Override + public WorldChallenge build(int challengeIndex, int challengeId, int timeLimit, int groupId, int targetCfgId, int param6, Scene scene, SceneGroup group) { + val realGroup = scene.getScriptManager().getGroupById(groupId); + return new WorldChallenge( + scene, realGroup, + challengeId, // Id + challengeIndex, // Index + List.of(timeLimit), + timeLimit, // Limit + 0, // Goal + List.of(new KillMonsterTrigger(targetCfgId), new InTimeTrigger()) + ); + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterTimeChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterTimeChallengeFactoryHandler.java index 303a2d7bf..fdced7377 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterTimeChallengeFactoryHandler.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterTimeChallengeFactoryHandler.java @@ -6,37 +6,30 @@ import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger; import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterCountTrigger; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.data.SceneGroup; -import java.util.List; import lombok.val; -public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryHandler { +import java.util.List; + +public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryHandler{ @Override public boolean isThisType(ChallengeType challengeType) { // ActiveChallenge with 180,180,45,133108061,1,0 // ActiveChallenge Fast with 1001, 5, 15, 240004005, 10, 0 - return challengeType == ChallengeType.CHALLENGE_KILL_COUNT_IN_TIME - || challengeType == ChallengeType.CHALLENGE_KILL_COUNT_FAST; + return challengeType == ChallengeType.CHALLENGE_KILL_COUNT_IN_TIME || + challengeType == ChallengeType.CHALLENGE_KILL_COUNT_FAST; } @Override - public WorldChallenge build( - int challengeIndex, - int challengeId, - int timeLimit, - int groupId, - int targetCount, - int param6, - Scene scene, - SceneGroup group) { + public WorldChallenge build(int challengeIndex, int challengeId, int timeLimit, int groupId, int targetCount, int param6, Scene scene, SceneGroup group) { val realGroup = scene.getScriptManager().getGroupById(groupId); return new WorldChallenge( - scene, - realGroup, + scene, realGroup, challengeId, // Id challengeIndex, // Index List.of(targetCount, timeLimit), timeLimit, // Limit - targetCount, // Goal - List.of(new KillMonsterCountTrigger(), new InTimeTrigger())); + targetCount, // Goal + List.of(new KillMonsterCountTrigger(), new InTimeTrigger()) + ); } } diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/SurviveChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/SurviveChallengeFactoryHandler.java index 9653f3ce9..ae2a1184e 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/SurviveChallengeFactoryHandler.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/SurviveChallengeFactoryHandler.java @@ -1,14 +1,15 @@ package emu.grasscutter.game.dungeons.challenge.factory; -import static emu.grasscutter.game.dungeons.challenge.enums.ChallengeType.CHALLENGE_SURVIVE; - import emu.grasscutter.game.dungeons.challenge.WorldChallenge; import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType; import emu.grasscutter.game.dungeons.challenge.trigger.ForTimeTrigger; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.data.SceneGroup; + import java.util.List; +import static emu.grasscutter.game.dungeons.challenge.enums.ChallengeType.CHALLENGE_SURVIVE; + public class SurviveChallengeFactoryHandler implements ChallengeFactoryHandler { @Override public boolean isThisType(ChallengeType challengeType) { @@ -18,23 +19,15 @@ public class SurviveChallengeFactoryHandler implements ChallengeFactoryHandler { } @Override - public WorldChallenge build( - int challengeIndex, - int challengeId, - int timeToSurvive, - int unused4, - int unused5, - int unused6, - Scene scene, - SceneGroup group) { + public WorldChallenge build(int challengeIndex, int challengeId, int timeToSurvive, int unused4, int unused5, int unused6, Scene scene, SceneGroup group) { return new WorldChallenge( - scene, - group, - challengeId, // Id - challengeIndex, // Index - List.of(timeToSurvive), - timeToSurvive, // Limit - 0, // Goal - List.of(new ForTimeTrigger())); + scene, group, + challengeId, // Id + challengeIndex, // Index + List.of(timeToSurvive), + timeToSurvive, // Limit + 0, // Goal + List.of(new ForTimeTrigger()) + ); } } diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/TriggerInTimeChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/TriggerInTimeChallengeFactoryHandler.java index 79d2d0bce..18b7877db 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/TriggerInTimeChallengeFactoryHandler.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/TriggerInTimeChallengeFactoryHandler.java @@ -1,15 +1,16 @@ package emu.grasscutter.game.dungeons.challenge.factory; -import static emu.grasscutter.game.dungeons.challenge.enums.ChallengeType.CHALLENGE_TRIGGER_IN_TIME; - import emu.grasscutter.game.dungeons.challenge.WorldChallenge; import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType; import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger; import emu.grasscutter.game.dungeons.challenge.trigger.TriggerGroupTriggerTrigger; import emu.grasscutter.game.world.Scene; import emu.grasscutter.scripts.data.SceneGroup; + import java.util.List; +import static emu.grasscutter.game.dungeons.challenge.enums.ChallengeType.CHALLENGE_TRIGGER_IN_TIME; + public class TriggerInTimeChallengeFactoryHandler implements ChallengeFactoryHandler { @Override public boolean isThisType(ChallengeType challengeType) { @@ -21,23 +22,15 @@ public class TriggerInTimeChallengeFactoryHandler implements ChallengeFactoryHan } @Override - public WorldChallenge build( - int challengeIndex, - int challengeId, - int timeLimit, - int param4, - int triggerTag, - int triggerCount, - Scene scene, - SceneGroup group) { + public WorldChallenge build(int challengeIndex, int challengeId, int timeLimit, int param4, int triggerTag, int triggerCount, Scene scene, SceneGroup group) { return new WorldChallenge( - scene, - group, + scene, group, challengeId, // Id challengeIndex, // Index List.of(timeLimit, triggerCount), timeLimit, // Limit - triggerCount, // Goal - List.of(new InTimeTrigger(), new TriggerGroupTriggerTrigger(Integer.toString(triggerTag)))); + triggerCount, // Goal + List.of(new InTimeTrigger(), new TriggerGroupTriggerTrigger(Integer.toString(triggerTag))) + ); } } diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/ChallengeTrigger.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/ChallengeTrigger.java index 1608cf144..17aa4c20f 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/ChallengeTrigger.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/ChallengeTrigger.java @@ -1,19 +1,16 @@ -package emu.grasscutter.game.dungeons.challenge.trigger; - -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityMonster; - -public abstract class ChallengeTrigger { - public void onBegin(WorldChallenge challenge) {} - - public void onFinish(WorldChallenge challenge) {} - - public void onMonsterDeath(WorldChallenge challenge, EntityMonster monster) {} - - public void onGadgetDeath(WorldChallenge challenge, EntityGadget gadget) {} - - public void onCheckTimeout(WorldChallenge challenge) {} - - public void onGadgetDamage(WorldChallenge challenge, EntityGadget gadget) {} -} +package emu.grasscutter.game.dungeons.challenge.trigger; + +import emu.grasscutter.game.dungeons.challenge.WorldChallenge; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.scripts.data.SceneTrigger; + +public abstract class ChallengeTrigger { + public void onBegin(WorldChallenge challenge) { } + public void onFinish(WorldChallenge challenge) { } + public void onMonsterDeath(WorldChallenge challenge, EntityMonster monster) { } + public void onGadgetDeath(WorldChallenge challenge, EntityGadget gadget) { } + public void onCheckTimeout(WorldChallenge challenge) { } + public void onGadgetDamage(WorldChallenge challenge, EntityGadget gadget) { } + public void onGroupTrigger(WorldChallenge challenge, SceneTrigger trigger) { } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/GuardTrigger.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/GuardTrigger.java index 8104931ef..a734bc04e 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/GuardTrigger.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/GuardTrigger.java @@ -1,26 +1,38 @@ -package emu.grasscutter.game.dungeons.challenge.trigger; - -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.server.packet.send.PacketChallengeDataNotify; - -public class GuardTrigger extends KillMonsterTrigger { - @Override - public void onBegin(WorldChallenge challenge) { - super.onBegin(challenge); - challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, 100)); - } - - @Override - public void onGadgetDamage(WorldChallenge challenge, EntityGadget gadget) { - var curHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId()); - var maxHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId()); - int percent = (int) (curHp / maxHp); - challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, percent)); - - if (percent <= 0) { - challenge.fail(); - } - } -} +package emu.grasscutter.game.dungeons.challenge.trigger; + +import emu.grasscutter.game.dungeons.challenge.WorldChallenge; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.server.packet.send.PacketChallengeDataNotify; + +public class GuardTrigger extends ChallengeTrigger { + private final int entityToProtectCFGId; + private int lastSendPercent = 100; + public GuardTrigger(int entityToProtectCFGId){ + this.entityToProtectCFGId = entityToProtectCFGId; + } + + public void onBegin(WorldChallenge challenge) { + challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, 100)); + } + + @Override + public void onGadgetDamage(WorldChallenge challenge, EntityGadget gadget) { + if(gadget.getConfigId() != entityToProtectCFGId){ + return; + } + var curHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_CUR_HP.getId()); + var maxHp = gadget.getFightProperties().get(FightProperty.FIGHT_PROP_BASE_HP.getId()); + int percent = (int) (curHp / maxHp); + + if(percent!=lastSendPercent) { + challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, percent)); + lastSendPercent = percent; + } + + if(percent <= 0){ + challenge.fail(); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/KillMonsterTrigger.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/KillMonsterTrigger.java index e41b1f0d7..8735824fe 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/KillMonsterTrigger.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/KillMonsterTrigger.java @@ -1,24 +1,22 @@ -package emu.grasscutter.game.dungeons.challenge.trigger; - -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.server.packet.send.PacketChallengeDataNotify; - -public class KillMonsterTrigger extends ChallengeTrigger { - @Override - public void onBegin(WorldChallenge challenge) { - challenge - .getScene() - .broadcastPacket(new PacketChallengeDataNotify(challenge, 1, challenge.getScore().get())); - } - - @Override - public void onMonsterDeath(WorldChallenge challenge, EntityMonster monster) { - var newScore = challenge.increaseScore(); - challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 1, newScore)); - - if (newScore >= challenge.getGoal()) { - challenge.done(); - } - } -} +package emu.grasscutter.game.dungeons.challenge.trigger; + +import emu.grasscutter.game.dungeons.challenge.WorldChallenge; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.server.packet.send.PacketChallengeDataNotify; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class KillMonsterTrigger extends ChallengeTrigger{ + private int monsterCfgId; + @Override + public void onBegin(WorldChallenge challenge) { + challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 1, challenge.getScore().get())); + } + + @Override + public void onMonsterDeath(WorldChallenge challenge, EntityMonster monster) { + if(monster.getConfigId() == monsterCfgId){ + challenge.done(); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java index 529f2dc00..184fe380e 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java @@ -1,347 +1,370 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.GameConstants; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.avatar.AvatarData; -import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.inventory.EquipType; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.AbilityControlBlockOuterClass.AbilityControlBlock; -import emu.grasscutter.net.proto.AbilityEmbryoOuterClass.AbilityEmbryo; -import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; -import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; -import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason; -import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason; -import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; -import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; -import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; -import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; -import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; -import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; -import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; -import emu.grasscutter.net.proto.SceneAvatarInfoOuterClass.SceneAvatarInfo; -import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; -import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; -import emu.grasscutter.net.proto.VectorOuterClass.Vector; -import emu.grasscutter.server.event.player.PlayerMoveEvent; -import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify; -import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.ProtoHelper; -import emu.grasscutter.utils.Utils; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import lombok.Getter; -import lombok.val; - -public class EntityAvatar extends GameEntity { - @Getter private final Avatar avatar; - - @Getter private PlayerDieType killedType; - @Getter private int killedBy; - - public EntityAvatar(Avatar avatar) { - this(null, avatar); - } - - public EntityAvatar(Scene scene, Avatar avatar) { - super(scene); - this.avatar = avatar; - this.avatar.setCurrentEnergy(); - if (getScene() != null) { - this.id = getScene().getWorld().getNextEntityId(EntityIdType.AVATAR); - - GameItem weapon = getAvatar().getWeapon(); - if (weapon != null) { - weapon.setWeaponEntityId(getScene().getWorld().getNextEntityId(EntityIdType.WEAPON)); - } - } - } - - public Player getPlayer() { - return this.avatar.getPlayer(); - } - - @Override - public Position getPosition() { - return getPlayer().getPosition(); - } - - @Override - public Position getRotation() { - return getPlayer().getRotation(); - } - - @Override - public boolean isAlive() { - return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f; - } - - @Override - public Int2FloatMap getFightProperties() { - return getAvatar().getFightProperties(); - } - - public int getWeaponEntityId() { - if (getAvatar().getWeapon() != null) { - return getAvatar().getWeapon().getWeaponEntityId(); - } - return 0; - } - - @Override - public void onDeath(int killerId) { - super.onDeath(killerId); // Invoke super class's onDeath() method. - - this.killedType = PlayerDieType.PLAYER_DIE_TYPE_KILL_BY_MONSTER; - this.killedBy = killerId; - clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE); - } - - public void onDeath(PlayerDieType dieType, int killerId) { - super.onDeath(killerId); // Invoke super class's onDeath() method. - - this.killedType = dieType; - this.killedBy = killerId; - clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE); - } - - @Override - public float heal(float amount) { - // Do not heal character if they are dead - if (!this.isAlive()) { - return 0f; - } - - float healed = super.heal(amount); - - if (healed > 0f) { - getScene() - .broadcastPacket( - new PacketEntityFightPropChangeReasonNotify( - this, - FightProperty.FIGHT_PROP_CUR_HP, - healed, - PropChangeReason.PROP_CHANGE_REASON_ABILITY, - ChangeHpReason.CHANGE_HP_REASON_ADD_ABILITY)); - } - - return healed; - } - - public void clearEnergy(ChangeEnergyReason reason) { - // Fight props. - val curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp(); - float curEnergy = this.getFightProperty(curEnergyProp); - - // Set energy to zero. - this.avatar.setCurrentEnergy(curEnergyProp, 0); - - // Send packets. - this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp)); - - if (reason == ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START) { - this.getScene() - .broadcastPacket( - new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, -curEnergy, reason)); - } - } - - public void addEnergy(float amount, PropChangeReason reason) { - this.addEnergy(amount, reason, false); - } - - public void addEnergy(float amount, PropChangeReason reason, boolean isFlat) { - // Get current and maximum energy for this avatar. - val elementType = this.getAvatar().getSkillDepot().getElementType(); - val curEnergyProp = elementType.getCurEnergyProp(); - val maxEnergyProp = elementType.getMaxEnergyProp(); - - float curEnergy = this.getFightProperty(curEnergyProp); - float maxEnergy = this.getFightProperty(maxEnergyProp); - - // Scale amount by energy recharge, if the amount is not flat. - if (!isFlat) { - amount *= this.getFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); - } - - // Determine the new energy value. - float newEnergy = Math.min(curEnergy + amount, maxEnergy); - - // Set energy and notify. - if (newEnergy != curEnergy) { - this.avatar.setCurrentEnergy(curEnergyProp, newEnergy); - - this.getScene() - .broadcastPacket(new PacketAvatarFightPropUpdateNotify(this.getAvatar(), curEnergyProp)); - this.getScene() - .broadcastPacket( - new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, newEnergy, reason)); - } - } - - public SceneAvatarInfo getSceneAvatarInfo() { - val avatar = this.getAvatar(); - val player = this.getPlayer(); - SceneAvatarInfo.Builder avatarInfo = - SceneAvatarInfo.newBuilder() - .setUid(player.getUid()) - .setAvatarId(avatar.getAvatarId()) - .setGuid(avatar.getGuid()) - .setPeerId(player.getPeerId()) - .addAllTalentIdList(avatar.getTalentIdList()) - .setCoreProudSkillLevel(avatar.getCoreProudSkillLevel()) - .putAllSkillLevelMap(avatar.getSkillLevelMap()) - .setSkillDepotId(avatar.getSkillDepotId()) - .addAllInherentProudSkillList(avatar.getProudSkillList()) - .putAllProudSkillExtraLevelMap(avatar.getProudSkillBonusMap()) - .addAllTeamResonanceList(player.getTeamManager().getTeamResonances()) - .setWearingFlycloakId(avatar.getFlyCloak()) - .setCostumeId(avatar.getCostume()) - .setBornTime(avatar.getBornTime()); - - for (GameItem item : avatar.getEquips().values()) { - if (item.getItemData().getEquipType() == EquipType.EQUIP_WEAPON) { - avatarInfo.setWeapon(item.createSceneWeaponInfo()); - } else { - avatarInfo.addReliquaryList(item.createSceneReliquaryInfo()); - } - avatarInfo.addEquipIdList(item.getItemId()); - } - - return avatarInfo.build(); - } - - @Override - public SceneEntityInfo toProto() { - EntityAuthorityInfo authority = - EntityAuthorityInfo.newBuilder() - .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) - .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) - .setAiInfo( - SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(Vector.newBuilder())) - .setBornPos(Vector.newBuilder()) - .build(); - - SceneEntityInfo.Builder entityInfo = - SceneEntityInfo.newBuilder() - .setEntityId(getId()) - .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_AVATAR) - .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) - .setEntityClientData(EntityClientData.newBuilder()) - .setEntityAuthorityInfo(authority) - .setLastMoveSceneTimeMs(this.getLastMoveSceneTimeMs()) - .setLastMoveReliableSeq(this.getLastMoveReliableSeq()) - .setLifeState(this.getLifeState().getValue()); - - if (this.getScene() != null) { - entityInfo.setMotionInfo(this.getMotionInfo()); - } - - this.addAllFightPropsToEntityInfo(entityInfo); - - PropPair pair = - PropPair.newBuilder() - .setType(PlayerProperty.PROP_LEVEL.getId()) - .setPropValue( - ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, getAvatar().getLevel())) - .build(); - entityInfo.addPropList(pair); - - entityInfo.setAvatar(this.getSceneAvatarInfo()); - - return entityInfo.build(); - } - - public AbilityControlBlock getAbilityControlBlock() { - AvatarData data = this.getAvatar().getAvatarData(); - AbilityControlBlock.Builder abilityControlBlock = AbilityControlBlock.newBuilder(); - int embryoId = 0; - - // Add avatar abilities - if (data.getAbilities() != null) { - for (int id : data.getAbilities()) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(id) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - } - // Add default abilities - for (int id : GameConstants.DEFAULT_ABILITY_HASHES) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(id) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - // Add team resonances - for (int id : this.getPlayer().getTeamManager().getTeamResonancesConfig()) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(id) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - // Add skill depot abilities - AvatarSkillDepotData skillDepot = - GameData.getAvatarSkillDepotDataMap().get(this.getAvatar().getSkillDepotId()); - if (skillDepot != null && skillDepot.getAbilities() != null) { - for (int id : skillDepot.getAbilities()) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(id) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - } - // Add equip abilities - if (this.getAvatar().getExtraAbilityEmbryos().size() > 0) { - for (String skill : this.getAvatar().getExtraAbilityEmbryos()) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(Utils.abilityHash(skill)) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - } - - // - return abilityControlBlock.build(); - } - - /** - * Move this entity to a new position. Additionally invoke player move event. - * - * @param newPosition The new position. - * @param rotation The new rotation. - */ - @Override - public void move(Position newPosition, Position rotation) { - // Invoke player move event. - PlayerMoveEvent event = - new PlayerMoveEvent( - this.getPlayer(), PlayerMoveEvent.MoveType.PLAYER, this.getPosition(), newPosition); - event.call(); - - // Set position and rotation. - super.move(event.getDestination(), rotation); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.GameConstants; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.avatar.AvatarData; +import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.inventory.EquipType; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.AbilityControlBlockOuterClass.AbilityControlBlock; +import emu.grasscutter.net.proto.AbilityEmbryoOuterClass.AbilityEmbryo; +import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; +import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; +import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason; +import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason; +import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; +import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; +import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; +import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; +import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; +import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; +import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; +import emu.grasscutter.net.proto.SceneAvatarInfoOuterClass.SceneAvatarInfo; +import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import emu.grasscutter.server.event.player.PlayerMoveEvent; +import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify; +import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.ProtoHelper; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2FloatMap; +import lombok.Getter; +import lombok.val; + +public class EntityAvatar extends GameEntity { + @Getter private final Avatar avatar; + + @Getter private PlayerDieType killedType; + @Getter private int killedBy; + + public EntityAvatar(Avatar avatar) { + this(null, avatar); + } + + public EntityAvatar(Scene scene, Avatar avatar) { + super(scene); + + this.avatar = avatar; + this.avatar.setCurrentEnergy(); + + if (getScene() != null) { + this.id = getScene().getWorld().getNextEntityId(EntityIdType.AVATAR); + + var weapon = getAvatar().getWeapon(); + if (weapon != null) { + weapon.setWeaponEntityId(getScene().getWorld().getNextEntityId(EntityIdType.WEAPON)); + } + } + } + + @Override + public int getEntityTypeId() { + return this.getAvatar().getAvatarId(); + } + + public Player getPlayer() { + return this.avatar.getPlayer(); + } + + @Override + public Position getPosition() { + return getPlayer().getPosition(); + } + + @Override + public Position getRotation() { + return getPlayer().getRotation(); + } + + @Override + public boolean isAlive() { + return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f; + } + + @Override + public Int2FloatMap getFightProperties() { + return getAvatar().getFightProperties(); + } + + public int getWeaponEntityId() { + if (getAvatar().getWeapon() != null) { + return getAvatar().getWeapon().getWeaponEntityId(); + } + return 0; + } + + @Override + public void onDeath(int killerId) { + super.onDeath(killerId); // Invoke super class's onDeath() method. + + this.killedType = PlayerDieType.PLAYER_DIE_TYPE_KILL_BY_MONSTER; + this.killedBy = killerId; + clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE); + } + + public void onDeath(PlayerDieType dieType, int killerId) { + super.onDeath(killerId); // Invoke super class's onDeath() method. + + this.killedType = dieType; + this.killedBy = killerId; + clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE); + } + + @Override + public float heal(float amount) { + // Do not heal character if they are dead + if (!this.isAlive()) { + return 0f; + } + + float healed = super.heal(amount); + + if (healed > 0f) { + getScene() + .broadcastPacket( + new PacketEntityFightPropChangeReasonNotify( + this, + FightProperty.FIGHT_PROP_CUR_HP, + healed, + PropChangeReason.PROP_CHANGE_REASON_ABILITY, + ChangeHpReason.CHANGE_HP_REASON_ADD_ABILITY)); + } + + return healed; + } + + public void clearEnergy(ChangeEnergyReason reason) { + // Fight props. + val curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp(); + float curEnergy = this.getFightProperty(curEnergyProp); + + // Set energy to zero. + this.avatar.setCurrentEnergy(curEnergyProp, 0); + + // Send packets. + this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp)); + + if (reason == ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START) { + this.getScene() + .broadcastPacket( + new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, -curEnergy, reason)); + } + } + + /** + * Adds a fixed amount of energy to the current avatar. + * + * @param amount The amount of energy to add. + * @return True if the energy was added, false if the energy was not added. + */ + public boolean addEnergy(float amount) { + var curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp(); + var curEnergy = this.getFightProperty(curEnergyProp); + if (curEnergy == amount) return false; + + this.getAvatar().setCurrentEnergy(curEnergyProp, amount); + this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp)); + return true; + } + + public void addEnergy(float amount, PropChangeReason reason) { + this.addEnergy(amount, reason, false); + } + + public void addEnergy(float amount, PropChangeReason reason, boolean isFlat) { + // Get current and maximum energy for this avatar. + val elementType = this.getAvatar().getSkillDepot().getElementType(); + val curEnergyProp = elementType.getCurEnergyProp(); + val maxEnergyProp = elementType.getMaxEnergyProp(); + + float curEnergy = this.getFightProperty(curEnergyProp); + float maxEnergy = this.getFightProperty(maxEnergyProp); + + // Scale amount by energy recharge, if the amount is not flat. + if (!isFlat) { + amount *= this.getFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); + } + + // Determine the new energy value. + float newEnergy = Math.min(curEnergy + amount, maxEnergy); + + // Set energy and notify. + if (newEnergy != curEnergy) { + this.avatar.setCurrentEnergy(curEnergyProp, newEnergy); + + this.getScene() + .broadcastPacket(new PacketAvatarFightPropUpdateNotify(this.getAvatar(), curEnergyProp)); + this.getScene() + .broadcastPacket( + new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, newEnergy, reason)); + } + } + + public SceneAvatarInfo getSceneAvatarInfo() { + val avatar = this.getAvatar(); + val player = this.getPlayer(); + SceneAvatarInfo.Builder avatarInfo = + SceneAvatarInfo.newBuilder() + .setUid(player.getUid()) + .setAvatarId(avatar.getAvatarId()) + .setGuid(avatar.getGuid()) + .setPeerId(player.getPeerId()) + .addAllTalentIdList(avatar.getTalentIdList()) + .setCoreProudSkillLevel(avatar.getCoreProudSkillLevel()) + .putAllSkillLevelMap(avatar.getSkillLevelMap()) + .setSkillDepotId(avatar.getSkillDepotId()) + .addAllInherentProudSkillList(avatar.getProudSkillList()) + .putAllProudSkillExtraLevelMap(avatar.getProudSkillBonusMap()) + .addAllTeamResonanceList(player.getTeamManager().getTeamResonances()) + .setWearingFlycloakId(avatar.getFlyCloak()) + .setCostumeId(avatar.getCostume()) + .setBornTime(avatar.getBornTime()); + + for (GameItem item : avatar.getEquips().values()) { + if (item.getItemData().getEquipType() == EquipType.EQUIP_WEAPON) { + avatarInfo.setWeapon(item.createSceneWeaponInfo()); + } else { + avatarInfo.addReliquaryList(item.createSceneReliquaryInfo()); + } + avatarInfo.addEquipIdList(item.getItemId()); + } + + return avatarInfo.build(); + } + + @Override + public SceneEntityInfo toProto() { + EntityAuthorityInfo authority = + EntityAuthorityInfo.newBuilder() + .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) + .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) + .setAiInfo( + SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(Vector.newBuilder())) + .setBornPos(Vector.newBuilder()) + .build(); + + SceneEntityInfo.Builder entityInfo = + SceneEntityInfo.newBuilder() + .setEntityId(getId()) + .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_AVATAR) + .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) + .setEntityClientData(EntityClientData.newBuilder()) + .setEntityAuthorityInfo(authority) + .setLastMoveSceneTimeMs(this.getLastMoveSceneTimeMs()) + .setLastMoveReliableSeq(this.getLastMoveReliableSeq()) + .setLifeState(this.getLifeState().getValue()); + + if (this.getScene() != null) { + entityInfo.setMotionInfo(this.getMotionInfo()); + } + + this.addAllFightPropsToEntityInfo(entityInfo); + + PropPair pair = + PropPair.newBuilder() + .setType(PlayerProperty.PROP_LEVEL.getId()) + .setPropValue( + ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, getAvatar().getLevel())) + .build(); + entityInfo.addPropList(pair); + + entityInfo.setAvatar(this.getSceneAvatarInfo()); + + return entityInfo.build(); + } + + public AbilityControlBlock getAbilityControlBlock() { + AvatarData data = this.getAvatar().getAvatarData(); + AbilityControlBlock.Builder abilityControlBlock = AbilityControlBlock.newBuilder(); + int embryoId = 0; + + // Add avatar abilities + if (data.getAbilities() != null) { + for (int id : data.getAbilities()) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(id) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + } + // Add default abilities + for (int id : GameConstants.DEFAULT_ABILITY_HASHES) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(id) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + // Add team resonances + for (int id : this.getPlayer().getTeamManager().getTeamResonancesConfig()) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(id) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + // Add skill depot abilities + AvatarSkillDepotData skillDepot = + GameData.getAvatarSkillDepotDataMap().get(this.getAvatar().getSkillDepotId()); + if (skillDepot != null && skillDepot.getAbilities() != null) { + for (int id : skillDepot.getAbilities()) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(id) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + } + // Add equip abilities + if (this.getAvatar().getExtraAbilityEmbryos().size() > 0) { + for (String skill : this.getAvatar().getExtraAbilityEmbryos()) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(Utils.abilityHash(skill)) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + } + + // + return abilityControlBlock.build(); + } + + /** + * Move this entity to a new position. Additionally invoke player move event. + * + * @param newPosition The new position. + * @param rotation The new rotation. + */ + @Override + public void move(Position newPosition, Position rotation) { + // Invoke player move event. + PlayerMoveEvent event = + new PlayerMoveEvent( + this.getPlayer(), PlayerMoveEvent.MoveType.PLAYER, this.getPosition(), newPosition); + event.call(); + + // Set position and rotation. + super.move(event.getDestination(), rotation); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java b/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java index 5d262b461..5b81c422e 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java @@ -1,58 +1,63 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.data.binout.ConfigGadget; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.utils.Position; -import lombok.Getter; - -public abstract class EntityBaseGadget extends GameEntity { - @Getter(onMethod_ = @Override) - protected final Position position; - - @Getter(onMethod_ = @Override) - protected final Position rotation; - - public EntityBaseGadget(Scene scene) { - this(scene, null, null); - } - - public EntityBaseGadget(Scene scene, Position position, Position rotation) { - super(scene); - this.position = position != null ? position.clone() : new Position(); - this.rotation = rotation != null ? rotation.clone() : new Position(); - } - - public abstract int getGadgetId(); - - @Override - public void onDeath(int killerId) { - super.onDeath(killerId); // Invoke super class's onDeath() method. - } - - protected void fillFightProps(ConfigGadget configGadget) { - if (configGadget == null || configGadget.getCombat() == null) { - return; - } - var combatData = configGadget.getCombat(); - var combatProperties = combatData.getProperty(); - - var targetHp = combatProperties.getHP(); - setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, targetHp); - setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, targetHp); - if (combatProperties.isInvincible()) { - targetHp = Float.POSITIVE_INFINITY; - } - setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, targetHp); - - var atk = combatProperties.getAttack(); - setFightProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, atk); - setFightProperty(FightProperty.FIGHT_PROP_CUR_ATTACK, atk); - - var def = combatProperties.getDefence(); - setFightProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, def); - setFightProperty(FightProperty.FIGHT_PROP_CUR_DEFENSE, def); - - setLockHP(combatProperties.isLockHP()); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.data.binout.config.ConfigEntityGadget; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.utils.Position; +import lombok.Getter; + +public abstract class EntityBaseGadget extends GameEntity { + @Getter(onMethod_ = @Override) + protected final Position position; + + @Getter(onMethod_ = @Override) + protected final Position rotation; + + public EntityBaseGadget(Scene scene) { + this(scene, null, null); + } + + public EntityBaseGadget(Scene scene, Position position, Position rotation) { + super(scene); + this.position = position != null ? position.clone() : new Position(); + this.rotation = rotation != null ? rotation.clone() : new Position(); + } + + public abstract int getGadgetId(); + + @Override + public int getEntityTypeId() { + return this.getGadgetId(); + } + + @Override + public void onDeath(int killerId) { + super.onDeath(killerId); // Invoke super class's onDeath() method. + } + + protected void fillFightProps(ConfigEntityGadget configGadget) { + if (configGadget == null || configGadget.getCombat() == null) { + return; + } + var combatData = configGadget.getCombat(); + var combatProperties = combatData.getProperty(); + + var targetHp = combatProperties.getHP(); + setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, targetHp); + setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, targetHp); + if (combatProperties.isInvincible()) { + targetHp = Float.POSITIVE_INFINITY; + } + setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, targetHp); + + var atk = combatProperties.getAttack(); + setFightProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, atk); + setFightProperty(FightProperty.FIGHT_PROP_CUR_ATTACK, atk); + + var def = combatProperties.getDefence(); + setFightProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, def); + setFightProperty(FightProperty.FIGHT_PROP_CUR_DEFENSE, def); + + setLockHP(combatProperties.isLockHP()); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityGadget.java b/src/main/java/emu/grasscutter/game/entity/EntityGadget.java index e8cb0aceb..90d639c8c 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityGadget.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityGadget.java @@ -1,206 +1,281 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.ConfigGadget; -import emu.grasscutter.data.excels.GadgetData; -import emu.grasscutter.game.entity.gadget.*; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; -import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; -import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; -import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; -import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; -import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; -import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; -import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; -import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; -import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; -import emu.grasscutter.net.proto.VectorOuterClass.Vector; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.data.SceneGadget; -import emu.grasscutter.scripts.data.ScriptArgs; -import emu.grasscutter.server.packet.send.PacketGadgetStateNotify; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.ProtoHelper; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; -import java.util.Optional; -import javax.annotation.Nullable; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -@ToString(callSuper = true) -public class EntityGadget extends EntityBaseGadget { - @Getter private final GadgetData gadgetData; - - @Getter(onMethod_ = @Override, lazy = true) - private final Int2FloatMap fightProperties = new Int2FloatOpenHashMap(); - - @Getter(onMethod_ = @Override) - @Setter - private int gadgetId; - - @Getter @Setter private int state; - @Getter @Setter private int pointType; - @Getter private GadgetContent content; - @Getter @Setter private SceneGadget metaGadget; - @Nullable @Getter private final ConfigGadget configGadget; - - public EntityGadget(Scene scene, int gadgetId, Position pos) { - this(scene, gadgetId, pos, null, null); - } - - public EntityGadget(Scene scene, int gadgetId, Position pos, Position rot) { - this(scene, gadgetId, pos, rot, null); - } - - public EntityGadget( - Scene scene, int gadgetId, Position pos, Position rot, GadgetContent content) { - super(scene, pos, rot); - this.gadgetData = GameData.getGadgetDataMap().get(gadgetId); - this.configGadget = - Optional.ofNullable(this.gadgetData) - .map(GadgetData::getJsonName) - .map(GameData.getGadgetConfigData()::get) - .orElse(null); - this.id = this.getScene().getWorld().getNextEntityId(EntityIdType.GADGET); - this.gadgetId = gadgetId; - this.content = content; - fillFightProps(configGadget); - } - - public void updateState(int state) { - this.setState(state); - this.getScene().broadcastPacket(new PacketGadgetStateNotify(this, state)); - getScene() - .getScriptManager() - .callEvent(EventType.EVENT_GADGET_STATE_CHANGE, new ScriptArgs(state, this.getConfigId())); - } - - @Deprecated(forRemoval = true) // Dont use! - public void setContent(GadgetContent content) { - this.content = this.content == null ? content : this.content; - } - - // TODO refactor - public void buildContent() { - if (this.getContent() != null - || this.getGadgetData() == null - || this.getGadgetData().getType() == null) { - return; - } - - this.content = - switch (this.getGadgetData().getType()) { - case GatherPoint -> new GadgetGatherPoint(this); - case GatherObject -> new GadgetGatherObject(this); - case Worktop -> new GadgetWorktop(this); - case RewardStatue -> new GadgetRewardStatue(this); - case Chest -> new GadgetChest(this); - case Gadget -> new GadgetObject(this); - default -> null; - }; - } - - @Override - public void onInteract(Player player, GadgetInteractReq interactReq) { - if (this.getContent() == null) { - return; - } - - boolean shouldDelete = this.getContent().onInteract(player, interactReq); - - if (shouldDelete) { - this.getScene().killEntity(this); - } - } - - @Override - public void onCreate() { - // Lua event - getScene() - .getScriptManager() - .callEvent(EventType.EVENT_GADGET_CREATE, new ScriptArgs(this.getConfigId())); - } - - @Override - public void onDeath(int killerId) { - super.onDeath(killerId); // Invoke super class's onDeath() method. - - if (this.getSpawnEntry() != null) { - this.getScene().getDeadSpawnedEntities().add(getSpawnEntry()); - } - if (getScene().getChallenge() != null) { - getScene().getChallenge().onGadgetDeath(this); - } - getScene() - .getScriptManager() - .callEvent(EventType.EVENT_ANY_GADGET_DIE, new ScriptArgs(this.getConfigId())); - } - - @Override - public SceneEntityInfo toProto() { - EntityAuthorityInfo authority = - EntityAuthorityInfo.newBuilder() - .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) - .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) - .setAiInfo( - SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(Vector.newBuilder())) - .setBornPos(Vector.newBuilder()) - .build(); - - SceneEntityInfo.Builder entityInfo = - SceneEntityInfo.newBuilder() - .setEntityId(getId()) - .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_GADGET) - .setMotionInfo( - MotionInfo.newBuilder() - .setPos(getPosition().toProto()) - .setRot(getRotation().toProto()) - .setSpeed(Vector.newBuilder())) - .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) - .setEntityClientData(EntityClientData.newBuilder()) - .setEntityAuthorityInfo(authority) - .setLifeState(1); - - PropPair pair = - PropPair.newBuilder() - .setType(PlayerProperty.PROP_LEVEL.getId()) - .setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, 1)) - .build(); - entityInfo.addPropList(pair); - - // We do not use the getter to null check because the getter will create a fight prop map if it - // is null - if (this.fightProperties != null) { - addAllFightPropsToEntityInfo(entityInfo); - } - - SceneGadgetInfo.Builder gadgetInfo = - SceneGadgetInfo.newBuilder() - .setGadgetId(this.getGadgetId()) - .setGroupId(this.getGroupId()) - .setConfigId(this.getConfigId()) - .setGadgetState(this.getState()) - .setIsEnableInteract(true) - .setAuthorityPeerId(this.getScene().getWorld().getHostPeerId()); - - if (this.metaGadget != null) { - gadgetInfo.setDraftId(this.metaGadget.draft_id); - } - - if (this.getContent() != null) { - this.getContent().onBuildProto(gadgetInfo); - } - - entityInfo.setGadget(gadgetInfo); - - return entityInfo.build(); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.config.ConfigEntityGadget; +import emu.grasscutter.data.excels.GadgetData; +import emu.grasscutter.game.entity.gadget.*; +import emu.grasscutter.game.entity.gadget.platform.BaseRoute; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.game.world.SceneGroupInstance; +import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; +import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; +import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; +import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; +import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; +import emu.grasscutter.net.proto.PlatformInfoOuterClass; +import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; +import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; +import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; +import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import emu.grasscutter.net.proto.VisionTypeOuterClass; +import emu.grasscutter.scripts.EntityControllerScriptManager; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.SceneGadget; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.packet.send.PacketGadgetStateNotify; +import emu.grasscutter.server.packet.send.PacketPlatformStartRouteNotify; +import emu.grasscutter.server.packet.send.PacketPlatformStopRouteNotify; +import emu.grasscutter.server.packet.send.PacketSceneTimeNotify; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.ProtoHelper; +import it.unimi.dsi.fastutil.ints.Int2FloatMap; +import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +@ToString(callSuper = true) +public class EntityGadget extends EntityBaseGadget { + @Getter private final GadgetData gadgetData; + @Getter(onMethod = @__(@Override)) @Setter + private int gadgetId; + @Getter private final Position bornPos; + @Getter private final Position bornRot; + @Getter @Setter private GameEntity owner = null; + @Getter @Setter private List children = new ArrayList<>(); + + @Getter private int state; + @Getter @Setter private int pointType; + @Getter private GadgetContent content; + @Getter(onMethod = @__(@Override), lazy = true) + private final Int2FloatMap fightProperties = new Int2FloatOpenHashMap(); + @Getter @Setter private SceneGadget metaGadget; + @Nullable @Getter + private ConfigEntityGadget configGadget; + @Getter @Setter private BaseRoute routeConfig; + + @Getter @Setter private int stopValue = 0; //Controller related, inited to zero + @Getter @Setter private int startValue = 0; //Controller related, inited to zero + @Getter @Setter private int ticksSinceChange; + + + public EntityGadget(Scene scene, int gadgetId, Position pos) { + this(scene, gadgetId, pos, null, null); + } + + public EntityGadget(Scene scene, int gadgetId, Position pos, Position rot) { + this(scene, gadgetId, pos, rot, null); + } + + public EntityGadget(Scene scene, int gadgetId, Position pos, Position rot, GadgetContent content) { + super(scene, pos, rot); + + this.gadgetData = GameData.getGadgetDataMap().get(gadgetId); + if (gadgetData != null && gadgetData.getJsonName() != null) { + this.configGadget = GameData.getGadgetConfigData().get(gadgetData.getJsonName()); + } + + this.id = this.getScene().getWorld().getNextEntityId(EntityIdType.GADGET); + this.gadgetId = gadgetId; + this.content = content; + this.bornPos = this.getPosition().clone(); + this.bornRot = this.getRotation().clone(); + this.fillFightProps(configGadget); + + if(GameData.getGadgetMappingMap().containsKey(gadgetId)) { + String controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController(); + this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName)); + } + } + + public void setState(int state) { + this.state = state; + //Cache the gadget state + if(metaGadget != null && metaGadget.group != null) { + var instance = getScene().getScriptManager().getCachedGroupInstanceById(metaGadget.group.id); + if(instance != null) instance.cacheGadgetState(metaGadget, state); + } + } + + public void updateState(int state) { + if(state == this.getState()) return; //Don't triggers events + + this.setState(state); + ticksSinceChange = getScene().getSceneTimeSeconds(); + this.getScene().broadcastPacket(new PacketGadgetStateNotify(this, state)); + getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_GADGET_STATE_CHANGE, state, this.getConfigId())); + } + + @Deprecated(forRemoval = true) // Dont use! + public void setContent(GadgetContent content) { + this.content = this.content == null ? content : this.content; + } + + // TODO refactor + public void buildContent() { + if (this.getContent() != null || this.getGadgetData() == null || this.getGadgetData().getType() == null) { + return; + } + + this.content = switch (this.getGadgetData().getType()) { + case GatherPoint -> new GadgetGatherPoint(this); + case GatherObject -> new GadgetGatherObject(this); + case Worktop, SealGadget -> new GadgetWorktop(this); + case RewardStatue -> new GadgetRewardStatue(this); + case Chest -> new GadgetChest(this); + case Gadget -> new GadgetObject(this); + default -> null; + }; + } + + @Override + public void onInteract(Player player, GadgetInteractReq interactReq) { + if (this.getContent() == null) { + return; + } + + boolean shouldDelete = this.getContent().onInteract(player, interactReq); + + if (shouldDelete) { + this.getScene().killEntity(this); + } + } + + @Override + public void onCreate() { + // Lua event + getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_GADGET_CREATE, this.getConfigId())); + } + + @Override + public void onRemoved() { + super.onRemoved(); + if(!children.isEmpty()) { + getScene().removeEntities(children, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE); + children.clear(); + } + } + + @Override + public void onDeath(int killerId) { + super.onDeath(killerId); // Invoke super class's onDeath() method. + + if (this.getSpawnEntry() != null) { + this.getScene().getDeadSpawnedEntities().add(getSpawnEntry()); + } + if (getScene().getChallenge() != null) { + getScene().getChallenge().onGadgetDeath(this); + } + getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_GADGET_DIE, this.getConfigId())); + + SceneGroupInstance groupInstance = getScene().getScriptManager().getCachedGroupInstanceById(this.getGroupId()); + if(groupInstance != null && metaGadget != null) + groupInstance.getDeadEntities().add(metaGadget.config_id); + } + + public boolean startPlatform(){ + if(routeConfig == null){ + return false; + } + + if(routeConfig.isStarted()){ + return true; + } + getScene().broadcastPacket(new PacketSceneTimeNotify(getScene())); + routeConfig.startRoute(getScene()); + getScene().broadcastPacket(new PacketPlatformStartRouteNotify(this)); + + return true; + } + + public boolean stopPlatform(){ + if(routeConfig == null){ + return false; + } + + if(!routeConfig.isStarted()){ + return true; + } + routeConfig.stopRoute(getScene()); + getScene().broadcastPacket(new PacketPlatformStopRouteNotify(this)); + + return true; + } + + @Override + public SceneEntityInfo toProto() { + EntityAuthorityInfo authority = EntityAuthorityInfo.newBuilder() + .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) + .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) + .setAiInfo(SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(bornPos.toProto())) + .setBornPos(bornPos.toProto()) + .build(); + + SceneEntityInfo.Builder entityInfo = SceneEntityInfo.newBuilder() + .setEntityId(getId()) + .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_GADGET) + .setMotionInfo(MotionInfo.newBuilder().setPos(getPosition().toProto()).setRot(getRotation().toProto()).setSpeed(Vector.newBuilder())) + .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) + .setEntityClientData(EntityClientData.newBuilder()) + .setEntityAuthorityInfo(authority) + .setLifeState(1); + + PropPair pair = PropPair.newBuilder() + .setType(PlayerProperty.PROP_LEVEL.getId()) + .setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, 1)) + .build(); + entityInfo.addPropList(pair); + + // We do not use the getter to null check because the getter will create a fight prop map if it is null + if (this.fightProperties != null) { + addAllFightPropsToEntityInfo(entityInfo); + } + + SceneGadgetInfo.Builder gadgetInfo = SceneGadgetInfo.newBuilder() + .setGadgetId(this.getGadgetId()) + .setGroupId(this.getGroupId()) + .setConfigId(this.getConfigId()) + .setGadgetState(this.getState()) + .setIsEnableInteract(true) + .setAuthorityPeerId(this.getScene().getWorld().getHostPeerId()); + + if (this.metaGadget != null) { + gadgetInfo.setDraftId(this.metaGadget.draft_id); + } + + if(owner != null){ + gadgetInfo.setOwnerEntityId(owner.getId()); + } + + if (this.getContent() != null) { + this.getContent().onBuildProto(gadgetInfo); + } + + if(routeConfig!=null){ + gadgetInfo.setPlatform(getPlatformInfo()); + } + + entityInfo.setGadget(gadgetInfo); + + return entityInfo.build(); + } + + public PlatformInfoOuterClass.PlatformInfo.Builder getPlatformInfo(){ + if(routeConfig != null){ + return routeConfig.toProto(); + } + + return PlatformInfoOuterClass.PlatformInfo.newBuilder(); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java index 3896ed674..9a05c4123 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityMonster.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityMonster.java @@ -1,260 +1,265 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.common.PropGrowCurve; -import emu.grasscutter.data.excels.EnvAnimalGatherConfigData; -import emu.grasscutter.data.excels.monster.MonsterCurveData; -import emu.grasscutter.data.excels.monster.MonsterData; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.*; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; -import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; -import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; -import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; -import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.net.proto.MonsterBornTypeOuterClass.MonsterBornType; -import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; -import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; -import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; -import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; -import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo; -import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.data.ScriptArgs; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.ProtoHelper; -import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; -import java.util.Optional; -import lombok.Getter; -import lombok.Setter; - -public class EntityMonster extends GameEntity { - @Getter private final MonsterData monsterData; - - @Getter(onMethod_ = @Override) - private final Int2FloatOpenHashMap fightProperties; - - @Getter(onMethod_ = @Override) - private final Position position; - - @Getter(onMethod_ = @Override) - private final Position rotation; - - @Getter private final Position bornPos; - @Getter private final int level; - private int weaponEntityId; - @Getter @Setter private int poseId; - @Getter @Setter private int aiId = -1; - - public EntityMonster(Scene scene, MonsterData monsterData, Position pos, int level) { - super(scene); - this.id = getWorld().getNextEntityId(EntityIdType.MONSTER); - this.monsterData = monsterData; - this.fightProperties = new Int2FloatOpenHashMap(); - this.position = new Position(pos); - this.rotation = new Position(); - this.bornPos = getPosition().clone(); - this.level = level; - - // Monster weapon - if (getMonsterWeaponId() > 0) { - this.weaponEntityId = getWorld().getNextEntityId(EntityIdType.WEAPON); - } - - this.recalcStats(); - } - - public int getMonsterWeaponId() { - return this.getMonsterData().getWeaponId(); - } - - private int getMonsterId() { - return this.getMonsterData().getId(); - } - - @Override - public boolean isAlive() { - return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f; - } - - @Override - public void onInteract(Player player, GadgetInteractReq interactReq) { - EnvAnimalGatherConfigData gatherData = - GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId()); - - if (gatherData == null) { - return; - } - - player.getInventory().addItem(gatherData.getGatherItem(), ActionReason.SubfieldDrop); - - this.getScene().killEntity(this); - } - - @Override - public void onCreate() { - // Lua event - getScene() - .getScriptManager() - .callEvent(EventType.EVENT_ANY_MONSTER_LIVE, new ScriptArgs(this.getConfigId())); - } - - @Override - public void damage(float amount, int killerId) { - // Get HP before damage. - float hpBeforeDamage = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - - // Apply damage. - super.damage(amount, killerId); - - // Get HP after damage. - float hpAfterDamage = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - - // Invoke energy drop logic. - for (Player player : this.getScene().getPlayers()) { - player.getEnergyManager().handleMonsterEnergyDrop(this, hpBeforeDamage, hpAfterDamage); - } - } - - @Override - public void onDeath(int killerId) { - super.onDeath(killerId); // Invoke super class's onDeath() method. - var scene = this.getScene(); - var challenge = Optional.ofNullable(scene.getChallenge()); - var scriptManager = scene.getScriptManager(); - - Optional.ofNullable(this.getSpawnEntry()).ifPresent(scene.getDeadSpawnedEntities()::add); - - // first set the challenge data - challenge.ifPresent(c -> c.onMonsterDeath(this)); - - if (scriptManager.isInit() && this.getGroupId() > 0) { - Optional.ofNullable(scriptManager.getScriptMonsterSpawnService()) - .ifPresent(s -> s.onMonsterDead(this)); - - // prevent spawn monster after success - if (challenge.map(c -> c.inProgress()).orElse(true)) - scriptManager.callEvent( - EventType.EVENT_ANY_MONSTER_DIE, new ScriptArgs().setParam1(this.getConfigId())); - } - // Battle Pass trigger - scene - .getPlayers() - .forEach( - p -> - p.getBattlePassManager() - .triggerMission( - WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1)); - } - - public void recalcStats() { - // Monster data - MonsterData data = this.getMonsterData(); - - // Get hp percent, set to 100% if none - float hpPercent = - this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 - ? 1f - : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) - / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - - // Clear properties - this.getFightProperties().clear(); - - // Base stats - MonsterData.definedFightProperties.forEach( - prop -> this.setFightProperty(prop, data.getFightProperty(prop))); - - // Level curve - MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(this.getLevel()); - if (curve != null) { - for (PropGrowCurve growCurve : data.getPropGrowCurves()) { - FightProperty prop = FightProperty.getPropByName(growCurve.getType()); - this.setFightProperty( - prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve())); - } - } - - // Set % stats - FightProperty.forEachCompoundProperty( - c -> - this.setFightProperty( - c.getResult(), - this.getFightProperty(c.getFlat()) - + (this.getFightProperty(c.getBase()) - * (1f + this.getFightProperty(c.getPercent()))))); - - // Set current hp - this.setFightProperty( - FightProperty.FIGHT_PROP_CUR_HP, - this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent); - } - - @Override - public SceneEntityInfo toProto() { - var authority = - EntityAuthorityInfo.newBuilder() - .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) - .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) - .setAiInfo( - SceneEntityAiInfo.newBuilder() - .setIsAiOpen(true) - .setBornPos(this.getBornPos().toProto())) - .setBornPos(this.getBornPos().toProto()) - .build(); - - var entityInfo = - SceneEntityInfo.newBuilder() - .setEntityId(getId()) - .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_MONSTER) - .setMotionInfo(this.getMotionInfo()) - .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) - .setEntityClientData(EntityClientData.newBuilder()) - .setEntityAuthorityInfo(authority) - .setLifeState(this.getLifeState().getValue()); - - this.addAllFightPropsToEntityInfo(entityInfo); - - entityInfo.addPropList( - PropPair.newBuilder() - .setType(PlayerProperty.PROP_LEVEL.getId()) - .setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, getLevel())) - .build()); - - var monsterInfo = - SceneMonsterInfo.newBuilder() - .setMonsterId(getMonsterId()) - .setGroupId(this.getGroupId()) - .setConfigId(this.getConfigId()) - .addAllAffixList(getMonsterData().getAffix()) - .setAuthorityPeerId(getWorld().getHostPeerId()) - .setPoseId(this.getPoseId()) - .setBlockId(3001) - .setBornType(MonsterBornType.MONSTER_BORN_TYPE_DEFAULT) - .setSpecialNameId(40); - - if (getMonsterData().getDescribeData() != null) { - monsterInfo.setTitleId(getMonsterData().getDescribeData().getTitleID()); - } - - if (this.getMonsterWeaponId() > 0) { - SceneWeaponInfo weaponInfo = - SceneWeaponInfo.newBuilder() - .setEntityId(this.weaponEntityId) - .setGadgetId(this.getMonsterWeaponId()) - .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) - .build(); - - monsterInfo.addWeaponList(weaponInfo); - } - if (this.aiId != -1) { - monsterInfo.setAiConfigId(aiId); - } - - entityInfo.setMonster(monsterInfo); - - return entityInfo.build(); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.common.PropGrowCurve; +import emu.grasscutter.data.excels.EnvAnimalGatherConfigData; +import emu.grasscutter.data.excels.monster.MonsterCurveData; +import emu.grasscutter.data.excels.monster.MonsterData; +import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.*; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.game.world.SceneGroupInstance; +import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; +import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; +import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; +import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; +import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.net.proto.MonsterBornTypeOuterClass.MonsterBornType; +import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; +import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; +import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; +import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo; +import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.SceneMonster; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.event.entity.EntityDamageEvent; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.ProtoHelper; +import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; +import lombok.Getter; +import lombok.Setter; + +import java.util.Optional; + +import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_MONSTER_HP_CHANGE; + +public class EntityMonster extends GameEntity { + @Getter(onMethod = @__(@Override)) + private final Int2FloatOpenHashMap fightProperties; + + @Getter(onMethod = @__(@Override)) + private final Position position; + @Getter(onMethod = @__(@Override)) + private final Position rotation; + @Getter private final MonsterData monsterData; + @Getter private final Position bornPos; + @Getter private final int level; + @Getter private int weaponEntityId; + @Getter @Setter private int poseId; + @Getter @Setter private int aiId = -1; + + @Getter @Setter private SceneMonster metaMonster; + + public EntityMonster(Scene scene, MonsterData monsterData, Position pos, int level) { + super(scene); + this.id = getWorld().getNextEntityId(EntityIdType.MONSTER); + this.monsterData = monsterData; + this.fightProperties = new Int2FloatOpenHashMap(); + this.position = new Position(pos); + this.rotation = new Position(); + this.bornPos = getPosition().clone(); + this.level = level; + + // Monster weapon + if (getMonsterWeaponId() > 0) { + this.weaponEntityId = getWorld().getNextEntityId(EntityIdType.WEAPON); + } + + this.recalcStats(); + } + + @Override + public int getEntityTypeId() { + return getMonsterId(); + } + + public int getMonsterWeaponId() { + return this.getMonsterData().getWeaponId(); + } + + private int getMonsterId() { + return this.getMonsterData().getId(); + } + + @Override + public boolean isAlive() { + return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f; + } + + @Override + public void onInteract(Player player, GadgetInteractReq interactReq) { + EnvAnimalGatherConfigData gatherData = GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId()); + + if (gatherData == null) { + return; + } + + player.getInventory().addItem(gatherData.getGatherItem(), ActionReason.SubfieldDrop); + + this.getScene().killEntity(this); + } + + @Override + public void onCreate() { + // Lua event + getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId())); + } + + @Override + public void damage(float amount, int killerId, ElementType attackType) { + // Get HP before damage. + float hpBeforeDamage = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + + // Apply damage. + super.damage(amount, killerId, attackType); + + // Get HP after damage. + float hpAfterDamage = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + + // Invoke energy drop logic. + for (Player player : this.getScene().getPlayers()) { + player.getEnergyManager().handleMonsterEnergyDrop(this, hpBeforeDamage, hpAfterDamage); + } + } + + @Override + public void runLuaCallbacks(EntityDamageEvent event) { + super.runLuaCallbacks(event); + getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EVENT_SPECIFIC_MONSTER_HP_CHANGE, getConfigId(), monsterData.getId()) + .setSourceEntityId(getId()) + .setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) + .setEventSource(Integer.toString(getConfigId()))); + } + + @Override + public void onDeath(int killerId) { + super.onDeath(killerId); // Invoke super class's onDeath() method. + var scene = this.getScene(); + var challenge = Optional.ofNullable(scene.getChallenge()); + var scriptManager = scene.getScriptManager(); + + Optional.ofNullable(this.getSpawnEntry()).ifPresent(scene.getDeadSpawnedEntities()::add); + + // first set the challenge data + challenge.ifPresent(c -> c.onMonsterDeath(this)); + + if (scriptManager.isInit() && this.getGroupId() > 0) { + Optional.ofNullable(scriptManager.getScriptMonsterSpawnService()).ifPresent(s -> s.onMonsterDead(this)); + + // prevent spawn monster after success + /*if (challenge.map(c -> c.inProgress()).orElse(true)) { + scriptManager.callEvent(new ScriptArgs(EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()).setGroupId(this.getGroupId())); + } else if (getScene().getChallenge() == null) { + }*/ + scriptManager.callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId())); + } + // Battle Pass trigger + scene.getPlayers().forEach(p -> p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1)); + + scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_MONSTER_DIE, this.getMonsterId())); + scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_KILL_MONSTER, this.getMonsterId())); + scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER, this.getGroupId())); + + SceneGroupInstance groupInstance = scene.getScriptManager().getGroupInstanceById(this.getGroupId()); + if(groupInstance != null && metaMonster != null) + groupInstance.getDeadEntities().add(metaMonster.config_id); + + scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId()); + scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER, this.getMonsterData().getType().getValue()); + scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId()); + } + + public void recalcStats() { + // Monster data + MonsterData data = this.getMonsterData(); + + // Get hp percent, set to 100% if none + float hpPercent = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 ? 1f : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + + // Clear properties + this.getFightProperties().clear(); + + // Base stats + MonsterData.definedFightProperties.forEach(prop -> this.setFightProperty(prop, data.getFightProperty(prop))); + + // Level curve + MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(this.getLevel()); + if (curve != null) { + for (PropGrowCurve growCurve : data.getPropGrowCurves()) { + FightProperty prop = FightProperty.getPropByName(growCurve.getType()); + this.setFightProperty(prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve())); + } + } + + // Set % stats + FightProperty.forEachCompoundProperty(c -> this.setFightProperty(c.getResult(), + this.getFightProperty(c.getFlat()) + (this.getFightProperty(c.getBase()) * (1f + this.getFightProperty(c.getPercent()))))); + + // Set current hp + this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent); + } + + @Override + public SceneEntityInfo toProto() { + var authority = EntityAuthorityInfo.newBuilder() + .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) + .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) + .setAiInfo(SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(this.getBornPos().toProto())) + .setBornPos(this.getBornPos().toProto()) + .build(); + + var entityInfo = SceneEntityInfo.newBuilder() + .setEntityId(getId()) + .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_MONSTER) + .setMotionInfo(this.getMotionInfo()) + .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) + .setEntityClientData(EntityClientData.newBuilder()) + .setEntityAuthorityInfo(authority) + .setLifeState(this.getLifeState().getValue()); + + this.addAllFightPropsToEntityInfo(entityInfo); + + entityInfo.addPropList(PropPair.newBuilder() + .setType(PlayerProperty.PROP_LEVEL.getId()) + .setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, getLevel())) + .build()); + + var monsterInfo = SceneMonsterInfo.newBuilder() + .setMonsterId(getMonsterId()) + .setGroupId(this.getGroupId()) + .setConfigId(this.getConfigId()) + .addAllAffixList(getMonsterData().getAffix()) + .setAuthorityPeerId(getWorld().getHostPeerId()) + .setPoseId(this.getPoseId()) + .setBlockId(getScene().getId()) + .setBornType(MonsterBornType.MONSTER_BORN_TYPE_DEFAULT); + + if (getMonsterData().getDescribeData() != null) { + monsterInfo.setTitleId(getMonsterData().getDescribeData().getTitleId()) + .setSpecialNameId(getMonsterData().getSpecialNameId()); + + } + + if (this.getMonsterWeaponId() > 0) { + SceneWeaponInfo weaponInfo = SceneWeaponInfo.newBuilder() + .setEntityId(this.weaponEntityId) + .setGadgetId(this.getMonsterWeaponId()) + .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) + .build(); + + monsterInfo.addWeaponList(weaponInfo); + } + if (this.aiId != -1) { + monsterInfo.setAiConfigId(aiId); + } + + entityInfo.setMonster(monsterInfo); + + return entityInfo.build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityNPC.java b/src/main/java/emu/grasscutter/game/entity/EntityNPC.java index 3356f8025..e9db22fa7 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityNPC.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityNPC.java @@ -1,77 +1,82 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.*; -import emu.grasscutter.scripts.data.SceneNPC; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import lombok.Getter; - -public class EntityNPC extends GameEntity { - @Getter(onMethod_ = @Override) - private final Position position; - - @Getter(onMethod_ = @Override) - private final Position rotation; - - private final SceneNPC metaNpc; - @Getter private final int suiteId; - - public EntityNPC(Scene scene, SceneNPC metaNPC, int blockId, int suiteId) { - super(scene); - this.id = getScene().getWorld().getNextEntityId(EntityIdType.NPC); - setConfigId(metaNPC.config_id); - setGroupId(metaNPC.group.id); - setBlockId(blockId); - this.suiteId = suiteId; - this.position = metaNPC.pos.clone(); - this.rotation = metaNPC.rot.clone(); - this.metaNpc = metaNPC; - } - - @Override - public Int2FloatMap getFightProperties() { - return null; - } - - @Override - public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { - - EntityAuthorityInfoOuterClass.EntityAuthorityInfo authority = - EntityAuthorityInfoOuterClass.EntityAuthorityInfo.newBuilder() - .setAbilityInfo(AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo.newBuilder()) - .setRendererChangedInfo( - EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo.newBuilder()) - .setAiInfo( - SceneEntityAiInfoOuterClass.SceneEntityAiInfo.newBuilder() - .setIsAiOpen(true) - .setBornPos(getPosition().toProto())) - .setBornPos(getPosition().toProto()) - .build(); - - SceneEntityInfoOuterClass.SceneEntityInfo.Builder entityInfo = - SceneEntityInfoOuterClass.SceneEntityInfo.newBuilder() - .setEntityId(getId()) - .setEntityType(ProtEntityTypeOuterClass.ProtEntityType.PROT_ENTITY_TYPE_NPC) - .setMotionInfo( - MotionInfoOuterClass.MotionInfo.newBuilder() - .setPos(getPosition().toProto()) - .setRot(getRotation().toProto()) - .setSpeed(VectorOuterClass.Vector.newBuilder())) - .addAnimatorParaList( - AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair - .newBuilder()) - .setEntityClientData(EntityClientDataOuterClass.EntityClientData.newBuilder()) - .setEntityAuthorityInfo(authority) - .setLifeState(1); - - entityInfo.setNpc( - SceneNpcInfoOuterClass.SceneNpcInfo.newBuilder() - .setNpcId(metaNpc.npc_id) - .setBlockId(getBlockId()) - .build()); - - return entityInfo.build(); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.*; +import emu.grasscutter.scripts.data.SceneNPC; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2FloatMap; +import lombok.Getter; + +public class EntityNPC extends GameEntity { + @Getter(onMethod_ = @Override) + private final Position position; + + @Getter(onMethod_ = @Override) + private final Position rotation; + + private final SceneNPC metaNpc; + @Getter private final int suiteId; + + public EntityNPC(Scene scene, SceneNPC metaNPC, int blockId, int suiteId) { + super(scene); + this.id = getScene().getWorld().getNextEntityId(EntityIdType.NPC); + setConfigId(metaNPC.config_id); + setGroupId(metaNPC.group.id); + setBlockId(blockId); + this.suiteId = suiteId; + this.position = metaNPC.pos.clone(); + this.rotation = metaNPC.rot.clone(); + this.metaNpc = metaNPC; + } + + @Override + public int getEntityTypeId() { + return this.metaNpc.npc_id; + } + + @Override + public Int2FloatMap getFightProperties() { + return null; + } + + @Override + public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { + + EntityAuthorityInfoOuterClass.EntityAuthorityInfo authority = + EntityAuthorityInfoOuterClass.EntityAuthorityInfo.newBuilder() + .setAbilityInfo(AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo.newBuilder()) + .setRendererChangedInfo( + EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo.newBuilder()) + .setAiInfo( + SceneEntityAiInfoOuterClass.SceneEntityAiInfo.newBuilder() + .setIsAiOpen(true) + .setBornPos(getPosition().toProto())) + .setBornPos(getPosition().toProto()) + .build(); + + SceneEntityInfoOuterClass.SceneEntityInfo.Builder entityInfo = + SceneEntityInfoOuterClass.SceneEntityInfo.newBuilder() + .setEntityId(getId()) + .setEntityType(ProtEntityTypeOuterClass.ProtEntityType.PROT_ENTITY_TYPE_NPC) + .setMotionInfo( + MotionInfoOuterClass.MotionInfo.newBuilder() + .setPos(getPosition().toProto()) + .setRot(getRotation().toProto()) + .setSpeed(VectorOuterClass.Vector.newBuilder())) + .addAnimatorParaList( + AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair + .newBuilder()) + .setEntityClientData(EntityClientDataOuterClass.EntityClientData.newBuilder()) + .setEntityAuthorityInfo(authority) + .setLifeState(1); + + entityInfo.setNpc( + SceneNpcInfoOuterClass.SceneNpcInfo.newBuilder() + .setNpcId(metaNpc.npc_id) + .setBlockId(getBlockId()) + .build()); + + return entityInfo.build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityRegion.java b/src/main/java/emu/grasscutter/game/entity/EntityRegion.java index cccb5b41f..5fe55ce63 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityRegion.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityRegion.java @@ -1,90 +1,96 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.SceneEntityInfoOuterClass; -import emu.grasscutter.scripts.data.SceneRegion; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import lombok.Getter; - -@Getter -public class EntityRegion extends GameEntity { - private final Position position; - private final Set entities; // Ids of entities inside this region - private final SceneRegion metaRegion; - private boolean hasNewEntities; - private boolean entityLeave; - - public EntityRegion(Scene scene, SceneRegion region) { - super(scene); - this.id = getScene().getWorld().getNextEntityId(EntityIdType.REGION); - setGroupId(region.group.id); - setBlockId(region.group.block_id); - setConfigId(region.config_id); - this.position = region.pos.clone(); - this.entities = ConcurrentHashMap.newKeySet(); - this.metaRegion = region; - } - - public void addEntity(GameEntity entity) { - if (this.getEntities().contains(entity.getId())) { - return; - } - this.getEntities().add(entity.getId()); - this.hasNewEntities = true; - } - - public boolean hasNewEntities() { - return hasNewEntities; - } - - public void resetNewEntities() { - hasNewEntities = false; - } - - public void removeEntity(int entityId) { - this.getEntities().remove(entityId); - this.entityLeave = true; - } - - public void removeEntity(GameEntity entity) { - this.getEntities().remove(entity.getId()); - this.entityLeave = true; - } - - public boolean entityLeave() { - return this.entityLeave; - } - - public void resetEntityLeave() { - this.entityLeave = false; - } - - @Override - public Int2FloatMap getFightProperties() { - return null; - } - - @Override - public Position getPosition() { - return position; - } - - @Override - public Position getRotation() { - return null; - } - - @Override - public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { - /** The Region Entity would not be sent to client. */ - return null; - } - - public int getFirstEntityId() { - return entities.stream().findFirst().orElse(0); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass; +import emu.grasscutter.scripts.data.SceneRegion; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2FloatMap; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import lombok.Getter; + +@Getter +public class EntityRegion extends GameEntity { + private final Position position; + private final Set entities; // Ids of entities inside this region + private final SceneRegion metaRegion; + private boolean hasNewEntities; + private boolean entityLeave; + + public EntityRegion(Scene scene, SceneRegion region) { + super(scene); + + this.id = getScene().getWorld().getNextEntityId(EntityIdType.REGION); + this.setGroupId(region.group.id); + this.setBlockId(region.group.block_id); + this.setConfigId(region.config_id); + this.position = region.pos.clone(); + this.entities = ConcurrentHashMap.newKeySet(); + this.metaRegion = region; + } + + @Override + public int getEntityTypeId() { + return this.metaRegion.config_id; + } + + public void addEntity(GameEntity entity) { + if (this.getEntities().contains(entity.getId())) { + return; + } + this.getEntities().add(entity.getId()); + this.hasNewEntities = true; + } + + public boolean hasNewEntities() { + return hasNewEntities; + } + + public void resetNewEntities() { + hasNewEntities = false; + } + + public void removeEntity(int entityId) { + this.getEntities().remove(entityId); + this.entityLeave = true; + } + + public void removeEntity(GameEntity entity) { + this.getEntities().remove(entity.getId()); + this.entityLeave = true; + } + + public boolean entityLeave() { + return this.entityLeave; + } + + public void resetEntityLeave() { + this.entityLeave = false; + } + + @Override + public Int2FloatMap getFightProperties() { + return null; + } + + @Override + public Position getPosition() { + return position; + } + + @Override + public Position getRotation() { + return null; + } + + @Override + public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { + /** The Region Entity would not be sent to client. */ + return null; + } + + public int getFirstEntityId() { + return entities.stream().findFirst().orElse(0); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntitySolarIsotomaClientGadget.java b/src/main/java/emu/grasscutter/game/entity/EntitySolarIsotomaClientGadget.java index 29007b8b4..79bb3bfe0 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntitySolarIsotomaClientGadget.java +++ b/src/main/java/emu/grasscutter/game/entity/EntitySolarIsotomaClientGadget.java @@ -1,34 +1,30 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.game.entity.platform.EntityPlatform; -import emu.grasscutter.game.entity.platform.EntitySolarIsotomaElevatorPlatform; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.EvtCreateGadgetNotifyOuterClass; -import lombok.Getter; - -public class EntitySolarIsotomaClientGadget extends EntityClientGadget { - public static final int GADGET_ID = 41038001; - public static final int ELEVATOR_GADGET_ID = 41038002; - @Getter private EntityPlatform platformGadget; - - public EntitySolarIsotomaClientGadget( - Scene scene, Player player, EvtCreateGadgetNotifyOuterClass.EvtCreateGadgetNotify notify) { - super(scene, player, notify); - } - - @Override - public void onCreate() { - // Create solar isotoma elevator and send to all. - this.platformGadget = - new EntitySolarIsotomaElevatorPlatform( - this, getScene(), getOwner(), ELEVATOR_GADGET_ID, getPosition(), getRotation()); - getScene().addEntity(this.platformGadget); - } - - @Override - public void onRemoved() { - // Remove solar isotoma elevator entity. - getScene().removeEntity(this.platformGadget); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.game.entity.platform.EntitySolarIsotomaElevatorPlatform; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.EvtCreateGadgetNotifyOuterClass; +import lombok.Getter; + +public class EntitySolarIsotomaClientGadget extends EntityClientGadget { + public static final int GADGET_ID = 41038001; + public static final int ELEVATOR_GADGET_ID = 41038002; + @Getter private EntityGadget platformGadget; + + public EntitySolarIsotomaClientGadget(Scene scene, Player player, EvtCreateGadgetNotifyOuterClass.EvtCreateGadgetNotify notify) { + super(scene, player, notify); + } + + @Override + public void onCreate() { + //Create solar isotoma elevator and send to all. + this.platformGadget = new EntitySolarIsotomaElevatorPlatform(this, getScene(), ELEVATOR_GADGET_ID, getPosition(), getRotation()); + getScene().addEntity(this.platformGadget); + } + + @Override + public void onRemoved() { + //Remove solar isotoma elevator entity. + getScene().removeEntity(this.platformGadget); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityVehicle.java b/src/main/java/emu/grasscutter/game/entity/EntityVehicle.java index fe783ac8d..d99ad29b1 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityVehicle.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityVehicle.java @@ -1,125 +1,112 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.ConfigGadget; -import emu.grasscutter.data.excels.GadgetData; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; -import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; -import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; -import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; -import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; -import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; -import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; -import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; -import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; -import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; -import emu.grasscutter.net.proto.VectorOuterClass.Vector; -import emu.grasscutter.net.proto.VehicleInfoOuterClass.VehicleInfo; -import emu.grasscutter.net.proto.VehicleMemberOuterClass.VehicleMember; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.ProtoHelper; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; -import java.util.ArrayList; -import java.util.List; -import javax.annotation.Nullable; -import lombok.Getter; -import lombok.Setter; - -public class EntityVehicle extends EntityBaseGadget { - - @Getter private final Player owner; - - @Getter(onMethod_ = @Override) - private final Int2FloatMap fightProperties; - - @Getter private final int pointId; - @Getter private final int gadgetId; - - @Getter @Setter private float curStamina; - @Getter private final List vehicleMembers; - @Nullable @Getter private ConfigGadget configGadget; - - public EntityVehicle( - Scene scene, Player player, int gadgetId, int pointId, Position pos, Position rot) { - super(scene, pos, rot); - this.owner = player; - this.id = getScene().getWorld().getNextEntityId(EntityIdType.GADGET); - this.fightProperties = new Int2FloatOpenHashMap(); - this.gadgetId = gadgetId; - this.pointId = pointId; - this.curStamina = 240; // might be in configGadget.GCALKECLLLP.JBAKBEFIMBN.ANBMPHPOALP - this.vehicleMembers = new ArrayList<>(); - GadgetData data = GameData.getGadgetDataMap().get(gadgetId); - if (data != null && data.getJsonName() != null) { - this.configGadget = GameData.getGadgetConfigData().get(data.getJsonName()); - } - - fillFightProps(configGadget); - } - - @Override - protected void fillFightProps(ConfigGadget configGadget) { - super.fillFightProps(configGadget); - this.addFightProperty(FightProperty.FIGHT_PROP_CUR_SPEED, 0); - this.addFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, 0); - } - - @Override - public SceneEntityInfo toProto() { - - VehicleInfo vehicle = - VehicleInfo.newBuilder() - .setOwnerUid(this.owner.getUid()) - .setCurStamina(getCurStamina()) - .build(); - - EntityAuthorityInfo authority = - EntityAuthorityInfo.newBuilder() - .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) - .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) - .setAiInfo( - SceneEntityAiInfo.newBuilder() - .setIsAiOpen(true) - .setBornPos(getPosition().toProto())) - .setBornPos(getPosition().toProto()) - .build(); - - SceneGadgetInfo.Builder gadgetInfo = - SceneGadgetInfo.newBuilder() - .setGadgetId(this.getGadgetId()) - .setAuthorityPeerId(this.getOwner().getPeerId()) - .setIsEnableInteract(true) - .setVehicleInfo(vehicle); - - SceneEntityInfo.Builder entityInfo = - SceneEntityInfo.newBuilder() - .setEntityId(getId()) - .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_GADGET) - .setMotionInfo( - MotionInfo.newBuilder() - .setPos(getPosition().toProto()) - .setRot(getRotation().toProto()) - .setSpeed(Vector.newBuilder())) - .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) - .setGadget(gadgetInfo) - .setEntityAuthorityInfo(authority) - .setLifeState(1); - - PropPair pair = - PropPair.newBuilder() - .setType(PlayerProperty.PROP_LEVEL.getId()) - .setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, 47)) - .build(); - - this.addAllFightPropsToEntityInfo(entityInfo); - entityInfo.addPropList(pair); - - return entityInfo.build(); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.config.ConfigEntityGadget; +import emu.grasscutter.data.excels.GadgetData; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; +import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; +import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; +import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; +import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; +import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; +import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; +import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; +import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import emu.grasscutter.net.proto.VehicleInfoOuterClass.VehicleInfo; +import emu.grasscutter.net.proto.VehicleMemberOuterClass.VehicleMember; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.ProtoHelper; +import it.unimi.dsi.fastutil.ints.Int2FloatMap; +import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +public class EntityVehicle extends EntityBaseGadget { + + @Getter private final Player owner; + @Getter(onMethod = @__(@Override)) + private final Int2FloatMap fightProperties; + + @Getter private final int pointId; + @Getter private final int gadgetId; + + @Getter @Setter private float curStamina; + @Getter private final List vehicleMembers; + @Nullable @Getter private ConfigEntityGadget configGadget; + + public EntityVehicle(Scene scene, Player player, int gadgetId, int pointId, Position pos, Position rot) { + super(scene, pos, rot); + this.owner = player; + this.id = getScene().getWorld().getNextEntityId(EntityIdType.GADGET); + this.fightProperties = new Int2FloatOpenHashMap(); + this.gadgetId = gadgetId; + this.pointId = pointId; + this.curStamina = 240; // might be in configGadget.GCALKECLLLP.JBAKBEFIMBN.ANBMPHPOALP + this.vehicleMembers = new ArrayList<>(); + GadgetData data = GameData.getGadgetDataMap().get(gadgetId); + if (data != null && data.getJsonName() != null) { + this.configGadget = GameData.getGadgetConfigData().get(data.getJsonName()); + } + + fillFightProps(configGadget); + } + + @Override + protected void fillFightProps(ConfigEntityGadget configGadget) { + super.fillFightProps(configGadget); + this.addFightProperty(FightProperty.FIGHT_PROP_CUR_SPEED, 0); + this.addFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, 0); + } + + @Override + public SceneEntityInfo toProto() { + + VehicleInfo vehicle = VehicleInfo.newBuilder() + .setOwnerUid(this.owner.getUid()) + .setCurStamina(getCurStamina()) + .build(); + + EntityAuthorityInfo authority = EntityAuthorityInfo.newBuilder() + .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) + .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) + .setAiInfo(SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(getPosition().toProto())) + .setBornPos(getPosition().toProto()) + .build(); + + SceneGadgetInfo.Builder gadgetInfo = SceneGadgetInfo.newBuilder() + .setGadgetId(this.getGadgetId()) + .setAuthorityPeerId(this.getOwner().getPeerId()) + .setIsEnableInteract(true) + .setVehicleInfo(vehicle); + + SceneEntityInfo.Builder entityInfo = SceneEntityInfo.newBuilder() + .setEntityId(getId()) + .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_GADGET) + .setMotionInfo(MotionInfo.newBuilder().setPos(getPosition().toProto()).setRot(getRotation().toProto()).setSpeed(Vector.newBuilder())) + .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) + .setGadget(gadgetInfo) + .setEntityAuthorityInfo(authority) + .setLifeState(1); + + PropPair pair = PropPair.newBuilder() + .setType(PlayerProperty.PROP_LEVEL.getId()) + .setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, 47)) + .build(); + + this.addAllFightPropsToEntityInfo(entityInfo); + entityInfo.addPropList(pair); + + return entityInfo.build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/GameEntity.java b/src/main/java/emu/grasscutter/game/entity/GameEntity.java index 9ea393a52..b774219b6 100644 --- a/src/main/java/emu/grasscutter/game/entity/GameEntity.java +++ b/src/main/java/emu/grasscutter/game/entity/GameEntity.java @@ -1,230 +1,264 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.LifeState; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.game.world.SpawnDataEntry; -import emu.grasscutter.game.world.World; -import emu.grasscutter.net.proto.FightPropPairOuterClass.FightPropPair; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; -import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; -import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; -import emu.grasscutter.net.proto.VectorOuterClass.Vector; -import emu.grasscutter.server.event.entity.EntityDamageEvent; -import emu.grasscutter.server.event.entity.EntityDeathEvent; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2FloatMap; -import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap; -import lombok.Getter; -import lombok.Setter; - -public abstract class GameEntity { - @Getter private final Scene scene; - @Getter protected int id; - @Getter @Setter private SpawnDataEntry spawnEntry; - - @Getter @Setter private int blockId; - @Getter @Setter private int configId; - @Getter @Setter private int groupId; - - @Getter @Setter private MotionState motionState; - @Getter @Setter private int lastMoveSceneTimeMs; - @Getter @Setter private int lastMoveReliableSeq; - - @Getter @Setter private boolean lockHP; - - // Abilities - private Object2FloatMap metaOverrideMap; - private Int2ObjectMap metaModifiers; - - public GameEntity(Scene scene) { - this.scene = scene; - this.motionState = MotionState.MOTION_STATE_NONE; - } - - public int getEntityType() { - return this.getId() >> 24; - } - - public World getWorld() { - return this.getScene().getWorld(); - } - - public boolean isAlive() { - return true; - } - - public LifeState getLifeState() { - return this.isAlive() ? LifeState.LIFE_ALIVE : LifeState.LIFE_DEAD; - } - - public Object2FloatMap getMetaOverrideMap() { - if (this.metaOverrideMap == null) { - this.metaOverrideMap = new Object2FloatOpenHashMap<>(); - } - return this.metaOverrideMap; - } - - public Int2ObjectMap getMetaModifiers() { - if (this.metaModifiers == null) { - this.metaModifiers = new Int2ObjectOpenHashMap<>(); - } - return this.metaModifiers; - } - - public abstract Int2FloatMap getFightProperties(); - - public abstract Position getPosition(); - - public abstract Position getRotation(); - - public void setFightProperty(FightProperty prop, float value) { - this.getFightProperties().put(prop.getId(), value); - } - - public void setFightProperty(int id, float value) { - this.getFightProperties().put(id, value); - } - - public void addFightProperty(FightProperty prop, float value) { - this.getFightProperties().put(prop.getId(), this.getFightProperty(prop) + value); - } - - public float getFightProperty(FightProperty prop) { - return this.getFightProperties().getOrDefault(prop.getId(), 0f); - } - - public boolean hasFightProperty(FightProperty prop) { - return this.getFightProperties().containsKey(prop.getId()); - } - - public void addAllFightPropsToEntityInfo(SceneEntityInfo.Builder entityInfo) { - this.getFightProperties() - .forEach( - (key, value) -> { - if (key == 0) return; - entityInfo.addFightPropList( - FightPropPair.newBuilder().setPropType(key).setPropValue(value).build()); - }); - } - - protected MotionInfo getMotionInfo() { - MotionInfo proto = - MotionInfo.newBuilder() - .setPos(this.getPosition().toProto()) - .setRot(this.getRotation().toProto()) - .setSpeed(Vector.newBuilder()) - .setState(this.getMotionState()) - .build(); - - return proto; - } - - public float heal(float amount) { - if (this.getFightProperties() == null) { - return 0f; - } - - float curHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - float maxHp = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - - if (curHp >= maxHp) { - return 0f; - } - - float healed = Math.min(maxHp - curHp, amount); - this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, healed); - - this.getScene() - .broadcastPacket( - new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); - - return healed; - } - - public void damage(float amount) { - this.damage(amount, 0); - } - - public void damage(float amount, int killerId) { - // Check if the entity has properties. - if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) { - return; - } - - // Invoke entity damage event. - EntityDamageEvent event = - new EntityDamageEvent(this, amount, this.getScene().getEntityById(killerId)); - event.call(); - if (event.isCanceled()) { - return; // If the event is canceled, do not damage the entity. - } - - float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) { - // Add negative HP to the current HP property. - this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage())); - } - - // Check if dead - boolean isDead = false; - if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) { - this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f); - isDead = true; - } - - // Packets - this.getScene() - .broadcastPacket( - new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); - - // Check if dead. - if (isDead) { - this.getScene().killEntity(this, killerId); - } - } - - /** - * Move this entity to a new position. - * - * @param position The new position. - * @param rotation The new rotation. - */ - public void move(Position position, Position rotation) { - // Set the position and rotation. - this.getPosition().set(position); - this.getRotation().set(rotation); - } - - /** - * Called when a player interacts with this entity - * - * @param player Player that is interacting with this entity - * @param interactReq Interact request protobuf data - */ - public void onInteract(Player player, GadgetInteractReq interactReq) {} - - /** Called when this entity is added to the world */ - public void onCreate() {} - - public void onRemoved() {} - - /** - * Called when this entity dies - * - * @param killerId Entity id of the entity that killed this entity - */ - public void onDeath(int killerId) { - // Invoke entity death event. - EntityDeathEvent event = new EntityDeathEvent(this, killerId); - event.call(); - } - - public abstract SceneEntityInfo toProto(); -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ElementType; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.LifeState; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.game.world.SpawnDataEntry; +import emu.grasscutter.game.world.World; +import emu.grasscutter.net.proto.FightPropPairOuterClass.FightPropPair; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; +import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import emu.grasscutter.scripts.data.controller.EntityController; +import emu.grasscutter.server.event.entity.EntityDamageEvent; +import emu.grasscutter.server.event.entity.EntityDeathEvent; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2FloatMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2FloatMap; +import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap; +import lombok.Getter; +import lombok.Setter; + +public abstract class GameEntity { + @Getter private final Scene scene; + @Getter protected int id; + @Getter @Setter private SpawnDataEntry spawnEntry; + + @Getter @Setter private int blockId; + @Getter @Setter private int configId; + @Getter @Setter private int groupId; + + @Getter @Setter private MotionState motionState; + @Getter @Setter private int lastMoveSceneTimeMs; + @Getter @Setter private int lastMoveReliableSeq; + + @Getter @Setter private boolean lockHP; + + // Lua controller for specific actions + @Getter @Setter private EntityController entityController; + @Getter private ElementType lastAttackType = ElementType.None; + + // Abilities + private Object2FloatMap metaOverrideMap; + private Int2ObjectMap metaModifiers; + + public GameEntity(Scene scene) { + this.scene = scene; + this.motionState = MotionState.MOTION_STATE_NONE; + } + + public int getEntityType() { + return this.getId() >> 24; + } + + public abstract int getEntityTypeId(); + + public World getWorld() { + return this.getScene().getWorld(); + } + + public boolean isAlive() { + return true; + } + + public LifeState getLifeState() { + return this.isAlive() ? LifeState.LIFE_ALIVE : LifeState.LIFE_DEAD; + } + + public Object2FloatMap getMetaOverrideMap() { + if (this.metaOverrideMap == null) { + this.metaOverrideMap = new Object2FloatOpenHashMap<>(); + } + return this.metaOverrideMap; + } + + public Int2ObjectMap getMetaModifiers() { + if (this.metaModifiers == null) { + this.metaModifiers = new Int2ObjectOpenHashMap<>(); + } + return this.metaModifiers; + } + + public abstract Int2FloatMap getFightProperties(); + + public abstract Position getPosition(); + + public abstract Position getRotation(); + + public void setFightProperty(FightProperty prop, float value) { + this.getFightProperties().put(prop.getId(), value); + } + + public void setFightProperty(int id, float value) { + this.getFightProperties().put(id, value); + } + + public void addFightProperty(FightProperty prop, float value) { + this.getFightProperties().put(prop.getId(), this.getFightProperty(prop) + value); + } + + public float getFightProperty(FightProperty prop) { + return this.getFightProperties().getOrDefault(prop.getId(), 0f); + } + + public boolean hasFightProperty(FightProperty prop) { + return this.getFightProperties().containsKey(prop.getId()); + } + + public void addAllFightPropsToEntityInfo(SceneEntityInfo.Builder entityInfo) { + this.getFightProperties() + .forEach( + (key, value) -> { + if (key == 0) return; + entityInfo.addFightPropList( + FightPropPair.newBuilder().setPropType(key).setPropValue(value).build()); + }); + } + + protected MotionInfo getMotionInfo() { + return MotionInfo.newBuilder() + .setPos(this.getPosition().toProto()) + .setRot(this.getRotation().toProto()) + .setSpeed(Vector.newBuilder()) + .setState(this.getMotionState()) + .build(); + } + + public float heal(float amount) { + if (this.getFightProperties() == null) { + return 0f; + } + + float curHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + float maxHp = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + + if (curHp >= maxHp) { + return 0f; + } + + float healed = Math.min(maxHp - curHp, amount); + this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, healed); + + this.getScene() + .broadcastPacket( + new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); + + return healed; + } + + public void damage(float amount) { + this.damage(amount, 0, ElementType.None); + } + + public void damage(float amount, int killerId, ElementType attackType) { + // Check if the entity has properties. + if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) { + return; + } + + // Invoke entity damage event. + EntityDamageEvent event = + new EntityDamageEvent(this, amount, attackType, this.getScene().getEntityById(killerId)); + event.call(); + if (event.isCanceled()) { + return; // If the event is canceled, do not damage the entity. + } + + float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) { + // Add negative HP to the current HP property. + this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage())); + } + + // Check if dead + boolean isDead = false; + if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) { + this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f); + isDead = true; + } + + // Packets + this.getScene() + .broadcastPacket( + new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); + + // Check if dead. + if (isDead) { + this.getScene().killEntity(this, killerId); + } + } + + /** + * Runs the Lua callbacks for {@link EntityDamageEvent}. + * + * @param event The damage event. + */ + public void runLuaCallbacks(EntityDamageEvent event) { + if (entityController != null) { + entityController.onBeHurt(this, event.getAttackElementType(), true);//todo is host handling + } + } + + /** + * Move this entity to a new position. + * + * @param position The new position. + * @param rotation The new rotation. + */ + public void move(Position position, Position rotation) { + // Set the position and rotation. + this.getPosition().set(position); + this.getRotation().set(rotation); + } + + /** + * Called when a player interacts with this entity + * + * @param player Player that is interacting with this entity + * @param interactReq Interact request protobuf data + */ + public void onInteract(Player player, GadgetInteractReq interactReq) {} + + /** Called when this entity is added to the world */ + public void onCreate() {} + + public void onRemoved() {} + + public void onTick(int sceneTime) { + if (entityController != null) { + entityController.onTimer(this, sceneTime); + } + } + + public int onClientExecuteRequest(int param1, int param2, int param3) { + if (entityController != null) { + return entityController.onClientExecuteRequest(this, param1, param2, param3); + } + return 0; + } + + /** + * Called when this entity dies + * + * @param killerId Entity id of the entity that killed this entity + */ + public void onDeath(int killerId) { + // Invoke entity death event. + EntityDeathEvent event = new EntityDeathEvent(this, killerId); + event.call(); + + // Run Lua callbacks. + if (entityController != null) { + entityController.onDie(this, getLastAttackType()); + } + } + + public abstract SceneEntityInfo toProto(); +} diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetRewardStatue.java b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetRewardStatue.java index b327b77e4..1e26c13c9 100644 --- a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetRewardStatue.java +++ b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetRewardStatue.java @@ -1,30 +1,33 @@ -package emu.grasscutter.game.entity.gadget; - -import emu.grasscutter.game.dungeons.challenge.DungeonChallenge; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType; -import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; -import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp; - -public class GadgetRewardStatue extends GadgetContent { - - public GadgetRewardStatue(EntityGadget gadget) { - super(gadget); - } - - public boolean onInteract(Player player, GadgetInteractReq req) { - if (player.getScene().getChallenge() != null - && player.getScene().getChallenge() instanceof DungeonChallenge dungeonChallenge) { - dungeonChallenge.getStatueDrops(player, req); - } - - player.sendPacket( - new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_OPEN_STATUE)); - - return false; - } - - public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) {} -} +package emu.grasscutter.game.entity.gadget; + +import emu.grasscutter.game.dungeons.challenge.DungeonChallenge; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType; +import emu.grasscutter.net.proto.ResinCostTypeOuterClass; +import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; +import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp; + +public final class GadgetRewardStatue extends GadgetContent { + + public GadgetRewardStatue(EntityGadget gadget) { + super(gadget); + } + + public boolean onInteract(Player player, GadgetInteractReq req) { + var dungeonManager = player.getScene().getDungeonManager(); + + if (player.getScene().getChallenge() instanceof DungeonChallenge) { + var useCondensed = req.getResinCostType() == ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE; + dungeonManager.getStatueDrops(player, useCondensed, getGadget().getGroupId()); + } + + player.sendPacket( + new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_OPEN_STATUE)); + + return false; + } + + public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) {} +} diff --git a/src/main/java/emu/grasscutter/game/entity/platform/EntityPlatform.java b/src/main/java/emu/grasscutter/game/entity/platform/EntityPlatform.java deleted file mode 100644 index 0dc55b548..000000000 --- a/src/main/java/emu/grasscutter/game/entity/platform/EntityPlatform.java +++ /dev/null @@ -1,101 +0,0 @@ -package emu.grasscutter.game.entity.platform; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.ConfigGadget; -import emu.grasscutter.data.excels.GadgetData; -import emu.grasscutter.game.entity.EntityBaseGadget; -import emu.grasscutter.game.entity.EntityClientGadget; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.*; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; -import javax.annotation.Nullable; -import lombok.Getter; -import lombok.Setter; - -public class EntityPlatform extends EntityBaseGadget { - @Getter private final Player owner; - - @Getter(onMethod_ = @Override) - private final int gadgetId; - - @Getter private final EntityClientGadget gadget; - - @Getter(onMethod_ = @Override) - private final Int2FloatMap fightProperties; - - @Getter private final MovingPlatformTypeOuterClass.MovingPlatformType movingPlatformType; - @Nullable @Getter private ConfigGadget configGadget; - @Getter @Setter private boolean isStarted; - @Getter @Setter private boolean isActive; - - public EntityPlatform( - EntityClientGadget gadget, - Scene scene, - Player player, - int gadgetId, - Position pos, - Position rot, - MovingPlatformTypeOuterClass.MovingPlatformType movingPlatformType) { - super(scene, pos, rot); - this.gadget = gadget; - this.owner = player; - this.id = getScene().getWorld().getNextEntityId(EntityIdType.GADGET); - this.fightProperties = new Int2FloatOpenHashMap(); - this.movingPlatformType = movingPlatformType; - this.gadgetId = gadgetId; - GadgetData data = GameData.getGadgetDataMap().get(gadgetId); - if (data != null && data.getJsonName() != null) { - this.configGadget = GameData.getGadgetConfigData().get(data.getJsonName()); - } - - fillFightProps(configGadget); - } - - @Override - public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { - var platform = - PlatformInfoOuterClass.PlatformInfo.newBuilder() - .setMovingPlatformType(movingPlatformType) - .build(); - - var gadgetInfo = - SceneGadgetInfoOuterClass.SceneGadgetInfo.newBuilder() - .setGadgetId(getGadgetId()) - .setAuthorityPeerId(getOwner().getPeerId()) - .setPlatform(platform); - - var entityInfo = - SceneEntityInfoOuterClass.SceneEntityInfo.newBuilder() - .setEntityId(getId()) - .setEntityType(ProtEntityTypeOuterClass.ProtEntityType.PROT_ENTITY_TYPE_GADGET) - .setGadget(gadgetInfo) - .setLifeState(1); - - this.addAllFightPropsToEntityInfo(entityInfo); - return entityInfo.build(); - } - - public PlatformInfoOuterClass.PlatformInfo onStartRoute() { - return PlatformInfoOuterClass.PlatformInfo.newBuilder() - .setStartSceneTime(getScene().getSceneTime()) - .setIsStarted(true) - .setPosOffset(getPosition().toProto()) - .setMovingPlatformType(getMovingPlatformType()) - .setIsActive(true) - .build(); - } - - public PlatformInfoOuterClass.PlatformInfo onStopRoute() { - var sceneTime = getScene().getSceneTime(); - return PlatformInfoOuterClass.PlatformInfo.newBuilder() - .setStartSceneTime(sceneTime) - .setStopSceneTime(sceneTime) - .setPosOffset(getPosition().toProto()) - .setMovingPlatformType(getMovingPlatformType()) - .build(); - } -} diff --git a/src/main/java/emu/grasscutter/game/entity/platform/EntitySolarIsotomaElevatorPlatform.java b/src/main/java/emu/grasscutter/game/entity/platform/EntitySolarIsotomaElevatorPlatform.java index fda82ed73..3efb29872 100644 --- a/src/main/java/emu/grasscutter/game/entity/platform/EntitySolarIsotomaElevatorPlatform.java +++ b/src/main/java/emu/grasscutter/game/entity/platform/EntitySolarIsotomaElevatorPlatform.java @@ -1,153 +1,40 @@ -package emu.grasscutter.game.entity.platform; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.binout.ConfigGadget; -import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.game.entity.EntitySolarIsotomaClientGadget; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.*; -import emu.grasscutter.server.packet.send.PacketSceneTimeNotify; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.ProtoHelper; - -public class EntitySolarIsotomaElevatorPlatform extends EntityPlatform { - public EntitySolarIsotomaElevatorPlatform( - EntitySolarIsotomaClientGadget isotoma, - Scene scene, - Player player, - int gadgetId, - Position pos, - Position rot) { - super( - isotoma, - scene, - player, - gadgetId, - pos, - rot, - MovingPlatformTypeOuterClass.MovingPlatformType.MOVING_PLATFORM_TYPE_ABILITY); - } - - @Override - protected void fillFightProps(ConfigGadget configGadget) { - if (configGadget == null || configGadget.getCombat() == null) { - return; - } - var combatData = configGadget.getCombat(); - var combatProperties = combatData.getProperty(); - - if (combatProperties.isUseCreatorProperty()) { - // If useCreatorProperty == true, use owner's property; - GameEntity ownerAvatar = getScene().getEntityById(getGadget().getOwnerEntityId()); - if (ownerAvatar != null) { - getFightProperties().putAll(ownerAvatar.getFightProperties()); - return; - } else { - Grasscutter.getLogger().warn("Why gadget owner is null?"); - } - } - - super.fillFightProps(configGadget); - } - - @Override - public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { - var gadget = - SceneGadgetInfoOuterClass.SceneGadgetInfo.newBuilder() - .setGadgetId(getGadgetId()) - .setOwnerEntityId(getGadget().getId()) - .setAuthorityPeerId(getOwner().getPeerId()) - .setIsEnableInteract(true) - .setAbilityGadget( - AbilityGadgetInfoOuterClass.AbilityGadgetInfo.newBuilder() - .setCampId(getGadget().getCampId()) - .setCampTargetType(getGadget().getCampType()) - .setTargetEntityId(getGadget().getId()) - .build()) - .setPlatform( - PlatformInfoOuterClass.PlatformInfo.newBuilder() - .setStartRot( - MathQuaternionOuterClass.MathQuaternion.newBuilder().setW(1.0F).build()) - .setPosOffset(getGadget().getPosition().toProto()) - .setRotOffset( - MathQuaternionOuterClass.MathQuaternion.newBuilder().setW(1.0F).build()) - .setMovingPlatformType( - MovingPlatformTypeOuterClass.MovingPlatformType - .MOVING_PLATFORM_TYPE_ABILITY) - .build()) - .build(); - - var authority = - EntityAuthorityInfoOuterClass.EntityAuthorityInfo.newBuilder() - .setAiInfo( - SceneEntityAiInfoOuterClass.SceneEntityAiInfo.newBuilder() - .setIsAiOpen(true) - .setBornPos(getGadget().getPosition().toProto())) - .setBornPos(getGadget().getPosition().toProto()) - .build(); - - var info = - SceneEntityInfoOuterClass.SceneEntityInfo.newBuilder() - .setEntityType(ProtEntityTypeOuterClass.ProtEntityType.PROT_ENTITY_TYPE_GADGET) - .setEntityId(getId()) - .setMotionInfo( - MotionInfoOuterClass.MotionInfo.newBuilder() - .setPos(getGadget().getPosition().toProto()) - .setRot(getGadget().getRotation().toProto()) - .build()); - - GameEntity entity = getScene().getEntityById(getGadget().getOwnerEntityId()); - if (entity instanceof EntityAvatar avatar) { - info.addPropList( - PropPairOuterClass.PropPair.newBuilder() - .setType(PlayerProperty.PROP_LEVEL.getId()) - .setPropValue( - ProtoHelper.newPropValue( - PlayerProperty.PROP_LEVEL, avatar.getAvatar().getLevel())) - .build()); - } else { - Grasscutter.getLogger().warn("Why gadget owner doesn't exist?"); - } - - this.addAllFightPropsToEntityInfo(info); - - info.setLifeState(1).setGadget(gadget).setEntityAuthorityInfo(authority); - - return info.build(); - } - - @Override - public PlatformInfoOuterClass.PlatformInfo onStartRoute() { - setStarted(true); - setActive(true); - - var sceneTime = getScene().getSceneTime(); - getOwner().sendPacket(new PacketSceneTimeNotify(getOwner())); - - return PlatformInfoOuterClass.PlatformInfo.newBuilder() - .setStartSceneTime(sceneTime + 300) - .setIsStarted(true) - .setPosOffset(getPosition().toProto()) - .setRotOffset(MathQuaternionOuterClass.MathQuaternion.newBuilder().setW(1.0F).build()) - .setMovingPlatformType(getMovingPlatformType()) - .setIsActive(true) - .build(); - } - - @Override - public PlatformInfoOuterClass.PlatformInfo onStopRoute() { - setStarted(false); - setActive(false); - - return PlatformInfoOuterClass.PlatformInfo.newBuilder() - .setStartSceneTime(getScene().getSceneTime()) - .setStopSceneTime(getScene().getSceneTime()) - .setPosOffset(getPosition().toProto()) - .setRotOffset(MathQuaternionOuterClass.MathQuaternion.newBuilder().setW(1.0F).build()) - .setMovingPlatformType(getMovingPlatformType()) - .build(); - } -} +package emu.grasscutter.game.entity.platform; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.binout.config.ConfigEntityGadget; +import emu.grasscutter.game.entity.*; +import emu.grasscutter.game.entity.gadget.GadgetAbility; +import emu.grasscutter.game.entity.gadget.platform.AbilityRoute; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.utils.Position; + +public class EntitySolarIsotomaElevatorPlatform extends EntityGadget { + public EntitySolarIsotomaElevatorPlatform(EntitySolarIsotomaClientGadget isotoma, Scene scene, int gadgetId, Position pos, Position rot) { + super(scene, gadgetId, pos, rot); + setOwner(isotoma); + this.setRouteConfig(new AbilityRoute(rot, false, false, pos)); + this.setContent(new GadgetAbility(this, isotoma)); + } + + @Override + protected void fillFightProps(ConfigEntityGadget configGadget) { + if (configGadget == null || configGadget.getCombat() == null) { + return; + } + var combatData = configGadget.getCombat(); + var combatProperties = combatData.getProperty(); + + if (combatProperties.isUseCreatorProperty()) { + //If useCreatorProperty == true, use owner's property; + GameEntity ownerEntity = getOwner(); + if (ownerEntity != null) { + getFightProperties().putAll(ownerEntity.getFightProperties()); + return; + } else { + Grasscutter.getLogger().warn("Why gadget owner is null?"); + } + } + + super.fillFightProps(configGadget); + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/blossom/BlossomActivity.java b/src/main/java/emu/grasscutter/game/managers/blossom/BlossomActivity.java index fab2e029a..0f313a83b 100644 --- a/src/main/java/emu/grasscutter/game/managers/blossom/BlossomActivity.java +++ b/src/main/java/emu/grasscutter/game/managers/blossom/BlossomActivity.java @@ -1,148 +1,133 @@ -package emu.grasscutter.game.managers.blossom; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.monster.MonsterData; -import emu.grasscutter.data.excels.world.WorldLevelData; -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; -import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterTrigger; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.data.SceneBossChest; -import emu.grasscutter.scripts.data.SceneGadget; -import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.Utils; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.List; -import java.util.Queue; - -public class BlossomActivity { - - private static final int BLOOMING_GADGET_ID = 70210109; - private final SceneGroup tempSceneGroup; - private final WorldChallenge challenge; - private final EntityGadget gadget; - private final int goal; - private final int worldLevel; - private final List activeMonsters = new ArrayList<>(); - private final Queue candidateMonsters = new ArrayDeque<>(); - private EntityGadget chest; - private int step; - private int generatedCount; - private boolean pass = false; - - public BlossomActivity( - EntityGadget entityGadget, List monsters, int timeout, int worldLevel) { - this.tempSceneGroup = new SceneGroup(); - this.tempSceneGroup.id = entityGadget.getId(); - this.gadget = entityGadget; - this.step = 0; - this.goal = monsters.size(); - this.candidateMonsters.addAll(monsters); - this.worldLevel = worldLevel; - ArrayList challengeTriggers = new ArrayList<>(); - this.challenge = - new WorldChallenge( - entityGadget.getScene(), - tempSceneGroup, - 1, - 1, - List.of(goal, timeout), - timeout, - goal, - challengeTriggers); - challengeTriggers.add(new KillMonsterTrigger()); - // this.challengeTriggers.add(new InTimeTrigger()); - } - - public WorldChallenge getChallenge() { - return this.challenge; - } - - public void setMonsters(List monsters) { - this.activeMonsters.clear(); - this.activeMonsters.addAll(monsters); - for (EntityMonster monster : monsters) { - monster.setGroupId(this.tempSceneGroup.id); - } - } - - public int getAliveMonstersCount() { - int count = 0; - for (EntityMonster monster : activeMonsters) { - if (monster.isAlive()) { - count++; - } - } - return count; - } - - public boolean getPass() { - return pass; - } - - public void start() { - challenge.start(); - } - - public void onTick() { - Scene scene = gadget.getScene(); - Position pos = gadget.getPosition(); - if (getAliveMonstersCount() <= 2) { - if (generatedCount < goal) { - step++; - - WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(worldLevel); - int worldLevelOverride = 0; - if (worldLevelData != null) { - worldLevelOverride = worldLevelData.getMonsterLevel(); - } - - List newMonsters = new ArrayList<>(); - int willSpawn = Utils.randomRange(3, 5); - if (generatedCount + willSpawn > goal) { - willSpawn = goal - generatedCount; - } - generatedCount += willSpawn; - for (int i = 0; i < willSpawn; i++) { - MonsterData monsterData = GameData.getMonsterDataMap().get(candidateMonsters.poll()); - int level = scene.getEntityLevel(1, worldLevelOverride); - EntityMonster entity = new EntityMonster(scene, monsterData, pos.nearby2d(4f), level); - scene.addEntity(entity); - newMonsters.add(entity); - } - setMonsters(newMonsters); - } else { - if (getAliveMonstersCount() == 0) { - this.pass = true; - this.challenge.done(); - } - } - } - } - - public EntityGadget getGadget() { - return gadget; - } - - public EntityGadget getChest() { - if (chest == null) { - EntityGadget rewardGadget = - new EntityGadget(gadget.getScene(), BLOOMING_GADGET_ID, gadget.getPosition()); - SceneGadget metaGadget = new SceneGadget(); - metaGadget.boss_chest = new SceneBossChest(); - metaGadget.boss_chest.resin = 20; - rewardGadget.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, Float.POSITIVE_INFINITY); - rewardGadget.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Float.POSITIVE_INFINITY); - rewardGadget.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, Float.POSITIVE_INFINITY); - rewardGadget.setMetaGadget(metaGadget); - rewardGadget.buildContent(); - chest = rewardGadget; - } - return chest; - } -} +package emu.grasscutter.game.managers.blossom; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.dungeons.challenge.WorldChallenge; +import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; +import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterCountTrigger; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.scripts.data.SceneBossChest; +import emu.grasscutter.scripts.data.SceneGadget; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.Utils; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +public final class BlossomActivity { + + private final SceneGroup tempSceneGroup; + private final WorldChallenge challenge; + private final EntityGadget gadget; + private EntityGadget chest; + private int step; + private final int goal; + private int generatedCount; + private final int worldLevel; + private boolean pass=false; + private final List activeMonsters = new ArrayList<>(); + private final Queue candidateMonsters = new ArrayDeque<>(); + private static final int BLOOMING_GADGET_ID = 70210109; + public BlossomActivity(EntityGadget entityGadget, List monsters, int timeout, int worldLevel) { + this.tempSceneGroup = new SceneGroup(); + this.tempSceneGroup.id = entityGadget.getId(); + this.gadget=entityGadget; + this.step=0; + this.goal = monsters.size(); + this.candidateMonsters.addAll(monsters); + this.worldLevel = worldLevel; + ArrayList challengeTriggers = new ArrayList<>(); + this.challenge = new WorldChallenge(entityGadget.getScene(), + tempSceneGroup, + 1, + 1, + List.of(goal, timeout), + timeout, + goal, challengeTriggers); + challengeTriggers.add(new KillMonsterCountTrigger()); + //this.challengeTriggers.add(new InTimeTrigger()); + } + public WorldChallenge getChallenge() { + return this.challenge; + } + public void setMonsters(List monsters) { + this.activeMonsters.clear(); + this.activeMonsters.addAll(monsters); + for (EntityMonster monster : monsters) { + monster.setGroupId(this.tempSceneGroup.id); + } + } + public int getAliveMonstersCount() { + int count=0; + for (EntityMonster monster: activeMonsters) { + if (monster.isAlive()) { + count++; + } + } + return count; + } + public boolean getPass() { + return pass; + } + public void start() { + challenge.start(); + } + public void onTick() { + Scene scene = gadget.getScene(); + Position pos = gadget.getPosition(); + if (getAliveMonstersCount() <= 2) { + if (generatedCount newMonsters = new ArrayList<>(); + int willSpawn = Utils.randomRange(3,5); + if (generatedCount+willSpawn>goal) { + willSpawn = goal - generatedCount; + } + generatedCount+=willSpawn; + for (int i = 0; i < willSpawn; i++) { + var monsterData = GameData.getMonsterDataMap().get(candidateMonsters.poll()); + int level = scene.getEntityLevel(1, worldLevelOverride); + EntityMonster entity = new EntityMonster(scene, monsterData, pos.nearby2d(4f), level); + scene.addEntity(entity); + newMonsters.add(entity); + } + setMonsters(newMonsters); + }else { + if (getAliveMonstersCount() == 0) { + this.pass = true; + this.challenge.done(); + } + } + } + } + public EntityGadget getGadget() { + return gadget; + } + public EntityGadget getChest() { + if (chest==null) { + EntityGadget rewardGadget = new EntityGadget(gadget.getScene(), BLOOMING_GADGET_ID, gadget.getPosition()); + SceneGadget metaGadget = new SceneGadget(); + metaGadget.boss_chest = new SceneBossChest(); + metaGadget.boss_chest.resin = 20; + rewardGadget.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, Float.POSITIVE_INFINITY); + rewardGadget.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Float.POSITIVE_INFINITY); + rewardGadget.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, Float.POSITIVE_INFINITY); + rewardGadget.setMetaGadget(metaGadget); + rewardGadget.buildContent(); + chest = rewardGadget; + } + return chest; + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java b/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java index b5aff68d2..3ec0c117e 100644 --- a/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java +++ b/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java @@ -1,396 +1,418 @@ -package emu.grasscutter.game.managers.energy; - -import static emu.grasscutter.config.Configuration.GAME_OPTIONS; - -import com.google.protobuf.InvalidProtocolBufferException; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.DataLoader; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.ItemData; -import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; -import emu.grasscutter.data.excels.monster.MonsterData.HpDrops; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.entity.*; -import emu.grasscutter.game.player.BasePlayerManager; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ElementType; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.MonsterType; -import emu.grasscutter.game.props.WeaponType; -import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall; -import emu.grasscutter.net.proto.AbilityIdentifierOuterClass.AbilityIdentifier; -import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; -import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; -import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason; -import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; -import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.ThreadLocalRandom; - -public class EnergyManager extends BasePlayerManager { - private static final Int2ObjectMap> energyDropData = - new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap> - skillParticleGenerationData = new Int2ObjectOpenHashMap<>(); - private final Object2IntMap avatarNormalProbabilities; - private boolean energyUsage; // Should energy usage be enabled for this player? - - public EnergyManager(Player player) { - super(player); - this.avatarNormalProbabilities = new Object2IntOpenHashMap<>(); - this.energyUsage = GAME_OPTIONS.energyUsage; - } - - public static void initialize() { - // Read the data we need for monster energy drops. - try { - DataLoader.loadList("EnergyDrop.json", EnergyDropEntry.class) - .forEach( - entry -> { - energyDropData.put(entry.getDropId(), entry.getDropList()); - }); - - Grasscutter.getLogger().debug("Energy drop data successfully loaded."); - } catch (Exception ex) { - Grasscutter.getLogger().error("Unable to load energy drop data.", ex); - } - - // Read the data for particle generation from skills - try { - DataLoader.loadList("SkillParticleGeneration.json", SkillParticleGenerationEntry.class) - .forEach( - entry -> { - skillParticleGenerationData.put(entry.getAvatarId(), entry.getAmountList()); - }); - - Grasscutter.getLogger().debug("Skill particle generation data successfully loaded."); - } catch (Exception ex) { - Grasscutter.getLogger().error("Unable to load skill particle generation data data.", ex); - } - } - - /** Particle creation for elemental skills. */ - private int getBallCountForAvatar(int avatarId) { - // We default to two particles. - int count = 2; - - // If we don't have any data for this avatar, stop. - if (!skillParticleGenerationData.containsKey(avatarId)) { - Grasscutter.getLogger().warn("No particle generation data for avatarId {} found.", avatarId); - } - // If we do have data, roll for how many particles we should generate. - else { - int roll = ThreadLocalRandom.current().nextInt(0, 100); - int percentageStack = 0; - for (SkillParticleGenerationInfo info : skillParticleGenerationData.get(avatarId)) { - int chance = info.getChance(); - percentageStack += chance; - if (roll < percentageStack) { - count = info.getValue(); - break; - } - } - } - - // Done. - return count; - } - - private int getBallIdForElement(ElementType element) { - // If we have no element, we default to an element-less particle. - if (element == null) { - return 2024; - } - - // Otherwise, we determine the particle's ID based on the element. - return switch (element) { - case Fire -> 2017; - case Water -> 2018; - case Grass -> 2019; - case Electric -> 2020; - case Wind -> 2021; - case Ice -> 2022; - case Rock -> 2023; - default -> 2024; - }; - } - - public void handleGenerateElemBall(AbilityInvokeEntry invoke) - throws InvalidProtocolBufferException { - // ToDo: - // This is also called when a weapon like Favonius Warbow etc. creates energy through its - // passive. - // We are not handling this correctly at the moment. - - // Get action info. - AbilityActionGenerateElemBall action = - AbilityActionGenerateElemBall.parseFrom(invoke.getAbilityData()); - if (action == null) { - return; - } - - // Default to an elementless particle. - int itemId = 2024; - - // Generate 2 particles by default. - int amount = 2; - - // Try to get the casting avatar from the player's party. - Optional avatarEntity = - this.getCastingAvatarEntityForEnergy(invoke.getEntityId()); - - // Bug: invokes twice sometimes, Ayato, Keqing - // ToDo: deal with press, hold difference. deal with charge(Beidou, Yunjin) - if (avatarEntity.isPresent()) { - Avatar avatar = avatarEntity.get().getAvatar(); - - if (avatar != null) { - int avatarId = avatar.getAvatarId(); - AvatarSkillDepotData skillDepotData = avatar.getSkillDepot(); - - // Determine how many particles we need to create for this avatar. - amount = this.getBallCountForAvatar(avatarId); - - // Determine the avatar's element, and based on that the ID of the - // particles we have to generate. - if (skillDepotData != null) { - ElementType element = skillDepotData.getElementType(); - itemId = this.getBallIdForElement(element); - } - } - } - - // Generate the particles. - var pos = new Position(action.getPos()); - for (int i = 0; i < amount; i++) { - this.generateElemBall(itemId, pos, 1); - } - } - - /** - * Energy generation for NAs/CAs. - * - * @param avatar The avatar. - */ - private void generateEnergyForNormalAndCharged(EntityAvatar avatar) { - // This logic is based on the descriptions given in - // https://genshin-impact.fandom.com/wiki/Energy#Energy_Generated_by_Normal_Attacks - // https://library.keqingmains.com/combat-mechanics/energy#auto-attacking - // Those descriptions are lacking in some information, so this implementation most likely - // does not fully replicate the behavior of the official server. Open questions: - // - Does the probability for a character reset after some time? - // - Does the probability for a character reset when switching them out? - // - Does this really count every individual hit separately? - - // Get the avatar's weapon type. - WeaponType weaponType = avatar.getAvatar().getAvatarData().getWeaponType(); - - // Check if we already have probability data for this avatar. If not, insert it. - if (!this.avatarNormalProbabilities.containsKey(avatar)) { - this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability()); - } - - // Roll for energy. - int currentProbability = this.avatarNormalProbabilities.getInt(avatar); - int roll = ThreadLocalRandom.current().nextInt(0, 100); - - // If the player wins the roll, we increase the avatar's energy and reset the probability. - if (roll < currentProbability) { - avatar.addEnergy(1.0f, PropChangeReason.PROP_CHANGE_REASON_ABILITY, true); - this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability()); - } - // Otherwise, we increase the probability for the next hit. - else { - this.avatarNormalProbabilities.put( - avatar, currentProbability + weaponType.getEnergyGainIncreaseProbability()); - } - } - - public void handleAttackHit(EvtBeingHitInfo hitInfo) { - // Get the attack result. - AttackResult attackRes = hitInfo.getAttackResult(); - - // Make sure the attack was performed by the currently active avatar. If not, we ignore the hit. - Optional attackerEntity = - this.getCastingAvatarEntityForEnergy(attackRes.getAttackerId()); - if (attackerEntity.isEmpty() - || this.player.getTeamManager().getCurrentAvatarEntity().getId() - != attackerEntity.get().getId()) { - return; - } - - // Make sure the target is an actual enemy. - GameEntity targetEntity = this.player.getScene().getEntityById(attackRes.getDefenseId()); - if (!(targetEntity instanceof EntityMonster targetMonster)) { - return; - } - - MonsterType targetType = targetMonster.getMonsterData().getType(); - if (targetType != MonsterType.MONSTER_ORDINARY && targetType != MonsterType.MONSTER_BOSS) { - return; - } - - // Get the ability that caused this hit. - AbilityIdentifier ability = attackRes.getAbilityIdentifier(); - - // Make sure there is no actual "ability" associated with the hit. For now, this is how we - // identify normal and charged attacks. Note that this is not completely accurate: - // - Many character's charged attacks have an ability associated with them. This means that, - // for now, we don't identify charged attacks reliably. - // - There might also be some cases where we incorrectly identify something as a normal or - // charged attack that is not (Diluc's E?). - // - Catalyst normal attacks have an ability, so we don't handle those for now. - // ToDo: Fix all of that. - if (ability != AbilityIdentifier.getDefaultInstance()) { - return; - } - - // Handle the energy generation. - this.generateEnergyForNormalAndCharged(attackerEntity.get()); - } - - /* - * Energy logic related to using skills. - */ - - private void handleBurstCast(Avatar avatar, int skillId) { - // Don't do anything if energy usage is disabled. - if (!GAME_OPTIONS.energyUsage || !this.energyUsage) { - return; - } - - // If the cast skill was a burst, consume energy. - if (avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) { - avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START); - } - } - - public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) { - // Determine the entity that has cast the skill. Cancel if we can't find that avatar. - Optional caster = - this.player.getTeamManager().getActiveTeam().stream() - .filter(character -> character.getId() == casterId) - .findFirst(); - - if (caster.isEmpty()) { - return; - } - - Avatar avatar = caster.get().getAvatar(); - - // Handle elemental burst. - this.handleBurstCast(avatar, skillId); - } - - /* - * Monster energy drops. - */ - - private void generateElemBallDrops(EntityMonster monster, int dropId) { - // Generate all drops specified for the given drop id. - if (!energyDropData.containsKey(dropId)) { - Grasscutter.getLogger().warn("No drop data for dropId {} found.", dropId); - return; - } - - for (EnergyDropInfo info : energyDropData.get(dropId)) { - this.generateElemBall(info.getBallId(), monster.getPosition(), info.getCount()); - } - } - - public void handleMonsterEnergyDrop( - EntityMonster monster, float hpBeforeDamage, float hpAfterDamage) { - // Make sure this is actually a monster. - // Note that some wildlife also has that type, like boars or birds. - MonsterType type = monster.getMonsterData().getType(); - if (type != MonsterType.MONSTER_ORDINARY && type != MonsterType.MONSTER_BOSS) { - return; - } - - // Calculate the HP thresholds for before and after the damage was taken. - float maxHp = monster.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - float thresholdBefore = hpBeforeDamage / maxHp; - float thresholdAfter = hpAfterDamage / maxHp; - - // Determine the thresholds the monster has passed, and generate drops based on that. - for (HpDrops drop : monster.getMonsterData().getHpDrops()) { - if (drop.getDropId() == 0) { - continue; - } - - float threshold = drop.getHpPercent() / 100.0f; - if (threshold < thresholdBefore && threshold >= thresholdAfter) { - this.generateElemBallDrops(monster, drop.getDropId()); - } - } - - // Handle kill drops. - if (hpAfterDamage <= 0 && monster.getMonsterData().getKillDropId() != 0) { - this.generateElemBallDrops(monster, monster.getMonsterData().getKillDropId()); - } - } - - /* - * Utilities. - */ - - private void generateElemBall(int ballId, Position position, int count) { - // Generate a particle/orb with the specified parameters. - ItemData itemData = GameData.getItemDataMap().get(ballId); - if (itemData == null) { - return; - } - - EntityItem energyBall = - new EntityItem(this.getPlayer().getScene(), this.getPlayer(), itemData, position, count); - this.getPlayer().getScene().addEntity(energyBall); - } - - private Optional getCastingAvatarEntityForEnergy(int invokeEntityId) { - // To determine the avatar that has cast the skill that caused the energy particle to be - // generated, - // we have to look at the entity that has invoked the ability. This can either be that avatar - // directly, - // or it can be an `EntityClientGadget`, owned (some way up the owner hierarchy) by the avatar - // that cast the skill. - - // Try to get the invoking entity from the scene. - GameEntity entity = this.player.getScene().getEntityById(invokeEntityId); - - // Determine the ID of the entity that originally cast this skill. If the scene entity is null, - // or not an `EntityClientGadget`, we assume that we are directly looking at the casting avatar - // (the null case will happen if the avatar was switched out between casting the skill and the - // particle being generated). If the scene entity is an `EntityClientGadget`, we need to find - // the - // ID of the original owner of that gadget. - int avatarEntityId = - (!(entity instanceof EntityClientGadget)) - ? invokeEntityId - : ((EntityClientGadget) entity).getOriginalOwnerEntityId(); - - // Finally, find the avatar entity in the player's team. - return this.player.getTeamManager().getActiveTeam().stream() - .filter(character -> character.getId() == avatarEntityId) - .findFirst(); - } - - public boolean getEnergyUsage() { - return this.energyUsage; - } - - public void setEnergyUsage(boolean energyUsage) { - this.energyUsage = energyUsage; - if (!energyUsage) { // Refill team energy if usage is disabled - for (EntityAvatar entityAvatar : this.player.getTeamManager().getActiveTeam()) { - entityAvatar.addEnergy(1000, PropChangeReason.PROP_CHANGE_REASON_GM, true); - } - } - } -} +package emu.grasscutter.game.managers.energy; + +import static emu.grasscutter.config.Configuration.GAME_OPTIONS; + +import com.google.protobuf.InvalidProtocolBufferException; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.DataLoader; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.ItemData; +import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; +import emu.grasscutter.data.excels.monster.MonsterData.HpDrops; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.entity.*; +import emu.grasscutter.game.player.BasePlayerManager; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ElementType; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.MonsterType; +import emu.grasscutter.game.props.WeaponType; +import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall; +import emu.grasscutter.net.proto.AbilityIdentifierOuterClass.AbilityIdentifier; +import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; +import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; +import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason; +import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; +import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import lombok.Getter; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; + +public class EnergyManager extends BasePlayerManager { + private static final Int2ObjectMap> energyDropData = + new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap> + skillParticleGenerationData = new Int2ObjectOpenHashMap<>(); + private final Object2IntMap avatarNormalProbabilities; + @Getter private boolean energyUsage; // Should energy usage be enabled for this player? + + public EnergyManager(Player player) { + super(player); + this.avatarNormalProbabilities = new Object2IntOpenHashMap<>(); + this.energyUsage = GAME_OPTIONS.energyUsage; + } + + public static void initialize() { + // Read the data we need for monster energy drops. + try { + DataLoader.loadList("EnergyDrop.json", EnergyDropEntry.class) + .forEach( + entry -> { + energyDropData.put(entry.getDropId(), entry.getDropList()); + }); + + Grasscutter.getLogger().debug("Energy drop data successfully loaded."); + } catch (Exception ex) { + Grasscutter.getLogger().error("Unable to load energy drop data.", ex); + } + + // Read the data for particle generation from skills + try { + DataLoader.loadList("SkillParticleGeneration.json", SkillParticleGenerationEntry.class) + .forEach( + entry -> { + skillParticleGenerationData.put(entry.getAvatarId(), entry.getAmountList()); + }); + + Grasscutter.getLogger().debug("Skill particle generation data successfully loaded."); + } catch (Exception ex) { + Grasscutter.getLogger().error("Unable to load skill particle generation data data.", ex); + } + } + + /** Particle creation for elemental skills. */ + private int getBallCountForAvatar(int avatarId) { + // We default to two particles. + int count = 2; + + // If we don't have any data for this avatar, stop. + if (!skillParticleGenerationData.containsKey(avatarId)) { + Grasscutter.getLogger().warn("No particle generation data for avatarId {} found.", avatarId); + } + // If we do have data, roll for how many particles we should generate. + else { + int roll = ThreadLocalRandom.current().nextInt(0, 100); + int percentageStack = 0; + for (SkillParticleGenerationInfo info : skillParticleGenerationData.get(avatarId)) { + int chance = info.getChance(); + percentageStack += chance; + if (roll < percentageStack) { + count = info.getValue(); + break; + } + } + } + + // Done. + return count; + } + + private int getBallIdForElement(ElementType element) { + // If we have no element, we default to an element-less particle. + if (element == null) { + return 2024; + } + + // Otherwise, we determine the particle's ID based on the element. + return switch (element) { + case Fire -> 2017; + case Water -> 2018; + case Grass -> 2019; + case Electric -> 2020; + case Wind -> 2021; + case Ice -> 2022; + case Rock -> 2023; + default -> 2024; + }; + } + + public void handleGenerateElemBall(AbilityInvokeEntry invoke) + throws InvalidProtocolBufferException { + // ToDo: + // This is also called when a weapon like Favonius Warbow etc. creates energy through its + // passive. + // We are not handling this correctly at the moment. + + // Get action info. + AbilityActionGenerateElemBall action = + AbilityActionGenerateElemBall.parseFrom(invoke.getAbilityData()); + if (action == null) { + return; + } + + // Default to an elementless particle. + int itemId = 2024; + + // Generate 2 particles by default. + int amount = 2; + + // Try to get the casting avatar from the player's party. + Optional avatarEntity = + this.getCastingAvatarEntityForEnergy(invoke.getEntityId()); + + // Bug: invokes twice sometimes, Ayato, Keqing + // ToDo: deal with press, hold difference. deal with charge(Beidou, Yunjin) + if (avatarEntity.isPresent()) { + Avatar avatar = avatarEntity.get().getAvatar(); + + if (avatar != null) { + int avatarId = avatar.getAvatarId(); + AvatarSkillDepotData skillDepotData = avatar.getSkillDepot(); + + // Determine how many particles we need to create for this avatar. + amount = this.getBallCountForAvatar(avatarId); + + // Determine the avatar's element, and based on that the ID of the + // particles we have to generate. + if (skillDepotData != null) { + ElementType element = skillDepotData.getElementType(); + itemId = this.getBallIdForElement(element); + } + } + } + + // Generate the particles. + var pos = new Position(action.getPos()); + for (int i = 0; i < amount; i++) { + this.generateElemBall(itemId, pos, 1); + } + } + + /** + * Energy generation for NAs/CAs. + * + * @param avatar The avatar. + */ + private void generateEnergyForNormalAndCharged(EntityAvatar avatar) { + // This logic is based on the descriptions given in + // https://genshin-impact.fandom.com/wiki/Energy#Energy_Generated_by_Normal_Attacks + // https://library.keqingmains.com/combat-mechanics/energy#auto-attacking + // Those descriptions are lacking in some information, so this implementation most likely + // does not fully replicate the behavior of the official server. Open questions: + // - Does the probability for a character reset after some time? + // - Does the probability for a character reset when switching them out? + // - Does this really count every individual hit separately? + + // Get the avatar's weapon type. + WeaponType weaponType = avatar.getAvatar().getAvatarData().getWeaponType(); + + // Check if we already have probability data for this avatar. If not, insert it. + if (!this.avatarNormalProbabilities.containsKey(avatar)) { + this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability()); + } + + // Roll for energy. + int currentProbability = this.avatarNormalProbabilities.getInt(avatar); + int roll = ThreadLocalRandom.current().nextInt(0, 100); + + // If the player wins the roll, we increase the avatar's energy and reset the probability. + if (roll < currentProbability) { + avatar.addEnergy(1.0f, PropChangeReason.PROP_CHANGE_REASON_ABILITY, true); + this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability()); + } + // Otherwise, we increase the probability for the next hit. + else { + this.avatarNormalProbabilities.put( + avatar, currentProbability + weaponType.getEnergyGainIncreaseProbability()); + } + } + + public void handleAttackHit(EvtBeingHitInfo hitInfo) { + // Get the attack result. + AttackResult attackRes = hitInfo.getAttackResult(); + + // Make sure the attack was performed by the currently active avatar. If not, we ignore the hit. + Optional attackerEntity = + this.getCastingAvatarEntityForEnergy(attackRes.getAttackerId()); + if (attackerEntity.isEmpty() + || this.player.getTeamManager().getCurrentAvatarEntity().getId() + != attackerEntity.get().getId()) { + return; + } + + // Make sure the target is an actual enemy. + GameEntity targetEntity = this.player.getScene().getEntityById(attackRes.getDefenseId()); + if (!(targetEntity instanceof EntityMonster targetMonster)) { + return; + } + + MonsterType targetType = targetMonster.getMonsterData().getType(); + if (targetType != MonsterType.MONSTER_ORDINARY && targetType != MonsterType.MONSTER_BOSS) { + return; + } + + // Get the ability that caused this hit. + AbilityIdentifier ability = attackRes.getAbilityIdentifier(); + + // Make sure there is no actual "ability" associated with the hit. For now, this is how we + // identify normal and charged attacks. Note that this is not completely accurate: + // - Many character's charged attacks have an ability associated with them. This means that, + // for now, we don't identify charged attacks reliably. + // - There might also be some cases where we incorrectly identify something as a normal or + // charged attack that is not (Diluc's E?). + // - Catalyst normal attacks have an ability, so we don't handle those for now. + // ToDo: Fix all of that. + if (ability != AbilityIdentifier.getDefaultInstance()) { + return; + } + + // Handle the energy generation. + this.generateEnergyForNormalAndCharged(attackerEntity.get()); + } + + /* + * Energy logic related to using skills. + */ + + private void handleBurstCast(Avatar avatar, int skillId) { + // Don't do anything if energy usage is disabled. + if (!GAME_OPTIONS.energyUsage || !this.energyUsage) { + return; + } + + // If the cast skill was a burst, consume energy. + if (avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) { + avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START); + } + } + + public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) { + // Determine the entity that has cast the skill. Cancel if we can't find that avatar. + Optional caster = + this.player.getTeamManager().getActiveTeam().stream() + .filter(character -> character.getId() == casterId) + .findFirst(); + + if (caster.isEmpty()) { + return; + } + + Avatar avatar = caster.get().getAvatar(); + + // Handle elemental burst. + this.handleBurstCast(avatar, skillId); + } + + /* + * Monster energy drops. + */ + + private void generateElemBallDrops(EntityMonster monster, int dropId) { + // Generate all drops specified for the given drop id. + if (!energyDropData.containsKey(dropId)) { + Grasscutter.getLogger().warn("No drop data for dropId {} found.", dropId); + return; + } + + for (EnergyDropInfo info : energyDropData.get(dropId)) { + this.generateElemBall(info.getBallId(), monster.getPosition(), info.getCount()); + } + } + + public void handleMonsterEnergyDrop( + EntityMonster monster, float hpBeforeDamage, float hpAfterDamage) { + // Make sure this is actually a monster. + // Note that some wildlife also has that type, like boars or birds. + MonsterType type = monster.getMonsterData().getType(); + if (type != MonsterType.MONSTER_ORDINARY && type != MonsterType.MONSTER_BOSS) { + return; + } + + // Calculate the HP thresholds for before and after the damage was taken. + float maxHp = monster.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + float thresholdBefore = hpBeforeDamage / maxHp; + float thresholdAfter = hpAfterDamage / maxHp; + + // Determine the thresholds the monster has passed, and generate drops based on that. + for (HpDrops drop : monster.getMonsterData().getHpDrops()) { + if (drop.getDropId() == 0) { + continue; + } + + float threshold = drop.getHpPercent() / 100.0f; + if (threshold < thresholdBefore && threshold >= thresholdAfter) { + this.generateElemBallDrops(monster, drop.getDropId()); + } + } + + // Handle kill drops. + if (hpAfterDamage <= 0 && monster.getMonsterData().getKillDropId() != 0) { + this.generateElemBallDrops(monster, monster.getMonsterData().getKillDropId()); + } + } + + /* + * Utilities. + */ + + private void generateElemBall(int ballId, Position position, int count) { + // Generate a particle/orb with the specified parameters. + ItemData itemData = GameData.getItemDataMap().get(ballId); + if (itemData == null) { + return; + } + + EntityItem energyBall = + new EntityItem(this.getPlayer().getScene(), this.getPlayer(), itemData, position, count); + this.getPlayer().getScene().addEntity(energyBall); + } + + private Optional getCastingAvatarEntityForEnergy(int invokeEntityId) { + // To determine the avatar that has cast the skill that caused the energy particle to be + // generated, + // we have to look at the entity that has invoked the ability. This can either be that avatar + // directly, + // or it can be an `EntityClientGadget`, owned (some way up the owner hierarchy) by the avatar + // that cast the skill. + + // Try to get the invoking entity from the scene. + GameEntity entity = this.player.getScene().getEntityById(invokeEntityId); + + // Determine the ID of the entity that originally cast this skill. If the scene entity is null, + // or not an `EntityClientGadget`, we assume that we are directly looking at the casting avatar + // (the null case will happen if the avatar was switched out between casting the skill and the + // particle being generated). If the scene entity is an `EntityClientGadget`, we need to find + // the + // ID of the original owner of that gadget. + int avatarEntityId = + (!(entity instanceof EntityClientGadget)) + ? invokeEntityId + : ((EntityClientGadget) entity).getOriginalOwnerEntityId(); + + // Finally, find the avatar entity in the player's team. + return this.player.getTeamManager().getActiveTeam().stream() + .filter(character -> character.getId() == avatarEntityId) + .findFirst(); + } + + /** + * Refills the energy of the active avatar. + * + * @return True if the energy was refilled, false otherwise. + */ + public boolean refillActiveEnergy() { + var activeEntity = this.player.getTeamManager().getCurrentAvatarEntity(); + return activeEntity.addEnergy(activeEntity.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal()); + } + + /** + * Refills the energy of the entire team. + * + * @param changeReason The reason for the energy change. + * @param isFlat Whether the energy should be added as a flat value. + */ + public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) { + for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) { + // giving the exact amount read off the AvatarSkillData.json + entityAvatar.addEnergy(entityAvatar.getAvatar().getSkillDepot() + .getEnergySkillData().getCostElemVal(), changeReason, isFlat); + } + } + + public void setEnergyUsage(boolean energyUsage) { + this.energyUsage = energyUsage; + if (!energyUsage) { // Refill team energy if usage is disabled + for (EntityAvatar entityAvatar : this.player.getTeamManager().getActiveTeam()) { + entityAvatar.addEnergy(1000, PropChangeReason.PROP_CHANGE_REASON_GM, true); + } + } + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/stamina/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/stamina/StaminaManager.java index 8f0f5dbb6..4e39cd582 100644 --- a/src/main/java/emu/grasscutter/game/managers/stamina/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/stamina/StaminaManager.java @@ -294,7 +294,7 @@ public class StaminaManager extends BasePlayerManager { // Returns new stamina and sends PlayerPropNotify or VehicleStaminaNotify public int setStamina(GameSession session, String reason, int newStamina, boolean isCharacterStamina) { // Target Player - if (!GAME_OPTIONS.staminaUsage || session.getPlayer().getUnlimitedStamina()) { + if (!GAME_OPTIONS.staminaUsage || session.getPlayer().isUnlimitedStamina()) { newStamina = getMaxCharacterStamina(); } diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 0189a96ba..df09240c2 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -45,7 +45,6 @@ import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.props.WatcherTriggerType; import emu.grasscutter.game.quest.QuestManager; import emu.grasscutter.game.quest.enums.QuestContent; -import emu.grasscutter.game.quest.enums.QuestTrigger; import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.tower.TowerData; import emu.grasscutter.game.tower.TowerManager; @@ -117,12 +116,13 @@ public class Player { @Getter @Setter private int sceneId; @Getter @Setter private int regionId; @Getter private int mainCharacterId; - @Setter private boolean godmode; // Getter is inGodmode - private boolean stamina; // Getter is getUnlimitedStamina, Setter is setUnlimitedStamina + @Getter @Setter private boolean inGodMode; + @Getter @Setter private boolean unlimitedStamina; @Getter private Set nameCardList; @Getter private Set flyCloakList; @Getter private Set costumeList; + @Getter private Set personalLineList; @Getter @Setter private Set rewardedLevels; @Getter @Setter private Set homeRewardedLevels; @Getter @Setter private Set realmList; @@ -793,18 +793,6 @@ public class Player { this.save(); } - public boolean getUnlimitedStamina() { - return stamina; - } - - public void setUnlimitedStamina(boolean stamina) { - this.stamina = stamina; - } - - public boolean inGodmode() { - return godmode; - } - public boolean hasSentLoginPackets() { return hasSentLoginPackets; } diff --git a/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java b/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java index 6fd5cf339..aa55a0d14 100644 --- a/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java +++ b/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java @@ -1,238 +1,280 @@ -package emu.grasscutter.game.player; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.ScenePointEntry; -import emu.grasscutter.data.excels.OpenStateData; -import emu.grasscutter.data.excels.OpenStateData.OpenStateCondType; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.quest.enums.QuestState; -import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; -import emu.grasscutter.server.packet.send.*; -import java.util.Set; -import java.util.stream.Collectors; - -// @Entity -public class PlayerProgressManager extends BasePlayerDataManager { - /****************************************************************************************************************** - ****************************************************************************************************************** - * OPEN STATES - ****************************************************************************************************************** - *****************************************************************************************************************/ - - // Set of open states that are never unlocked, whether they fulfill the conditions or not. - public static final Set BLACKLIST_OPEN_STATES = - Set.of( - 48 // blacklist OPEN_STATE_LIMIT_REGION_GLOBAL to make Meledy happy. =D Remove this as - // soon as quest unlocks are fully implemented. - ); - // Set of open states that are set per default for all accounts. Can be overwritten by an entry in - // `map`. - public static final Set DEFAULT_OPEN_STATES = - GameData.getOpenStateList().stream() - .filter( - s -> - s.isDefaultState() // Actual default-opened states. - // All states whose unlock we don't handle correctly yet. - || (s.getCond().stream() - .filter( - c -> - c.getCondType() - == OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL) - .count() - == 0) - // Always unlock OPEN_STATE_PAIMON, otherwise the player will not have a - // working chat. - || s.getId() == 1) - .filter( - s -> - !BLACKLIST_OPEN_STATES.contains(s.getId())) // Filter out states in the blacklist. - .map(s -> s.getId()) - .collect(Collectors.toSet()); - - public PlayerProgressManager(Player player) { - super(player); - } - - /********** - * Handler for player login. - **********/ - public void onPlayerLogin() { - // Try unlocking open states on player login. This handles accounts where unlock conditions were - // already met before certain open state unlocks were implemented. - this.tryUnlockOpenStates(false); - - // Send notify to the client. - player.getSession().send(new PacketOpenStateUpdateNotify(this.player)); - - // Add statue quests if necessary. - this.addStatueQuestsOnLogin(); - - // Auto-unlock the first statue and map area, until we figure out how to make - // that particular statue interactable. - this.player.getUnlockedScenePoints(3).add(7); - this.player.getUnlockedSceneAreas(3).add(1); - } - - /********** - * Direct getters and setters for open states. - **********/ - public int getOpenState(int openState) { - return this.player.getOpenStates().getOrDefault(openState, 0); - } - - private void setOpenState(int openState, int value, boolean sendNotify) { - int previousValue = this.player.getOpenStates().getOrDefault(openState, 0); - - if (value != previousValue) { - this.player.getOpenStates().put(openState, value); - - if (sendNotify) { - player.getSession().send(new PacketOpenStateChangeNotify(openState, value)); - } - } - } - - private void setOpenState(int openState, int value) { - this.setOpenState(openState, value, true); - } - - /********** - * Condition checking for setting open states. - **********/ - private boolean areConditionsMet(OpenStateData openState) { - // Check all conditions and test if at least one of them is violated. - for (var condition : openState.getCond()) { - // For level conditions, check if the player has reached the necessary level. - if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL) { - if (this.player.getLevel() < condition.getParam()) { - return false; - } - } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_QUEST) { - // ToDo: Implement. - } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_PARENT_QUEST) { - // ToDo: Implement. - } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_OFFERING_LEVEL) { - // ToDo: Implement. - } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_CITY_REPUTATION_LEVEL) { - // ToDo: Implement. - } - } - - // Done. If we didn't find any violations, all conditions are met. - return true; - } - - /********** - * Setting open states from the client (via `SetOpenStateReq`). - **********/ - public void setOpenStateFromClient(int openState, int value) { - // Get the data for this open state. - OpenStateData data = GameData.getOpenStateDataMap().get(openState); - if (data == null) { - this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL)); - return; - } - - // Make sure that this is an open state that the client is allowed to set, - // and that it doesn't have any further conditions attached. - if (!data.isAllowClientOpen() || !this.areConditionsMet(data)) { - this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL)); - return; - } - - // Set. - this.setOpenState(openState, value); - this.player.sendPacket(new PacketSetOpenStateRsp(openState, value)); - } - - /********** - * Triggered unlocking of open states (unlock states whose conditions have been met.) - **********/ - public void tryUnlockOpenStates(boolean sendNotify) { - // Get list of open states that are not yet unlocked. - var lockedStates = - GameData.getOpenStateList().stream() - .filter(s -> this.player.getOpenStates().getOrDefault(s, 0) == 0) - .toList(); - - // Try unlocking all of them. - for (var state : lockedStates) { - // To auto-unlock a state, it has to meet three conditions: - // * it can not be a state that is unlocked by the client, - // * it has to meet all its unlock conditions, and - // * it can not be in the blacklist. - if (!state.isAllowClientOpen() - && this.areConditionsMet(state) - && !BLACKLIST_OPEN_STATES.contains(state.getId())) { - this.setOpenState(state.getId(), 1, sendNotify); - } - } - } - - public void tryUnlockOpenStates() { - this.tryUnlockOpenStates(true); - } - - /****************************************************************************************************************** - ****************************************************************************************************************** - * MAP AREAS AND POINTS - ****************************************************************************************************************** - *****************************************************************************************************************/ - private void addStatueQuestsOnLogin() { - // Get all currently existing subquests for the "unlock all statues" main quest. - var statueMainQuest = GameData.getMainQuestDataMap().get(303); - var statueSubQuests = statueMainQuest.getSubQuests(); - - // Add the main statue quest if it isn't active yet. - var statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303); - if (statueGameMainQuest == null) { - this.player.getQuestManager().addQuest(30302); - statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303); - } - - // Set all subquests to active if they aren't already finished. - for (var subData : statueSubQuests) { - var subGameQuest = statueGameMainQuest.getChildQuestById(subData.getSubId()); - if (subGameQuest != null && subGameQuest.getState() == QuestState.QUEST_STATE_UNSTARTED) { - this.player.getQuestManager().addQuest(subData.getSubId()); - } - } - } - - public boolean unlockTransPoint(int sceneId, int pointId, boolean isStatue) { - // Check whether the unlocked point exists and whether it is still locked. - ScenePointEntry scenePointEntry = GameData.getScenePointEntryById(sceneId, pointId); - - if (scenePointEntry == null || this.player.getUnlockedScenePoints(sceneId).contains(pointId)) { - return false; - } - - // Add the point to the list of unlocked points for its scene. - this.player.getUnlockedScenePoints(sceneId).add(pointId); - - // Give primogems and Adventure EXP for unlocking. - this.player.getInventory().addItem(201, 5, ActionReason.UnlockPointReward); - this.player.getInventory().addItem(102, isStatue ? 50 : 10, ActionReason.UnlockPointReward); - - // this.player.sendPacket(new - // PacketPlayerPropChangeReasonNotify(this.player.getProperty(PlayerProperty.PROP_PLAYER_EXP), - // PlayerProperty.PROP_PLAYER_EXP, PropChangeReason.PROP_CHANGE_REASON_PLAYER_ADD_EXP)); - - // Fire quest trigger for trans point unlock. - this.player - .getQuestManager() - .triggerEvent(QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT, sceneId, pointId); - - // Send packet. - this.player.sendPacket(new PacketScenePointUnlockNotify(sceneId, pointId)); - return true; - } - - public void unlockSceneArea(int sceneId, int areaId) { - // Add the area to the list of unlocked areas in its scene. - this.player.getUnlockedSceneAreas(sceneId).add(areaId); - - // Send packet. - this.player.sendPacket(new PacketSceneAreaUnlockNotify(sceneId, areaId)); - } -} +package emu.grasscutter.game.player; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.ScenePointEntry; +import emu.grasscutter.data.excels.OpenStateData; +import emu.grasscutter.data.excels.OpenStateData.OpenStateCondType; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.quest.enums.QuestCond; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.game.quest.enums.QuestState; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; +import emu.grasscutter.server.packet.send.*; +import lombok.val; + +import java.util.Set; +import java.util.stream.Collectors; + +// @Entity +public final class PlayerProgressManager extends BasePlayerDataManager { + /****************************************************************************************************************** + ****************************************************************************************************************** + * OPEN STATES + ****************************************************************************************************************** + *****************************************************************************************************************/ + + // Set of open states that are never unlocked, whether they fulfill the conditions or not. + public static final Set BLACKLIST_OPEN_STATES = + Set.of( + 48 // blacklist OPEN_STATE_LIMIT_REGION_GLOBAL to make Meledy happy. =D Remove this as + // soon as quest unlocks are fully implemented. + ); + // Set of open states that are set per default for all accounts. Can be overwritten by an entry in + // `map`. + public static final Set DEFAULT_OPEN_STATES = + GameData.getOpenStateList().stream() + .filter( + s -> + s.isDefaultState() // Actual default-opened states. + // All states whose unlock we don't handle correctly yet. + || (s.getCond().stream() + .filter( + c -> + c.getCondType() + == OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL) + .count() + == 0) + // Always unlock OPEN_STATE_PAIMON, otherwise the player will not have a + // working chat. + || s.getId() == 1) + .filter( + s -> + !BLACKLIST_OPEN_STATES.contains(s.getId())) // Filter out states in the blacklist. + .map(s -> s.getId()) + .collect(Collectors.toSet()); + + public PlayerProgressManager(Player player) { + super(player); + } + + /********** + * Handler for player login. + **********/ + public void onPlayerLogin() { + // Try unlocking open states on player login. This handles accounts where unlock conditions were + // already met before certain open state unlocks were implemented. + this.tryUnlockOpenStates(false); + + // Send notify to the client. + player.getSession().send(new PacketOpenStateUpdateNotify(this.player)); + + // Add statue quests if necessary. + this.addStatueQuestsOnLogin(); + + // Auto-unlock the first statue and map area, until we figure out how to make + // that particular statue interactable. + this.player.getUnlockedScenePoints(3).add(7); + this.player.getUnlockedSceneAreas(3).add(1); + } + + /********** + * Direct getters and setters for open states. + **********/ + public int getOpenState(int openState) { + return this.player.getOpenStates().getOrDefault(openState, 0); + } + + private void setOpenState(int openState, int value, boolean sendNotify) { + int previousValue = this.player.getOpenStates().getOrDefault(openState, 0); + + if (value != previousValue) { + this.player.getOpenStates().put(openState, value); + + if (sendNotify) { + player.getSession().send(new PacketOpenStateChangeNotify(openState, value)); + } + } + } + + private void setOpenState(int openState, int value) { + this.setOpenState(openState, value, true); + } + + /********** + * Condition checking for setting open states. + **********/ + private boolean areConditionsMet(OpenStateData openState) { + // Check all conditions and test if at least one of them is violated. + for (var condition : openState.getCond()) { + // For level conditions, check if the player has reached the necessary level. + if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL) { + if (this.player.getLevel() < condition.getParam()) { + return false; + } + } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_QUEST) { + // ToDo: Implement. + } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_PARENT_QUEST) { + // ToDo: Implement. + } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_OFFERING_LEVEL) { + // ToDo: Implement. + } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_CITY_REPUTATION_LEVEL) { + // ToDo: Implement. + } + } + + // Done. If we didn't find any violations, all conditions are met. + return true; + } + + /********** + * Setting open states from the client (via `SetOpenStateReq`). + **********/ + public void setOpenStateFromClient(int openState, int value) { + // Get the data for this open state. + OpenStateData data = GameData.getOpenStateDataMap().get(openState); + if (data == null) { + this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL)); + return; + } + + // Make sure that this is an open state that the client is allowed to set, + // and that it doesn't have any further conditions attached. + if (!data.isAllowClientOpen() || !this.areConditionsMet(data)) { + this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL)); + return; + } + + // Set. + this.setOpenState(openState, value); + this.player.sendPacket(new PacketSetOpenStateRsp(openState, value)); + } + + /** + * This force sets an open state, ignoring all conditions and permissions + */ + public void forceSetOpenState(int openState, int value) { + this.setOpenState(openState, value); + } + + /********** + * Triggered unlocking of open states (unlock states whose conditions have been met.) + **********/ + public void tryUnlockOpenStates(boolean sendNotify) { + // Get list of open states that are not yet unlocked. + var lockedStates = + GameData.getOpenStateList().stream() + .filter(s -> this.player.getOpenStates().getOrDefault(s, 0) == 0) + .toList(); + + // Try unlocking all of them. + for (var state : lockedStates) { + // To auto-unlock a state, it has to meet three conditions: + // * it can not be a state that is unlocked by the client, + // * it has to meet all its unlock conditions, and + // * it can not be in the blacklist. + if (!state.isAllowClientOpen() + && this.areConditionsMet(state) + && !BLACKLIST_OPEN_STATES.contains(state.getId())) { + this.setOpenState(state.getId(), 1, sendNotify); + } + } + } + + public void tryUnlockOpenStates() { + this.tryUnlockOpenStates(true); + } + + /****************************************************************************************************************** + ****************************************************************************************************************** + * MAP AREAS AND POINTS + ****************************************************************************************************************** + *****************************************************************************************************************/ + private void addStatueQuestsOnLogin() { + // Get all currently existing subquests for the "unlock all statues" main quest. + var statueMainQuest = GameData.getMainQuestDataMap().get(303); + var statueSubQuests = statueMainQuest.getSubQuests(); + + // Add the main statue quest if it isn't active yet. + var statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303); + if (statueGameMainQuest == null) { + this.player.getQuestManager().addQuest(30302); + statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303); + } + + // Set all subquests to active if they aren't already finished. + for (var subData : statueSubQuests) { + var subGameQuest = statueGameMainQuest.getChildQuestById(subData.getSubId()); + if (subGameQuest != null && subGameQuest.getState() == QuestState.QUEST_STATE_UNSTARTED) { + this.player.getQuestManager().addQuest(subData.getSubId()); + } + } + } + + public boolean unlockTransPoint(int sceneId, int pointId, boolean isStatue) { + // Check whether the unlocked point exists and whether it is still locked. + ScenePointEntry scenePointEntry = GameData.getScenePointEntryById(sceneId, pointId); + + if (scenePointEntry == null || this.player.getUnlockedScenePoints(sceneId).contains(pointId)) { + return false; + } + + // Add the point to the list of unlocked points for its scene. + this.player.getUnlockedScenePoints(sceneId).add(pointId); + + // Give primogems and Adventure EXP for unlocking. + this.player.getInventory().addItem(201, 5, ActionReason.UnlockPointReward); + this.player.getInventory().addItem(102, isStatue ? 50 : 10, ActionReason.UnlockPointReward); + + // this.player.sendPacket(new + // PacketPlayerPropChangeReasonNotify(this.player.getProperty(PlayerProperty.PROP_PLAYER_EXP), + // PlayerProperty.PROP_PLAYER_EXP, PropChangeReason.PROP_CHANGE_REASON_PLAYER_ADD_EXP)); + + // Fire quest trigger for trans point unlock. + this.player + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT, sceneId, pointId); + + // Send packet. + this.player.sendPacket(new PacketScenePointUnlockNotify(sceneId, pointId)); + return true; + } + + public void unlockSceneArea(int sceneId, int areaId) { + // Add the area to the list of unlocked areas in its scene. + this.player.getUnlockedSceneAreas(sceneId).add(areaId); + + // Send packet. + this.player.sendPacket(new PacketSceneAreaUnlockNotify(sceneId, areaId)); + } + + /** + * Give replace costume to player (Amber, Jean, Mona, Rosaria) + */ + public void addReplaceCostumes(){ + var currentPlayerCostumes = player.getCostumeList(); + GameData.getAvatarReplaceCostumeDataMap().keySet().forEach(costumeId -> { + if (GameData.getAvatarCostumeDataMap().get(costumeId) == null || currentPlayerCostumes.contains(costumeId)){ + return; + } + this.player.addCostume(costumeId); + }); + } + + /** + * Quest progress + */ + public void addQuestProgress(int id, int count){ + var newCount = player.getPlayerProgress().addToCurrentProgress(id, count); + player.save(); + player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_ADD_QUEST_PROGRESS, id, newCount); + } + + /** + * Item history + */ + public void addItemObtainedHistory(int id, int count){ + var newCount = player.getPlayerProgress().addToItemHistory(id, count); + player.save(); + player.getQuestManager().queueEvent(QuestCond.QUEST_COND_HISTORY_GOT_ANY_ITEM, id, newCount); + } +} diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 0e5d76cac..7d2441c42 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -1,1069 +1,1080 @@ -package emu.grasscutter.game.player; - -import static emu.grasscutter.config.Configuration.GAME_OPTIONS; - -import dev.morphia.annotations.Entity; -import dev.morphia.annotations.Transient; -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.config.fields.ConfigAbilityData; -import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.game.entity.EntityBaseGadget; -import emu.grasscutter.game.props.ElementType; -import emu.grasscutter.game.props.EnterReason; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.game.world.World; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType; -import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; -import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; -import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; -import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGrantRecord.GrantReason; -import emu.grasscutter.net.proto.VisionTypeOuterClass; -import emu.grasscutter.server.event.player.PlayerTeamDeathEvent; -import emu.grasscutter.server.packet.send.*; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.Utils; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import java.util.*; -import java.util.stream.Stream; -import lombok.Getter; -import lombok.Setter; -import lombok.val; - -@Entity -public class TeamManager extends BasePlayerDataManager { - @Transient private final List avatars; - @Transient @Getter private final Set gadgets; - @Transient @Getter private final IntSet teamResonances; - @Transient @Getter private final IntSet teamResonancesConfig; - // This needs to be a LinkedHashMap to guarantee insertion order. - @Getter private LinkedHashMap teams; - private int currentTeamIndex; - @Getter @Setter private int currentCharacterIndex; - @Transient @Getter @Setter private TeamInfo mpTeam; - @Transient @Getter @Setter private int entityId; - - @Transient private int useTemporarilyTeamIndex = -1; - @Transient private List temporaryTeam; // Temporary Team for tower - @Transient @Getter @Setter private boolean usingTrialTeam; - @Transient @Getter @Setter private TeamInfo trialAvatarTeam; - // hold trial avatars for later use in rebuilding active team - @Transient @Getter @Setter private Map trialAvatars; - - @Transient @Getter @Setter - private int previousIndex = -1; // index of character selection in team before adding trial avatar - - public TeamManager() { - this.mpTeam = new TeamInfo(); - this.avatars = new ArrayList<>(); - this.gadgets = new HashSet<>(); - this.teamResonances = new IntOpenHashSet(); - this.teamResonancesConfig = new IntOpenHashSet(); - } - - public TeamManager(Player player) { - this(); - this.setPlayer(player); - - this.teams = new LinkedHashMap<>(); - this.currentTeamIndex = 1; - for (int i = 1; i <= GameConstants.DEFAULT_TEAMS; i++) { - this.teams.put(i, new TeamInfo()); - } - } - - public World getWorld() { - return this.getPlayer().getWorld(); - } - - /** - * Search through all teams and if the team matches, return that index. Otherwise, return -1. No - * match could mean that the team does not currently belong to the player. - */ - public int getTeamId(TeamInfo team) { - for (int i = 1; i <= this.teams.size(); i++) { - if (this.teams.get(i).equals(team)) { - return i; - } - } - return -1; - } - - public int getCurrentTeamId() { - // Starts from 1 - return currentTeamIndex; - } - - private void setCurrentTeamId(int currentTeamIndex) { - this.currentTeamIndex = currentTeamIndex; - } - - public long getCurrentCharacterGuid() { - return this.getCurrentAvatarEntity().getAvatar().getGuid(); - } - - public TeamInfo getCurrentTeamInfo() { - if (useTemporarilyTeamIndex >= 0 && useTemporarilyTeamIndex < temporaryTeam.size()) { - return temporaryTeam.get(useTemporarilyTeamIndex); - } - if (this.getPlayer().isInMultiplayer()) { - return this.getMpTeam(); - } - return this.getTeams().get(this.currentTeamIndex); - } - - public TeamInfo getCurrentSinglePlayerTeamInfo() { - return this.getTeams().get(this.currentTeamIndex); - } - - public List getActiveTeam() { - return avatars; - } - - public EntityAvatar getCurrentAvatarEntity() { - return this.getActiveTeam().get(currentCharacterIndex); - } - - public boolean isSpawned() { - return this.getPlayer().getScene() != null - && this.getPlayer() - .getScene() - .getEntities() - .containsKey(this.getCurrentAvatarEntity().getId()); - } - - public int getMaxTeamSize() { - if (this.getPlayer().isInMultiplayer()) { - int max = GAME_OPTIONS.avatarLimits.multiplayerTeam; - if (this.getPlayer().getWorld().getHost() == this.getPlayer()) { - return Math.max(1, (int) Math.ceil(max / (double) this.getWorld().getPlayerCount())); - } - return Math.max(1, (int) Math.floor(max / (double) this.getWorld().getPlayerCount())); - } - - return GAME_OPTIONS.avatarLimits.singlePlayerTeam; - } - - // Methods - - /** Returns true if there is space to add the number of avatars to the team. */ - public boolean canAddAvatarsToTeam(TeamInfo team, int avatars) { - return team.size() + avatars <= this.getMaxTeamSize(); - } - - /** Returns true if there is space to add to the team. */ - public boolean canAddAvatarToTeam(TeamInfo team) { - return this.canAddAvatarsToTeam(team, 1); - } - - /** - * Returns true if there is space to add the number of avatars to the current team. If the current - * team is temporary, returns false. - */ - public boolean canAddAvatarsToCurrentTeam(int avatars) { - if (this.useTemporarilyTeamIndex != -1) { - return false; - } - return this.canAddAvatarsToTeam(this.getCurrentTeamInfo(), avatars); - } - - /** - * Returns true if there is space to add to the current team. If the current team is temporary, - * returns false. - */ - public boolean canAddAvatarToCurrentTeam() { - return this.canAddAvatarsToCurrentTeam(1); - } - - /** - * Try to add the collection of avatars to the team. Returns true if all were successfully added. - * If some can not be added, returns false and does not add any. - */ - public boolean addAvatarsToTeam(TeamInfo team, Collection avatars) { - if (!this.canAddAvatarsToTeam(team, avatars.size())) { - return false; - } - - // Convert avatars into a collection of avatar IDs, then add - team.getAvatars().addAll(avatars.stream().map(a -> a.getAvatarId()).toList()); - - // Update team - if (this.getPlayer().isInMultiplayer()) { - if (team.equals(this.getMpTeam())) { - // MP team Packet - this.updateTeamEntities(new PacketChangeMpTeamAvatarRsp(this.getPlayer(), team)); - } - } else { - // SP team update packet - this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(this.getPlayer())); - - int teamId = this.getTeamId(team); - if (teamId != -1) { - // This is one of the player's teams - // Update entites - if (teamId == this.getCurrentTeamId()) { - this.updateTeamEntities(new PacketSetUpAvatarTeamRsp(this.getPlayer(), teamId, team)); - } else { - this.getPlayer().sendPacket(new PacketSetUpAvatarTeamRsp(this.getPlayer(), teamId, team)); - } - } - } - - return true; - } - - /** Try to add an avatar to a team. Returns true if successful. */ - public boolean addAvatarToTeam(TeamInfo team, Avatar avatar) { - return this.addAvatarsToTeam(team, Collections.singleton(avatar)); - } - - /** - * Try to add the collection of avatars to the current team. Will not modify a temporary team. - * Returns true if all were successfully added. If some can not be added, returns false and does - * not add any. - */ - public boolean addAvatarsToCurrentTeam(Collection avatars) { - if (this.useTemporarilyTeamIndex != -1) { - return false; - } - return this.addAvatarsToTeam(this.getCurrentTeamInfo(), avatars); - } - - /** - * Try to add an avatar to the current team. Will not modify a temporary team. Returns true if - * successful. - */ - public boolean addAvatarToCurrentTeam(Avatar avatar) { - return this.addAvatarsToCurrentTeam(Collections.singleton(avatar)); - } - - private void updateTeamResonances() { - this.getTeamResonances().clear(); - this.getTeamResonancesConfig().clear(); - // Official resonances require a full party - if (this.avatars.size() < 4) return; - - // TODO: make this actually read from TeamResonanceExcelConfigData.json for the real resonances - // and conditions - // Currently we just hardcode these conditions, but this won't work for modded resources or - // future changes - var elementCounts = new Object2IntOpenHashMap(); - this.getActiveTeam().stream() - .map(EntityAvatar::getAvatar) - .filter(Objects::nonNull) - .map(Avatar::getSkillDepot) - .filter(Objects::nonNull) - .map(AvatarSkillDepotData::getElementType) - .filter(Objects::nonNull) - .forEach(elementType -> elementCounts.addTo(elementType, 1)); - - // Dual element resonances - elementCounts.object2IntEntrySet().stream() - .filter(e -> e.getIntValue() >= 2) - .map(e -> e.getKey()) - .filter(elementType -> elementType.getTeamResonanceId() != 0) - .forEach( - elementType -> { - this.teamResonances.add(elementType.getTeamResonanceId()); - this.teamResonancesConfig.add(elementType.getConfigHash()); - }); - - // Four element resonance - if (elementCounts.size() >= 4) { - this.teamResonances.add(ElementType.Default.getTeamResonanceId()); - this.teamResonancesConfig.add(ElementType.Default.getConfigHash()); - } - } - - /** Updates all properties of the active team. */ - public void updateTeamProperties() { - this.updateTeamResonances(); // Update team resonances. - this.getPlayer() - .sendPacket(new PacketSceneTeamUpdateNotify(this.getPlayer())); // Notify the player. - - // Skill charges packet - Yes, this is official server behavior as of 2.6.0 - this.getActiveTeam().stream() - .map(EntityAvatar::getAvatar) - .forEach(Avatar::sendSkillExtraChargeMap); - } - - public void updateTeamEntities(BasePacket responsePacket) { - // Sanity check - Should never happen - if (this.getCurrentTeamInfo().getAvatars().size() <= 0) { - return; - } - - // If current team has changed - var currentEntity = this.getCurrentAvatarEntity(); - var existingAvatars = new Int2ObjectOpenHashMap(); - var prevSelectedAvatarIndex = -1; - - for (EntityAvatar entity : this.getActiveTeam()) { - existingAvatars.put(entity.getAvatar().getAvatarId(), entity); - } - - // Clear active team entity list - this.getActiveTeam().clear(); - - // Add back entities into team - for (int i = 0; i < this.getCurrentTeamInfo().getAvatars().size(); i++) { - var avatarId = (int) this.getCurrentTeamInfo().getAvatars().get(i); - EntityAvatar entity; - if (existingAvatars.containsKey(avatarId)) { - entity = existingAvatars.get(avatarId); - existingAvatars.remove(avatarId); - if (entity == currentEntity) { - prevSelectedAvatarIndex = i; - } - } else { - entity = - new EntityAvatar( - this.getPlayer().getScene(), this.getPlayer().getAvatars().getAvatarById(avatarId)); - } - - this.getActiveTeam().add(entity); - } - - // Unload removed entities - for (var entity : existingAvatars.values()) { - this.getPlayer().getScene().removeEntity(entity); - entity.getAvatar().save(); - } - - // Set new selected character index - if (prevSelectedAvatarIndex == -1) { - // Previous selected avatar is not in the same spot, we will select the current one in the - // prev slot - prevSelectedAvatarIndex = - Math.min(this.currentCharacterIndex, this.getActiveTeam().size() - 1); - } - this.currentCharacterIndex = prevSelectedAvatarIndex; - - // Update properties. - // Notify player. - this.updateTeamProperties(); - - // Send response packet. - if (responsePacket != null) { - this.getPlayer().sendPacket(responsePacket); - } - - // Check if character changed - if (currentEntity != this.getCurrentAvatarEntity()) { - // Remove and Add - this.getPlayer().getScene().replaceEntity(currentEntity, this.getCurrentAvatarEntity()); - } - } - - public synchronized void setupAvatarTeam(int teamId, List list) { - // Sanity checks - if (list.size() == 0 - || list.size() > this.getMaxTeamSize() - || this.getPlayer().isInMultiplayer()) { - return; - } - - // Get team - TeamInfo teamInfo = this.getTeams().get(teamId); - if (teamInfo == null) { - return; - } - - // Set team data - LinkedHashSet newTeam = new LinkedHashSet<>(); - for (Long aLong : list) { - Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); - if (avatar == null || newTeam.contains(avatar)) { - // Should never happen - return; - } - newTeam.add(avatar); - } - - // Clear current team info and add avatars from our new team - teamInfo.getAvatars().clear(); - this.addAvatarsToTeam(teamInfo, newTeam); - } - - public void setupMpTeam(List list) { - // Sanity checks - if (list.size() == 0 - || list.size() > this.getMaxTeamSize() - || !this.getPlayer().isInMultiplayer()) { - return; - } - - TeamInfo teamInfo = this.getMpTeam(); - - // Set team data - LinkedHashSet newTeam = new LinkedHashSet<>(); - for (Long aLong : list) { - Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); - if (avatar == null || newTeam.contains(avatar)) { - // Should never happen - return; - } - newTeam.add(avatar); - } - - // Clear current team info and add avatars from our new team - teamInfo.getAvatars().clear(); - this.addAvatarsToTeam(teamInfo, newTeam); - } - - /** - * Setup avatars for a trial avatar team. - * - * @param save Should the original team be saved? - */ - public void setupTrialAvatars(boolean save) { - this.setPreviousIndex(this.getCurrentCharacterIndex()); - - if (save) { - var originalTeam = getCurrentTeamInfo(); - this.getTrialAvatarTeam().copyFrom(originalTeam); - } else this.getActiveTeam().clear(); - - this.usingTrialTeam = true; - } - - /** Displays the trial avatars. Picks the last avatar in the team. */ - public void trialAvatarTeamPostUpdate() { - this.trialAvatarTeamPostUpdate(this.getActiveTeam().size() - 1); - } - - /** - * Displays the trial avatars. - * - * @param newCharacterIndex The avatar to equip. - */ - public void trialAvatarTeamPostUpdate(int newCharacterIndex) { - this.setCurrentCharacterIndex(Math.min(newCharacterIndex, this.getActiveTeam().size() - 1)); - - this.updateTeamProperties(); - this.getPlayer().getScene().addEntity(this.getCurrentAvatarEntity()); - } - - /** - * Adds an avatar to the trial team. - * - * @param trialAvatar The avatar to add. - */ - public void addAvatarToTrialTeam(Avatar trialAvatar) { - // Remove the existing team's avatars. - this.getActiveTeam() - .forEach( - x -> - this.getPlayer() - .getScene() - .removeEntity(x, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE)); - // Remove the existing avatar from the teams if it exists. - this.getActiveTeam().removeIf(x -> x.getAvatar().getAvatarId() == trialAvatar.getAvatarId()); - this.getCurrentTeamInfo().getAvatars().removeIf(x -> x == trialAvatar.getAvatarId()); - // Add the avatar to the teams. - this.getActiveTeam().add(new EntityAvatar(this.getPlayer().getScene(), trialAvatar)); - this.getCurrentTeamInfo().addAvatar(trialAvatar); - this.getTrialAvatars().put(trialAvatar.getAvatarId(), trialAvatar); - } - - /** - * Get the GUID of a trial avatar. - * - * @param avatarId The avatar ID. - * @return The GUID of the avatar. - */ - public long getTrialAvatarGuid(int avatarId) { - return getTrialAvatars().values().stream() - .filter(avatar -> avatar.getTrialAvatarId() == avatarId) - .map(avatar -> avatar.getGuid()) - .findFirst() - .orElse(0L); - } - - /** Rollback changes from using a trial avatar team. */ - public void unsetTrialAvatarTeam() { - this.trialAvatarTeamPostUpdate(this.getPreviousIndex()); - this.setPreviousIndex(-1); - } - - /** Removes all avatars from the trial avatar team. */ - public void removeTrialAvatarTeam() { - this.removeTrialAvatarTeam( - this.getActiveTeam().stream().map(avatar -> avatar.getAvatar().getAvatarId()).toList()); - } - - /** - * Removes one avatar from the trial avatar team. - * - * @param avatarId The avatar ID to remove. - */ - public void removeTrialAvatarTeam(int avatarId) { - this.removeTrialAvatarTeam(List.of(avatarId)); - } - - /** - * Removes a collection of avatars from the trial avatar team. - * - * @param avatarIds The avatar IDs to remove. - */ - public void removeTrialAvatarTeam(List avatarIds) { - var player = this.getPlayer(); - - // Disable the trial team. - this.usingTrialTeam = false; - this.trialAvatarTeam = new TeamInfo(); - - // Remove the avatars from the team. - avatarIds.forEach( - avatarId -> { - this.getActiveTeam() - .forEach( - x -> - player - .getScene() - .removeEntity(x, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE)); - this.getActiveTeam().removeIf(x -> x.getAvatar().getTrialAvatarId() == avatarId); - this.getTrialAvatars().values().removeIf(x -> x.getTrialAvatarId() == avatarId); - }); - - // Re-add the avatars to the team. - var index = 0; - for (var avatar : this.getCurrentTeamInfo().getAvatars()) { - if (this.getActiveTeam().stream() - .map(entity -> entity.getAvatar().getAvatarId()) - .toList() - .contains(avatar)) return; - - this.getActiveTeam() - .add( - index++, - new EntityAvatar(player.getScene(), player.getAvatars().getAvatarById(avatar))); - } - - this.unsetTrialAvatarTeam(); - } - - public void setupTemporaryTeam(List> guidList) { - this.temporaryTeam = - guidList.stream() - .map( - list -> { - // Sanity checks - if (list.size() == 0 || list.size() > this.getMaxTeamSize()) { - return null; - } - - // Set team data - LinkedHashSet newTeam = new LinkedHashSet<>(); - for (Long aLong : list) { - Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); - if (avatar == null || newTeam.contains(avatar)) { - // Should never happen - return null; - } - newTeam.add(avatar); - } - - // convert to avatar ids - return newTeam.stream().map(Avatar::getAvatarId).toList(); - }) - .filter(Objects::nonNull) - .map(TeamInfo::new) - .toList(); - } - - public void useTemporaryTeam(int index) { - this.useTemporarilyTeamIndex = index; - this.updateTeamEntities(null); - } - - public void cleanTemporaryTeam() { - // check if using temporary team - if (useTemporarilyTeamIndex < 0) { - return; - } - - this.useTemporarilyTeamIndex = -1; - this.temporaryTeam = null; - this.updateTeamEntities(null); - } - - public synchronized void setCurrentTeam(int teamId) { - // - if (this.getPlayer().isInMultiplayer()) { - return; - } - - // Get team - TeamInfo teamInfo = this.getTeams().get(teamId); - if (teamInfo == null || teamInfo.getAvatars().size() == 0) { - return; - } - - // Set - this.setCurrentTeamId(teamId); - this.updateTeamEntities(new PacketChooseCurAvatarTeamRsp(teamId)); - } - - public synchronized void setTeamName(int teamId, String teamName) { - // Get team - TeamInfo teamInfo = this.getTeams().get(teamId); - if (teamInfo == null) { - return; - } - - teamInfo.setName(teamName); - - // Packet - this.getPlayer().sendPacket(new PacketChangeTeamNameRsp(teamId, teamName)); - } - - public synchronized void changeAvatar(long guid) { - EntityAvatar oldEntity = this.getCurrentAvatarEntity(); - - if (guid == oldEntity.getAvatar().getGuid()) { - return; - } - - EntityAvatar newEntity = null; - int index = -1; - for (int i = 0; i < this.getActiveTeam().size(); i++) { - if (guid == this.getActiveTeam().get(i).getAvatar().getGuid()) { - index = i; - newEntity = this.getActiveTeam().get(i); - } - } - - if (index < 0 || newEntity == oldEntity) { - return; - } - - // Set index - this.setCurrentCharacterIndex(index); - - // Old entity motion state - oldEntity.setMotionState(MotionState.MOTION_STATE_STANDBY); - - // Remove and Add - this.getPlayer().getScene().replaceEntity(oldEntity, newEntity); - this.getPlayer().sendPacket(new PacketChangeAvatarRsp(guid)); - } - - public void onAvatarDie(long dieGuid) { - EntityAvatar deadAvatar = this.getCurrentAvatarEntity(); - - if (deadAvatar.isAlive() || deadAvatar.getId() != dieGuid) { - return; - } - - PlayerDieType dieType = deadAvatar.getKilledType(); - int killedBy = deadAvatar.getKilledBy(); - - if (dieType == PlayerDieType.PLAYER_DIE_TYPE_DRAWN) { - // Died in water. Do not replace - // The official server has skipped this notify and will just respawn the team immediately - // after the animation. - // TODO: Perhaps find a way to get vanilla experience? - this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); - } else { - // Replacement avatar - EntityAvatar replacement = null; - int replaceIndex = -1; - - for (int i = 0; i < this.getActiveTeam().size(); i++) { - EntityAvatar entity = this.getActiveTeam().get(i); - if (entity.isAlive()) { - replaceIndex = i; - replacement = entity; - break; - } - } - - if (replacement == null) { - // No more living team members... - this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); - // Invoke player team death event. - PlayerTeamDeathEvent event = - new PlayerTeamDeathEvent( - this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex())); - event.call(); - } else { - // Set index and spawn replacement member - this.setCurrentCharacterIndex(replaceIndex); - this.getPlayer().getScene().addEntity(replacement); - } - } - - // Response packet - this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0)); - } - - public boolean reviveAvatar(Avatar avatar) { - for (EntityAvatar entity : this.getActiveTeam()) { - if (entity.getAvatar() == avatar) { - if (entity.isAlive()) { - return false; - } - - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f); - // Satiation is reset when reviving an avatar - player.getSatiationManager().removeSatiationDirectly(entity.getAvatar(), 15000); - this.getPlayer() - .sendPacket( - new PacketAvatarFightPropUpdateNotify( - entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); - this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); - return true; - } - } - - return false; - } - - public boolean healAvatar(Avatar avatar, int healRate, int healAmount) { - for (EntityAvatar entity : this.getActiveTeam()) { - if (entity.getAvatar() == avatar) { - if (!entity.isAlive()) { - return false; - } - - entity.setFightProperty( - FightProperty.FIGHT_PROP_CUR_HP, - (float) - Math.min( - (entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) - + entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) - * (float) healRate - / 100.0 - + (float) healAmount / 100.0), - entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP))); - this.getPlayer() - .sendPacket( - new PacketAvatarFightPropUpdateNotify( - entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); - this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); - return true; - } - } - return false; - } - - public void respawnTeam() { - // Make sure all team members are dead - // Drowning needs revive when there may be other team members still alive. - // for (EntityAvatar entity : getActiveTeam()) { - // if (entity.isAlive()) { - // return; - // } - // } - player - .getStaminaManager() - .stopSustainedStaminaHandler(); // prevent drowning immediately after respawn - - // Revive all team members - for (EntityAvatar entity : this.getActiveTeam()) { - entity.setFightProperty( - FightProperty.FIGHT_PROP_CUR_HP, - entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * .4f); - player.getSatiationManager().removeSatiationDirectly(entity.getAvatar(), 15000); - this.getPlayer() - .sendPacket( - new PacketAvatarFightPropUpdateNotify( - entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); - this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); - } - - // Teleport player and set player position - try { - this.getPlayer() - .sendPacket( - new PacketPlayerEnterSceneNotify( - this.getPlayer(), - EnterType.ENTER_TYPE_SELF, - EnterReason.Revival, - player.getSceneId(), - getRespawnPosition())); - player.getPosition().set(getRespawnPosition()); - } catch (Exception e) { - this.getPlayer() - .sendPacket( - new PacketPlayerEnterSceneNotify( - this.getPlayer(), - EnterType.ENTER_TYPE_SELF, - EnterReason.Revival, - 3, - GameConstants.START_POSITION)); - player - .getPosition() - .set(GameConstants.START_POSITION); // If something goes wrong, the resurrection is here - } - - // Packets - this.getPlayer().sendPacket(new BasePacket(PacketOpcodes.WorldPlayerReviveRsp)); - } - - public Position getRespawnPosition() { - var deathPos = this.getPlayer().getPosition(); - int sceneId = this.getPlayer().getSceneId(); - - // Get the closest trans point to where the player died. - var respawnPoint = - this.getPlayer().getUnlockedScenePoints(sceneId).stream() - .map(pointId -> GameData.getScenePointEntryById(sceneId, pointId)) - .filter(point -> point.getPointData().getType().equals("SceneTransPoint")) - .min( - (Comparator.comparingDouble( - pos -> Utils.getDist(pos.getPointData().getTranPos(), deathPos)))); - - return respawnPoint.get().getPointData().getTranPos(); - } - - public void saveAvatars() { - // Save all avatars from active team - for (EntityAvatar entity : this.getActiveTeam()) { - entity.getAvatar().save(); - } - } - - public void onPlayerLogin() { // Hack for now to fix resonances on login - this.updateTeamResonances(); - } - - public synchronized void addNewCustomTeam() { - // Sanity check - max number of teams. - if (this.teams.size() == GameConstants.MAX_TEAMS) { - player.sendPacket(new PacketAddBackupAvatarTeamRsp(Retcode.RET_FAIL)); - return; - } - - // The id of the new custom team is the lowest id in [5,MAX_TEAMS] that is not yet taken. - int id = -1; - for (int i = 5; i <= GameConstants.MAX_TEAMS; i++) { - if (!this.teams.containsKey(i)) { - id = i; - break; - } - } - - // Create the new team. - this.teams.put(id, new TeamInfo()); - - // Send packets. - player.sendPacket(new PacketAvatarTeamAllDataNotify(player)); - player.sendPacket(new PacketAddBackupAvatarTeamRsp()); - } - - public synchronized void removeCustomTeam(int id) { - // Check if the target id exists. - if (!this.teams.containsKey(id)) { - player.sendPacket(new PacketDelBackupAvatarTeamRsp(Retcode.RET_FAIL, id)); - } - - // Remove team. - this.teams.remove(id); - - // Send packets. - player.sendPacket(new PacketAvatarTeamAllDataNotify(player)); - player.sendPacket(new PacketDelBackupAvatarTeamRsp(id)); - } - - /** - * Applies abilities for the currently selected team. These abilities are sourced from the scene. - * - * @param scene The scene with the abilities to apply. - */ - public void applyAbilities(Scene scene) { - try { - var levelEntityConfig = scene.getSceneData().getLevelEntityConfig(); - var config = GameData.getConfigLevelEntityDataMap().get(levelEntityConfig); - if (config == null) return; - - var avatars = this.getPlayer().getAvatars(); - var avatarIds = scene.getSceneData().getSpecifiedAvatarList(); - var specifiedAvatarList = this.getActiveTeam(); - - if (avatarIds != null && avatarIds.size() > 0) { - // certain scene could limit specific avatars' entry - specifiedAvatarList.clear(); - for (int id : avatarIds) { - var avatar = avatars.getAvatarById(id); - if (avatar == null) continue; - - specifiedAvatarList.add(new EntityAvatar(scene, avatar)); - } - } - - for (var entityAvatar : specifiedAvatarList) { - var avatarData = entityAvatar.getAvatar().getAvatarData(); - if (avatarData == null) { - continue; - } - - avatarData.buildEmbryo(); // Create avatar abilities. - if (config.getAvatarAbilities() == null) { - continue; // continue and not break because has to rebuild ability for the next avatar if - // any - } - - for (ConfigAbilityData abilities : config.getAvatarAbilities()) { - avatarData.getAbilities().add(Utils.abilityHash(abilities.getAbilityName())); - } - } - } catch (Exception e) { - Grasscutter.getLogger() - .error( - "Error applying level entity config for scene {}", scene.getSceneData().getId(), e); - } - } - - public List getTrialAvatarParam(int trialAvatarId) { - if (GameData.getTrialAvatarCustomData() - .isEmpty()) { // use default data if custom data not available - if (GameData.getTrialAvatarDataMap().get(trialAvatarId) == null) return List.of(); - - return GameData.getTrialAvatarDataMap().get(trialAvatarId).getTrialAvatarParamList(); - } - // use custom data - if (GameData.getTrialAvatarCustomData().get(trialAvatarId) == null) return List.of(); - - val trialCustomParams = - GameData.getTrialAvatarCustomData().get(trialAvatarId).getTrialAvatarParamList(); - return trialCustomParams.isEmpty() - ? List.of() - : Stream.of(trialCustomParams.get(0).split(";")).map(Integer::parseInt).toList(); - } - - /** - * Adds a trial avatar to the player's team. - * - * @param avatarId The ID of the avatar. - * @param questMainId The quest ID associated with the quest. - * @param reason The reason for granting the avatar. - * @return True if the avatar was added, false otherwise. - */ - public boolean addTrialAvatar(int avatarId, int questMainId, GrantReason reason) { - List trialAvatarBasicParam = getTrialAvatarParam(avatarId); - if (trialAvatarBasicParam.isEmpty()) return false; - - var avatar = new Avatar(trialAvatarBasicParam.get(0)); - if (avatar.getAvatarData() == null || !this.getPlayer().hasSentLoginPackets()) return false; - - avatar.setOwner(this.getPlayer()); - // Add trial weapons and relics. - avatar.setTrialAvatarInfo(trialAvatarBasicParam.get(1), avatarId, reason, questMainId); - avatar.equipTrialItems(); - // Re-calculate stats - avatar.recalcStats(); - - // Packet, mimic official server behaviour, add to player's bag but not saving to database. - this.getPlayer().sendPacket(new PacketAvatarAddNotify(avatar, false)); - // Add to avatar to the temporary trial team. - this.addAvatarToTrialTeam(avatar); - return true; - } - - /** - * Adds a trial avatar to the player's team. - * - * @param avatarId The ID of the avatar. - * @param questMainId The quest ID associated with the quest. - */ - public void addTrialAvatar(int avatarId, int questMainId) { - this.addTrialAvatars(List.of(avatarId), questMainId, true); - - // Packet, mimic official server behaviour, necessary to stop player from modifying team. - this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(this.getPlayer())); - } - - /** - * Adds a collection of trial avatars to the player's team. - * - * @param avatarIds List of trial avatar IDs. - */ - public void addTrialAvatars(List avatarIds) { - this.addTrialAvatars(avatarIds, 0, false); - } - - /** - * Adds a collection of trial avatars to the player's team. - * - * @param avatarIds List of trial avatar IDs. - * @param save Whether to retain the currently equipped avatars. - */ - public void addTrialAvatars(List avatarIds, boolean save) { - this.addTrialAvatars(avatarIds, 0, save); - } - - /** - * Adds a list of trial avatars to the player's team. - * - * @param avatarIds List of trial avatar IDs. - * @param questId The ID of the quest this trial team is associated with. - * @param save Whether to retain the currently equipped avatars. - */ - public void addTrialAvatars(List avatarIds, int questId, boolean save) { - this.setupTrialAvatars(save); // Perform initial setup. - - // Add the avatars to the team. - avatarIds.forEach( - avatarId -> { - var result = - this.addTrialAvatar( - avatarId, - questId, - questId == 0 - ? GrantReason.GRANT_REASON_BY_QUEST - : GrantReason.GRANT_REASON_BY_TRIAL_AVATAR_ACTIVITY); - - if (!result) throw new RuntimeException("Unable to add trial avatar to team."); - }); - - // Update the team. - this.trialAvatarTeamPostUpdate(questId == 0 ? getActiveTeam().size() - 1 : 0); - } - - /** Removes all trial avatars from the player's team. */ - public void removeTrialAvatar() { - this.removeTrialAvatar( - this.getActiveTeam().stream() - .map(EntityAvatar::getAvatar) - .map(Avatar::getAvatarId) - .toList()); - } - - /** - * Removes a trial avatar from the player's team. Additionally, unlocks the ability to change the - * team configuration. - * - * @param avatarId The ID of the avatar. - */ - public void removeTrialAvatar(int avatarId) { - this.removeTrialAvatar(List.of(avatarId)); - } - - /** - * Removes a collection of trial avatars from the player's team. - * - * @param avatarIds List of trial avatar IDs. - */ - public void removeTrialAvatar(List avatarIds) { - if (!this.isUsingTrialTeam()) throw new RuntimeException("Player is not using a trial team."); - - this.getPlayer() - .sendPacket( - new PacketAvatarDelNotify(avatarIds.stream().map(this::getTrialAvatarGuid).toList())); - this.removeTrialAvatarTeam(avatarIds); - - // Update the team. - if (avatarIds.size() == 1) this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify()); - } -} +package emu.grasscutter.game.player; + +import static emu.grasscutter.config.Configuration.GAME_OPTIONS; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Transient; +import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.config.fields.ConfigAbilityData; +import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.entity.EntityAvatar; +import emu.grasscutter.game.entity.EntityBaseGadget; +import emu.grasscutter.game.props.ElementType; +import emu.grasscutter.game.props.EnterReason; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.game.world.World; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType; +import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; +import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; +import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGrantRecord.GrantReason; +import emu.grasscutter.net.proto.VisionTypeOuterClass; +import emu.grasscutter.server.event.player.PlayerTeamDeathEvent; +import emu.grasscutter.server.packet.send.*; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import java.util.*; +import java.util.stream.Stream; +import lombok.Getter; +import lombok.Setter; +import lombok.val; + +@Entity +public final class TeamManager extends BasePlayerDataManager { + @Transient private final List avatars; + @Transient @Getter private final Set gadgets; + @Transient @Getter private final IntSet teamResonances; + @Transient @Getter private final IntSet teamResonancesConfig; + // This needs to be a LinkedHashMap to guarantee insertion order. + @Getter private LinkedHashMap teams; + private int currentTeamIndex; + @Getter @Setter private int currentCharacterIndex; + @Transient @Getter @Setter private TeamInfo mpTeam; + @Transient @Getter @Setter private int entityId; + + @Transient private int useTemporarilyTeamIndex = -1; + @Transient private List temporaryTeam; // Temporary Team for tower + @Transient @Getter @Setter private boolean usingTrialTeam; + @Transient @Getter @Setter private TeamInfo trialAvatarTeam; + // hold trial avatars for later use in rebuilding active team + @Transient @Getter @Setter private Map trialAvatars; + + @Transient @Getter @Setter + private int previousIndex = -1; // index of character selection in team before adding trial avatar + + public TeamManager() { + this.mpTeam = new TeamInfo(); + this.avatars = new ArrayList<>(); + this.gadgets = new HashSet<>(); + this.teamResonances = new IntOpenHashSet(); + this.teamResonancesConfig = new IntOpenHashSet(); + } + + public TeamManager(Player player) { + this(); + this.setPlayer(player); + + this.teams = new LinkedHashMap<>(); + this.currentTeamIndex = 1; + for (int i = 1; i <= GameConstants.DEFAULT_TEAMS; i++) { + this.teams.put(i, new TeamInfo()); + } + } + + public World getWorld() { + return this.getPlayer().getWorld(); + } + + /** + * Search through all teams and if the team matches, return that index. Otherwise, return -1. No + * match could mean that the team does not currently belong to the player. + */ + public int getTeamId(TeamInfo team) { + for (int i = 1; i <= this.teams.size(); i++) { + if (this.teams.get(i).equals(team)) { + return i; + } + } + return -1; + } + + public int getCurrentTeamId() { + // Starts from 1 + return currentTeamIndex; + } + + private void setCurrentTeamId(int currentTeamIndex) { + this.currentTeamIndex = currentTeamIndex; + } + + public long getCurrentCharacterGuid() { + return this.getCurrentAvatarEntity().getAvatar().getGuid(); + } + + public TeamInfo getCurrentTeamInfo() { + if (useTemporarilyTeamIndex >= 0 && useTemporarilyTeamIndex < temporaryTeam.size()) { + return temporaryTeam.get(useTemporarilyTeamIndex); + } + if (this.getPlayer().isInMultiplayer()) { + return this.getMpTeam(); + } + return this.getTeams().get(this.currentTeamIndex); + } + + public TeamInfo getCurrentSinglePlayerTeamInfo() { + return this.getTeams().get(this.currentTeamIndex); + } + + public List getActiveTeam() { + return avatars; + } + + public EntityAvatar getCurrentAvatarEntity() { + return this.getActiveTeam().get(currentCharacterIndex); + } + + public boolean isSpawned() { + return this.getPlayer().getScene() != null + && this.getPlayer() + .getScene() + .getEntities() + .containsKey(this.getCurrentAvatarEntity().getId()); + } + + public int getMaxTeamSize() { + if (this.getPlayer().isInMultiplayer()) { + int max = GAME_OPTIONS.avatarLimits.multiplayerTeam; + if (this.getPlayer().getWorld().getHost() == this.getPlayer()) { + return Math.max(1, (int) Math.ceil(max / (double) this.getWorld().getPlayerCount())); + } + return Math.max(1, (int) Math.floor(max / (double) this.getWorld().getPlayerCount())); + } + + return GAME_OPTIONS.avatarLimits.singlePlayerTeam; + } + + // Methods + + /** Returns true if there is space to add the number of avatars to the team. */ + public boolean canAddAvatarsToTeam(TeamInfo team, int avatars) { + return team.size() + avatars <= this.getMaxTeamSize(); + } + + /** Returns true if there is space to add to the team. */ + public boolean canAddAvatarToTeam(TeamInfo team) { + return this.canAddAvatarsToTeam(team, 1); + } + + /** + * Returns true if there is space to add the number of avatars to the current team. If the current + * team is temporary, returns false. + */ + public boolean canAddAvatarsToCurrentTeam(int avatars) { + if (this.useTemporarilyTeamIndex != -1) { + return false; + } + return this.canAddAvatarsToTeam(this.getCurrentTeamInfo(), avatars); + } + + /** + * Returns true if there is space to add to the current team. If the current team is temporary, + * returns false. + */ + public boolean canAddAvatarToCurrentTeam() { + return this.canAddAvatarsToCurrentTeam(1); + } + + /** + * Try to add the collection of avatars to the team. Returns true if all were successfully added. + * If some can not be added, returns false and does not add any. + */ + public boolean addAvatarsToTeam(TeamInfo team, Collection avatars) { + if (!this.canAddAvatarsToTeam(team, avatars.size())) { + return false; + } + + // Convert avatars into a collection of avatar IDs, then add + team.getAvatars().addAll(avatars.stream().map(a -> a.getAvatarId()).toList()); + + // Update team + if (this.getPlayer().isInMultiplayer()) { + if (team.equals(this.getMpTeam())) { + // MP team Packet + this.updateTeamEntities(new PacketChangeMpTeamAvatarRsp(this.getPlayer(), team)); + } + } else { + // SP team update packet + this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(this.getPlayer())); + + int teamId = this.getTeamId(team); + if (teamId != -1) { + // This is one of the player's teams + // Update entites + if (teamId == this.getCurrentTeamId()) { + this.updateTeamEntities(new PacketSetUpAvatarTeamRsp(this.getPlayer(), teamId, team)); + } else { + this.getPlayer().sendPacket(new PacketSetUpAvatarTeamRsp(this.getPlayer(), teamId, team)); + } + } + } + + return true; + } + + /** Try to add an avatar to a team. Returns true if successful. */ + public boolean addAvatarToTeam(TeamInfo team, Avatar avatar) { + return this.addAvatarsToTeam(team, Collections.singleton(avatar)); + } + + /** + * Try to add the collection of avatars to the current team. Will not modify a temporary team. + * Returns true if all were successfully added. If some can not be added, returns false and does + * not add any. + */ + public boolean addAvatarsToCurrentTeam(Collection avatars) { + if (this.useTemporarilyTeamIndex != -1) { + return false; + } + return this.addAvatarsToTeam(this.getCurrentTeamInfo(), avatars); + } + + /** + * Try to add an avatar to the current team. Will not modify a temporary team. Returns true if + * successful. + */ + public boolean addAvatarToCurrentTeam(Avatar avatar) { + return this.addAvatarsToCurrentTeam(Collections.singleton(avatar)); + } + + private void updateTeamResonances() { + this.getTeamResonances().clear(); + this.getTeamResonancesConfig().clear(); + // Official resonances require a full party + if (this.avatars.size() < 4) return; + + // TODO: make this actually read from TeamResonanceExcelConfigData.json for the real resonances + // and conditions + // Currently we just hardcode these conditions, but this won't work for modded resources or + // future changes + var elementCounts = new Object2IntOpenHashMap(); + this.getActiveTeam().stream() + .map(EntityAvatar::getAvatar) + .filter(Objects::nonNull) + .map(Avatar::getSkillDepot) + .filter(Objects::nonNull) + .map(AvatarSkillDepotData::getElementType) + .filter(Objects::nonNull) + .forEach(elementType -> elementCounts.addTo(elementType, 1)); + + // Dual element resonances + elementCounts.object2IntEntrySet().stream() + .filter(e -> e.getIntValue() >= 2) + .map(e -> e.getKey()) + .filter(elementType -> elementType.getTeamResonanceId() != 0) + .forEach( + elementType -> { + this.teamResonances.add(elementType.getTeamResonanceId()); + this.teamResonancesConfig.add(elementType.getConfigHash()); + }); + + // Four element resonance + if (elementCounts.size() >= 4) { + this.teamResonances.add(ElementType.Default.getTeamResonanceId()); + this.teamResonancesConfig.add(ElementType.Default.getConfigHash()); + } + } + + /** Updates all properties of the active team. */ + public void updateTeamProperties() { + this.updateTeamResonances(); // Update team resonances. + this.getPlayer() + .sendPacket(new PacketSceneTeamUpdateNotify(this.getPlayer())); // Notify the player. + + // Skill charges packet - Yes, this is official server behavior as of 2.6.0 + this.getActiveTeam().stream() + .map(EntityAvatar::getAvatar) + .forEach(Avatar::sendSkillExtraChargeMap); + } + + public void updateTeamEntities(BasePacket responsePacket) { + // Sanity check - Should never happen + if (this.getCurrentTeamInfo().getAvatars().size() <= 0) { + return; + } + + // If current team has changed + var currentEntity = this.getCurrentAvatarEntity(); + var existingAvatars = new Int2ObjectOpenHashMap(); + var prevSelectedAvatarIndex = -1; + + for (EntityAvatar entity : this.getActiveTeam()) { + existingAvatars.put(entity.getAvatar().getAvatarId(), entity); + } + + // Clear active team entity list + this.getActiveTeam().clear(); + + // Add back entities into team + for (int i = 0; i < this.getCurrentTeamInfo().getAvatars().size(); i++) { + var avatarId = (int) this.getCurrentTeamInfo().getAvatars().get(i); + EntityAvatar entity; + if (existingAvatars.containsKey(avatarId)) { + entity = existingAvatars.get(avatarId); + existingAvatars.remove(avatarId); + if (entity == currentEntity) { + prevSelectedAvatarIndex = i; + } + } else { + entity = + new EntityAvatar( + this.getPlayer().getScene(), this.getPlayer().getAvatars().getAvatarById(avatarId)); + } + + this.getActiveTeam().add(entity); + } + + // Unload removed entities + for (var entity : existingAvatars.values()) { + this.getPlayer().getScene().removeEntity(entity); + entity.getAvatar().save(); + } + + // Set new selected character index + if (prevSelectedAvatarIndex == -1) { + // Previous selected avatar is not in the same spot, we will select the current one in the + // prev slot + prevSelectedAvatarIndex = + Math.min(this.currentCharacterIndex, this.getActiveTeam().size() - 1); + } + this.currentCharacterIndex = prevSelectedAvatarIndex; + + // Update properties. + // Notify player. + this.updateTeamProperties(); + + // Send response packet. + if (responsePacket != null) { + this.getPlayer().sendPacket(responsePacket); + } + + // Check if character changed + if (currentEntity != this.getCurrentAvatarEntity()) { + // Remove and Add + this.getPlayer().getScene().replaceEntity(currentEntity, this.getCurrentAvatarEntity()); + } + } + + public synchronized void setupAvatarTeam(int teamId, List list) { + // Sanity checks + if (list.size() == 0 + || list.size() > this.getMaxTeamSize() + || this.getPlayer().isInMultiplayer()) { + return; + } + + // Get team + TeamInfo teamInfo = this.getTeams().get(teamId); + if (teamInfo == null) { + return; + } + + // Set team data + LinkedHashSet newTeam = new LinkedHashSet<>(); + for (Long aLong : list) { + Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); + if (avatar == null || newTeam.contains(avatar)) { + // Should never happen + return; + } + newTeam.add(avatar); + } + + // Clear current team info and add avatars from our new team + teamInfo.getAvatars().clear(); + this.addAvatarsToTeam(teamInfo, newTeam); + } + + public void setupMpTeam(List list) { + // Sanity checks + if (list.size() == 0 + || list.size() > this.getMaxTeamSize() + || !this.getPlayer().isInMultiplayer()) { + return; + } + + TeamInfo teamInfo = this.getMpTeam(); + + // Set team data + LinkedHashSet newTeam = new LinkedHashSet<>(); + for (Long aLong : list) { + Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); + if (avatar == null || newTeam.contains(avatar)) { + // Should never happen + return; + } + newTeam.add(avatar); + } + + // Clear current team info and add avatars from our new team + teamInfo.getAvatars().clear(); + this.addAvatarsToTeam(teamInfo, newTeam); + } + + /** + * Setup avatars for a trial avatar team. + * + * @param save Should the original team be saved? + */ + public void setupTrialAvatars(boolean save) { + this.setPreviousIndex(this.getCurrentCharacterIndex()); + + if (save) { + var originalTeam = getCurrentTeamInfo(); + this.getTrialAvatarTeam().copyFrom(originalTeam); + } else this.getActiveTeam().clear(); + + this.usingTrialTeam = true; + } + + /** Displays the trial avatars. Picks the last avatar in the team. */ + public void trialAvatarTeamPostUpdate() { + this.trialAvatarTeamPostUpdate(this.getActiveTeam().size() - 1); + } + + /** + * Displays the trial avatars. + * + * @param newCharacterIndex The avatar to equip. + */ + public void trialAvatarTeamPostUpdate(int newCharacterIndex) { + this.setCurrentCharacterIndex(Math.min(newCharacterIndex, this.getActiveTeam().size() - 1)); + + this.updateTeamProperties(); + this.getPlayer().getScene().addEntity(this.getCurrentAvatarEntity()); + } + + /** + * Adds an avatar to the trial team. + * + * @param trialAvatar The avatar to add. + */ + public void addAvatarToTrialTeam(Avatar trialAvatar) { + // Remove the existing team's avatars. + this.getActiveTeam() + .forEach( + x -> + this.getPlayer() + .getScene() + .removeEntity(x, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE)); + // Remove the existing avatar from the teams if it exists. + this.getActiveTeam().removeIf(x -> x.getAvatar().getAvatarId() == trialAvatar.getAvatarId()); + this.getCurrentTeamInfo().getAvatars().removeIf(x -> x == trialAvatar.getAvatarId()); + // Add the avatar to the teams. + this.getActiveTeam().add(new EntityAvatar(this.getPlayer().getScene(), trialAvatar)); + this.getCurrentTeamInfo().addAvatar(trialAvatar); + this.getTrialAvatars().put(trialAvatar.getAvatarId(), trialAvatar); + } + + /** + * Get the GUID of a trial avatar. + * + * @param avatarId The avatar ID. + * @return The GUID of the avatar. + */ + public long getTrialAvatarGuid(int avatarId) { + return getTrialAvatars().values().stream() + .filter(avatar -> avatar.getTrialAvatarId() == avatarId) + .map(avatar -> avatar.getGuid()) + .findFirst() + .orElse(0L); + } + + /** Rollback changes from using a trial avatar team. */ + public void unsetTrialAvatarTeam() { + this.trialAvatarTeamPostUpdate(this.getPreviousIndex()); + this.setPreviousIndex(-1); + } + + /** Removes all avatars from the trial avatar team. */ + public void removeTrialAvatarTeam() { + this.removeTrialAvatarTeam( + this.getActiveTeam().stream().map(avatar -> avatar.getAvatar().getAvatarId()).toList()); + } + + /** + * Removes one avatar from the trial avatar team. + * + * @param avatarId The avatar ID to remove. + */ + public void removeTrialAvatarTeam(int avatarId) { + this.removeTrialAvatarTeam(List.of(avatarId)); + } + + /** + * Removes a collection of avatars from the trial avatar team. + * + * @param avatarIds The avatar IDs to remove. + */ + public void removeTrialAvatarTeam(List avatarIds) { + var player = this.getPlayer(); + + // Disable the trial team. + this.usingTrialTeam = false; + this.trialAvatarTeam = new TeamInfo(); + + // Remove the avatars from the team. + avatarIds.forEach( + avatarId -> { + this.getActiveTeam() + .forEach( + x -> + player + .getScene() + .removeEntity(x, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE)); + this.getActiveTeam().removeIf(x -> x.getAvatar().getTrialAvatarId() == avatarId); + this.getTrialAvatars().values().removeIf(x -> x.getTrialAvatarId() == avatarId); + }); + + // Re-add the avatars to the team. + var index = 0; + for (var avatar : this.getCurrentTeamInfo().getAvatars()) { + if (this.getActiveTeam().stream() + .map(entity -> entity.getAvatar().getAvatarId()) + .toList() + .contains(avatar)) return; + + this.getActiveTeam() + .add( + index++, + new EntityAvatar(player.getScene(), player.getAvatars().getAvatarById(avatar))); + } + + this.unsetTrialAvatarTeam(); + } + + public void setupTemporaryTeam(List> guidList) { + this.temporaryTeam = + guidList.stream() + .map( + list -> { + // Sanity checks + if (list.size() == 0 || list.size() > this.getMaxTeamSize()) { + return null; + } + + // Set team data + LinkedHashSet newTeam = new LinkedHashSet<>(); + for (Long aLong : list) { + Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); + if (avatar == null || newTeam.contains(avatar)) { + // Should never happen + return null; + } + newTeam.add(avatar); + } + + // convert to avatar ids + return newTeam.stream().map(Avatar::getAvatarId).toList(); + }) + .filter(Objects::nonNull) + .map(TeamInfo::new) + .toList(); + } + + public void useTemporaryTeam(int index) { + this.useTemporarilyTeamIndex = index; + this.updateTeamEntities(null); + } + + public void cleanTemporaryTeam() { + // check if using temporary team + if (useTemporarilyTeamIndex < 0) { + return; + } + + this.useTemporarilyTeamIndex = -1; + this.temporaryTeam = null; + this.updateTeamEntities(null); + } + + public synchronized void setCurrentTeam(int teamId) { + // + if (this.getPlayer().isInMultiplayer()) { + return; + } + + // Get team + TeamInfo teamInfo = this.getTeams().get(teamId); + if (teamInfo == null || teamInfo.getAvatars().size() == 0) { + return; + } + + // Set + this.setCurrentTeamId(teamId); + this.updateTeamEntities(new PacketChooseCurAvatarTeamRsp(teamId)); + } + + public synchronized void setTeamName(int teamId, String teamName) { + // Get team + TeamInfo teamInfo = this.getTeams().get(teamId); + if (teamInfo == null) { + return; + } + + teamInfo.setName(teamName); + + // Packet + this.getPlayer().sendPacket(new PacketChangeTeamNameRsp(teamId, teamName)); + } + + public synchronized void changeAvatar(long guid) { + EntityAvatar oldEntity = this.getCurrentAvatarEntity(); + + if (guid == oldEntity.getAvatar().getGuid()) { + return; + } + + EntityAvatar newEntity = null; + int index = -1; + for (int i = 0; i < this.getActiveTeam().size(); i++) { + if (guid == this.getActiveTeam().get(i).getAvatar().getGuid()) { + index = i; + newEntity = this.getActiveTeam().get(i); + } + } + + if (index < 0 || newEntity == oldEntity) { + return; + } + + // Set index + this.setCurrentCharacterIndex(index); + + // Old entity motion state + oldEntity.setMotionState(MotionState.MOTION_STATE_STANDBY); + + // Remove and Add + this.getPlayer().getScene().replaceEntity(oldEntity, newEntity); + this.getPlayer().sendPacket(new PacketChangeAvatarRsp(guid)); + } + + /** + * Applies 10% of the avatar's max HP as damage. + * This occurs when the avatar is killed by the void. + */ + public void applyVoidDamage() { + this.getActiveTeam().forEach(entity -> { + entity.damage(entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * .1f); + player.sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + }); + } + + public void onAvatarDie(long dieGuid) { + EntityAvatar deadAvatar = this.getCurrentAvatarEntity(); + + if (deadAvatar.isAlive() || deadAvatar.getId() != dieGuid) { + return; + } + + PlayerDieType dieType = deadAvatar.getKilledType(); + int killedBy = deadAvatar.getKilledBy(); + + if (dieType == PlayerDieType.PLAYER_DIE_TYPE_DRAWN) { + // Died in water. Do not replace + // The official server has skipped this notify and will just respawn the team immediately + // after the animation. + // TODO: Perhaps find a way to get vanilla experience? + this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); + } else { + // Replacement avatar + EntityAvatar replacement = null; + int replaceIndex = -1; + + for (int i = 0; i < this.getActiveTeam().size(); i++) { + EntityAvatar entity = this.getActiveTeam().get(i); + if (entity.isAlive()) { + replaceIndex = i; + replacement = entity; + break; + } + } + + if (replacement == null) { + // No more living team members... + this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); + // Invoke player team death event. + PlayerTeamDeathEvent event = + new PlayerTeamDeathEvent( + this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex())); + event.call(); + } else { + // Set index and spawn replacement member + this.setCurrentCharacterIndex(replaceIndex); + this.getPlayer().getScene().addEntity(replacement); + } + } + + // Response packet + this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0)); + } + + public boolean reviveAvatar(Avatar avatar) { + for (EntityAvatar entity : this.getActiveTeam()) { + if (entity.getAvatar() == avatar) { + if (entity.isAlive()) { + return false; + } + + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f); + // Satiation is reset when reviving an avatar + player.getSatiationManager().removeSatiationDirectly(entity.getAvatar(), 15000); + this.getPlayer() + .sendPacket( + new PacketAvatarFightPropUpdateNotify( + entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); + this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + return true; + } + } + + return false; + } + + public boolean healAvatar(Avatar avatar, int healRate, int healAmount) { + for (EntityAvatar entity : this.getActiveTeam()) { + if (entity.getAvatar() == avatar) { + if (!entity.isAlive()) { + return false; + } + + entity.setFightProperty( + FightProperty.FIGHT_PROP_CUR_HP, + (float) + Math.min( + (entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) + + entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) + * (float) healRate + / 100.0 + + (float) healAmount / 100.0), + entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP))); + this.getPlayer() + .sendPacket( + new PacketAvatarFightPropUpdateNotify( + entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); + this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + return true; + } + } + return false; + } + + public void respawnTeam() { + // Make sure all team members are dead + // Drowning needs revive when there may be other team members still alive. + // for (EntityAvatar entity : getActiveTeam()) { + // if (entity.isAlive()) { + // return; + // } + // } + player + .getStaminaManager() + .stopSustainedStaminaHandler(); // prevent drowning immediately after respawn + + // Revive all team members + for (EntityAvatar entity : this.getActiveTeam()) { + entity.setFightProperty( + FightProperty.FIGHT_PROP_CUR_HP, + entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * .4f); + player.getSatiationManager().removeSatiationDirectly(entity.getAvatar(), 15000); + this.getPlayer() + .sendPacket( + new PacketAvatarFightPropUpdateNotify( + entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); + this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + } + + // Teleport player and set player position + try { + this.getPlayer() + .sendPacket( + new PacketPlayerEnterSceneNotify( + this.getPlayer(), + EnterType.ENTER_TYPE_SELF, + EnterReason.Revival, + player.getSceneId(), + getRespawnPosition())); + player.getPosition().set(getRespawnPosition()); + } catch (Exception e) { + this.getPlayer() + .sendPacket( + new PacketPlayerEnterSceneNotify( + this.getPlayer(), + EnterType.ENTER_TYPE_SELF, + EnterReason.Revival, + 3, + GameConstants.START_POSITION)); + player + .getPosition() + .set(GameConstants.START_POSITION); // If something goes wrong, the resurrection is here + } + + // Packets + this.getPlayer().sendPacket(new BasePacket(PacketOpcodes.WorldPlayerReviveRsp)); + } + + public Position getRespawnPosition() { + var deathPos = this.getPlayer().getPosition(); + int sceneId = this.getPlayer().getSceneId(); + + // Get the closest trans point to where the player died. + var respawnPoint = + this.getPlayer().getUnlockedScenePoints(sceneId).stream() + .map(pointId -> GameData.getScenePointEntryById(sceneId, pointId)) + .filter(point -> point.getPointData().getType().equals("SceneTransPoint")) + .min( + (Comparator.comparingDouble( + pos -> Utils.getDist(pos.getPointData().getTranPos(), deathPos)))); + + return respawnPoint.get().getPointData().getTranPos(); + } + + public void saveAvatars() { + // Save all avatars from active team + for (EntityAvatar entity : this.getActiveTeam()) { + entity.getAvatar().save(); + } + } + + public void onPlayerLogin() { // Hack for now to fix resonances on login + this.updateTeamResonances(); + } + + public synchronized void addNewCustomTeam() { + // Sanity check - max number of teams. + if (this.teams.size() == GameConstants.MAX_TEAMS) { + player.sendPacket(new PacketAddBackupAvatarTeamRsp(Retcode.RET_FAIL)); + return; + } + + // The id of the new custom team is the lowest id in [5,MAX_TEAMS] that is not yet taken. + int id = -1; + for (int i = 5; i <= GameConstants.MAX_TEAMS; i++) { + if (!this.teams.containsKey(i)) { + id = i; + break; + } + } + + // Create the new team. + this.teams.put(id, new TeamInfo()); + + // Send packets. + player.sendPacket(new PacketAvatarTeamAllDataNotify(player)); + player.sendPacket(new PacketAddBackupAvatarTeamRsp()); + } + + public synchronized void removeCustomTeam(int id) { + // Check if the target id exists. + if (!this.teams.containsKey(id)) { + player.sendPacket(new PacketDelBackupAvatarTeamRsp(Retcode.RET_FAIL, id)); + } + + // Remove team. + this.teams.remove(id); + + // Send packets. + player.sendPacket(new PacketAvatarTeamAllDataNotify(player)); + player.sendPacket(new PacketDelBackupAvatarTeamRsp(id)); + } + + /** + * Applies abilities for the currently selected team. These abilities are sourced from the scene. + * + * @param scene The scene with the abilities to apply. + */ + public void applyAbilities(Scene scene) { + try { + var levelEntityConfig = scene.getSceneData().getLevelEntityConfig(); + var config = GameData.getConfigLevelEntityDataMap().get(levelEntityConfig); + if (config == null) return; + + var avatars = this.getPlayer().getAvatars(); + var avatarIds = scene.getSceneData().getSpecifiedAvatarList(); + var specifiedAvatarList = this.getActiveTeam(); + + if (avatarIds != null && avatarIds.size() > 0) { + // certain scene could limit specific avatars' entry + specifiedAvatarList.clear(); + for (int id : avatarIds) { + var avatar = avatars.getAvatarById(id); + if (avatar == null) continue; + + specifiedAvatarList.add(new EntityAvatar(scene, avatar)); + } + } + + for (var entityAvatar : specifiedAvatarList) { + var avatarData = entityAvatar.getAvatar().getAvatarData(); + if (avatarData == null) { + continue; + } + + avatarData.buildEmbryo(); // Create avatar abilities. + if (config.getAvatarAbilities() == null) { + continue; // continue and not break because has to rebuild ability for the next avatar if + // any + } + + for (ConfigAbilityData abilities : config.getAvatarAbilities()) { + avatarData.getAbilities().add(Utils.abilityHash(abilities.getAbilityName())); + } + } + } catch (Exception e) { + Grasscutter.getLogger() + .error( + "Error applying level entity config for scene {}", scene.getSceneData().getId(), e); + } + } + + public List getTrialAvatarParam(int trialAvatarId) { + if (GameData.getTrialAvatarCustomData() + .isEmpty()) { // use default data if custom data not available + if (GameData.getTrialAvatarDataMap().get(trialAvatarId) == null) return List.of(); + + return GameData.getTrialAvatarDataMap().get(trialAvatarId).getTrialAvatarParamList(); + } + // use custom data + if (GameData.getTrialAvatarCustomData().get(trialAvatarId) == null) return List.of(); + + val trialCustomParams = + GameData.getTrialAvatarCustomData().get(trialAvatarId).getTrialAvatarParamList(); + return trialCustomParams.isEmpty() + ? List.of() + : Stream.of(trialCustomParams.get(0).split(";")).map(Integer::parseInt).toList(); + } + + /** + * Adds a trial avatar to the player's team. + * + * @param avatarId The ID of the avatar. + * @param questMainId The quest ID associated with the quest. + * @param reason The reason for granting the avatar. + * @return True if the avatar was added, false otherwise. + */ + public boolean addTrialAvatar(int avatarId, int questMainId, GrantReason reason) { + List trialAvatarBasicParam = getTrialAvatarParam(avatarId); + if (trialAvatarBasicParam.isEmpty()) return false; + + var avatar = new Avatar(trialAvatarBasicParam.get(0)); + if (avatar.getAvatarData() == null || !this.getPlayer().hasSentLoginPackets()) return false; + + avatar.setOwner(this.getPlayer()); + // Add trial weapons and relics. + avatar.setTrialAvatarInfo(trialAvatarBasicParam.get(1), avatarId, reason, questMainId); + avatar.equipTrialItems(); + // Re-calculate stats + avatar.recalcStats(); + + // Packet, mimic official server behaviour, add to player's bag but not saving to database. + this.getPlayer().sendPacket(new PacketAvatarAddNotify(avatar, false)); + // Add to avatar to the temporary trial team. + this.addAvatarToTrialTeam(avatar); + return true; + } + + /** + * Adds a trial avatar to the player's team. + * + * @param avatarId The ID of the avatar. + * @param questMainId The quest ID associated with the quest. + */ + public void addTrialAvatar(int avatarId, int questMainId) { + this.addTrialAvatars(List.of(avatarId), questMainId, true); + + // Packet, mimic official server behaviour, necessary to stop player from modifying team. + this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(this.getPlayer())); + } + + /** + * Adds a collection of trial avatars to the player's team. + * + * @param avatarIds List of trial avatar IDs. + */ + public void addTrialAvatars(List avatarIds) { + this.addTrialAvatars(avatarIds, 0, false); + } + + /** + * Adds a collection of trial avatars to the player's team. + * + * @param avatarIds List of trial avatar IDs. + * @param save Whether to retain the currently equipped avatars. + */ + public void addTrialAvatars(List avatarIds, boolean save) { + this.addTrialAvatars(avatarIds, 0, save); + } + + /** + * Adds a list of trial avatars to the player's team. + * + * @param avatarIds List of trial avatar IDs. + * @param questId The ID of the quest this trial team is associated with. + * @param save Whether to retain the currently equipped avatars. + */ + public void addTrialAvatars(List avatarIds, int questId, boolean save) { + this.setupTrialAvatars(save); // Perform initial setup. + + // Add the avatars to the team. + avatarIds.forEach( + avatarId -> { + var result = + this.addTrialAvatar( + avatarId, + questId, + questId == 0 + ? GrantReason.GRANT_REASON_BY_QUEST + : GrantReason.GRANT_REASON_BY_TRIAL_AVATAR_ACTIVITY); + + if (!result) throw new RuntimeException("Unable to add trial avatar to team."); + }); + + // Update the team. + this.trialAvatarTeamPostUpdate(questId == 0 ? getActiveTeam().size() - 1 : 0); + } + + /** Removes all trial avatars from the player's team. */ + public void removeTrialAvatar() { + this.removeTrialAvatar( + this.getActiveTeam().stream() + .map(EntityAvatar::getAvatar) + .map(Avatar::getAvatarId) + .toList()); + } + + /** + * Removes a trial avatar from the player's team. Additionally, unlocks the ability to change the + * team configuration. + * + * @param avatarId The ID of the avatar. + */ + public void removeTrialAvatar(int avatarId) { + this.removeTrialAvatar(List.of(avatarId)); + } + + /** + * Removes a collection of trial avatars from the player's team. + * + * @param avatarIds List of trial avatar IDs. + */ + public void removeTrialAvatar(List avatarIds) { + if (!this.isUsingTrialTeam()) throw new RuntimeException("Player is not using a trial team."); + + this.getPlayer() + .sendPacket( + new PacketAvatarDelNotify(avatarIds.stream().map(this::getTrialAvatarGuid).toList())); + this.removeTrialAvatarTeam(avatarIds); + + // Update the team. + if (avatarIds.size() == 1) this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify()); + } +} diff --git a/src/main/java/emu/grasscutter/game/props/ElementType.java b/src/main/java/emu/grasscutter/game/props/ElementType.java index 97fc9e6bf..26d942cfe 100644 --- a/src/main/java/emu/grasscutter/game/props/ElementType.java +++ b/src/main/java/emu/grasscutter/game/props/ElementType.java @@ -1,129 +1,72 @@ -package emu.grasscutter.game.props; - -import emu.grasscutter.utils.Utils; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; -import lombok.Getter; - -public enum ElementType { - None(0, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), - Fire( - 1, - FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, - FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, - 10101, - "TeamResonance_Fire_Lv2", - 2), - Water( - 2, - FightProperty.FIGHT_PROP_CUR_WATER_ENERGY, - FightProperty.FIGHT_PROP_MAX_WATER_ENERGY, - 10201, - "TeamResonance_Water_Lv2", - 3), - Grass( - 3, - FightProperty.FIGHT_PROP_CUR_GRASS_ENERGY, - FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY, - 10501, - "TeamResonance_Grass_Lv2", - 8), - Electric( - 4, - FightProperty.FIGHT_PROP_CUR_ELEC_ENERGY, - FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY, - 10401, - "TeamResonance_Electric_Lv2", - 7), - Ice( - 5, - FightProperty.FIGHT_PROP_CUR_ICE_ENERGY, - FightProperty.FIGHT_PROP_MAX_ICE_ENERGY, - 10601, - "TeamResonance_Ice_Lv2", - 5), - Frozen(6, FightProperty.FIGHT_PROP_CUR_ICE_ENERGY, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY), - Wind( - 7, - FightProperty.FIGHT_PROP_CUR_WIND_ENERGY, - FightProperty.FIGHT_PROP_MAX_WIND_ENERGY, - 10301, - "TeamResonance_Wind_Lv2", - 4), - Rock( - 8, - FightProperty.FIGHT_PROP_CUR_ROCK_ENERGY, - FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY, - 10701, - "TeamResonance_Rock_Lv2", - 6), - AntiFire(9, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), - Default( - 255, - FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, - FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, - 10801, - "TeamResonance_AllDifferent"); - - private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); - private static final Map stringMap = new HashMap<>(); - - static { - Stream.of(values()) - .forEach( - e -> { - map.put(e.getValue(), e); - stringMap.put(e.name(), e); - }); - } - - @Getter private final int value; - @Getter private final int teamResonanceId; - @Getter private final FightProperty curEnergyProp; - @Getter private final FightProperty maxEnergyProp; - @Getter private final int depotValue; - @Getter private final int configHash; - - ElementType(int value, FightProperty curEnergyProp, FightProperty maxEnergyProp) { - this(value, curEnergyProp, maxEnergyProp, 0, null, 1); - } - - ElementType( - int value, - FightProperty curEnergyProp, - FightProperty maxEnergyProp, - int teamResonanceId, - String configName) { - this(value, curEnergyProp, maxEnergyProp, teamResonanceId, configName, 1); - } - - ElementType( - int value, - FightProperty curEnergyProp, - FightProperty maxEnergyProp, - int teamResonanceId, - String configName, - int depotValue) { - this.value = value; - this.curEnergyProp = curEnergyProp; - this.maxEnergyProp = maxEnergyProp; - this.teamResonanceId = teamResonanceId; - this.depotValue = depotValue; - if (configName != null) { - this.configHash = Utils.abilityHash(configName); - } else { - this.configHash = 0; - } - } - - public static ElementType getTypeByValue(int value) { - return map.getOrDefault(value, None); - } - - public static ElementType getTypeByName(String name) { - return stringMap.getOrDefault(name, None); - } -} +package emu.grasscutter.game.props; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import emu.grasscutter.scripts.constants.IntValueEnum; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; + +public enum ElementType implements IntValueEnum { + None (0, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), + Fire (1, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10101, "TeamResonance_Fire_Lv2", 1), + Water (2, FightProperty.FIGHT_PROP_CUR_WATER_ENERGY, FightProperty.FIGHT_PROP_MAX_WATER_ENERGY, 10201, "TeamResonance_Water_Lv2", 2), + Grass (3, FightProperty.FIGHT_PROP_CUR_GRASS_ENERGY, FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY, 10501, "TeamResonance_Grass_Lv2", 7), + Electric (4, FightProperty.FIGHT_PROP_CUR_ELEC_ENERGY, FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY, 10401, "TeamResonance_Electric_Lv2", 6), + Ice (5, FightProperty.FIGHT_PROP_CUR_ICE_ENERGY, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY, 10601, "TeamResonance_Ice_Lv2", 4), + Frozen (6, FightProperty.FIGHT_PROP_CUR_ICE_ENERGY, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY), + Wind (7, FightProperty.FIGHT_PROP_CUR_WIND_ENERGY, FightProperty.FIGHT_PROP_MAX_WIND_ENERGY, 10301, "TeamResonance_Wind_Lv2", 3), + Rock (8, FightProperty.FIGHT_PROP_CUR_ROCK_ENERGY, FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY, 10701, "TeamResonance_Rock_Lv2", 5), + AntiFire (9, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), + Default (255, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10801, "TeamResonance_AllDifferent"); + + private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); + private static final Map stringMap = new HashMap<>(); + + static { + // Create bindings for each value. + Stream.of(ElementType.values()).forEach(entry -> { + map.put(entry.getValue(), entry); + stringMap.put(entry.name(), entry); + }); + } + + @Getter private final int value; + @Getter private final int teamResonanceId; + @Getter private final FightProperty curEnergyProp; + @Getter private final FightProperty maxEnergyProp; + @Getter private final int depotIndex; + @Getter private final int configHash; + + ElementType(int value, FightProperty curEnergyProp, FightProperty maxEnergyProp) { + this(value, curEnergyProp, maxEnergyProp, 0, null, 1); + } + + ElementType(int value, FightProperty curEnergyProp, FightProperty maxEnergyProp, int teamResonanceId, String configName) { + this(value, curEnergyProp, maxEnergyProp, teamResonanceId, configName, 1); + } + + ElementType(int value, FightProperty curEnergyProp, FightProperty maxEnergyProp, int teamResonanceId, String configName, int depotIndex) { + this.value = value; + this.curEnergyProp = curEnergyProp; + this.maxEnergyProp = maxEnergyProp; + this.teamResonanceId = teamResonanceId; + this.depotIndex = depotIndex; + if (configName != null) { + this.configHash = Utils.abilityHash(configName); + } else { + this.configHash = 0; + } + } + + public static ElementType getTypeByValue(int value) { + return map.getOrDefault(value, None); + } + + public static ElementType getTypeByName(String name) { + return stringMap.getOrDefault(name, None); + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/QuestValue.java b/src/main/java/emu/grasscutter/game/quest/QuestValue.java deleted file mode 100644 index 464106ef0..000000000 --- a/src/main/java/emu/grasscutter/game/quest/QuestValue.java +++ /dev/null @@ -1,10 +0,0 @@ -package emu.grasscutter.game.quest; - -import emu.grasscutter.game.quest.enums.QuestTrigger; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -public @interface QuestValue { - QuestTrigger value(); -} diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPersonalLineUnlock.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPersonalLineUnlock.java index 38f012171..c4f9ce10e 100644 --- a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPersonalLineUnlock.java +++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPersonalLineUnlock.java @@ -1,22 +1,23 @@ -package emu.grasscutter.game.quest.conditions; - -import emu.grasscutter.data.excels.QuestData; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.quest.QuestValueCond; -import emu.grasscutter.game.quest.enums.QuestCond; -import lombok.val; - -@QuestValueCond(QuestCond.QUEST_COND_PERSONAL_LINE_UNLOCK) -public class ConditionPersonalLineUnlock extends BaseCondition { - - @Override - public boolean execute( - Player owner, - QuestData questData, - QuestData.QuestAcceptCondition condition, - String paramStr, - int... params) { - val personalLineId = condition.getParam()[0]; - return owner.getPersonalLineList().contains(personalLineId); - } -} +package emu.grasscutter.game.quest.conditions; + +import emu.grasscutter.data.excels.QuestData; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.QuestValueCond; +import emu.grasscutter.game.quest.enums.QuestCond; +import lombok.val; + +@QuestValueCond(QuestCond.QUEST_COND_PERSONAL_LINE_UNLOCK) +public class ConditionPersonalLineUnlock extends BaseCondition { + + @Override + public boolean execute( + Player owner, + QuestData questData, + QuestData.QuestAcceptCondition condition, + String paramStr, + int... params + ) { + var personalLineId = condition.getParam()[0]; + return owner.getPersonalLineList().contains(personalLineId); + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecAddCurAvatarEnergy.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecAddCurAvatarEnergy.java index 619fc6d38..b8ba76adc 100644 --- a/src/main/java/emu/grasscutter/game/quest/exec/ExecAddCurAvatarEnergy.java +++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecAddCurAvatarEnergy.java @@ -1,17 +1,17 @@ -package emu.grasscutter.game.quest.exec; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.excels.QuestData; -import emu.grasscutter.game.quest.GameQuest; -import emu.grasscutter.game.quest.QuestValueExec; -import emu.grasscutter.game.quest.enums.QuestExec; -import emu.grasscutter.game.quest.handlers.QuestExecHandler; - -@QuestValueExec(QuestExec.QUEST_EXEC_ADD_CUR_AVATAR_ENERGY) -public class ExecAddCurAvatarEnergy extends QuestExecHandler { - @Override - public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) { - Grasscutter.getLogger().info("Energy refilled"); - return quest.getOwner().getEnergyManager().refillEntityAvatarEnergy(); - } -} +package emu.grasscutter.game.quest.exec; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.excels.QuestData; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.game.quest.QuestValueExec; +import emu.grasscutter.game.quest.enums.QuestExec; +import emu.grasscutter.game.quest.handlers.QuestExecHandler; + +@QuestValueExec(QuestExec.QUEST_EXEC_ADD_CUR_AVATAR_ENERGY) +public class ExecAddCurAvatarEnergy extends QuestExecHandler { + @Override + public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) { + Grasscutter.getLogger().info("Energy refilled"); + return quest.getOwner().getEnergyManager().refillActiveEnergy(); + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecAddQuestProgress.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecAddQuestProgress.java index d3f0ef8c5..76d3b45cd 100644 --- a/src/main/java/emu/grasscutter/game/quest/exec/ExecAddQuestProgress.java +++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecAddQuestProgress.java @@ -1,21 +1,21 @@ -package emu.grasscutter.game.quest.exec; - -import emu.grasscutter.data.excels.QuestData; -import emu.grasscutter.game.quest.GameQuest; -import emu.grasscutter.game.quest.QuestValueExec; -import emu.grasscutter.game.quest.enums.QuestExec; -import emu.grasscutter.game.quest.handlers.QuestExecHandler; -import java.util.Arrays; - -@QuestValueExec(QuestExec.QUEST_EXEC_ADD_QUEST_PROGRESS) -public class ExecAddQuestProgress extends QuestExecHandler { - @Override - public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) { - var param = - Arrays.stream(paramStr).filter(i -> !i.isBlank()).mapToInt(Integer::parseInt).toArray(); - - quest.getOwner().getProgressManager().addQuestProgress(param[0], param[1]); - - return true; - } -} +package emu.grasscutter.game.quest.exec; + +import emu.grasscutter.data.excels.QuestData; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.game.quest.QuestValueExec; +import emu.grasscutter.game.quest.enums.QuestExec; +import emu.grasscutter.game.quest.handlers.QuestExecHandler; +import java.util.Arrays; + +@QuestValueExec(QuestExec.QUEST_EXEC_ADD_QUEST_PROGRESS) +public final class ExecAddQuestProgress extends QuestExecHandler { + @Override + public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) { + var param = + Arrays.stream(paramStr).filter(i -> !i.isBlank()).mapToInt(Integer::parseInt).toArray(); + + quest.getOwner().getProgressManager().addQuestProgress(param[0], param[1]); + + return true; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecSetOpenState.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecSetOpenState.java index d53bfbaa6..314ee310c 100644 --- a/src/main/java/emu/grasscutter/game/quest/exec/ExecSetOpenState.java +++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecSetOpenState.java @@ -1,21 +1,20 @@ -package emu.grasscutter.game.quest.exec; - -import emu.grasscutter.data.excels.QuestData; -import emu.grasscutter.game.quest.GameQuest; -import emu.grasscutter.game.quest.QuestValueExec; -import emu.grasscutter.game.quest.enums.QuestExec; -import emu.grasscutter.game.quest.handlers.QuestExecHandler; -import java.util.Arrays; -import lombok.val; - -@QuestValueExec(QuestExec.QUEST_EXEC_SET_OPEN_STATE) -public class ExecSetOpenState extends QuestExecHandler { - @Override - public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) { - val param = - Arrays.stream(paramStr).filter(i -> !i.isBlank()).mapToInt(Integer::parseInt).toArray(); - - quest.getOwner().getProgressManager().forceSetOpenState(param[0], param[1]); - return true; - } -} +package emu.grasscutter.game.quest.exec; + +import emu.grasscutter.data.excels.QuestData; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.game.quest.QuestValueExec; +import emu.grasscutter.game.quest.enums.QuestExec; +import emu.grasscutter.game.quest.handlers.QuestExecHandler; +import java.util.Arrays; + +@QuestValueExec(QuestExec.QUEST_EXEC_SET_OPEN_STATE) +public class ExecSetOpenState extends QuestExecHandler { + @Override + public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) { + var param = + Arrays.stream(paramStr).filter(i -> !i.isBlank()).mapToInt(Integer::parseInt).toArray(); + + quest.getOwner().getProgressManager().forceSetOpenState(param[0], param[1]); + return true; + } +} diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index a59e5dff9..af22fb43c 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -1,866 +1,1144 @@ -package emu.grasscutter.game.world; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.GameDepot; -import emu.grasscutter.data.binout.SceneNpcBornEntry; -import emu.grasscutter.data.binout.routes.Route; -import emu.grasscutter.data.excels.*; -import emu.grasscutter.data.excels.codex.CodexAnimalData; -import emu.grasscutter.data.excels.monster.MonsterData; -import emu.grasscutter.data.excels.world.WorldLevelData; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.dungeons.DungeonManager; -import emu.grasscutter.game.dungeons.DungeonSettleListener; -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; -import emu.grasscutter.game.entity.*; -import emu.grasscutter.game.entity.gadget.GadgetWorktop; -import emu.grasscutter.game.managers.blossom.BlossomManager; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.player.TeamInfo; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.LifeState; -import emu.grasscutter.game.props.SceneType; -import emu.grasscutter.game.quest.QuestGroupSuite; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; -import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass; -import emu.grasscutter.net.proto.VisionTypeOuterClass.VisionType; -import emu.grasscutter.scripts.SceneIndexManager; -import emu.grasscutter.scripts.SceneScriptManager; -import emu.grasscutter.scripts.data.SceneBlock; -import emu.grasscutter.scripts.data.SceneGadget; -import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.server.packet.send.*; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; -import lombok.Getter; -import lombok.Setter; - -public class Scene { - @Getter private final World world; - @Getter private final SceneData sceneData; - @Getter private final List players; - @Getter private final Map entities; - @Getter private final Set spawnedEntities; - @Getter private final Set deadSpawnedEntities; - @Getter private final Set loadedBlocks; - @Getter private final Set loadedGroups; - @Getter private final BlossomManager blossomManager; - private final HashSet unlockedForces; - private final List afterLoadedCallbacks = new ArrayList<>(); - private final long startWorldTime; - @Getter @Setter DungeonManager dungeonManager; - @Getter Int2ObjectMap sceneRoutes; - private Set loadedGridBlocks; - @Getter @Setter private boolean dontDestroyWhenEmpty; - @Getter private final SceneScriptManager scriptManager; - @Getter @Setter private WorldChallenge challenge; - @Getter private List dungeonSettleListeners; - @Getter @Setter private int prevScene; // Id of the previous scene - @Getter @Setter private int prevScenePoint; - @Getter @Setter private int killedMonsterCount; - private Set npcBornEntrySet; - @Getter private boolean finishedLoading = false; - @Getter private int tickCount = 0; - @Getter private boolean isPaused = false; - - public Scene(World world, SceneData sceneData) { - this.world = world; - this.sceneData = sceneData; - this.players = new CopyOnWriteArrayList<>(); - this.entities = new ConcurrentHashMap<>(); - - this.prevScene = 3; - this.sceneRoutes = GameData.getSceneRoutes(getId()); - - this.startWorldTime = world.getWorldTime(); - - this.spawnedEntities = ConcurrentHashMap.newKeySet(); - this.deadSpawnedEntities = ConcurrentHashMap.newKeySet(); - this.loadedBlocks = ConcurrentHashMap.newKeySet(); - this.loadedGroups = ConcurrentHashMap.newKeySet(); - this.loadedGridBlocks = new HashSet<>(); - this.npcBornEntrySet = ConcurrentHashMap.newKeySet(); - this.scriptManager = new SceneScriptManager(this); - this.blossomManager = new BlossomManager(this); - this.unlockedForces = new HashSet<>(); - } - - public int getId() { - return sceneData.getId(); - } - - public SceneType getSceneType() { - return getSceneData().getSceneType(); - } - - public int getPlayerCount() { - return this.getPlayers().size(); - } - - public GameEntity getEntityById(int id) { - return this.entities.get(id); - } - - public GameEntity getEntityByConfigId(int configId) { - return this.entities.values().stream() - .filter(x -> x.getConfigId() == configId) - .findFirst() - .orElse(null); - } - - /** - * Sets the scene's pause state. Sends the current scene's time to all players. - * - * @param paused The new pause state. - */ - public void setPaused(boolean paused) { - if (this.isPaused != paused) { - this.isPaused = paused; - this.broadcastPacket(new PacketSceneTimeNotify(this)); - } - } - - /** - * Gets the time in seconds since the scene started. - * - * @return The time in seconds since the scene started. - */ - public int getSceneTime() { - return (int) (this.getWorld().getWorldTime() - this.startWorldTime); - } - - /** - * Gets {@link Scene#getSceneTime()} in seconds. - * - * @return The time in seconds since the scene started. - */ - public int getSceneTimeSeconds() { - return this.getSceneTime() / 1000; - } - - public void addDungeonSettleObserver(DungeonSettleListener dungeonSettleListener) { - if (dungeonSettleListeners == null) { - dungeonSettleListeners = new ArrayList<>(); - } - - dungeonSettleListeners.add(dungeonSettleListener); - } - - /** - * Triggers an event in the dungeon manager. - * - * @param conditionType The condition type to trigger. - * @param params The parameters to pass to the event. - */ - public void triggerDungeonEvent(DungeonPassConditionType conditionType, int... params) { - if (this.dungeonManager == null) return; - this.dungeonManager.triggerEvent(conditionType, params); - } - - public boolean isInScene(GameEntity entity) { - return this.entities.containsKey(entity.getId()); - } - - public synchronized void addPlayer(Player player) { - // Check if player already in - if (getPlayers().contains(player)) { - return; - } - - // Remove player from prev scene - if (player.getScene() != null) { - player.getScene().removePlayer(player); - } - - // Add - getPlayers().add(player); - player.setSceneId(this.getId()); - player.setScene(this); - - this.setupPlayerAvatars(player); - } - - public synchronized void removePlayer(Player player) { - // Remove from challenge if leaving - if (this.getChallenge() != null && this.getChallenge().inProgress()) { - player.sendPacket(new PacketDungeonChallengeFinishNotify(this.getChallenge())); - } - - // Remove player from scene - getPlayers().remove(player); - player.setScene(null); - - // Remove player avatars - this.removePlayerAvatars(player); - - // Remove player gadgets - for (EntityBaseGadget gadget : player.getTeamManager().getGadgets()) { - this.removeEntity(gadget); - } - - // Deregister scene if not in use - if (this.getPlayerCount() <= 0 && !this.dontDestroyWhenEmpty) { - this.getWorld().deregisterScene(this); - } - } - - private void setupPlayerAvatars(Player player) { - // Clear entities from old team - player.getTeamManager().getActiveTeam().clear(); - - // Add new entities for player - TeamInfo teamInfo = player.getTeamManager().getCurrentTeamInfo(); - for (int avatarId : teamInfo.getAvatars()) { - EntityAvatar entity = - new EntityAvatar(player.getScene(), player.getAvatars().getAvatarById(avatarId)); - player.getTeamManager().getActiveTeam().add(entity); - } - - // Limit character index in case its out of bounds - if (player.getTeamManager().getCurrentCharacterIndex() - >= player.getTeamManager().getActiveTeam().size() - || player.getTeamManager().getCurrentCharacterIndex() < 0) { - player - .getTeamManager() - .setCurrentCharacterIndex(player.getTeamManager().getCurrentCharacterIndex() - 1); - } - } - - private synchronized void removePlayerAvatars(Player player) { - var team = player.getTeamManager().getActiveTeam(); - // removeEntities(team, VisionType.VISION_TYPE_REMOVE); // List isn't cool apparently - // :( - team.forEach(e -> removeEntity(e, VisionType.VISION_TYPE_REMOVE)); - team.clear(); - } - - public void spawnPlayer(Player player) { - var teamManager = player.getTeamManager(); - if (this.isInScene(teamManager.getCurrentAvatarEntity())) { - return; - } - - if (teamManager.getCurrentAvatarEntity().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) - <= 0f) { - teamManager.getCurrentAvatarEntity().setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f); - } - - this.addEntity(teamManager.getCurrentAvatarEntity()); - - // Notify the client of any extra skill charges - teamManager.getActiveTeam().stream() - .map(EntityAvatar::getAvatar) - .forEach(Avatar::sendSkillExtraChargeMap); - } - - private void addEntityDirectly(GameEntity entity) { - getEntities().put(entity.getId(), entity); - entity.onCreate(); // Call entity create event - } - - public synchronized void addEntity(GameEntity entity) { - this.addEntityDirectly(entity); - this.broadcastPacket(new PacketSceneEntityAppearNotify(entity)); - } - - public synchronized void addEntityToSingleClient(Player player, GameEntity entity) { - this.addEntityDirectly(entity); - player.sendPacket(new PacketSceneEntityAppearNotify(entity)); - } - - public void addEntities(Collection entities) { - addEntities(entities, VisionType.VISION_TYPE_BORN); - } - - public synchronized void addEntities( - Collection entities, VisionType visionType) { - if (entities == null || entities.isEmpty()) { - return; - } - for (GameEntity entity : entities) { - this.addEntityDirectly(entity); - } - - this.broadcastPacket(new PacketSceneEntityAppearNotify(entities, visionType)); - } - - private GameEntity removeEntityDirectly(GameEntity entity) { - var removed = getEntities().remove(entity.getId()); - if (removed != null) { - removed.onRemoved(); // Call entity remove event - } - return removed; - } - - public void removeEntity(GameEntity entity) { - this.removeEntity(entity, VisionType.VISION_TYPE_DIE); - } - - public synchronized void removeEntity(GameEntity entity, VisionType visionType) { - GameEntity removed = this.removeEntityDirectly(entity); - if (removed != null) { - this.broadcastPacket(new PacketSceneEntityDisappearNotify(removed, visionType)); - } - } - - public synchronized void removeEntities(List entity, VisionType visionType) { - var toRemove = entity.stream().map(this::removeEntityDirectly).toList(); - if (toRemove.size() > 0) { - this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, visionType)); - } - } - - public synchronized void replaceEntity(EntityAvatar oldEntity, EntityAvatar newEntity) { - this.removeEntityDirectly(oldEntity); - this.addEntityDirectly(newEntity); - this.broadcastPacket( - new PacketSceneEntityDisappearNotify(oldEntity, VisionType.VISION_TYPE_REPLACE)); - this.broadcastPacket( - new PacketSceneEntityAppearNotify( - newEntity, VisionType.VISION_TYPE_REPLACE, oldEntity.getId())); - } - - public void showOtherEntities(Player player) { - GameEntity currentEntity = player.getTeamManager().getCurrentAvatarEntity(); - List entities = - this.getEntities().values().stream().filter(entity -> entity != currentEntity).toList(); - - player.sendPacket(new PacketSceneEntityAppearNotify(entities, VisionType.VISION_TYPE_MEET)); - } - - public void handleAttack(AttackResult result) { - // GameEntity attacker = getEntityById(result.getAttackerId()); - GameEntity target = getEntityById(result.getDefenseId()); - - if (target == null) { - return; - } - - // Godmode check - if (target instanceof EntityAvatar) { - if (((EntityAvatar) target).getPlayer().inGodmode()) { - return; - } - } - - // Sanity check - target.damage(result.getDamage(), result.getAttackerId()); - } - - public void killEntity(GameEntity target) { - killEntity(target, 0); - } - - public void killEntity(GameEntity target, int attackerId) { - GameEntity attacker = null; - - if (attackerId > 0) { - attacker = getEntityById(attackerId); - } - - if (attacker != null) { - // Check codex - if (attacker instanceof EntityClientGadget gadgetAttacker) { - var clientGadgetOwner = getEntityById(gadgetAttacker.getOwnerEntityId()); - if (clientGadgetOwner instanceof EntityAvatar) { - ((EntityClientGadget) attacker) - .getOwner() - .getCodex() - .checkAnimal(target, CodexAnimalData.CountType.CODEX_COUNT_TYPE_KILL); - } - } else if (attacker instanceof EntityAvatar avatarAttacker) { - avatarAttacker - .getPlayer() - .getCodex() - .checkAnimal(target, CodexAnimalData.CountType.CODEX_COUNT_TYPE_KILL); - } - } - - // Packet - this.broadcastPacket(new PacketLifeStateChangeNotify(attackerId, target, LifeState.LIFE_DEAD)); - - // Reward drop - if (target instanceof EntityMonster && this.getSceneType() != SceneType.SCENE_DUNGEON) { - getWorld().getServer().getDropSystem().callDrop((EntityMonster) target); - } - - // Remove entity from world - this.removeEntity(target); - - // Death event - target.onDeath(attackerId); - } - - public void onTick() { - // disable script for home - if (this.getSceneType() == SceneType.SCENE_HOME_WORLD - || this.getSceneType() == SceneType.SCENE_HOME_ROOM) { - return; - } - if (this.getScriptManager().isInit()) { - this.checkBlocks(); - } else { - // TEMPORARY - this.checkSpawns(); - } - // Triggers - this.scriptManager.checkRegions(); - - if (challenge != null) { - challenge.onCheckTimeOut(); - } - - blossomManager.onTick(); - - checkNpcGroup(); - } - - public int getEntityLevel(int baseLevel, int worldLevelOverride) { - int level = worldLevelOverride > 0 ? worldLevelOverride + baseLevel - 22 : baseLevel; - level = level >= 100 ? 100 : level; - level = level <= 0 ? 1 : level; - - return level; - } - - public void checkNpcGroup() { - Set npcBornEntries = ConcurrentHashMap.newKeySet(); - for (Player player : this.getPlayers()) { - npcBornEntries.addAll(loadNpcForPlayer(player)); - } - - // clear the unreachable group for client - var toUnload = - this.npcBornEntrySet.stream() - .filter(i -> !npcBornEntries.contains(i)) - .map(SceneNpcBornEntry::getGroupId) - .toList(); - - if (toUnload.size() > 0) { - broadcastPacket(new PacketGroupUnloadNotify(toUnload)); - Grasscutter.getLogger().debug("Unload NPC Group {}", toUnload); - } - // exchange the new npcBornEntry Set - this.npcBornEntrySet = npcBornEntries; - } - - public synchronized void checkSpawns() { - Set loadedGridBlocks = new HashSet<>(); - for (Player player : this.getPlayers()) { - Collections.addAll( - loadedGridBlocks, - SpawnDataEntry.GridBlockId.getAdjacentGridBlockIds( - player.getSceneId(), player.getPosition())); - } - if (this.loadedGridBlocks.containsAll( - loadedGridBlocks)) { // Don't recalculate static spawns if nothing has changed - return; - } - this.loadedGridBlocks = loadedGridBlocks; - var spawnLists = GameDepot.getSpawnLists(); - Set visible = new HashSet<>(); - for (var block : loadedGridBlocks) { - var spawns = spawnLists.get(block); - if (spawns != null) { - visible.addAll(spawns); - } - } - - // World level - WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getWorld().getWorldLevel()); - int worldLevelOverride = 0; - - if (worldLevelData != null) { - worldLevelOverride = worldLevelData.getMonsterLevel(); - } - - // Todo - List toAdd = new ArrayList<>(); - List toRemove = new ArrayList<>(); - var spawnedEntities = this.getSpawnedEntities(); - for (SpawnDataEntry entry : visible) { - // If spawn entry is in our view and hasnt been spawned/killed yet, we should spawn it - if (!spawnedEntities.contains(entry) && !this.getDeadSpawnedEntities().contains(entry)) { - // Entity object holder - GameEntity entity = null; - - // Check if spawn entry is monster or gadget - if (entry.getMonsterId() > 0) { - MonsterData data = GameData.getMonsterDataMap().get(entry.getMonsterId()); - if (data == null) continue; - - int level = this.getEntityLevel(entry.getLevel(), worldLevelOverride); - - EntityMonster monster = new EntityMonster(this, data, entry.getPos(), level); - monster.getRotation().set(entry.getRot()); - monster.setGroupId(entry.getGroup().getGroupId()); - monster.setPoseId(entry.getPoseId()); - monster.setConfigId(entry.getConfigId()); - monster.setSpawnEntry(entry); - - entity = monster; - } else if (entry.getGadgetId() > 0) { - EntityGadget gadget = - new EntityGadget(this, entry.getGadgetId(), entry.getPos(), entry.getRot()); - gadget.setGroupId(entry.getGroup().getGroupId()); - gadget.setConfigId(entry.getConfigId()); - gadget.setSpawnEntry(entry); - int state = entry.getGadgetState(); - if (state > 0) { - gadget.setState(state); - } - gadget.buildContent(); - - gadget.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, Float.POSITIVE_INFINITY); - gadget.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Float.POSITIVE_INFINITY); - gadget.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, Float.POSITIVE_INFINITY); - - entity = gadget; - blossomManager.initBlossom(gadget); - } - - if (entity == null) continue; - - // Add to scene and spawned list - toAdd.add(entity); - spawnedEntities.add(entry); - } - } - - for (GameEntity entity : this.getEntities().values()) { - var spawnEntry = entity.getSpawnEntry(); - if (spawnEntry != null && !visible.contains(spawnEntry)) { - toRemove.add(entity); - spawnedEntities.remove(spawnEntry); - } - } - - if (toAdd.size() > 0) { - toAdd.stream().forEach(this::addEntityDirectly); - this.broadcastPacket(new PacketSceneEntityAppearNotify(toAdd, VisionType.VISION_TYPE_BORN)); - } - if (toRemove.size() > 0) { - toRemove.stream().forEach(this::removeEntityDirectly); - this.broadcastPacket( - new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); - blossomManager.recycleGadgetEntity(toRemove); - } - } - - public List getPlayerActiveBlocks(Player player) { - // consider the borders' entities of blocks, so we check if contains by index - return SceneIndexManager.queryNeighbors( - getScriptManager().getBlocksIndex(), - player.getPosition().toXZDoubleArray(), - Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); - } - - private boolean unloadBlockIfNotVisible(Collection visible, SceneBlock block) { - if (visible.contains(block)) return false; - this.onUnloadBlock(block); - return true; - } - - private synchronized boolean loadBlock(SceneBlock block) { - if (this.loadedBlocks.contains(block)) return false; - this.onLoadBlock(block, this.players); - this.loadedBlocks.add(block); - return true; - } - - public synchronized void checkBlocks() { - Set visible = - this.players.stream() - .map(player -> this.getPlayerActiveBlocks(player)) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - - this.loadedBlocks.removeIf(block -> unloadBlockIfNotVisible(visible, block)); - visible.stream() - .filter(block -> !this.loadBlock(block)) - .forEach( - block -> { - // dynamic load the groups for players in a loaded block - var toLoad = - this.players.stream() - .filter(p -> block.contains(p.getPosition())) - .map(p -> this.playerMeetGroups(p, block)) - .flatMap(Collection::stream) - .toList(); - this.onLoadGroup(toLoad); - }); - } - - public List playerMeetGroups(Player player, SceneBlock block) { - List sceneGroups = - SceneIndexManager.queryNeighbors( - block.sceneGroupIndex, - player.getPosition().toDoubleArray(), - Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); - - List groups = - sceneGroups.stream() - .filter( - group -> !scriptManager.getLoadedGroupSetPerBlock().get(block.id).contains(group)) - .peek(group -> scriptManager.getLoadedGroupSetPerBlock().get(block.id).add(group)) - .toList(); - - if (groups.size() == 0) { - return List.of(); - } - - return groups; - } - - public void onLoadBlock(SceneBlock block, List players) { - this.getScriptManager().loadBlockFromScript(block); - scriptManager.getLoadedGroupSetPerBlock().put(block.id, new HashSet<>()); - - // the groups form here is not added in current scene - var groups = - players.stream() - .filter(player -> block.contains(player.getPosition())) - .map(p -> playerMeetGroups(p, block)) - .flatMap(Collection::stream) - .toList(); - - onLoadGroup(groups); - Grasscutter.getLogger().info("Scene {} Block {} loaded.", this.getId(), block.id); - } - - public void loadTriggerFromGroup(SceneGroup group, String triggerName) { - // Load triggers and regions - getScriptManager() - .registerTrigger( - group.triggers.values().stream().filter(p -> p.name.contains(triggerName)).toList()); - group.regions.values().stream() - .filter(q -> q.config_id == Integer.parseInt(triggerName.substring(13))) - .map(region -> new EntityRegion(this, region)) - .forEach(getScriptManager()::registerRegion); - } - - public void onLoadGroup(List groups) { - if (groups == null || groups.isEmpty()) { - return; - } - for (SceneGroup group : groups) { - // We load the script files for the groups here - this.getScriptManager().loadGroupFromScript(group); - } - - // Spawn gadgets AFTER triggers are added - // TODO - var entities = new ArrayList(); - for (SceneGroup group : groups) { - if (group.init_config == null) { - continue; - } - - // Load garbages - List garbageGadgets = group.getGarbageGadgets(); - - if (garbageGadgets != null) { - entities.addAll( - garbageGadgets.stream() - .map(g -> scriptManager.createGadget(group.id, group.block_id, g)) - .filter(Objects::nonNull) - .toList()); - } - - // Load suites - int suite = group.init_config.suite; - - if (suite == 0 || group.suites == null || group.suites.size() == 0) { - continue; - } - - // just load the 'init' suite, avoid spawn the suite added by AddExtraGroupSuite etc. - var suiteData = group.getSuiteByIndex(suite); - suiteData.sceneTriggers.forEach(getScriptManager()::registerTrigger); - - entities.addAll(scriptManager.getGadgetsInGroupSuite(group, suiteData)); - entities.addAll(scriptManager.getMonstersInGroupSuite(group, suiteData)); - - scriptManager.registerRegionInGroupSuite(group, suiteData); - } - - scriptManager.meetEntities(entities); - // scriptManager.callEvent(EventType.EVENT_GROUP_LOAD, null); - // groups.forEach(g -> scriptManager.callEvent(EventType.EVENT_GROUP_LOAD, null)); - Grasscutter.getLogger().info("Scene {} loaded {} group(s)", this.getId(), groups.size()); - } - - public void onUnloadBlock(SceneBlock block) { - List toRemove = - this.getEntities().values().stream().filter(e -> e.getBlockId() == block.id).toList(); - - if (toRemove.size() > 0) { - toRemove.forEach(this::removeEntityDirectly); - this.broadcastPacket( - new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); - } - - for (SceneGroup group : block.groups.values()) { - if (group.triggers != null) { - group.triggers.values().forEach(getScriptManager()::deregisterTrigger); - } - if (group.regions != null) { - group.regions.values().forEach(getScriptManager()::deregisterRegion); - } - } - scriptManager.getLoadedGroupSetPerBlock().remove(block.id); - Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id); - } - // Gadgets - - public void onPlayerCreateGadget(EntityClientGadget gadget) { - // Directly add - this.addEntityDirectly(gadget); - - // Add to owner's gadget list - gadget.getOwner().getTeamManager().getGadgets().add(gadget); - - // Optimization - if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { - return; - } - - this.broadcastPacketToOthers(gadget.getOwner(), new PacketSceneEntityAppearNotify(gadget)); - } - - public void onPlayerDestroyGadget(int entityId) { - GameEntity entity = getEntities().get(entityId); - - if (entity == null || !(entity instanceof EntityClientGadget gadget)) { - return; - } - - // Get and remove entity - this.removeEntityDirectly(gadget); - - // Remove from owner's gadget list - gadget.getOwner().getTeamManager().getGadgets().remove(gadget); - - // Optimization - if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { - return; - } - - this.broadcastPacketToOthers( - gadget.getOwner(), - new PacketSceneEntityDisappearNotify(gadget, VisionType.VISION_TYPE_DIE)); - } - - // Broadcasting - - public void broadcastPacket(BasePacket packet) { - // Send to all players - might have to check if player has been sent data packets - for (Player player : this.getPlayers()) { - player.getSession().send(packet); - } - } - - public void broadcastPacketToOthers(Player excludedPlayer, BasePacket packet) { - // Optimization - if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == excludedPlayer) { - return; - } - // Send to all players - might have to check if player has been sent data packets - for (Player player : this.getPlayers()) { - if (player == excludedPlayer) { - continue; - } - // Send - player.getSession().send(packet); - } - } - - public void addItemEntity(int itemId, int amount, GameEntity bornForm) { - ItemData itemData = GameData.getItemDataMap().get(itemId); - if (itemData == null) { - return; - } - if (itemData.isEquip()) { - float range = (1.5f + (.05f * amount)); - for (int i = 0; i < amount; i++) { - Position pos = bornForm.getPosition().nearby2d(range).addZ(.9f); // Why Z? - EntityItem entity = new EntityItem(this, null, itemData, pos, 1); - addEntity(entity); - } - } else { - EntityItem entity = - new EntityItem( - this, null, itemData, bornForm.getPosition().clone().addZ(.9f), amount); // Why Z? - addEntity(entity); - } - } - - public void loadNpcForPlayerEnter(Player player) { - this.npcBornEntrySet.addAll(loadNpcForPlayer(player)); - } - - private List loadNpcForPlayer(Player player) { - var pos = player.getPosition(); - var data = GameData.getSceneNpcBornData().get(getId()); - if (data == null) { - return List.of(); - } - - var npcList = - SceneIndexManager.queryNeighbors( - data.getIndex(), - pos.toDoubleArray(), - Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); - - var sceneNpcBornEntries = - npcList.stream().filter(i -> !this.npcBornEntrySet.contains(i)).toList(); - - if (sceneNpcBornEntries.size() > 0) { - this.broadcastPacket(new PacketGroupSuiteNotify(sceneNpcBornEntries)); - Grasscutter.getLogger().debug("Loaded Npc Group Suite {}", sceneNpcBornEntries); - } - return npcList; - } - - public void loadGroupForQuest(List sceneGroupSuite) { - if (!scriptManager.isInit()) { - return; - } - - sceneGroupSuite.forEach( - i -> { - var group = scriptManager.getGroupById(i.getGroup()); - if (group == null) { - return; - } - var suite = group.getSuiteByIndex(i.getSuite()); - if (suite == null) { - return; - } - scriptManager.addGroupSuite(group, suite); - }); - } - - public void selectWorktopOptionWith(SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq req) { - GameEntity entity = getEntityById(req.getGadgetEntityId()); - if (entity == null) { - return; - } - // Handle - if (entity instanceof EntityGadget gadget) { - if (gadget.getContent() instanceof GadgetWorktop worktop) { - boolean shouldDelete = worktop.onSelectWorktopOption(req); - if (shouldDelete) { - entity.getScene().removeEntity(entity, VisionType.VISION_TYPE_REMOVE); - } - } - } - } -} +package emu.grasscutter.game.world; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.GameDepot; +import emu.grasscutter.data.binout.SceneNpcBornEntry; +import emu.grasscutter.data.binout.routes.Route; +import emu.grasscutter.data.excels.*; +import emu.grasscutter.data.excels.codex.CodexAnimalData; +import emu.grasscutter.data.excels.monster.MonsterData; +import emu.grasscutter.data.excels.world.WorldLevelData; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.dungeons.DungeonManager; +import emu.grasscutter.game.dungeons.DungeonSettleListener; +import emu.grasscutter.game.dungeons.challenge.WorldChallenge; +import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; +import emu.grasscutter.game.entity.*; +import emu.grasscutter.game.entity.gadget.GadgetWorktop; +import emu.grasscutter.game.managers.blossom.BlossomManager; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.player.TeamInfo; +import emu.grasscutter.game.props.*; +import emu.grasscutter.game.quest.QuestGroupSuite; +import emu.grasscutter.game.world.data.TeleportProperties; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; +import emu.grasscutter.net.proto.EnterTypeOuterClass; +import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass; +import emu.grasscutter.net.proto.VisionTypeOuterClass.VisionType; +import emu.grasscutter.scripts.SceneIndexManager; +import emu.grasscutter.scripts.SceneScriptManager; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.SceneBlock; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.event.player.PlayerTeleportEvent; +import emu.grasscutter.server.packet.send.*; +import emu.grasscutter.utils.KahnsSort; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; +import lombok.val; + +public final class Scene { + @Getter private final World world; + @Getter private final SceneData sceneData; + @Getter private final List players; + @Getter private final Map entities; + @Getter private final Set spawnedEntities; + @Getter private final Set deadSpawnedEntities; + @Getter private final Set loadedBlocks; + @Getter private final Set loadedGroups; + @Getter private final BlossomManager blossomManager; + private final HashSet unlockedForces; + private final List afterLoadedCallbacks = new ArrayList<>(); + private final long startWorldTime; + @Getter @Setter DungeonManager dungeonManager; + @Getter Int2ObjectMap sceneRoutes; + private Set loadedGridBlocks; + @Getter @Setter private boolean dontDestroyWhenEmpty; + @Getter private final SceneScriptManager scriptManager; + @Getter @Setter private WorldChallenge challenge; + @Getter private List dungeonSettleListeners; + @Getter @Setter private int prevScene; // Id of the previous scene + @Getter @Setter private int prevScenePoint; + @Getter @Setter private int killedMonsterCount; + private Set npcBornEntrySet; + @Getter private boolean finishedLoading = false; + @Getter private int tickCount = 0; + @Getter private boolean isPaused = false; + + public Scene(World world, SceneData sceneData) { + this.world = world; + this.sceneData = sceneData; + this.players = new CopyOnWriteArrayList<>(); + this.entities = new ConcurrentHashMap<>(); + + this.prevScene = 3; + this.sceneRoutes = GameData.getSceneRoutes(getId()); + + this.startWorldTime = world.getWorldTime(); + + this.spawnedEntities = ConcurrentHashMap.newKeySet(); + this.deadSpawnedEntities = ConcurrentHashMap.newKeySet(); + this.loadedBlocks = ConcurrentHashMap.newKeySet(); + this.loadedGroups = ConcurrentHashMap.newKeySet(); + this.loadedGridBlocks = new HashSet<>(); + this.npcBornEntrySet = ConcurrentHashMap.newKeySet(); + this.scriptManager = new SceneScriptManager(this); + this.blossomManager = new BlossomManager(this); + this.unlockedForces = new HashSet<>(); + } + + public int getId() { + return sceneData.getId(); + } + + public SceneType getSceneType() { + return getSceneData().getSceneType(); + } + + public int getPlayerCount() { + return this.getPlayers().size(); + } + + public GameEntity getEntityById(int id) { + return this.entities.get(id); + } + + public GameEntity getEntityByConfigId(int configId) { + return this.entities.values().stream() + .filter(x -> x.getConfigId() == configId) + .findFirst() + .orElse(null); + } + + public GameEntity getEntityByConfigId(int configId, int groupId) { + return this.entities.values().stream() + .filter(x -> x.getConfigId() == configId && x.getGroupId() == groupId) + .findFirst() + .orElse(null); + } + + /** + * Sets the scene's pause state. Sends the current scene's time to all players. + * + * @param paused The new pause state. + */ + public void setPaused(boolean paused) { + if (this.isPaused != paused) { + this.isPaused = paused; + this.broadcastPacket(new PacketSceneTimeNotify(this)); + } + } + + /** + * Gets the time in seconds since the scene started. + * + * @return The time in seconds since the scene started. + */ + public int getSceneTime() { + return (int) (this.getWorld().getWorldTime() - this.startWorldTime); + } + + /** + * Gets {@link Scene#getSceneTime()} in seconds. + * + * @return The time in seconds since the scene started. + */ + public int getSceneTimeSeconds() { + return this.getSceneTime() / 1000; + } + + public void addDungeonSettleObserver(DungeonSettleListener dungeonSettleListener) { + if (dungeonSettleListeners == null) { + dungeonSettleListeners = new ArrayList<>(); + } + + dungeonSettleListeners.add(dungeonSettleListener); + } + + /** + * Triggers an event in the dungeon manager. + * + * @param conditionType The condition type to trigger. + * @param params The parameters to pass to the event. + */ + public void triggerDungeonEvent(DungeonPassConditionType conditionType, int... params) { + if (this.dungeonManager == null) return; + this.dungeonManager.triggerEvent(conditionType, params); + } + + public boolean isInScene(GameEntity entity) { + return this.entities.containsKey(entity.getId()); + } + + public synchronized void addPlayer(Player player) { + // Check if player already in + if (getPlayers().contains(player)) { + return; + } + + // Remove player from prev scene + if (player.getScene() != null) { + player.getScene().removePlayer(player); + } + + // Add + getPlayers().add(player); + player.setSceneId(this.getId()); + player.setScene(this); + + this.setupPlayerAvatars(player); + } + + public synchronized void removePlayer(Player player) { + // Remove from challenge if leaving + if (this.getChallenge() != null && this.getChallenge().inProgress()) { + player.sendPacket(new PacketDungeonChallengeFinishNotify(this.getChallenge())); + } + + // Remove player from scene + getPlayers().remove(player); + player.setScene(null); + + // Remove player avatars + this.removePlayerAvatars(player); + + // Remove player gadgets + for (EntityBaseGadget gadget : player.getTeamManager().getGadgets()) { + this.removeEntity(gadget); + } + + // Deregister scene if not in use + if (this.getPlayerCount() <= 0 && !this.dontDestroyWhenEmpty) { + this.getWorld().deregisterScene(this); + } + } + + private void setupPlayerAvatars(Player player) { + // Clear entities from old team + player.getTeamManager().getActiveTeam().clear(); + + // Add new entities for player + TeamInfo teamInfo = player.getTeamManager().getCurrentTeamInfo(); + for (int avatarId : teamInfo.getAvatars()) { + EntityAvatar entity = + new EntityAvatar(player.getScene(), player.getAvatars().getAvatarById(avatarId)); + player.getTeamManager().getActiveTeam().add(entity); + } + + // Limit character index in case its out of bounds + if (player.getTeamManager().getCurrentCharacterIndex() + >= player.getTeamManager().getActiveTeam().size() + || player.getTeamManager().getCurrentCharacterIndex() < 0) { + player + .getTeamManager() + .setCurrentCharacterIndex(player.getTeamManager().getCurrentCharacterIndex() - 1); + } + } + + private synchronized void removePlayerAvatars(Player player) { + var team = player.getTeamManager().getActiveTeam(); + // removeEntities(team, VisionType.VISION_TYPE_REMOVE); // List isn't cool apparently + // :( + team.forEach(e -> removeEntity(e, VisionType.VISION_TYPE_REMOVE)); + team.clear(); + } + + public void spawnPlayer(Player player) { + var teamManager = player.getTeamManager(); + if (this.isInScene(teamManager.getCurrentAvatarEntity())) { + return; + } + + if (teamManager.getCurrentAvatarEntity().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) + <= 0f) { + teamManager.getCurrentAvatarEntity().setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f); + } + + this.addEntity(teamManager.getCurrentAvatarEntity()); + + // Notify the client of any extra skill charges + teamManager.getActiveTeam().stream() + .map(EntityAvatar::getAvatar) + .forEach(Avatar::sendSkillExtraChargeMap); + } + + private void addEntityDirectly(GameEntity entity) { + getEntities().put(entity.getId(), entity); + entity.onCreate(); // Call entity create event + } + + public synchronized void addEntity(GameEntity entity) { + this.addEntityDirectly(entity); + this.broadcastPacket(new PacketSceneEntityAppearNotify(entity)); + } + + public synchronized void addEntityToSingleClient(Player player, GameEntity entity) { + this.addEntityDirectly(entity); + player.sendPacket(new PacketSceneEntityAppearNotify(entity)); + } + + public void addEntities(Collection entities) { + addEntities(entities, VisionType.VISION_TYPE_BORN); + } + + public synchronized void addEntities( + Collection entities, VisionType visionType) { + if (entities == null || entities.isEmpty()) { + return; + } + for (GameEntity entity : entities) { + this.addEntityDirectly(entity); + } + + this.broadcastPacket(new PacketSceneEntityAppearNotify(entities, visionType)); + } + + private GameEntity removeEntityDirectly(GameEntity entity) { + var removed = getEntities().remove(entity.getId()); + if (removed != null) { + removed.onRemoved(); // Call entity remove event + } + return removed; + } + + public void removeEntity(GameEntity entity) { + this.removeEntity(entity, VisionType.VISION_TYPE_DIE); + } + + public synchronized void removeEntity(GameEntity entity, VisionType visionType) { + GameEntity removed = this.removeEntityDirectly(entity); + if (removed != null) { + this.broadcastPacket(new PacketSceneEntityDisappearNotify(removed, visionType)); + } + } + + public synchronized void removeEntities(List entity, VisionType visionType) { + var toRemove = entity.stream().map(this::removeEntityDirectly).toList(); + if (toRemove.size() > 0) { + this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, visionType)); + } + } + + public synchronized void replaceEntity(EntityAvatar oldEntity, EntityAvatar newEntity) { + this.removeEntityDirectly(oldEntity); + this.addEntityDirectly(newEntity); + this.broadcastPacket( + new PacketSceneEntityDisappearNotify(oldEntity, VisionType.VISION_TYPE_REPLACE)); + this.broadcastPacket( + new PacketSceneEntityAppearNotify( + newEntity, VisionType.VISION_TYPE_REPLACE, oldEntity.getId())); + } + + public void showOtherEntities(Player player) { + GameEntity currentEntity = player.getTeamManager().getCurrentAvatarEntity(); + List entities = + this.getEntities().values().stream().filter(entity -> entity != currentEntity).toList(); + + player.sendPacket(new PacketSceneEntityAppearNotify(entities, VisionType.VISION_TYPE_MEET)); + } + + public void handleAttack(AttackResult result) { + // GameEntity attacker = getEntityById(result.getAttackerId()); + GameEntity target = getEntityById(result.getDefenseId()); + ElementType attackType = ElementType.getTypeByValue(result.getElementType()); + + if (target == null) { + return; + } + + // Godmode check + if (target instanceof EntityAvatar) { + if (((EntityAvatar) target).getPlayer().isInGodMode()) { + return; + } + } + + // Sanity check + target.damage(result.getDamage(), result.getAttackerId(), attackType); + } + + public void killEntity(GameEntity target) { + killEntity(target, 0); + } + + public void killEntity(GameEntity target, int attackerId) { + GameEntity attacker = null; + + if (attackerId > 0) { + attacker = getEntityById(attackerId); + } + + if (attacker != null) { + // Check codex + if (attacker instanceof EntityClientGadget gadgetAttacker) { + var clientGadgetOwner = getEntityById(gadgetAttacker.getOwnerEntityId()); + if (clientGadgetOwner instanceof EntityAvatar) { + ((EntityClientGadget) attacker) + .getOwner() + .getCodex() + .checkAnimal(target, CodexAnimalData.CountType.CODEX_COUNT_TYPE_KILL); + } + } else if (attacker instanceof EntityAvatar avatarAttacker) { + avatarAttacker + .getPlayer() + .getCodex() + .checkAnimal(target, CodexAnimalData.CountType.CODEX_COUNT_TYPE_KILL); + } + } + + // Packet + this.broadcastPacket(new PacketLifeStateChangeNotify(attackerId, target, LifeState.LIFE_DEAD)); + + // Reward drop + if (target instanceof EntityMonster && this.getSceneType() != SceneType.SCENE_DUNGEON) { + getWorld().getServer().getDropSystem().callDrop((EntityMonster) target); + } + + // Remove entity from world + this.removeEntity(target); + + // Death event + target.onDeath(attackerId); + } + + public void onTick() { + // Disable ticking for the player's home world. + if (this.getSceneType() == SceneType.SCENE_HOME_WORLD + || this.getSceneType() == SceneType.SCENE_HOME_ROOM) { + return; + } + + if (this.getScriptManager().isInit()) { + this.checkBlocks(); + } else { + // TEMPORARY + this.checkSpawns(); + } + + // Triggers + this.scriptManager.checkRegions(); + + if (challenge != null) { + challenge.onCheckTimeOut(); + } + + this.blossomManager.onTick(); + + checkNpcGroup(); + + this.finishLoading(); + this.checkPlayerRespawn(); + if (this.tickCount++ % 10 == 0) + broadcastPacket(new PacketSceneTimeNotify(this)); + } + + /** + * Validates a player's current position. + * Teleports the player if the player is out of bounds. + */ + private void checkPlayerRespawn() { + var diePos = this.getScriptManager().getConfig().die_y; + + // Check players in the scene. + this.players.forEach(player -> { + if (this.getScriptManager().getConfig() == null) return; + + // Check if we need a respawn + if (diePos >= player.getPosition().getY()) { + //Respawn the player. + this.respawnPlayer(player); + } + }); + + // Check entities in the scene. + this.getEntities().forEach((id, entity) -> { + if (diePos >= entity.getPosition().getY()){ + this.killEntity(entity); + } + }); + } + + /** + * @return The script's default location, or the player's location. + */ + public Position getDefaultLocation(Player player) { + val defaultPosition = getScriptManager().getConfig().born_pos; + return defaultPosition != null ? defaultPosition : player.getPosition(); + } + + /** + * @return The script's default rotation, or the player's rotation. + */ + private Position getDefaultRot(Player player) { + var defaultRotation = this.getScriptManager().getConfig().born_rot; + return defaultRotation != null ? defaultRotation : player.getRotation(); + } + + /** + * Gets the respawn position for the player. + * + * @param player The player to get the respawn position for. + * @return The respawn position for the player. + */ + private Position getRespawnLocation(Player player) { + // TODO: Get the last valid location the player stood on. + var lastCheckpointPos = dungeonManager != null ? dungeonManager.getRespawnLocation() : null; + return lastCheckpointPos != null ? lastCheckpointPos : getDefaultLocation(player); + } + + /** + * Gets the respawn rotation for the player. + * + * @param player The player to get the respawn rotation for. + * @return The respawn rotation for the player. + */ + private Position getRespawnRotation(Player player) { + var lastCheckpointRot = this.dungeonManager != null ? this.dungeonManager.getRespawnRotation() : null; + return lastCheckpointRot != null ? lastCheckpointRot : this.getDefaultRot(player); + } + + /** + * Teleports the player to the respawn location. + * + * @param player The player to respawn. + * @return true if the player was successfully respawned, false otherwise. + */ + public boolean respawnPlayer(Player player) { + // Apply void damage as a penalty. + player.getTeamManager().applyVoidDamage(); + + // TODO: Respawn the player at the last valid location. + var targetPos = getRespawnLocation(player); + var targetRot = getRespawnRotation(player); + var teleportProps = TeleportProperties.builder() + .sceneId(getId()) + .teleportTo(targetPos) + .teleportRot(targetRot) + .teleportType(PlayerTeleportEvent.TeleportType.INTERNAL) + .enterType(EnterTypeOuterClass.EnterType.ENTER_TYPE_GOTO) + .enterReason(dungeonManager != null ? EnterReason.DungeonReviveOnWaypoint : EnterReason.Revival); + + return this.getWorld().transferPlayerToScene(player, teleportProps.build()); + } + + /** + * Invoked when the scene finishes loading. + * Runs all callbacks that were added with {@link #runWhenFinished(Runnable)}. + */ + public void finishLoading() { + if (this.finishedLoading) return; + + this.finishedLoading = true; + this.afterLoadedCallbacks.forEach(Runnable::run); + this.afterLoadedCallbacks.clear(); + } + + /** + * Adds a callback to be executed when the scene is finished loading. + * If the scene is already finished loading, the callback will be executed immediately. + * + * @param runnable The callback to be executed. + */ + public void runWhenFinished(Runnable runnable) { + if (this.isFinishedLoading()) { + runnable.run();return; + } + + this.afterLoadedCallbacks.add(runnable); + } + + public int getEntityLevel(int baseLevel, int worldLevelOverride) { + int level = worldLevelOverride > 0 ? worldLevelOverride + baseLevel - 22 : baseLevel; + level = Math.min(level, 100); + level = level <= 0 ? 1 : level; + + return level; + } + + public void checkNpcGroup() { + Set npcBornEntries = ConcurrentHashMap.newKeySet(); + for (Player player : this.getPlayers()) { + npcBornEntries.addAll(loadNpcForPlayer(player)); + } + + // clear the unreachable group for client + var toUnload = + this.npcBornEntrySet.stream() + .filter(i -> !npcBornEntries.contains(i)) + .map(SceneNpcBornEntry::getGroupId) + .toList(); + + if (toUnload.size() > 0) { + broadcastPacket(new PacketGroupUnloadNotify(toUnload)); + Grasscutter.getLogger().debug("Unload NPC Group {}", toUnload); + } + // exchange the new npcBornEntry Set + this.npcBornEntrySet = npcBornEntries; + } + + public synchronized void checkSpawns() { + Set loadedGridBlocks = new HashSet<>(); + for (Player player : this.getPlayers()) { + Collections.addAll( + loadedGridBlocks, + SpawnDataEntry.GridBlockId.getAdjacentGridBlockIds( + player.getSceneId(), player.getPosition())); + } + if (this.loadedGridBlocks.containsAll( + loadedGridBlocks)) { // Don't recalculate static spawns if nothing has changed + return; + } + this.loadedGridBlocks = loadedGridBlocks; + var spawnLists = GameDepot.getSpawnLists(); + Set visible = new HashSet<>(); + for (var block : loadedGridBlocks) { + var spawns = spawnLists.get(block); + if (spawns != null) { + visible.addAll(spawns); + } + } + + // World level + WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getWorld().getWorldLevel()); + int worldLevelOverride = 0; + + if (worldLevelData != null) { + worldLevelOverride = worldLevelData.getMonsterLevel(); + } + + // Todo + List toAdd = new ArrayList<>(); + List toRemove = new ArrayList<>(); + var spawnedEntities = this.getSpawnedEntities(); + for (SpawnDataEntry entry : visible) { + // If spawn entry is in our view and hasnt been spawned/killed yet, we should spawn it + if (!spawnedEntities.contains(entry) && !this.getDeadSpawnedEntities().contains(entry)) { + // Entity object holder + GameEntity entity = null; + + // Check if spawn entry is monster or gadget + if (entry.getMonsterId() > 0) { + MonsterData data = GameData.getMonsterDataMap().get(entry.getMonsterId()); + if (data == null) continue; + + int level = this.getEntityLevel(entry.getLevel(), worldLevelOverride); + + EntityMonster monster = new EntityMonster(this, data, entry.getPos(), level); + monster.getRotation().set(entry.getRot()); + monster.setGroupId(entry.getGroup().getGroupId()); + monster.setPoseId(entry.getPoseId()); + monster.setConfigId(entry.getConfigId()); + monster.setSpawnEntry(entry); + + entity = monster; + } else if (entry.getGadgetId() > 0) { + EntityGadget gadget = + new EntityGadget(this, entry.getGadgetId(), entry.getPos(), entry.getRot()); + gadget.setGroupId(entry.getGroup().getGroupId()); + gadget.setConfigId(entry.getConfigId()); + gadget.setSpawnEntry(entry); + int state = entry.getGadgetState(); + if (state > 0) { + gadget.setState(state); + } + gadget.buildContent(); + + gadget.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, Float.POSITIVE_INFINITY); + gadget.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Float.POSITIVE_INFINITY); + gadget.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, Float.POSITIVE_INFINITY); + + entity = gadget; + blossomManager.initBlossom(gadget); + } + + if (entity == null) continue; + + // Add to scene and spawned list + toAdd.add(entity); + spawnedEntities.add(entry); + } + } + + for (GameEntity entity : this.getEntities().values()) { + var spawnEntry = entity.getSpawnEntry(); + if (spawnEntry != null && !visible.contains(spawnEntry)) { + toRemove.add(entity); + spawnedEntities.remove(spawnEntry); + } + } + + if (toAdd.size() > 0) { + toAdd.forEach(this::addEntityDirectly); + this.broadcastPacket(new PacketSceneEntityAppearNotify(toAdd, VisionType.VISION_TYPE_BORN)); + } + + if (toRemove.size() > 0) { + toRemove.forEach(this::removeEntityDirectly); + this.broadcastPacket( + new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); + blossomManager.recycleGadgetEntity(toRemove); + } + } + + public List getPlayerActiveBlocks(Player player) { + // consider the borders' entities of blocks, so we check if contains by index + return SceneIndexManager.queryNeighbors( + getScriptManager().getBlocksIndex(), + player.getPosition().toXZDoubleArray(), + Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); + } + + private boolean unloadBlockIfNotVisible(Collection visible, SceneBlock block) { + if (visible.contains(block)) return false; + this.onUnloadBlock(block); + return true; + } + + public synchronized boolean loadBlock(SceneBlock block) { + if (this.loadedBlocks.contains(block)) return false; + + this.onLoadBlock(block, this.players); + this.loadedBlocks.add(block); + return true; + } + + public synchronized void checkBlocks() { + Set visible = + this.players.stream() + .map(this::getPlayerActiveBlocks) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + + this.loadedBlocks.removeIf(block -> unloadBlockIfNotVisible(visible, block)); + visible.stream() + .filter(block -> !this.loadBlock(block)) + .forEach( + block -> { + // dynamic load the groups for players in a loaded block + var toLoad = + this.players.stream() + .filter(p -> block.contains(p.getPosition())) + .map(p -> this.playerMeetGroups(p, block)) + .flatMap(Collection::stream) + .toList(); + this.onLoadGroup(toLoad); + }); + } + + public List playerMeetGroups(Player player, SceneBlock block) { + List sceneGroups = + SceneIndexManager.queryNeighbors( + block.sceneGroupIndex, + player.getPosition().toDoubleArray(), + Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); + + List groups = + sceneGroups.stream() + .filter( + group -> !scriptManager.getLoadedGroupSetPerBlock().get(block.id).contains(group)) + .peek(group -> scriptManager.getLoadedGroupSetPerBlock().get(block.id).add(group)) + .toList(); + + if (groups.size() == 0) { + return List.of(); + } + + return groups; + } + + public void onLoadBlock(SceneBlock block, List players) { + this.getScriptManager().loadBlockFromScript(block); + scriptManager.getLoadedGroupSetPerBlock().put(block.id, new HashSet<>()); + + // the groups form here is not added in current scene + var groups = + players.stream() + .filter(player -> block.contains(player.getPosition())) + .map(p -> playerMeetGroups(p, block)) + .flatMap(Collection::stream) + .toList(); + + onLoadGroup(groups); + Grasscutter.getLogger().info("Scene {} Block {} loaded.", this.getId(), block.id); + } + + public int loadDynamicGroup(int group_id) { + SceneGroup group = getScriptManager().getGroupById(group_id); + if(group == null || getScriptManager().getGroupInstanceById(group_id) != null) return -1; //Group not found or already instanced + + onLoadGroup(new ArrayList<>(List.of(group))); + + if(GameData.getGroupReplacements().containsKey(group_id)) onRegisterGroups(); + + if (group.init_config == null) return -1; + return group.init_config.suite; + } + + public boolean unregisterDynamicGroup(int groupId){ + var group = getScriptManager().getGroupById(groupId); + if (group == null) return false; + + var block = getScriptManager().getBlocks().get(group.block_id); + this.unloadGroup(block, groupId); + return true; + } + + public void onRegisterGroups() { + var sceneGroups = this.loadedGroups; + var sceneGroupMap = sceneGroups.stream() + .collect(Collectors.toMap(item -> item.id, item -> item)); + var sceneGroupsIds = sceneGroups.stream() + .map(group -> group.id) + .toList(); + var dynamicGroups = sceneGroups.stream() + .filter(group -> group.dynamic_load) + .map(group -> group.id) + .toList(); + + //Create the graph + var nodes = new ArrayList(); + var groupList = new ArrayList(); + GameData.getGroupReplacements().values().stream().filter(replacement -> dynamicGroups.contains(replacement.id)).forEach(replacement -> { + Grasscutter.getLogger().info("Graph ordering replacement {}", replacement); + replacement.replace_groups.forEach(group -> { + nodes.add(new KahnsSort.Node(replacement.id, group)); + if (!groupList.contains(group)) + groupList.add(group); + }); + + if (!groupList.contains(replacement.id)) + groupList.add(replacement.id); + }); + + KahnsSort.Graph graph = new KahnsSort.Graph(nodes, groupList); + List dynamicGroupsOrdered = KahnsSort.doSort(graph); + if (dynamicGroupsOrdered == null) throw new RuntimeException("Invalid group replacement graph"); + + // Now we can start unloading and loading groups :D + dynamicGroupsOrdered.forEach(group -> { + if (GameData.getGroupReplacements().containsKey((int)group)) { //isGroupJoinReplacement + var data = GameData.getGroupReplacements().get((int)group); + var sceneGroupReplacement = this.loadedGroups.stream().filter(g -> g.id == group).findFirst().orElseThrow(); + if (sceneGroupReplacement.is_replaceable != null) { + var it = data.replace_groups.iterator(); + while (it.hasNext()) { + var replace_group = it.next(); + if (!sceneGroupsIds.contains(replace_group)) continue; + + // Check if we can replace this group + SceneGroup sceneGroup = sceneGroupMap.get(replace_group); + if (sceneGroup != null && sceneGroup.is_replaceable != null && + ((sceneGroup.is_replaceable.value && + sceneGroup.is_replaceable.version <= sceneGroupReplacement.is_replaceable.version) || + sceneGroup.is_replaceable.new_bin_only)) { + this.unloadGroup(scriptManager.getBlocks().get(sceneGroup.block_id), replace_group); + it.remove(); + Grasscutter.getLogger().info("Graph ordering: unloaded {}", replace_group); + } + } + } + } + }); + } + + public void loadTriggerFromGroup(SceneGroup group, String triggerName) { + // Load triggers and regions + this.getScriptManager().registerTrigger(group.triggers.values().stream() + .filter(p -> p.getName().contains(triggerName)).toList()); + group.regions.values().stream() + .filter(q -> q.config_id == Integer.parseInt(triggerName.substring(13))) + .map(region -> new EntityRegion(this, region)) + .forEach(getScriptManager()::registerRegion); + } + + public void onLoadGroup(List groups) { + if (groups == null || groups.isEmpty()) { + return; + } + + for (var group : groups) { + if(this.loadedGroups.contains(group)) continue; + + // We load the script files for the groups here + this.getScriptManager().loadGroupFromScript(group); + if (!this.scriptManager.getLoadedGroupSetPerBlock().containsKey(group.block_id)) + this.onLoadBlock(scriptManager.getBlocks().get(group.block_id), players); + this.scriptManager.getLoadedGroupSetPerBlock().get(group.block_id).add(group); + } + + // Spawn gadgets AFTER triggers are added + // TODO + var entities = new ArrayList(); + for (var group : groups) { + if(this.loadedGroups.contains(group)) continue; + + if (group.init_config == null) { + continue; + } + + var groupInstance = this.getScriptManager().getGroupInstanceById(group.id); + var cachedInstance = this.getScriptManager().getCachedGroupInstanceById(group.id); + if (cachedInstance != null) { + cachedInstance.setLuaGroup(group); + groupInstance = cachedInstance; + } + + // Load garbages + var garbageGadgets = group.getGarbageGadgets(); + + if (garbageGadgets != null) { + entities.addAll(garbageGadgets.stream() + .map(g -> scriptManager.createGadget(group.id, group.block_id, g)) + .filter(Objects::nonNull).toList()); + } + + // Load suites + //int suite = group.findInitSuiteIndex(0); + this.getScriptManager().refreshGroup(groupInstance, 0, false); //This is what the official server does + + this.loadedGroups.add(group); + } + + this.scriptManager.meetEntities(entities); + groups.forEach(g -> scriptManager.callEvent(new ScriptArgs(g.id, EventType.EVENT_GROUP_LOAD, g.id))); + + Grasscutter.getLogger().info("Scene {} loaded {} group(s)", this.getId(), groups.size()); + } + + public void onUnloadBlock(SceneBlock block) { + List toRemove = + this.getEntities().values().stream().filter(e -> e.getBlockId() == block.id).toList(); + + if (toRemove.size() > 0) { + toRemove.forEach(this::removeEntityDirectly); + this.broadcastPacket( + new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); + } + + for (SceneGroup group : block.groups.values()) { + if (group.triggers != null) { + group.triggers.values().forEach(getScriptManager()::deregisterTrigger); + } + if (group.regions != null) { + group.regions.values().forEach(getScriptManager()::deregisterRegion); + } + } + scriptManager.getLoadedGroupSetPerBlock().remove(block.id); + Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id); + } + + /** + * Unloads a Lua group. + * + * @param block The block that contains the group. + * @param groupId The group ID. + */ + public void unloadGroup(SceneBlock block, int groupId) { + var toRemove = this.getEntities().values().stream() + .filter(e -> e != null && ( + e.getBlockId() == block.id && + e.getGroupId() == groupId) + ).toList(); + + if (toRemove.size() > 0) { + toRemove.forEach(this::removeEntityDirectly); + this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); + } + + var group = block.groups.get(groupId); + if (group.triggers != null) { + group.triggers.values().forEach( + this.getScriptManager()::deregisterTrigger); + } + if (group.regions != null) { + group.regions.values().forEach( + this.getScriptManager()::deregisterRegion); + } + + this.scriptManager.getLoadedGroupSetPerBlock().get(block.id).remove(group); + this.loadedGroups.remove(group); + + if (this.scriptManager.getLoadedGroupSetPerBlock().get(block.id).isEmpty()) { + this.scriptManager.getLoadedGroupSetPerBlock().remove(block.id); + Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id); + } + + this.broadcastPacket(new PacketGroupUnloadNotify(List.of(groupId))); + this.scriptManager.unregisterGroup(group); + } + + // Gadgets + + public void onPlayerCreateGadget(EntityClientGadget gadget) { + // Directly add + this.addEntityDirectly(gadget); + + // Add to owner's gadget list + gadget.getOwner().getTeamManager().getGadgets().add(gadget); + + // Optimization + if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { + return; + } + + this.broadcastPacketToOthers(gadget.getOwner(), new PacketSceneEntityAppearNotify(gadget)); + } + + public void onPlayerDestroyGadget(int entityId) { + GameEntity entity = getEntities().get(entityId); + + if (!(entity instanceof EntityClientGadget gadget)) { + return; + } + + // Get and remove entity + this.removeEntityDirectly(gadget); + + // Remove from owner's gadget list + gadget.getOwner().getTeamManager().getGadgets().remove(gadget); + + // Optimization + if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { + return; + } + + this.broadcastPacketToOthers( + gadget.getOwner(), + new PacketSceneEntityDisappearNotify(gadget, VisionType.VISION_TYPE_DIE)); + } + + // Broadcasting + + public void broadcastPacket(BasePacket packet) { + // Send to all players - might have to check if player has been sent data packets + for (Player player : this.getPlayers()) { + player.getSession().send(packet); + } + } + + public void broadcastPacketToOthers(Player excludedPlayer, BasePacket packet) { + // Optimization + if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == excludedPlayer) { + return; + } + // Send to all players - might have to check if player has been sent data packets + for (Player player : this.getPlayers()) { + if (player == excludedPlayer) { + continue; + } + // Send + player.getSession().send(packet); + } + } + + public void addItemEntity(int itemId, int amount, GameEntity bornForm) { + ItemData itemData = GameData.getItemDataMap().get(itemId); + if (itemData == null) { + return; + } + if (itemData.isEquip()) { + float range = (1.5f + (.05f * amount)); + for (int i = 0; i < amount; i++) { + Position pos = bornForm.getPosition().nearby2d(range).addZ(.9f); // Why Z? + EntityItem entity = new EntityItem(this, null, itemData, pos, 1); + addEntity(entity); + } + } else { + EntityItem entity = + new EntityItem( + this, null, itemData, bornForm.getPosition().clone().addZ(.9f), amount); // Why Z? + addEntity(entity); + } + } + + public void loadNpcForPlayerEnter(Player player) { + this.npcBornEntrySet.addAll(loadNpcForPlayer(player)); + } + + private List loadNpcForPlayer(Player player) { + var pos = player.getPosition(); + var data = GameData.getSceneNpcBornData().get(getId()); + if (data == null) { + return List.of(); + } + + var npcList = + SceneIndexManager.queryNeighbors( + data.getIndex(), + pos.toDoubleArray(), + Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); + + var sceneNpcBornEntries = + npcList.stream().filter(i -> !this.npcBornEntrySet.contains(i)).toList(); + + if (sceneNpcBornEntries.size() > 0) { + this.broadcastPacket(new PacketGroupSuiteNotify(sceneNpcBornEntries)); + Grasscutter.getLogger().debug("Loaded Npc Group Suite {}", sceneNpcBornEntries); + } + return npcList; + } + + public void loadGroupForQuest(List sceneGroupSuite) { + if (!scriptManager.isInit()) { + return; + } + + sceneGroupSuite.forEach(i -> { + var group = scriptManager.getGroupById(i.getGroup()); + if (group == null) return; + + var groupInstance = scriptManager.getGroupInstanceById(i.getGroup()); + var suite = group.getSuiteByIndex(i.getSuite()); + if (suite == null || groupInstance == null) { + return; + } + + scriptManager.refreshGroup(groupInstance, i.getSuite(), false); + }); + } + + /** + * Adds an unlocked force to the scene. + * + * @param force The ID of the force to unlock. + */ + public void unlockForce(int force) { + this.unlockedForces.add(force); + this.broadcastPacket(new PacketSceneForceUnlockNotify(force, true)); + } + + /** + * Removes an unlocked force from the scene. + * + * @param force The ID of the force to lock. + */ + public void lockForce(int force) { + this.unlockedForces.remove(force); + this.broadcastPacket(new PacketSceneForceLockNotify(force)); + } + + public void selectWorktopOptionWith(SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq req) { + GameEntity entity = getEntityById(req.getGadgetEntityId()); + if (entity == null) { + return; + } + // Handle + if (entity instanceof EntityGadget gadget) { + if (gadget.getContent() instanceof GadgetWorktop worktop) { + boolean shouldDelete = worktop.onSelectWorktopOption(req); + if (shouldDelete) { + entity.getScene().removeEntity(entity, VisionType.VISION_TYPE_REMOVE); + } + } + } + } +} diff --git a/src/main/java/emu/grasscutter/game/world/SceneGroupInstance.java b/src/main/java/emu/grasscutter/game/world/SceneGroupInstance.java index fdf064809..691e2fde1 100644 --- a/src/main/java/emu/grasscutter/game/world/SceneGroupInstance.java +++ b/src/main/java/emu/grasscutter/game/world/SceneGroupInstance.java @@ -1,85 +1,86 @@ -package emu.grasscutter.game.world; - -import dev.morphia.annotations.Entity; -import dev.morphia.annotations.Id; -import dev.morphia.annotations.Indexed; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.scripts.data.SceneGadget; -import emu.grasscutter.scripts.data.SceneGroup; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import lombok.Getter; -import lombok.Setter; -import org.bson.types.ObjectId; - -@Entity(value = "group_instances", useDiscriminator = false) -public class SceneGroupInstance { - @Id private ObjectId id; - - @Indexed private int ownerUid; // This group is owned by the host player - @Getter private int groupId; - - @Getter private transient SceneGroup luaGroup; - @Getter @Setter private int targetSuiteId; - @Getter @Setter private int activeSuiteId; - @Getter private Set deadEntities; // Config_ids - private boolean isCached; - - @Getter private Map cachedGadgetStates; - @Getter private Map cachedVariables; - - @Getter @Setter private int lastTimeRefreshed; - - public SceneGroupInstance(SceneGroup group, Player owner) { - this.luaGroup = group; - this.groupId = group.id; - this.targetSuiteId = 0; - this.activeSuiteId = 0; - this.lastTimeRefreshed = 0; - this.ownerUid = owner.getUid(); - this.deadEntities = new HashSet<>(); - this.cachedGadgetStates = new ConcurrentHashMap<>(); - this.cachedVariables = new ConcurrentHashMap<>(); - - this.isCached = - false; // This is true when the group is not loaded on scene but caches suite data - } - - @Deprecated // Morphia only! - SceneGroupInstance() { - this.cachedVariables = new ConcurrentHashMap<>(); - this.deadEntities = new HashSet<>(); - this.cachedGadgetStates = new ConcurrentHashMap<>(); - } - - public void setLuaGroup(SceneGroup group) { - this.luaGroup = group; - this.groupId = group.id; - } - - public boolean isCached() { - return this.isCached; - } - - public void setCached(boolean value) { - this.isCached = value; - save(); // Save each time a group is registered or unregistered - } - - public void cacheGadgetState(SceneGadget g, int state) { - if (g.persistent) // Only cache when is persistent - cachedGadgetStates.put(g.config_id, state); - } - - public int getCachedGadgetState(SceneGadget g) { - Integer state = cachedGadgetStates.getOrDefault(g.config_id, null); - return (state == null) ? g.state : state; - } - - public void save() { - DatabaseHelper.saveGroupInstance(this); - } -} +package emu.grasscutter.game.world; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.bson.types.ObjectId; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Id; +import dev.morphia.annotations.Indexed; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.scripts.data.SceneGadget; +import emu.grasscutter.scripts.data.SceneGroup; +import lombok.Getter; +import lombok.Setter; + +@Entity(value = "group_instances", useDiscriminator = false) +public final class SceneGroupInstance { + @Id private ObjectId id; + + @Indexed private int ownerUid; //This group is owned by the host player + @Getter private int groupId; + + @Getter private transient SceneGroup luaGroup; + @Getter @Setter private int targetSuiteId; + @Getter @Setter private int activeSuiteId; + @Getter private Set deadEntities; //Config_ids + private boolean isCached; + + @Getter private Map cachedGadgetStates; + @Getter private Map cachedVariables; + + @Getter @Setter private int lastTimeRefreshed; + + public SceneGroupInstance(SceneGroup group, Player owner) { + this.luaGroup = group; + this.groupId = group.id; + this.targetSuiteId = 0; + this.activeSuiteId = 0; + this.lastTimeRefreshed = 0; + this.ownerUid = owner.getUid(); + this.deadEntities = new HashSet<>(); + this.cachedGadgetStates = new ConcurrentHashMap<>(); + this.cachedVariables = new ConcurrentHashMap<>(); + + this.isCached = false; //This is true when the group is not loaded on scene but caches suite data + } + + @Deprecated // Morphia only! + SceneGroupInstance(){ + this.cachedVariables = new ConcurrentHashMap<>(); + this.deadEntities = new HashSet<>(); + this.cachedGadgetStates = new ConcurrentHashMap<>(); + } + + public void setLuaGroup(SceneGroup group) { + this.luaGroup = group; + this.groupId = group.id; + } + + public boolean isCached() { + return this.isCached; + } + + public void setCached(boolean value) { + this.isCached = value; + save(); //Save each time a group is registered or unregistered + } + + public void cacheGadgetState(SceneGadget g, int state) { + if(g.persistent) //Only cache when is persistent + cachedGadgetStates.put(g.config_id, state); + } + + public int getCachedGadgetState(SceneGadget g) { + Integer state = cachedGadgetStates.getOrDefault(g.config_id, null); + return (state == null) ? g.state : state; + } + + public void save() { + DatabaseHelper.saveGroupInstance(this); + } +} diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index 697a8f20c..d694e5b67 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -1,564 +1,968 @@ -package emu.grasscutter.scripts; - -import com.github.davidmoten.rtreemulti.RTree; -import com.github.davidmoten.rtreemulti.geometry.Geometry; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.monster.MonsterData; -import emu.grasscutter.data.excels.world.WorldLevelData; -import emu.grasscutter.game.entity.*; -import emu.grasscutter.game.props.EntityType; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.VisionTypeOuterClass; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.data.*; -import emu.grasscutter.scripts.service.ScriptMonsterSpawnService; -import emu.grasscutter.scripts.service.ScriptMonsterTideService; -import io.netty.util.concurrent.FastThreadLocalThread; -import java.util.*; -import java.util.concurrent.*; -import java.util.stream.Collectors; -import org.luaj.vm2.LuaError; -import org.luaj.vm2.LuaValue; -import org.luaj.vm2.lib.jse.CoerceJavaToLua; - -public class SceneScriptManager { - public static final ExecutorService eventExecutor; - - static { - eventExecutor = - new ThreadPoolExecutor( - 4, - 4, - 60, - TimeUnit.SECONDS, - new LinkedBlockingDeque<>(1000), - FastThreadLocalThread::new, - new ThreadPoolExecutor.AbortPolicy()); - } - - private final Scene scene; - private final Map variables; - /** current triggers controlled by RefreshGroup */ - private final Map> currentTriggers; - - private final Map regions; // - private final Map sceneGroups; - /** blockid - loaded groupSet */ - private final Map> loadedGroupSetPerBlock; - - private SceneMeta meta; - private boolean isInit; - private ScriptMonsterTideService scriptMonsterTideService; - private final ScriptMonsterSpawnService scriptMonsterSpawnService; - - public SceneScriptManager(Scene scene) { - this.scene = scene; - this.currentTriggers = new ConcurrentHashMap<>(); - - this.regions = new ConcurrentHashMap<>(); - this.variables = new ConcurrentHashMap<>(); - this.sceneGroups = new ConcurrentHashMap<>(); - this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this); - this.loadedGroupSetPerBlock = new ConcurrentHashMap<>(); - - // TEMPORARY - if (this.getScene().getId() < 10 - && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) { - return; - } - - // Create - this.init(); - } - - public Scene getScene() { - return scene; - } - - public SceneConfig getConfig() { - if (!isInit) { - return null; - } - return meta.config; - } - - public Map getBlocks() { - return meta.blocks; - } - - public Map getVariables() { - return variables; - } - - public Set getTriggersByEvent(int eventId) { - return currentTriggers.computeIfAbsent(eventId, e -> new HashSet<>()); - } - - public void registerTrigger(List triggers) { - triggers.forEach(this::registerTrigger); - } - - public void registerTrigger(SceneTrigger trigger) { - getTriggersByEvent(trigger.event).add(trigger); - Grasscutter.getLogger().debug("Registered trigger {}", trigger.name); - } - - public void deregisterTrigger(List triggers) { - triggers.forEach(this::deregisterTrigger); - } - - public void deregisterTrigger(SceneTrigger trigger) { - getTriggersByEvent(trigger.event).remove(trigger); - } - - public void resetTriggers(int eventId) { - currentTriggers.put(eventId, new HashSet<>()); - } - - public void refreshGroup(SceneGroup group, int suiteIndex) { - if (group == null) { - return; - } - - var suite = group.getSuiteByIndex(suiteIndex); - if (suite == null) { - return; - } - - if (suite.sceneTriggers.size() > 0) { - for (var trigger : suite.sceneTriggers) { - resetTriggers(trigger.event); - this.currentTriggers.get(trigger.event).add(trigger); - } - } - - spawnMonstersInGroup(group, suite); - spawnGadgetsInGroup(group, suite); - } - - public EntityRegion getRegionById(int id) { - return regions.get(id); - } - - public void registerRegion(EntityRegion region) { - regions.put(region.getId(), region); - Grasscutter.getLogger() - .debug( - "Registered region {} from group {}", - region.getMetaRegion().config_id, - region.getGroupId()); - } - - public void registerRegionInGroupSuite(SceneGroup group, SceneSuite suite) { - suite.sceneRegions.stream() - .map(region -> new EntityRegion(this.getScene(), region)) - .forEach(this::registerRegion); - } - - public synchronized void deregisterRegion(SceneRegion region) { - var instance = - regions.values().stream().filter(r -> r.getConfigId() == region.config_id).findFirst(); - instance.ifPresent(entityRegion -> regions.remove(entityRegion.getId())); - } - - public Map> getLoadedGroupSetPerBlock() { - return loadedGroupSetPerBlock; - } - - // TODO optimize - public SceneGroup getGroupById(int groupId) { - for (SceneBlock block : this.getScene().getLoadedBlocks()) { - var group = block.groups.get(groupId); - if (group == null) { - continue; - } - - if (!group.isLoaded()) { - getScene().onLoadGroup(List.of(group)); - } - return group; - } - return null; - } - - private void init() { - var meta = ScriptLoader.getSceneMeta(getScene().getId()); - if (meta == null) { - return; - } - this.meta = meta; - - // TEMP - this.isInit = true; - } - - public boolean isInit() { - return isInit; - } - - public void loadBlockFromScript(SceneBlock block) { - block.load(scene.getId(), meta.context); - } - - public void loadGroupFromScript(SceneGroup group) { - group.load(getScene().getId()); - - if (group.variables != null) { - group.variables.forEach(var -> this.getVariables().put(var.name, var.value)); - } - - this.sceneGroups.put(group.id, group); - } - - public void checkRegions() { - if (this.regions.size() == 0) { - return; - } - - for (var region : this.regions.values()) { - // currently all condition_ENTER_REGION Events check for avatar, so we have no necessary to - // add other types of entity - getScene().getEntities().values().stream() - .filter( - e -> - e.getEntityType() == EntityType.Avatar.getValue() - && region.getMetaRegion().contains(e.getPosition())) - .forEach(region::addEntity); - - var players = region.getScene().getPlayers(); - int targetID = 0; - if (players.size() > 0) targetID = players.get(0).getUid(); - - if (region.hasNewEntities()) { - Grasscutter.getLogger() - .trace("Call EVENT_ENTER_REGION_{}", region.getMetaRegion().config_id); - callEvent( - EventType.EVENT_ENTER_REGION, - new ScriptArgs(region.getConfigId()) - .setSourceEntityId(region.getId()) - .setTargetEntityId(targetID)); - - region.resetNewEntities(); - } - - for (int entityId : region.getEntities()) { - if (getScene().getEntityById(entityId) == null - || !region.getMetaRegion().contains(getScene().getEntityById(entityId).getPosition())) { - region.removeEntity(entityId); - } - } - if (region.entityLeave()) { - callEvent( - EventType.EVENT_LEAVE_REGION, - new ScriptArgs(region.getConfigId()) - .setSourceEntityId(region.getId()) - .setTargetEntityId(region.getFirstEntityId())); - - region.resetNewEntities(); - } - } - } - - public List getGadgetsInGroupSuite(SceneGroup group, SceneSuite suite) { - return suite.sceneGadgets.stream() - .map(g -> createGadget(group.id, group.block_id, g)) - .filter(Objects::nonNull) - .toList(); - } - - public List getMonstersInGroupSuite(SceneGroup group, SceneSuite suite) { - return suite.sceneMonsters.stream() - .map(mob -> createMonster(group.id, group.block_id, mob)) - .filter(Objects::nonNull) - .toList(); - } - - public void addGroupSuite(SceneGroup group, SceneSuite suite) { - // we added trigger first - registerTrigger(suite.sceneTriggers); - - var toCreate = new ArrayList(); - toCreate.addAll(getGadgetsInGroupSuite(group, suite)); - toCreate.addAll(getMonstersInGroupSuite(group, suite)); - addEntities(toCreate); - - registerRegionInGroupSuite(group, suite); - } - - public void removeGroupSuite(SceneGroup group, SceneSuite suite) { - deregisterTrigger(suite.sceneTriggers); - removeMonstersInGroup(group, suite); - removeGadgetsInGroup(group, suite); - - suite.sceneRegions.forEach(this::deregisterRegion); - } - - public void spawnGadgetsInGroup(SceneGroup group, SceneSuite suite) { - var gadgets = group.gadgets.values(); - - if (suite != null) { - gadgets = suite.sceneGadgets; - } - - var toCreate = - gadgets.stream() - .map(g -> createGadget(g.group.id, group.block_id, g)) - .filter(Objects::nonNull) - .toList(); - this.addEntities(toCreate); - } - - public void spawnMonstersInGroup(SceneGroup group, SceneSuite suite) { - if (suite == null || suite.sceneMonsters.size() <= 0) { - return; - } - this.addEntities( - suite.sceneMonsters.stream() - .map(mob -> createMonster(group.id, group.block_id, mob)) - .toList()); - } - - public void startMonsterTideInGroup( - SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) { - this.scriptMonsterTideService = - new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId); - } - - public void unloadCurrentMonsterTide() { - if (this.getScriptMonsterTideService() == null) { - return; - } - this.getScriptMonsterTideService().unload(); - } - - public void spawnMonstersByConfigId(SceneGroup group, int configId, int delayTime) { - // TODO delay - getScene().addEntity(createMonster(group.id, group.block_id, group.monsters.get(configId))); - } - - // Events - public void callEvent(int eventType, ScriptArgs params) { - /** - * We use ThreadLocal to trans SceneScriptManager context to ScriptLib, to avoid eval script for - * every groups' trigger in every scene instances. But when callEvent is called in a ScriptLib - * func, it may cause NPE because the inner call cleans the ThreadLocal so that outer call could - * not get it. e.g. CallEvent -> set -> ScriptLib.xxx -> CallEvent -> set -> remove -> NPE -> - * (remove) So we use thread pool to clean the stack to avoid this new issue. - */ - eventExecutor.submit(() -> this.realCallEvent(eventType, params)); - } - - private void realCallEvent(int eventType, ScriptArgs params) { - try { - ScriptLoader.getScriptLib().setSceneScriptManager(this); - Set relevantTriggers = new HashSet<>(); - if (eventType == EventType.EVENT_ENTER_REGION || eventType == EventType.EVENT_LEAVE_REGION) { - List relevantTriggersList = - this.getTriggersByEvent(eventType).stream() - .filter(p -> p.condition.contains(String.valueOf(params.param1))) - .toList(); - relevantTriggers = new HashSet<>(relevantTriggersList); - } else { - relevantTriggers = this.getTriggersByEvent(eventType); - } - for (SceneTrigger trigger : relevantTriggers) { - try { - ScriptLoader.getScriptLib().setCurrentGroup(trigger.currentGroup); - LuaValue ret = this.callScriptFunc(trigger.condition, trigger.currentGroup, params); - Grasscutter.getLogger() - .trace( - "Call Condition Trigger {}, [{},{},{}]", - trigger.condition, - params.param1, - params.source_eid, - params.target_eid); - if (ret.isboolean() && ret.checkboolean()) { - // the SetGroupVariableValueByGroup in tower need the param to record the first stage - // time - this.callScriptFunc(trigger.action, trigger.currentGroup, params); - Grasscutter.getLogger().trace("Call Action Trigger {}", trigger.action); - if (trigger.event == EventType.EVENT_ENTER_REGION) { - EntityRegion region = - this.regions.values().stream() - .filter(p -> p.getConfigId() == params.param1) - .toList() - .get(0); - getScene().getPlayers().forEach(p -> p.onEnterRegion(region.getMetaRegion())); - deregisterRegion(region.getMetaRegion()); - } else if (trigger.event == EventType.EVENT_LEAVE_REGION) { - EntityRegion region = - this.regions.values().stream() - .filter(p -> p.getConfigId() == params.param1) - .toList() - .get(0); - getScene().getPlayers().forEach(p -> p.onLeaveRegion(region.getMetaRegion())); - deregisterRegion(region.getMetaRegion()); - } - deregisterTrigger(trigger); - } else { - Grasscutter.getLogger() - .debug("Condition Trigger {} returned {}", trigger.condition, ret); - } - // TODO some ret do not bool - } finally { - ScriptLoader.getScriptLib().removeCurrentGroup(); - } - } - } finally { - // make sure it is removed - ScriptLoader.getScriptLib().removeSceneScriptManager(); - } - } - - private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) { - LuaValue funcLua = null; - if (funcName != null && !funcName.isEmpty()) { - funcLua = (LuaValue) group.getBindings().get(funcName); - } - - LuaValue ret = LuaValue.TRUE; - - if (funcLua != null) { - LuaValue args = LuaValue.NIL; - - if (params != null) { - args = CoerceJavaToLua.coerce(params); - } - - ret = safetyCall(funcName, funcLua, args); - } - return ret; - } - - public LuaValue safetyCall(String name, LuaValue func, LuaValue args) { - try { - return func.call(ScriptLoader.getScriptLibLua(), args); - } catch (LuaError error) { - ScriptLib.logger.error("[LUA] call trigger failed {},{}", name, args, error); - return LuaValue.valueOf(-1); - } - } - - public ScriptMonsterTideService getScriptMonsterTideService() { - return scriptMonsterTideService; - } - - public ScriptMonsterSpawnService getScriptMonsterSpawnService() { - return scriptMonsterSpawnService; - } - - public EntityGadget createGadget(int groupId, int blockId, SceneGadget g) { - if (g.isOneoff) { - var hasEntity = - getScene().getEntities().values().stream() - .filter(e -> e instanceof EntityGadget) - .filter(e -> e.getGroupId() == g.group.id) - .filter(e -> e.getConfigId() == g.config_id) - .findFirst(); - if (hasEntity.isPresent()) { - return null; - } - } - EntityGadget entity = new EntityGadget(getScene(), g.gadget_id, g.pos); - - if (entity.getGadgetData() == null) { - return null; - } - - entity.setBlockId(blockId); - entity.setConfigId(g.config_id); - entity.setGroupId(groupId); - entity.getRotation().set(g.rot); - entity.setState(g.state); - entity.setPointType(g.point_type); - entity.setMetaGadget(g); - entity.buildContent(); - - return entity; - } - - public EntityNPC createNPC(SceneNPC npc, int blockId, int suiteId) { - return new EntityNPC(getScene(), npc, blockId, suiteId); - } - - public EntityMonster createMonster(int groupId, int blockId, SceneMonster monster) { - if (monster == null) { - return null; - } - - MonsterData data = GameData.getMonsterDataMap().get(monster.monster_id); - - if (data == null) { - return null; - } - - // Calculate level - int level = monster.level; - - if (getScene().getDungeonData() != null) { - level = getScene().getDungeonData().getShowLevel(); - } else if (getScene().getWorld().getWorldLevel() > 0) { - WorldLevelData worldLevelData = - GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel()); - - if (worldLevelData != null) { - level = worldLevelData.getMonsterLevel(); - } - } - - // Spawn mob - EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level); - entity.getRotation().set(monster.rot); - entity.setGroupId(groupId); - entity.setBlockId(blockId); - entity.setConfigId(monster.config_id); - entity.setPoseId(monster.pose_id); - - this.getScriptMonsterSpawnService() - .onMonsterCreatedListener - .forEach(action -> action.onNotify(entity)); - - return entity; - } - - public void addEntity(GameEntity gameEntity) { - getScene().addEntity(gameEntity); - } - - public void meetEntities(List gameEntity) { - getScene().addEntities(gameEntity, VisionTypeOuterClass.VisionType.VISION_TYPE_MEET); - } - - public void addEntities(List gameEntity) { - getScene().addEntities(gameEntity); - } - - public RTree getBlocksIndex() { - return meta.sceneBlockIndex; - } - - public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) { - var configSet = suite.sceneMonsters.stream().map(m -> m.config_id).collect(Collectors.toSet()); - var toRemove = - getScene().getEntities().values().stream() - .filter(e -> e instanceof EntityMonster) - .filter(e -> e.getGroupId() == group.id) - .filter(e -> configSet.contains(e.getConfigId())) - .toList(); - - getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS); - } - - public void removeGadgetsInGroup(SceneGroup group, SceneSuite suite) { - var configSet = suite.sceneGadgets.stream().map(m -> m.config_id).collect(Collectors.toSet()); - var toRemove = - getScene().getEntities().values().stream() - .filter(e -> e instanceof EntityGadget) - .filter(e -> e.getGroupId() == group.id) - .filter(e -> configSet.contains(e.getConfigId())) - .toList(); - - getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS); - } -} +package emu.grasscutter.scripts; + +import com.github.davidmoten.rtreemulti.RTree; +import com.github.davidmoten.rtreemulti.geometry.Geometry; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.server.Grid; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.entity.*; +import emu.grasscutter.game.entity.gadget.platform.BaseRoute; +import emu.grasscutter.game.props.EntityType; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.game.quest.QuestGroupSuite; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.game.world.SceneGroupInstance; +import emu.grasscutter.net.proto.VisionTypeOuterClass; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.*; +import emu.grasscutter.scripts.service.ScriptMonsterSpawnService; +import emu.grasscutter.scripts.service.ScriptMonsterTideService; +import emu.grasscutter.server.packet.send.PacketGroupSuiteNotify; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.GridPosition; +import emu.grasscutter.utils.JsonUtils; +import emu.grasscutter.utils.Position; +import io.netty.util.concurrent.FastThreadLocalThread; +import kotlin.Pair; +import lombok.val; + +import org.luaj.vm2.LuaError; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.CoerceJavaToLua; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static emu.grasscutter.scripts.constants.EventType.*; + +public class SceneScriptManager { + private final Scene scene; + private final Map variables; + private SceneMeta meta; + private boolean isInit; + /** + * current triggers controlled by RefreshGroup + */ + private final Map> currentTriggers; + private final Map> triggersByGroupScene; + private final Map>> activeGroupTimers; + private final Map triggerInvocations; + private final Map regions; // + private final Map sceneGroups; + private final Map sceneGroupsInstances; + private final Map cachedSceneGroupsInstances; + private ScriptMonsterTideService scriptMonsterTideService; + private ScriptMonsterSpawnService scriptMonsterSpawnService; + /** + * blockid - loaded groupSet + */ + private final Map> loadedGroupSetPerBlock; + private List groupGrids; + public static final ExecutorService eventExecutor; + static { + eventExecutor = new ThreadPoolExecutor(4, 4, + 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10000), + FastThreadLocalThread::new, new ThreadPoolExecutor.AbortPolicy()); + } + public SceneScriptManager(Scene scene) { + this.scene = scene; + this.currentTriggers = new ConcurrentHashMap<>(); + this.triggersByGroupScene = new ConcurrentHashMap<>(); + this.activeGroupTimers = new ConcurrentHashMap<>(); + this.triggerInvocations = new ConcurrentHashMap<>(); + + this.regions = new ConcurrentHashMap<>(); + this.variables = new ConcurrentHashMap<>(); + this.sceneGroups = new ConcurrentHashMap<>(); + this.sceneGroupsInstances = new ConcurrentHashMap<>(); + this.cachedSceneGroupsInstances = new ConcurrentHashMap<>(); + this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this); + this.loadedGroupSetPerBlock = new ConcurrentHashMap<>(); + this.groupGrids = null; //This is changed on init + + // TEMPORARY + if (this.getScene().getId() < 10 && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) { + return; + } + + // Create + this.init(); + } + + public Scene getScene() { + return scene; + } + + public List getGroupGrids() { + return groupGrids; + } + + public SceneConfig getConfig() { + return this.isInit ? this.meta.config : null; + } + + public Map getBlocks() { + return meta.blocks; + } + + @Nullable + public Map getVariables(int group_id) { + if(getCachedGroupInstanceById(group_id) == null) return null; + return getCachedGroupInstanceById(group_id).getCachedVariables(); + } + + public Set getTriggersByEvent(int eventId) { + return currentTriggers.computeIfAbsent(eventId, e -> ConcurrentHashMap.newKeySet()); + } + public int getTriggerCount() { + return currentTriggers.size(); + } + public void registerTrigger(List triggers) { + triggers.forEach(this::registerTrigger); + } + public void registerTrigger(SceneTrigger trigger) { + triggerInvocations.put(trigger.getName(), new AtomicInteger(0)); + getTriggersByEvent(trigger.getEvent()).add(trigger); + Grasscutter.getLogger().debug("Registered trigger {}", trigger.getName()); + } + + public void deregisterTrigger(List triggers) { + triggers.forEach(this::deregisterTrigger); + } + public void deregisterTrigger(SceneTrigger trigger) { + getTriggersByEvent(trigger.getEvent()).remove(trigger); + Grasscutter.getLogger().debug("deregistered trigger {}", trigger.getName()); + } + + public void resetTriggers(int eventId) { + currentTriggers.put(eventId, ConcurrentHashMap.newKeySet()); + } + + public void resetTriggersForGroupSuite(SceneGroup group, int suiteIndex) { + Grasscutter.getLogger().debug("reset triggers for group {} suite {}", group.id, suiteIndex); + var suite = group.getSuiteByIndex(suiteIndex); + if (suite == null) { + Grasscutter.getLogger().warn("Trying to load null suite Triggers for group {} with suiteindex {}", group.id, suiteIndex); + return; + } + + var groupSceneTriggers = triggersByGroupScene.get(group.id+"_"+suiteIndex); + if(groupSceneTriggers == null){ + groupSceneTriggers = new HashSet<>(); + } + + if(!groupSceneTriggers.isEmpty()) { + for (var trigger : groupSceneTriggers) { + currentTriggers.get(trigger.getEvent()).remove(trigger); + } + groupSceneTriggers.clear(); + } + + if (!suite.sceneTriggers.isEmpty()) { + groupSceneTriggers.addAll(suite.sceneTriggers); + for (var trigger : groupSceneTriggers) { + registerTrigger(trigger); + /*this.currentTriggers.computeIfAbsent(trigger.event, k -> ConcurrentHashMap.newKeySet()) + .add(trigger);*/ + } + } + triggersByGroupScene.put(group.id+"_"+suiteIndex, groupSceneTriggers); + } + + public void refreshGroup(SceneGroupInstance groupInstance) { + if(groupInstance == null || groupInstance.getLuaGroup().suites==null){ + return; + } + //for (int i = 1; i<= group.suites.size();i++){ + //refreshGroup(group, i); + refreshGroup(groupInstance, groupInstance.getActiveSuiteId(), false); //Refresh the last group triggers + //} + } + public int refreshGroup(SceneGroupInstance groupInstance, int suiteIndex, boolean excludePrevSuite) { + SceneGroup group = groupInstance.getLuaGroup(); + if(suiteIndex == 0) { + if(excludePrevSuite) { + suiteIndex = group.findInitSuiteIndex(groupInstance.getActiveSuiteId()); + } else + suiteIndex = group.findInitSuiteIndex(0); + } + if(suiteIndex == 0) return 0; + + var suiteData = group.getSuiteByIndex(suiteIndex); + if (suiteData == null) { + Grasscutter.getLogger().warn("Group {} suite {} not found", group.id, suiteIndex); + return 0; + } + + int prevSuiteIndex = groupInstance.getActiveSuiteId(); + boolean waitForOne = false; + SceneSuite prevSuiteData = null; + if(prevSuiteIndex != 0) { + prevSuiteData = group.getSuiteByIndex(prevSuiteIndex); + if (prevSuiteData != null) { + if(prevSuiteData.ban_refresh && !suiteData.ban_refresh) { + waitForOne = true; + } + } + } + + if(waitForOne && (groupInstance.getTargetSuiteId() == 0 || prevSuiteIndex != groupInstance.getTargetSuiteId())) { + groupInstance.setTargetSuiteId(suiteIndex); + Grasscutter.getLogger().debug("Group {} suite {} wating one more refresh", group.id, suiteIndex); + return 0; + } + + groupInstance.setTargetSuiteId(0); + + if(prevSuiteData != null) { + removeGroupSuite(group, prevSuiteData); + } //Remove old group suite + + addGroupSuite(groupInstance, suiteData); + + //Refesh variables here + group.variables.forEach(variable -> { + if(!variable.no_refresh) + groupInstance.getCachedVariables().put(variable.name, variable.value); + }); + + groupInstance.setActiveSuiteId(suiteIndex); + groupInstance.setLastTimeRefreshed(getScene().getWorld().getGameTime()); + return suiteIndex; + } + + public boolean refreshGroupSuite(int groupId, int suiteId, GameQuest quest) { + var targetGroupInstance = getGroupInstanceById(groupId); + if (targetGroupInstance == null) { + getGroupById(groupId); //Load the group, this ensures an instance is created and the if neccesary unloaded, but the suite data is stored + targetGroupInstance = getGroupInstanceById(groupId); + Grasscutter.getLogger().debug("trying to regresh group suite {} in an unloaded and uncached group {} in scene {}", suiteId, groupId, getScene().getId()); + } else { + Grasscutter.getLogger().debug("Refreshing group {} suite {}", groupId, suiteId); + suiteId = refreshGroup(targetGroupInstance, suiteId, false); //If suiteId is zero, the value of suiteId changes + quest.getOwner().sendPacket(new PacketGroupSuiteNotify(groupId, suiteId)); + } + + if(suiteId != 0 && quest != null) { + quest.getMainQuest().getQuestGroupSuites().add(QuestGroupSuite.of() + .scene(getScene().getId()) + .group(groupId) + .suite(suiteId) + .build()); + } + + return true; + } + + public boolean refreshGroupMonster(int groupId) { + var groupInstance = getGroupInstanceById(groupId); + if (groupInstance == null) { + Grasscutter.getLogger().warn("trying to refesh monster group in unloaded and uncached group {} in scene {}", groupId, getScene().getId()); + return false; + } + + var group = groupInstance.getLuaGroup(); + var monstersToSpawn = group.monsters.values().stream() + .filter(m -> { + var entity = scene.getEntityByConfigId(m.config_id); + return (entity == null || entity.getGroupId()!=group.id);/*&& !groupInstance.getDeadEntities().contains(entity); */ //TODO: Investigate the usage of deadEntities + }) + .map(mob -> createMonster(group.id, group.block_id, mob)) + .toList();//TODO check if it interferes with bigworld or anything else + this.addEntities(monstersToSpawn); + + return true; + } + public EntityRegion getRegionById(int id) { + return regions.get(id); + } + + public void registerRegion(EntityRegion region) { + regions.put(region.getId(), region); + Grasscutter.getLogger().debug("Registered region {} from group {}", region.getMetaRegion().config_id, region.getGroupId()); + } + public void registerRegionInGroupSuite(SceneGroup group, SceneSuite suite) { + suite.sceneRegions.stream().map(region -> new EntityRegion(this.getScene(), region)) + .forEach(this::registerRegion); + } + public synchronized void deregisterRegion(SceneRegion region) { + var instance = regions.values().stream() + .filter(r -> r.getConfigId() == region.config_id) + .findFirst(); + instance.ifPresent(entityRegion -> regions.remove(entityRegion.getId())); + } + + public Map> getLoadedGroupSetPerBlock() { + return loadedGroupSetPerBlock; + } + + // TODO optimize + public SceneGroup getGroupById(int groupId) { + for (var block : getBlocks().values()) { + this.getScene().loadBlock(block); + + var group = block.groups.get(groupId); + if (group == null) { + continue; + } + + if (!this.sceneGroupsInstances.containsKey(groupId)) { + this.getScene().onLoadGroup(List.of(group)); + this.getScene().onRegisterGroups(); + } + return group; + } + return null; + } + + public SceneGroupInstance getGroupInstanceById(int groupId) { + return sceneGroupsInstances.getOrDefault(groupId, null); + } + + public Map getCachedGroupInstances() { + return cachedSceneGroupsInstances; + } + + public SceneGroupInstance getCachedGroupInstanceById(int groupId) { + var instance = cachedSceneGroupsInstances.getOrDefault(groupId, null); + if(instance == null) { + instance = DatabaseHelper.loadGroupInstance(groupId, scene.getWorld().getHost()); + if(instance != null) cachedSceneGroupsInstances.put(groupId, instance); + } + + return instance; + } + + private static void addGridPositionToMap(Map> map, int group_id, int vision_level, Position position) { + //Convert position to grid position + GridPosition gridPos; + int width = Grasscutter.getConfig().server.game.visionOptions[vision_level].gridWidth; + gridPos = new GridPosition((int)(position.getX() / width), (int)(position.getZ() / width), width); + + Set groups = map.getOrDefault(gridPos, new HashSet<>()); + groups.add(group_id); + map.put(gridPos, groups); + } + + private static int getGadgetVisionLevel(int gadget_id) { + var gadget = GameData.getGadgetDataMap().get(gadget_id); + if(gadget == null || gadget.getVisionLevel() == null) return 0; + + var visionOptions = Grasscutter.getConfig().server.game.visionOptions; + for(int i = 0; i < visionOptions.length; i++) + if(visionOptions[i].name.compareTo(gadget.getVisionLevel()) == 0) { + return i; + } + + return 0; + } + + private void init() { + var meta = ScriptLoader.getSceneMeta(getScene().getId()); + if (meta == null) { + return; + } + this.meta = meta; + + var path = FileUtils.getScriptPath("Scene/" + getScene().getId() + "/scene_grid.json"); + + try { + this.groupGrids = JsonUtils.loadToList(path, Grid.class); + } catch (IOException ignored) { + Grasscutter.getLogger().error("Scene {} unable to load grid file.", getScene().getId()); + } catch (Exception e) { + Grasscutter.getLogger().error("Scene {} unable to load grid file.", e, getScene().getId()); + } + + boolean runForFirstTime = this.groupGrids == null; + + //Find if the scene entities are already generated, if not generate it + if(Grasscutter.getConfig().server.game.cacheSceneEntitiesEveryRun || runForFirstTime) { + List>> groupPositions = new ArrayList<>(); + for(int i = 0; i < 6; i++) groupPositions.add(new HashMap<>()); + + var visionOptions = Grasscutter.getConfig().server.game.visionOptions; + meta.blocks.values().forEach(block -> { + block.load(scene.getId(), meta.context); + block.groups.values().stream().filter(g -> !g.dynamic_load).forEach(group -> { + group.load(this.scene.getId()); + + //Add all entitites here + Set vision_levels = new HashSet<>(); + group.monsters.values().forEach(m -> { + addGridPositionToMap(groupPositions.get(m.vision_level), group.id, m.vision_level, m.pos); + vision_levels.add(m.vision_level); + }); + group.gadgets.values().forEach(g -> { + int vision_level = Math.max(getGadgetVisionLevel(g.gadget_id), g.vision_level); + addGridPositionToMap(groupPositions.get(vision_level), group.id, vision_level, g.pos); + vision_levels.add(vision_level); + }); + group.npcs.values().forEach(n -> addGridPositionToMap(groupPositions.get(n.vision_level), group.id, n.vision_level, n.pos)); + group.regions.values().forEach(r -> addGridPositionToMap(groupPositions.get(0), group.id, 0, r.pos)); + if(group.garbages != null && group.garbages.gadgets != null) group.garbages.gadgets.forEach(g -> addGridPositionToMap(groupPositions.get(g.vision_level), group.id, g.vision_level, g.pos)); + + int max_vision_level = -1; + if(!vision_levels.isEmpty()) { + for(int vision_level : vision_levels) { + if(max_vision_level == -1 || visionOptions[max_vision_level].visionRange < visionOptions[vision_level].visionRange) + max_vision_level = vision_level; + } + } + if(max_vision_level == -1) max_vision_level = 0; + + addGridPositionToMap(groupPositions.get(max_vision_level), group.id, max_vision_level, group.pos); + }); + }); + + this.groupGrids = new ArrayList<>(); + for(int i = 0; i < 6; i++) { + this.groupGrids.add(new Grid()); + this.groupGrids.get(i).grid = groupPositions.get(i); + } + + try (FileWriter file = new FileWriter(path.toFile())) { + file.write(JsonUtils.encode(groupGrids)); + } catch (IOException ignored) { + Grasscutter.getLogger().error("Scene {} unable to write to grid file.", getScene().getId()); + } catch (Exception e) { + Grasscutter.getLogger().error("Scene {} unable to save grid file.", e, getScene().getId()); + } + + Grasscutter.getLogger().info("Scene {} saved grid file.", getScene().getId()); + } + + // TEMP + this.isInit = true; + } + + public boolean isInit() { + return isInit; + } + + public void loadBlockFromScript(SceneBlock block) { + block.load(scene.getId(), meta.context); + } + + public void loadGroupFromScript(SceneGroup group) { + group.load(getScene().getId()); + + this.sceneGroups.put(group.id, group); + if(this.getCachedGroupInstanceById(group.id) != null) { + this.sceneGroupsInstances.put(group.id, this.cachedSceneGroupsInstances.get(group.id)); + this.cachedSceneGroupsInstances.get(group.id).setCached(false); + this.cachedSceneGroupsInstances.get(group.id).setLuaGroup(group); + } else { + var instance = new SceneGroupInstance(group, getScene().getWorld().getHost()); + this.sceneGroupsInstances.put(group.id, instance); + this.cachedSceneGroupsInstances.put(group.id, instance); + instance.save(); //Save the instance + } + + if (group.variables != null) { + group.variables.forEach(variable -> { + val variables = this.getVariables(group.id); + if(variables != null && !variables.containsKey(variable.name)) + variables.put(variable.name, variable.value); + }); + } + } + + public void unregisterGroup(SceneGroup group) { + this.sceneGroups.remove(group.id); + this.sceneGroupsInstances.values().removeIf(i -> i.getLuaGroup().equals(group)); + this.cachedSceneGroupsInstances.values().stream().filter(i -> i.getLuaGroup().equals(group)).forEach(s -> s.setCached(true)); + } + + public void checkRegions() { + if (this.regions.size() == 0) { + return; + } + + for (var region : this.regions.values()) { + // currently all condition_ENTER_REGION Events check for avatar, so we have no necessary to add other types of entity + var entities = getScene().getEntities().values() + .stream() + .filter(e -> e.getEntityType() == EntityType.Avatar.getValue() && region.getMetaRegion().contains(e.getPosition())) + .toList(); + entities.forEach(region::addEntity); + + int targetID = 0; + if (entities.size() > 0) { + targetID = entities.get(0).getId(); + } + + if (region.hasNewEntities()) { + Grasscutter.getLogger().trace("Call EVENT_ENTER_REGION_{}",region.getMetaRegion().config_id); + callEvent(new ScriptArgs(region.getGroupId(), EventType.EVENT_ENTER_REGION, region.getConfigId()) + .setSourceEntityId(region.getId()) + .setTargetEntityId(targetID) + ); + + region.resetNewEntities(); + } + + for (int entityId : region.getEntities()) { + if (getScene().getEntityById(entityId) == null || !region.getMetaRegion().contains(getScene().getEntityById(entityId).getPosition())) { + region.removeEntity(entityId); + + } + } + if (region.entityLeave()) { + callEvent(new ScriptArgs(region.getGroupId(), EventType.EVENT_LEAVE_REGION, region.getConfigId()) + .setSourceEntityId(region.getId()) + .setTargetEntityId(region.getFirstEntityId()) + ); + + region.resetNewEntities(); + } + } + } + + public List getGadgetsInGroupSuite(SceneGroupInstance groupInstance, SceneSuite suite) { + var group = groupInstance.getLuaGroup(); + return suite.sceneGadgets.stream() + .filter(m -> { + var entity = scene.getEntityByConfigId(m.config_id); + return (entity == null || entity.getGroupId()!=group.id) && (!m.isOneoff || !m.persistent || !groupInstance.getDeadEntities().contains(m.config_id)); + }) + .map(g -> createGadget(group.id, group.block_id, g, groupInstance.getCachedGadgetState(g))) + .peek(g -> groupInstance.cacheGadgetState(g.getMetaGadget(), g.getState())) + .filter(Objects::nonNull) + .toList(); + } + public List getMonstersInGroupSuite(SceneGroupInstance groupInstance, SceneSuite suite) { + var group = groupInstance.getLuaGroup(); + return suite.sceneMonsters.stream() + .filter(m -> { + var entity = scene.getEntityByConfigId(m.config_id); + return (entity == null || entity.getGroupId()!=group.id);/*&& !groupInstance.getDeadEntities().contains(entity); */ //TODO: Investigate the usage of deadEntities + }) //TODO: Add persistent monster cached data + .map(mob -> createMonster(group.id, group.block_id, mob)) + .filter(Objects::nonNull) + .toList(); + } + + public void addGroupSuite(SceneGroupInstance groupInstance, SceneSuite suite) { + // we added trigger first + registerTrigger(suite.sceneTriggers); + + var group = groupInstance.getLuaGroup(); + var toCreate = new ArrayList(); + toCreate.addAll(getGadgetsInGroupSuite(groupInstance, suite)); + toCreate.addAll(getMonstersInGroupSuite(groupInstance, suite)); + addEntities(toCreate); + + registerRegionInGroupSuite(group, suite); + } + public void refreshGroupSuite(SceneGroupInstance groupInstance, SceneSuite suite) { + // we added trigger first + registerTrigger(suite.sceneTriggers); + + var group = groupInstance.getLuaGroup(); + var toCreate = new ArrayList(); + toCreate.addAll(getGadgetsInGroupSuite(groupInstance, suite)); + toCreate.addAll(getMonstersInGroupSuite(groupInstance, suite)); + addEntities(toCreate); + + registerRegionInGroupSuite(group, suite); + } + public void removeGroupSuite(SceneGroup group, SceneSuite suite) { + deregisterTrigger(suite.sceneTriggers); + removeMonstersInGroup(group, suite); + removeGadgetsInGroup(group, suite); + + suite.sceneRegions.forEach(this::deregisterRegion); + } + public void killGroupSuite(SceneGroup group, SceneSuite suite) { + deregisterTrigger(suite.sceneTriggers); + + killMonstersInGroup(group, suite); + killGadgetsInGroup(group, suite); + + suite.sceneRegions.forEach(this::deregisterRegion); + } + + public void startMonsterTideInGroup(SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) { + this.scriptMonsterTideService = + new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId); + + } + public void unloadCurrentMonsterTide() { + if (this.getScriptMonsterTideService() == null) { + return; + } + this.getScriptMonsterTideService().unload(); + } + public void spawnMonstersByConfigId(SceneGroup group, int configId, int delayTime) { + // TODO delay + var entity = scene.getEntityByConfigId(configId); + if(entity!=null && entity.getGroupId() == group.id){ + Grasscutter.getLogger().debug("entity already exists failed in group {} with config {}", group.id, configId); + return; + } + entity = createMonster(group.id, group.block_id, group.monsters.get(configId)); + if(entity!=null){ + getScene().addEntity(entity); + } else { + Grasscutter.getLogger().warn("failed to create entity with group {} and config {}", group.id, configId); + } + } + // Events + public void callEvent(int groupId, int eventType) { + callEvent(new ScriptArgs(groupId, eventType)); + } + public void callEvent(@Nonnull ScriptArgs params) { + /** + * We use ThreadLocal to trans SceneScriptManager context to ScriptLib, to avoid eval script for every groups' trigger in every scene instances. + * But when callEvent is called in a ScriptLib func, it may cause NPE because the inner call cleans the ThreadLocal so that outer call could not get it. + * e.g. CallEvent -> set -> ScriptLib.xxx -> CallEvent -> set -> remove -> NPE -> (remove) + * So we use thread pool to clean the stack to avoid this new issue. + */ + eventExecutor.submit(() -> this.realCallEvent(params)); + } + + private void realCallEvent(@Nonnull ScriptArgs params) { + try { + ScriptLoader.getScriptLib().setSceneScriptManager(this); + int eventType = params.type; + Set relevantTriggers = new HashSet<>(); + if (eventType == EventType.EVENT_ENTER_REGION || eventType == EventType.EVENT_LEAVE_REGION) { + List relevantTriggersList = this.getTriggersByEvent(eventType).stream() + .filter(p -> p.getCondition().contains(String.valueOf(params.param1)) && + (p.getSource().isEmpty() || p.getSource().equals(params.getEventSource()))).toList(); + relevantTriggers = new HashSet<>(relevantTriggersList); + } else { + relevantTriggers = this.getTriggersByEvent(eventType).stream() + .filter(t -> params.getGroupId() == 0 || t.getCurrentGroup().id == params.getGroupId()) + .collect(Collectors.toSet()); + } + for (SceneTrigger trigger : relevantTriggers) { + handleEventForTrigger(params, trigger); + } + } catch (Throwable throwable){ + Grasscutter.getLogger().error("Condition Trigger "+ params.type +" triggered exception", throwable); + } finally { + // make sure it is removed + ScriptLoader.getScriptLib().removeSceneScriptManager(); + } + } + + private boolean handleEventForTrigger(ScriptArgs params, SceneTrigger trigger ){ + Grasscutter.getLogger().debug("checking trigger {} for event {}", trigger.getName(), params.type); + try { + // setup execution + ScriptLoader.getScriptLib().setCurrentGroup(trigger.currentGroup); + ScriptLoader.getScriptLib().setCurrentCallParams(params); + + if (evaluateTriggerCondition(trigger, params)) { + callTrigger(trigger, params); + return true; + } else { + Grasscutter.getLogger().debug("Condition Trigger {} returned false", trigger.getCondition()); + } + //TODO some ret do not bool + return false; + } + catch (Throwable ex){ + Grasscutter.getLogger().error("Condition Trigger "+trigger.getName()+" triggered exception", ex); + return false; + }finally { + ScriptLoader.getScriptLib().removeCurrentGroup(); + } + } + + private boolean evaluateTriggerCondition(SceneTrigger trigger, ScriptArgs params){ + Grasscutter.getLogger().trace("Call Condition Trigger {}, [{},{},{}]", trigger.getCondition(), params.param1, params.source_eid, params.target_eid); + LuaValue ret = this.callScriptFunc(trigger.getCondition(), trigger.currentGroup, params); + return ret.isboolean() && ret.checkboolean(); + } + + private void callTrigger(SceneTrigger trigger, ScriptArgs params){ + // the SetGroupVariableValueByGroup in tower need the param to record the first stage time + var ret = this.callScriptFunc(trigger.getAction(), trigger.currentGroup, params); + var invocationsCounter = triggerInvocations.get(trigger.getName()); + var invocations = invocationsCounter.incrementAndGet(); + Grasscutter.getLogger().trace("Call Action Trigger {}", trigger.getAction()); + + var activeChallenge = scene.getChallenge(); + if (activeChallenge != null){ + activeChallenge.onGroupTriggerDeath(trigger); + } + + if (trigger.getEvent() == EventType.EVENT_ENTER_REGION) { + var region = this.regions.values().stream() + .filter(p -> p.getConfigId() == params.param1) + .toList().get(0); + this.getScene().getPlayers() + .forEach(p -> p.onEnterRegion(region.getMetaRegion())); + this.deregisterRegion(region.getMetaRegion()); + } else if (trigger.getEvent() == EventType.EVENT_LEAVE_REGION) { + var region = this.regions.values().stream() + .filter(p -> p.getConfigId() == params.param1) + .toList().get(0); + this.getScene().getPlayers() + .forEach(p -> p.onLeaveRegion(region.getMetaRegion())); + this.deregisterRegion(region.getMetaRegion()); + } + + if (trigger.getEvent() == EVENT_TIMER_EVENT){ + cancelGroupTimerEvent(trigger.currentGroup.id, trigger.getSource()); + } + + // always deregister on error, otherwise only if the count is reached + if (ret.isboolean() && !ret.checkboolean() || ret.isint() && ret.checkint()!=0 + || trigger.getTrigger_count()>0 && invocations >= trigger.getTrigger_count()) { + deregisterTrigger(trigger); + } + } + + private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) { + LuaValue funcLua = null; + if (funcName != null && !funcName.isEmpty()) { + funcLua = (LuaValue) group.getBindings().get(funcName); + } + + LuaValue ret = LuaValue.TRUE; + + if (funcLua != null) { + LuaValue args = LuaValue.NIL; + + if (params != null) { + args = CoerceJavaToLua.coerce(params); + } + + ret = safetyCall(funcName, funcLua, args, group); + } + return ret; + } + + public LuaValue safetyCall(String name, LuaValue func, LuaValue args, SceneGroup group) { + try { + return func.call(ScriptLoader.getScriptLibLua(), args); + }catch (LuaError error) { + ScriptLib.logger.error("[LUA] call trigger failed in group {} with {},{}",group.id,name,args,error); + return LuaValue.valueOf(-1); + } + } + + public ScriptMonsterTideService getScriptMonsterTideService() { + return scriptMonsterTideService; + } + + public ScriptMonsterSpawnService getScriptMonsterSpawnService() { + return scriptMonsterSpawnService; + } + + public EntityGadget createGadget(int groupId, int blockId, SceneGadget g) { + return createGadget(groupId, blockId, g, g.state); + } + + public EntityGadget createGadget(int groupId, int blockId, SceneGadget g, int state) { + if (g.isOneoff) { + var hasEntity = getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityGadget) + .filter(e -> e.getGroupId() == g.group.id) + .filter(e -> e.getConfigId() == g.config_id) + .findFirst(); + if (hasEntity.isPresent()) { + return null; + } + } + EntityGadget entity = new EntityGadget(getScene(), g.gadget_id, g.pos); + + if (entity.getGadgetData() == null) { + return null; + } + + entity.setBlockId(blockId); + entity.setConfigId(g.config_id); + entity.setGroupId(groupId); + entity.getRotation().set(g.rot); + entity.setState(state); + + entity.setPointType(g.point_type); + entity.setRouteConfig(BaseRoute.fromSceneGadget(g)); + entity.setMetaGadget(g); + entity.buildContent(); + + return entity; + } + public EntityNPC createNPC(SceneNPC npc, int blockId, int suiteId) { + return new EntityNPC(getScene(), npc, blockId, suiteId); + } + public EntityMonster createMonster(int groupId, int blockId, SceneMonster monster) { + if (monster == null) { + return null; + } + + var data = GameData.getMonsterDataMap().get(monster.monster_id); + + if (data == null) { + return null; + } + + // Calculate level + int level = monster.level; + + if (getScene().getDungeonManager() != null) { + level = getScene().getDungeonManager().getLevelForMonster(monster.config_id); + } else if (getScene().getWorld().getWorldLevel() > 0) { + var worldLevelData = GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel()); + + if (worldLevelData != null) { + level = worldLevelData.getMonsterLevel(); + } + } + + // Spawn mob + EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level); + entity.getRotation().set(monster.rot); + entity.setGroupId(groupId); + entity.setBlockId(blockId); + entity.setConfigId(monster.config_id); + entity.setPoseId(monster.pose_id); + entity.setMetaMonster(monster); + + this.getScriptMonsterSpawnService() + .onMonsterCreatedListener.forEach(action -> action.onNotify(entity)); + + return entity; + } + + public void addEntity(GameEntity gameEntity) { + getScene().addEntity(gameEntity); + } + + public void meetEntities(List gameEntity) { + getScene().addEntities(gameEntity, VisionTypeOuterClass.VisionType.VISION_TYPE_MEET); + } + + public void addEntities(List gameEntity) { + getScene().addEntities(gameEntity); + } + + public void removeEntities(List gameEntity) { + getScene().removeEntities(gameEntity.stream().map(e -> (GameEntity) e).collect(Collectors.toList()), VisionTypeOuterClass.VisionType.VISION_TYPE_REFRESH); + } + + public RTree getBlocksIndex() { + return meta.sceneBlockIndex; + } + public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) { + var configSet = suite.sceneMonsters.stream() + .map(m -> m.config_id) + .collect(Collectors.toSet()); + var toRemove = getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityMonster) + .filter(e -> e.getGroupId() == group.id) + .filter(e -> configSet.contains(e.getConfigId())) + .toList(); + + getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS); + } + public void removeGadgetsInGroup(SceneGroup group, SceneSuite suite) { + var configSet = suite.sceneGadgets.stream() + .map(m -> m.config_id) + .collect(Collectors.toSet()); + var toRemove = getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityGadget) + .filter(e -> e.getGroupId() == group.id) + .filter(e -> configSet.contains(e.getConfigId())) + .toList(); + + getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS); + } + + public void killMonstersInGroup(SceneGroup group, SceneSuite suite) { + var configSet = suite.sceneMonsters.stream() + .map(m -> m.config_id) + .collect(Collectors.toSet()); + var toRemove = getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityMonster) + .filter(e -> e.getGroupId() == group.id) + .filter(e -> configSet.contains(e.getConfigId())) + .toList(); + + toRemove.forEach(getScene()::killEntity); + } + public void killGadgetsInGroup(SceneGroup group, SceneSuite suite) { + var configSet = suite.sceneGadgets.stream() + .map(m -> m.config_id) + .collect(Collectors.toSet()); + var toRemove = getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityGadget) + .filter(e -> e.getGroupId() == group.id) + .filter(e -> configSet.contains(e.getConfigId())) + .toList(); + + toRemove.forEach(getScene()::killEntity); + } + + public int createGroupTimerEvent(int groupID, String source, double time) { + //TODO also remove timers when refreshing and test + var group = getGroupById(groupID); + if(group == null || group.triggers == null){ + Grasscutter.getLogger().warn("trying to create a timer for unknown group with id {} and source {}", groupID, source); + return 1; + } + Grasscutter.getLogger().info("creating group timer event for group {} with source {} and time {}", + groupID, source, time); + for(SceneTrigger trigger : group.triggers.values()){ + if(trigger.getEvent() == EVENT_TIMER_EVENT &&trigger.getSource().equals(source)){ + Grasscutter.getLogger().warn("[LUA] Found timer trigger with source {} for group {} : {}", + source, groupID, trigger.getName()); + var taskIdentifier = Grasscutter.getGameServer().getScheduler().scheduleDelayedRepeatingTask(() -> + callEvent(new ScriptArgs(groupID, EVENT_TIMER_EVENT) + .setEventSource(source)), (int)time, (int)time); + var groupTasks = activeGroupTimers.computeIfAbsent(groupID, k -> new HashSet<>()); + groupTasks.add(new Pair<>(source, taskIdentifier)); + + } + } + return 0; + } + public int cancelGroupTimerEvent(int groupID, String source) { + //TODO test + var groupTimers = activeGroupTimers.get(groupID); + if(groupTimers!=null && !groupTimers.isEmpty()) + for(var timer : groupTimers){ + if(timer.component1().equals(source)){ + Grasscutter.getGameServer().getScheduler().cancelTask(timer.component2()); + return 0; + } + } + + Grasscutter.getLogger().warn("trying to cancel a timer that's not active {} {}", groupID, source); + return 1; + } + + // todo use killed monsters instead of spawned entites for check? + public boolean isClearedGroupMonsters(int groupId) { + val groupInstance = getGroupInstanceById(groupId); + if (groupInstance == null || groupInstance.getLuaGroup() == null) return false; + + val monsters = groupInstance.getLuaGroup().monsters; + + if(monsters == null || monsters.isEmpty()) return true; + + return monsters.values().stream().noneMatch(m -> { + val entity = scene.getEntityByConfigId(m.config_id); + return entity != null && entity.getGroupId() == groupId; + }); + } + + public void onDestroy(){ + activeGroupTimers.forEach((gid,times) -> times.forEach((e)->Grasscutter.getGameServer().getScheduler().cancelTask(e.getSecond()))); + activeGroupTimers.clear(); + } +} diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index b499ced19..2f0ad750e 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -1,597 +1,1565 @@ -package emu.grasscutter.scripts; - -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.dungeons.challenge.DungeonChallenge; -import emu.grasscutter.game.dungeons.challenge.factory.ChallengeFactory; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.entity.gadget.GadgetWorktop; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.EntityType; -import emu.grasscutter.game.quest.enums.QuestState; -import emu.grasscutter.game.quest.enums.QuestTrigger; -import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.server.packet.send.PacketCanUseSkillNotify; -import emu.grasscutter.server.packet.send.PacketDungeonShowReminderNotify; -import emu.grasscutter.server.packet.send.PacketWorktopOptionNotify; -import io.netty.util.concurrent.FastThreadLocal; -import java.util.Optional; -import org.luaj.vm2.LuaTable; -import org.luaj.vm2.LuaValue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ScriptLib { - public static final Logger logger = LoggerFactory.getLogger(ScriptLib.class); - private final FastThreadLocal sceneScriptManager; - private final FastThreadLocal currentGroup; - - public ScriptLib() { - this.sceneScriptManager = new FastThreadLocal<>(); - this.currentGroup = new FastThreadLocal<>(); - } - - public void removeSceneScriptManager() { - this.sceneScriptManager.remove(); - } - - public SceneScriptManager getSceneScriptManager() { - // normally not null - return Optional.of(sceneScriptManager.get()).get(); - } - - public void setSceneScriptManager(SceneScriptManager sceneScriptManager) { - this.sceneScriptManager.set(sceneScriptManager); - } - - private String printTable(LuaTable table) { - StringBuilder sb = new StringBuilder(); - sb.append("{"); - for (var meta : table.keys()) { - sb.append(meta).append(":").append(table.get(meta)).append(","); - } - sb.append("}"); - return sb.toString(); - } - - public Optional getCurrentGroup() { - return Optional.of(this.currentGroup.get()); - } - - public void setCurrentGroup(SceneGroup currentGroup) { - this.currentGroup.set(currentGroup); - } - - public void removeCurrentGroup() { - this.currentGroup.remove(); - } - - public int SetGadgetStateByConfigId(int configId, int gadgetState) { - logger.debug("[LUA] Call SetGadgetStateByConfigId with {},{}", configId, gadgetState); - Optional entity = - getSceneScriptManager().getScene().getEntities().values().stream() - .filter(e -> e.getConfigId() == configId) - .findFirst(); - - if (entity.isEmpty()) { - return 1; - } - - if (entity.get() instanceof EntityGadget entityGadget) { - entityGadget.updateState(gadgetState); - return 0; - } - - return 1; - } - - public int SetGroupGadgetStateByConfigId(int groupId, int configId, int gadgetState) { - logger.debug( - "[LUA] Call SetGroupGadgetStateByConfigId with {},{},{}", groupId, configId, gadgetState); - - getSceneScriptManager().getScene().getEntities().values().stream() - .filter(e -> e.getGroupId() == groupId) - .filter(e -> e instanceof EntityGadget) - .map(e -> (EntityGadget) e) - .forEach(e -> e.updateState(gadgetState)); - - return 0; - } - - public int SetWorktopOptionsByGroupId(int groupId, int configId, int[] options) { - logger.debug("[LUA] Call SetWorktopOptionsByGroupId with {},{},{}", groupId, configId, options); - - Optional entity = - getSceneScriptManager().getScene().getEntities().values().stream() - .filter(e -> e.getConfigId() == configId && e.getGroupId() == groupId) - .findFirst(); - - if (entity.isEmpty() || !(entity.get() instanceof EntityGadget gadget)) { - return 1; - } - - if (!(gadget.getContent() instanceof GadgetWorktop worktop)) { - return 1; - } - - worktop.addWorktopOptions(options); - getSceneScriptManager().getScene().broadcastPacket(new PacketWorktopOptionNotify(gadget)); - - return 0; - } - - public int SetWorktopOptions(LuaTable table) { - logger.debug("[LUA] Call SetWorktopOptions with {}", printTable(table)); - // TODO - return 0; - } - - public int DelWorktopOptionByGroupId(int groupId, int configId, int option) { - logger.debug("[LUA] Call DelWorktopOptionByGroupId with {},{},{}", groupId, configId, option); - - Optional entity = - getSceneScriptManager().getScene().getEntities().values().stream() - .filter(e -> e.getConfigId() == configId && e.getGroupId() == groupId) - .findFirst(); - - if (entity.isEmpty() || !(entity.get() instanceof EntityGadget gadget)) { - return 1; - } - - if (!(gadget.getContent() instanceof GadgetWorktop worktop)) { - return 1; - } - - worktop.removeWorktopOption(option); - getSceneScriptManager().getScene().broadcastPacket(new PacketWorktopOptionNotify(gadget)); - - return 0; - } - - // Some fields are guessed - public int AutoMonsterTide( - int challengeIndex, - int groupId, - Integer[] ordersConfigId, - int tideCount, - int sceneLimit, - int param6) { - logger.debug( - "[LUA] Call AutoMonsterTide with {},{},{},{},{},{}", - challengeIndex, - groupId, - ordersConfigId, - tideCount, - sceneLimit, - param6); - - SceneGroup group = getSceneScriptManager().getGroupById(groupId); - - if (group == null || group.monsters == null) { - return 1; - } - - this.getSceneScriptManager() - .startMonsterTideInGroup(group, ordersConfigId, tideCount, sceneLimit); - - return 0; - } - - public int AddExtraGroupSuite(int groupId, int suite) { - logger.debug("[LUA] Call AddExtraGroupSuite with {},{}", groupId, suite); - SceneGroup group = getSceneScriptManager().getGroupById(groupId); - - if (group == null || group.monsters == null) { - return 1; - } - var suiteData = group.getSuiteByIndex(suite); - if (suiteData == null) { - return 1; - } - // avoid spawn wrong monster - if (getSceneScriptManager().getScene().getChallenge() != null) - if (!getSceneScriptManager().getScene().getChallenge().inProgress() - || getSceneScriptManager().getScene().getChallenge().getGroup().id != groupId) { - return 0; - } - this.getSceneScriptManager().addGroupSuite(group, suiteData); - - return 0; - } - - public int GoToGroupSuite(int groupId, int suite) { - logger.debug("[LUA] Call GoToGroupSuite with {},{}", groupId, suite); - SceneGroup group = getSceneScriptManager().getGroupById(groupId); - if (group == null || group.monsters == null) { - return 1; - } - var suiteData = group.getSuiteByIndex(suite); - if (suiteData == null) { - return 1; - } - - for (var suiteItem : group.suites) { - if (suiteData == suiteItem) { - continue; - } - this.getSceneScriptManager().removeGroupSuite(group, suiteItem); - } - this.getSceneScriptManager().addGroupSuite(group, suiteData); - - return 0; - } - - public int RemoveExtraGroupSuite(int groupId, int suite) { - logger.debug("[LUA] Call RemoveExtraGroupSuite with {},{}", groupId, suite); - - SceneGroup group = getSceneScriptManager().getGroupById(groupId); - if (group == null || group.monsters == null) { - return 1; - } - var suiteData = group.getSuiteByIndex(suite); - if (suiteData == null) { - return 1; - } - - this.getSceneScriptManager().removeGroupSuite(group, suiteData); - - return 0; - } - - public int KillExtraGroupSuite(int groupId, int suite) { - logger.debug("[LUA] Call KillExtraGroupSuite with {},{}", groupId, suite); - - SceneGroup group = getSceneScriptManager().getGroupById(groupId); - if (group == null || group.monsters == null) { - return 1; - } - var suiteData = group.getSuiteByIndex(suite); - if (suiteData == null) { - return 1; - } - - this.getSceneScriptManager().removeGroupSuite(group, suiteData); - - return 0; - } - - // param3 (probably time limit for timed dungeons) - public int ActiveChallenge( - int challengeId, - int challengeIndex, - int timeLimitOrGroupId, - int groupId, - int objectiveKills, - int param5) { - logger.debug( - "[LUA] Call ActiveChallenge with {},{},{},{},{},{}", - challengeId, - challengeIndex, - timeLimitOrGroupId, - groupId, - objectiveKills, - param5); - - var challenge = - ChallengeFactory.getChallenge( - challengeId, - challengeIndex, - timeLimitOrGroupId, - groupId, - objectiveKills, - param5, - getSceneScriptManager().getScene(), - getCurrentGroup().get()); - - if (challenge == null) { - return 1; - } - - if (challenge instanceof DungeonChallenge dungeonChallenge) { - // set if tower first stage (6-1) - dungeonChallenge.setStage( - getSceneScriptManager().getVariables().getOrDefault("stage", -1) == 0); - } - - getSceneScriptManager().getScene().setChallenge(challenge); - challenge.start(); - return 0; - } - - public int GetGroupMonsterCountByGroupId(int groupId) { - logger.debug("[LUA] Call GetGroupMonsterCountByGroupId with {}", groupId); - return (int) - getSceneScriptManager().getScene().getEntities().values().stream() - .filter(e -> e instanceof EntityMonster && e.getGroupId() == groupId) - .count(); - } - - public int GetGroupVariableValue(String var) { - logger.debug("[LUA] Call GetGroupVariableValue with {}", var); - return getSceneScriptManager().getVariables().getOrDefault(var, 0); - } - - public int SetGroupVariableValue(String var, int value) { - logger.debug("[LUA] Call SetGroupVariableValue with {},{}", var, value); - getSceneScriptManager().getVariables().put(var, value); - return 0; - } - - public LuaValue ChangeGroupVariableValue(String var, int value) { - logger.debug("[LUA] Call ChangeGroupVariableValue with {},{}", var, value); - - getSceneScriptManager() - .getVariables() - .put(var, getSceneScriptManager().getVariables().get(var) + value); - return LuaValue.ZERO; - } - - /** Set the actions and triggers to designated group */ - public int RefreshGroup(LuaTable table) { - logger.debug("[LUA] Call RefreshGroup with {}", printTable(table)); - // Kill and Respawn? - int groupId = table.get("group_id").toint(); - int suite = table.get("suite").toint(); - - SceneGroup group = getSceneScriptManager().getGroupById(groupId); - - if (group == null || group.monsters == null) { - return 1; - } - - getSceneScriptManager().refreshGroup(group, suite); - - return 0; - } - - public int GetRegionEntityCount(LuaTable table) { - logger.debug("[LUA] Call GetRegionEntityCount with {}", printTable(table)); - int regionId = table.get("region_eid").toint(); - int entityType = table.get("entity_type").toint(); - - var region = this.getSceneScriptManager().getRegionById(regionId); - - if (region == null) { - return 0; - } - - return (int) region.getEntities().stream().filter(e -> e >> 24 == entityType).count(); - } - - public void PrintContextLog(String msg) { - logger.info("[LUA] " + msg); - } - - public int TowerCountTimeStatus(int isDone, int var2) { - logger.debug("[LUA] Call TowerCountTimeStatus with {},{}", isDone, var2); - // TODO record time - return 0; - } - - public int GetGroupMonsterCount() { - logger.debug("[LUA] Call GetGroupMonsterCount "); - - return (int) - getSceneScriptManager().getScene().getEntities().values().stream() - .filter( - e -> - e instanceof EntityMonster - && e.getGroupId() - == getCurrentGroup().map(sceneGroup -> sceneGroup.id).orElse(-1)) - .count(); - } - - public int SetMonsterBattleByGroup(int var1, int var2, int var3) { - logger.debug("[LUA] Call SetMonsterBattleByGroup with {},{},{}", var1, var2, var3); - // TODO - return 0; - } - - public int CauseDungeonFail(int var1) { - logger.debug("[LUA] Call CauseDungeonFail with {}", var1); - - return 0; - } - - public int GetGroupVariableValueByGroup(String name, int groupId) { - logger.debug("[LUA] Call GetGroupVariableValueByGroup with {},{}", name, groupId); - - return getSceneScriptManager().getVariables().getOrDefault(name, 0); - } - - public int SetIsAllowUseSkill(int canUse, int var2) { - logger.debug("[LUA] Call SetIsAllowUseSkill with {},{}", canUse, var2); - - getSceneScriptManager().getScene().broadcastPacket(new PacketCanUseSkillNotify(canUse == 1)); - return 0; - } - - public int KillEntityByConfigId(LuaTable table) { - logger.debug("[LUA] Call KillEntityByConfigId with {}", printTable(table)); - var configId = table.get("config_id"); - if (configId == LuaValue.NIL) { - return 1; - } - - var entity = getSceneScriptManager().getScene().getEntityByConfigId(configId.toint()); - if (entity == null) { - return 0; - } - getSceneScriptManager().getScene().killEntity(entity, 0); - return 0; - } - - public int SetGroupVariableValueByGroup(String key, int value, int groupId) { - logger.debug("[LUA] Call SetGroupVariableValueByGroup with {},{},{}", key, value, groupId); - - getSceneScriptManager().getVariables().put(key, value); - return 0; - } - - public int CreateMonster(LuaTable table) { - logger.debug("[LUA] Call CreateMonster with {}", printTable(table)); - var configId = table.get("config_id").toint(); - var delayTime = table.get("delay_time").toint(); - - if (getCurrentGroup().isEmpty()) { - return 1; - } - - getSceneScriptManager().spawnMonstersByConfigId(getCurrentGroup().get(), configId, delayTime); - return 0; - } - - public int TowerMirrorTeamSetUp(int team, int var1) { - logger.debug("[LUA] Call TowerMirrorTeamSetUp with {},{}", team, var1); - - getSceneScriptManager().unloadCurrentMonsterTide(); - getSceneScriptManager() - .getScene() - .getPlayers() - .get(0) - .getTowerManager() - .mirrorTeamSetUp(team - 1); - - return 0; - } - - public int CreateGadget(LuaTable table) { - logger.debug("[LUA] Call CreateGadget with {}", printTable(table)); - var configId = table.get("config_id").toint(); - - var group = getCurrentGroup(); - - if (group.isEmpty()) { - return 1; - } - - var gadget = group.get().gadgets.get(configId); - var entity = getSceneScriptManager().createGadget(group.get().id, group.get().block_id, gadget); - - getSceneScriptManager().addEntity(entity); - - return 0; - } - - public int CheckRemainGadgetCountByGroupId(LuaTable table) { - logger.debug("[LUA] Call CheckRemainGadgetCountByGroupId with {}", printTable(table)); - var groupId = table.get("group_id").toint(); - - var count = - getSceneScriptManager().getScene().getEntities().values().stream() - .filter( - g -> g instanceof EntityGadget entityGadget && entityGadget.getGroupId() == groupId) - .count(); - return (int) count; - } - - public int GetGadgetStateByConfigId(int groupId, int configId) { - logger.debug("[LUA] Call GetGadgetStateByConfigId with {},{}", groupId, configId); - - if (groupId == 0) { - groupId = getCurrentGroup().get().id; - } - final int realGroupId = groupId; - var gadget = - getSceneScriptManager().getScene().getEntities().values().stream() - .filter( - g -> - g instanceof EntityGadget entityGadget - && entityGadget.getGroupId() == realGroupId) - .filter(g -> g.getConfigId() == configId) - .findFirst(); - if (gadget.isEmpty()) { - return 1; - } - return ((EntityGadget) gadget.get()).getState(); - } - - public int MarkPlayerAction(int var1, int var2, int var3, int var4) { - logger.debug("[LUA] Call MarkPlayerAction with {},{},{},{}", var1, var2, var3, var4); - - return 0; - } - - public int AddQuestProgress(String var1) { - logger.debug("[LUA] Call AddQuestProgress with {}", var1); - - for (var player : getSceneScriptManager().getScene().getPlayers()) { - player.getQuestManager().triggerEvent(QuestTrigger.QUEST_COND_LUA_NOTIFY, var1); - player.getQuestManager().triggerEvent(QuestContent.QUEST_CONTENT_LUA_NOTIFY, var1); - } - - return 0; - } - - /** change the state of gadget */ - public int ChangeGroupGadget(LuaTable table) { - logger.debug("[LUA] Call ChangeGroupGadget with {}", printTable(table)); - var configId = table.get("config_id").toint(); - var state = table.get("state").toint(); - - var entity = getSceneScriptManager().getScene().getEntityByConfigId(configId); - if (entity == null) { - return 1; - } - - if (entity instanceof EntityGadget entityGadget) { - entityGadget.updateState(state); - return 0; - } - - return 1; - } - - public int GetEntityType(int entityId) { - var entity = getSceneScriptManager().getScene().getEntityById(entityId); - if (entity == null) { - // check players - Player player = DatabaseHelper.getPlayerByUid(entityId); - if (player != null) { - return EntityType.Avatar.getValue(); - } - - return EntityType.None.getValue(); - } - - return entity.getEntityType(); - } - - public int GetQuestState(int entityId, int questId) { - var player = getSceneScriptManager().getScene().getWorld().getHost(); - - var quest = player.getQuestManager().getQuestById(questId); - if (quest == null) { - return QuestState.QUEST_STATE_NONE.getValue(); - } - - return quest.getState().getValue(); - } - - public int ShowReminder(int reminderId) { - getSceneScriptManager() - .getScene() - .broadcastPacket(new PacketDungeonShowReminderNotify(reminderId)); - return 0; - } - - public int RemoveEntityByConfigId(int groupId, int entityType, int configId) { - logger.debug("[LUA] Call RemoveEntityByConfigId"); - - var entity = - getSceneScriptManager().getScene().getEntities().values().stream() - .filter(e -> e.getGroupId() == groupId) - .filter(e -> e.getEntityType() == entityType) - .filter(e -> e.getConfigId() == configId) - .findFirst(); - - if (entity.isEmpty()) { - return 1; - } - - getSceneScriptManager().getScene().removeEntity(entity.get()); - - return 0; - } -} +package emu.grasscutter.scripts; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.activity.ActivityManager; +import emu.grasscutter.game.dungeons.challenge.DungeonChallenge; +import emu.grasscutter.game.dungeons.challenge.enums.FatherChallengeProperty; +import emu.grasscutter.game.dungeons.challenge.factory.ChallengeFactory; +import emu.grasscutter.game.entity.*; +import emu.grasscutter.game.entity.gadget.GadgetWorktop; +import emu.grasscutter.game.entity.gadget.platform.ConfigRoute; +import emu.grasscutter.game.entity.gadget.platform.PointArrayRoute; +import emu.grasscutter.game.props.ClimateType; +import emu.grasscutter.game.props.EntityType; +import emu.grasscutter.game.quest.enums.QuestCond; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.game.quest.enums.QuestState; +import emu.grasscutter.game.world.SceneGroupInstance; +import emu.grasscutter.net.proto.EnterTypeOuterClass; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.constants.GroupKillPolicy; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.scripts.data.SceneObject; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.packet.send.*; +import emu.grasscutter.utils.Position; +import io.netty.util.concurrent.FastThreadLocal; +import lombok.val; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.*; + +import static emu.grasscutter.game.props.EnterReason.Lua; +import static emu.grasscutter.scripts.ScriptUtils.luaToPos; +import static emu.grasscutter.scripts.ScriptUtils.posToLua; +import static emu.grasscutter.scripts.constants.GroupKillPolicy.*; + +@SuppressWarnings("unused") +public class ScriptLib { + public static final Logger logger = LoggerFactory.getLogger(ScriptLib.class); + private final FastThreadLocal sceneScriptManager; + private final FastThreadLocal currentGroup; + private final FastThreadLocal callParams; + private final FastThreadLocal currentEntity; + + public ScriptLib() { + this.sceneScriptManager = new FastThreadLocal<>(); + this.currentGroup = new FastThreadLocal<>(); + this.callParams = new FastThreadLocal<>(); + this.currentEntity = new FastThreadLocal<>(); + } + + public void setSceneScriptManager(SceneScriptManager sceneScriptManager){ + this.sceneScriptManager.set(sceneScriptManager); + } + + public void removeSceneScriptManager(){ + this.sceneScriptManager.remove(); + } + + public SceneScriptManager getSceneScriptManager() { + // normally not null + return Optional.of(sceneScriptManager.get()).get(); + } + + private String printTable(LuaTable table){ + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for(var meta : table.keys()){ + sb.append(meta).append(":").append(table.get(meta)).append(","); + } + sb.append("}"); + return sb.toString(); + } + public void setCurrentGroup(SceneGroup currentGroup){ + this.currentGroup.set(currentGroup); + } + public void setCurrentCallParams(ScriptArgs callArgs){ + this.callParams.set(callArgs); + } + public Optional getCurrentGroup(){ + return Optional.of(this.currentGroup.get()); + } + public void removeCurrentGroup(){ + this.currentGroup.remove(); + } + + + public void setCurrentEntity(GameEntity currentGroup){ + this.currentEntity.set(currentGroup); + } + public void removeCurrentEntity(){ + this.currentEntity.remove(); + } + public Optional getCurrentEntity(){ + return Optional.of(this.currentEntity.get()); + } + + public int SetGadgetStateByConfigId(int configId, int gadgetState) { + logger.debug("[LUA] Call SetGadgetStateByConfigId with {},{}", + configId,gadgetState); + GameEntity entity = getSceneScriptManager().getScene().getEntityByConfigId(configId); + + if (!(entity instanceof EntityGadget)) { + return 1; + } + + ((EntityGadget) entity).updateState(gadgetState); + return 0; + } + + public int SetGroupGadgetStateByConfigId(int groupId, int configId, int gadgetState) { + logger.debug("[LUA] Call SetGroupGadgetStateByConfigId with {},{},{}", + groupId,configId,gadgetState); + + val entity = getSceneScriptManager().getScene().getEntityByConfigId(configId, groupId); + if(!(entity instanceof EntityGadget)){ + return -1; + } + ((EntityGadget) entity).updateState(gadgetState); + + return 0; + } + + public int SetWorktopOptionsByGroupId(int groupId, int configId, int[] options) { + logger.debug("[LUA] Call SetWorktopOptionsByGroupId with {},{},{}", + groupId,configId,options); + + val entity = getSceneScriptManager().getScene().getEntityByConfigId(configId, groupId); + + if (!(entity instanceof EntityGadget gadget)) { + return 1; + } + + if (!(gadget.getContent() instanceof GadgetWorktop worktop)) { + return 2; + } + + worktop.addWorktopOptions(options); + getSceneScriptManager().getScene().broadcastPacket(new PacketWorktopOptionNotify(gadget)); + + return 0; + } + + public int SetWorktopOptions(LuaTable table){ + logger.debug("[LUA] Call SetWorktopOptions with {}", printTable(table)); + var callParams = this.callParams.getIfExists(); + var group = this.currentGroup.getIfExists(); + if(callParams == null || group == null){ + return 1; + } + var configId = callParams.param1; + var entity = getSceneScriptManager().getScene().getEntityByConfigId(configId); + + + int[] worktopOptions = new int[table.length()]; + for(int i = 1 ;i<=table.length() ;i++){ + worktopOptions[i-1] = table.get(i).optint(-1); + } + if(!(entity instanceof EntityGadget gadget)|| worktopOptions.length == 0){ + return 2; + } + + if (!(gadget.getContent() instanceof GadgetWorktop worktop)) { + return 3; + } + + worktop.addWorktopOptions(worktopOptions); + var scene = getSceneScriptManager().getScene(); + Grasscutter.getGameServer().getScheduler().scheduleDelayedTask(() -> { + scene.broadcastPacket(new PacketWorktopOptionNotify(gadget)); + },1); + return 0; + } + + public int DelWorktopOptionByGroupId(int groupId, int configId, int option) { + logger.debug("[LUA] Call DelWorktopOptionByGroupId with {},{},{}",groupId,configId,option); + + + val entity = getSceneScriptManager().getScene().getEntityByConfigId(configId, groupId); + + if (!(entity instanceof EntityGadget gadget)) { + return 1; + } + + if (!(gadget.getContent() instanceof GadgetWorktop worktop)) { + return 1; + } + + worktop.removeWorktopOption(option); + getSceneScriptManager().getScene().broadcastPacket(new PacketWorktopOptionNotify(gadget)); + + return 0; + } + public int DelWorktopOption(int var1){ + logger.warn("[LUA] Call unimplemented DelWorktopOption with {}", var1); + var callParams = this.callParams.getIfExists(); + var group = this.currentGroup.getIfExists(); + if(callParams == null || group == null){ + return 1; + } + var configId = callParams.param1; + var entity = getSceneScriptManager().getScene().getEntityByConfigId(configId); + if (!(entity instanceof EntityGadget gadget)) { + return 1; + } + + if (!(gadget.getContent() instanceof GadgetWorktop worktop)) { + return 2; + } + + worktop.removeWorktopOption(callParams.param2); + + var scene = getSceneScriptManager().getScene(); + Grasscutter.getGameServer().getScheduler().scheduleDelayedTask(() -> { + scene.broadcastPacket(new PacketWorktopOptionNotify(gadget)); + },1); + + return 0; + } + + // Some fields are guessed + public int AutoMonsterTide(int challengeIndex, int groupId, Integer[] ordersConfigId, int tideCount, int sceneLimit, int param6) { + logger.debug("[LUA] Call AutoMonsterTide with {},{},{},{},{},{}", + challengeIndex,groupId,ordersConfigId,tideCount,sceneLimit,param6); + + SceneGroup group = getSceneScriptManager().getGroupById(groupId); + + if (group == null || group.monsters == null) { + return 1; + } + + this.getSceneScriptManager().startMonsterTideInGroup(group, ordersConfigId, tideCount, sceneLimit); + + return 0; + } + + public int AddExtraGroupSuite(int groupId, int suite) { + logger.debug("[LUA] Call AddExtraGroupSuite with {},{}", + groupId,suite); + SceneGroup group = getSceneScriptManager().getGroupById(groupId); + SceneGroupInstance groupInstance = getSceneScriptManager().getGroupInstanceById(groupId); + + if (group == null || groupInstance == null || group.monsters == null) { + return 1; + } + var suiteData = group.getSuiteByIndex(suite); + if(suiteData == null){ + Grasscutter.getLogger().warn("trying to get suite that doesn't exist: {} {}", groupId, suite); + return 1; + } + // avoid spawn wrong monster + if(getSceneScriptManager().getScene().getChallenge() != null) + if(!getSceneScriptManager().getScene().getChallenge().inProgress() || + getSceneScriptManager().getScene().getChallenge().getGroup().id != groupId){ + return 0; + } + this.getSceneScriptManager().addGroupSuite(groupInstance, suiteData); + + return 0; + } + public int GoToGroupSuite(int groupId, int suite) { + logger.debug("[LUA] Call GoToGroupSuite with {},{}", + groupId,suite); + SceneGroup group = getSceneScriptManager().getGroupById(groupId); + SceneGroupInstance groupInstance = getSceneScriptManager().getGroupInstanceById(groupId); + if (group == null || groupInstance == null || group.monsters == null) { + return 1; + } + var suiteData = group.getSuiteByIndex(suite); + if(suiteData == null){ + return 1; + } + + /*for(var suiteItem : group.suites){ + if(suiteData == suiteItem){ + continue; + } + this.getSceneScriptManager().removeGroupSuite(group, suiteItem); + }*/ + if(groupInstance.getActiveSuiteId() == 0 || groupInstance.getActiveSuiteId() != suite) { + groupInstance.getDeadEntities().clear(); + this.getSceneScriptManager().addGroupSuite(groupInstance, suiteData); + groupInstance.setActiveSuiteId(suite); + } + + return 0; + } + public int RemoveExtraGroupSuite(int groupId, int suite) { + logger.debug("[LUA] Call RemoveExtraGroupSuite with {},{}", + groupId,suite); + + SceneGroup group = getSceneScriptManager().getGroupById(groupId); + if (group == null || group.monsters == null) { + return 1; + } + var suiteData = group.getSuiteByIndex(suite); + if(suiteData == null){ + return 1; + } + + this.getSceneScriptManager().removeGroupSuite(group, suiteData); + + return 0; + } + public int KillExtraGroupSuite(int groupId, int suite) { + logger.debug("[LUA] Call KillExtraGroupSuite with {},{}", + groupId,suite); + + SceneGroup group = getSceneScriptManager().getGroupById(groupId); + if (group == null || group.monsters == null) { + return 1; + } + var suiteData = group.getSuiteByIndex(suite); + if(suiteData == null){ + return 1; + } + + this.getSceneScriptManager().killGroupSuite(group, suiteData); + + return 0; + } + // param3 (probably time limit for timed dungeons) + public int ActiveChallenge(int challengeId, int challengeIndex, int timeLimitOrGroupId, int groupId, int objectiveKills, int param5) { + logger.debug("[LUA] Call ActiveChallenge with {},{},{},{},{},{}", + challengeId,challengeIndex,timeLimitOrGroupId,groupId,objectiveKills,param5); + + var challenge = ChallengeFactory.getChallenge( + challengeId, + challengeIndex, + timeLimitOrGroupId, + groupId, + objectiveKills, + param5, + getSceneScriptManager().getScene(), + getCurrentGroup().get() + ); + + if(challenge == null){ + return 1; + } + + if(challenge instanceof DungeonChallenge dungeonChallenge){ + // set if tower first stage (6-1) + dungeonChallenge.setStage(getSceneScriptManager().getVariables(groupId).getOrDefault("stage", -1) == 0); + } + + getSceneScriptManager().getScene().setChallenge(challenge); + challenge.start(); + return 0; + } + + public int GetGroupMonsterCountByGroupId(int groupId) { + logger.debug("[LUA] Call GetGroupMonsterCountByGroupId with {}", + groupId); + return (int) getSceneScriptManager().getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityMonster && e.getGroupId() == groupId) + .count(); + } + + public int CreateVariable(String type, Object value) { + logger.warn("[LUA] Call unimplemented CreateVariable with {} {}", + type, value); + //TODO implement + switch (type){ + case "int": + default: + logger.warn("[LUA] Call CreateVariable with unsupported type {} and value {}", type, value); + } + return 0; + } + public int SetVariableValue(int var1) { + logger.warn("[LUA] Call unimplemented SetVariableValue with {}", + var1); + //TODO implement var1 type + return 0; + } + public int GetVariableValue(int var1) { + logger.warn("[LUA] Call unimplemented GetVariableValue with {}", + var1); + //TODO implement var1 type + return 0; + } + public int GetGroupVariableValue(String var) { + logger.debug("[LUA] Call GetGroupVariableValue with {}", + var); + return getSceneScriptManager().getVariables(currentGroup.get().id).getOrDefault(var, 0); + } + + public int SetGroupVariableValue(String var, int value) { + logger.debug("[LUA] Call SetGroupVariableValue with {},{}", + var, value); + + val groupId= currentGroup.get().id; + val variables = getSceneScriptManager().getVariables(groupId); + + val old = variables.getOrDefault(var, value); + variables.put(var, value); + getSceneScriptManager().callEvent(new ScriptArgs(groupId, EventType.EVENT_VARIABLE_CHANGE, value, old)); + return 0; + } + + public LuaValue ChangeGroupVariableValue(String var, int value) { + logger.debug("[LUA] Call ChangeGroupVariableValue with {},{}", + var, value); + + val groupId= currentGroup.get().id; + val variables = getSceneScriptManager().getVariables(groupId); + + val old = variables.getOrDefault(var, 0); + variables.put(var, old + value); + logger.debug("[LUA] Call ChangeGroupVariableValue with {},{}", + old, old+value); + getSceneScriptManager().callEvent(new ScriptArgs(groupId, EventType.EVENT_VARIABLE_CHANGE, old+value, old)); + return LuaValue.ZERO; + } + + /** + * Set the actions and triggers to designated group + */ + public int RefreshGroup(LuaTable table) { + logger.debug("[LUA] Call RefreshGroup with {}", + printTable(table)); + // Kill and Respawn? + int groupId = table.get("group_id").toint(); + int suite = table.get("suite").toint(); + + SceneGroupInstance groupInstance = getSceneScriptManager().getGroupInstanceById(groupId); + + if (groupInstance == null) { + logger.warn("[LUA] trying to refresh unloaded group {}", groupId); + return 1; + } + + getSceneScriptManager().refreshGroup(groupInstance, suite, false); + + return 0; + } + + public int GetRegionEntityCount(LuaTable table) { + logger.debug("[LUA] Call GetRegionEntityCount with {}", + printTable(table)); + int regionId = table.get("region_eid").toint(); + int entityType = table.get("entity_type").toint(); + + var region = this.getSceneScriptManager().getRegionById(regionId); + + if (region == null) { + return 0; + } + + return (int) region.getEntities().stream().filter(e -> e >> 24 == entityType).count(); + } + + private void printLog(String source, String msg){ + var currentGroup = this.currentGroup.getIfExists(); + if(currentGroup!=null) { + logger.debug("[LUA] {} {} {}", source, currentGroup.id, msg); + } else { + logger.debug("[LUA] {} {}", source, msg); + } + } + + public void PrintContextLog(String msg) { + printLog("PrintContextLog", msg); + } + public void PrintLog(String msg) { + printLog("PrintLog", msg); + } + + public int TowerCountTimeStatus(int isDone, int var2){ + logger.debug("[LUA] Call TowerCountTimeStatus with {},{}", + isDone,var2); + // TODO record time + return 0; + } + public int GetGroupMonsterCount(){ + logger.debug("[LUA] Call GetGroupMonsterCount "); + + return (int) getSceneScriptManager().getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityMonster && + e.getGroupId() == getCurrentGroup().map(sceneGroup -> sceneGroup.id).orElse(-1)) + .count(); + } + + public int SetMonsterBattleByGroup(int configId, int groupId) { + logger.debug("[LUA] Call SetMonsterBattleByGroup with {} {}", + configId,groupId); + // TODO implement scene50008_group250008057.lua uses incomplete group numbers + return 0; + } + + public int CauseDungeonFail(){ + logger.debug("[LUA] Call CauseDungeonFail with"); + + var scriptManager = sceneScriptManager.getIfExists(); + if(scriptManager==null){ + return 1; + } + + var dungeonManager = scriptManager.getScene().getDungeonManager(); + if(dungeonManager==null){ + return 2; + } + + dungeonManager.failDungeon(); + return 0; + } + + public int SetEntityServerGlobalValueByConfigId(int cfgId, String sgvName, int value){ + logger.warn("[LUA] Call unimplemented SetEntityServerGlobalValueByConfigId with {} {} {}", cfgId, sgvName, value); + //TODO implement + return 0; + } + + public int GetGroupVariableValueByGroup(String name, int groupId){ + logger.debug("[LUA] Call GetGroupVariableValueByGroup with {},{}", + name,groupId); + + return getSceneScriptManager().getVariables(groupId).getOrDefault(name, 0); + } + public int ChangeGroupVariableValueByGroup(String name, int value, int groupId){ + logger.debug("[LUA] Call ChangeGroupVariableValueByGroup with {},{}", + name,groupId); + //TODO test + getSceneScriptManager().getVariables(groupId).put(name, value); + return 0; + } + + public int SetIsAllowUseSkill(int canUse){ + logger.debug("[LUA] Call SetIsAllowUseSkill with {}", + canUse); + + getSceneScriptManager().getScene().broadcastPacket(new PacketCanUseSkillNotify(canUse == 1)); + return 0; + } + + public int KillEntityByConfigId(LuaTable table){ + logger.debug("[LUA] Call KillEntityByConfigId with {}", + printTable(table)); + var configId = table.get("config_id"); + if(configId == LuaValue.NIL){ + return 1; + } + + var entity = getSceneScriptManager().getScene().getEntityByConfigId(configId.toint()); + if(entity == null){ + return 0; + } + getSceneScriptManager().getScene().killEntity(entity, 0); + return 0; + } + + public int SetGroupVariableValueByGroup(String key, int value, int groupId){ + logger.debug("[LUA] Call SetGroupVariableValueByGroup with {},{},{}", + key,value,groupId); + + getSceneScriptManager().getVariables(groupId).put(key, value); + return 0; + } + + public int CreateMonster(LuaTable table){ + logger.debug("[LUA] Call CreateMonster with {}", + printTable(table)); + var configId = table.get("config_id").toint(); + var delayTime = table.get("delay_time").toint(); + + if(getCurrentGroup().isEmpty()){ + return 1; + } + + getSceneScriptManager().spawnMonstersByConfigId(getCurrentGroup().get(), configId, delayTime); + return 0; + } + + public int TowerMirrorTeamSetUp(int team, int var1) { + logger.debug("[LUA] Call TowerMirrorTeamSetUp with {},{}", + team,var1); + + getSceneScriptManager().unloadCurrentMonsterTide(); + getSceneScriptManager().getScene().getPlayers().get(0).getTowerManager().mirrorTeamSetUp(team-1); + + return 0; + } + + public int CreateGadget(LuaTable table){ + logger.debug("[LUA] Call CreateGadget with {}", + printTable(table)); + var configId = table.get("config_id").toint(); + + var group = getCurrentGroup(); + + if (group.isEmpty()) { + return 1; + } + createGadget(configId, group.get()); + + return 0; + } + + private GameEntity createGadget(int configId, SceneGroup group){ + var gadget = group.gadgets.get(configId); + var entity = getSceneScriptManager().createGadget(group.id, group.block_id, gadget); + if(entity==null){ + logger.warn("[LUA] Create gadget null with cid: {} gid: {} bid: {}", configId, group.id, group.block_id); + return null; + } + + getSceneScriptManager().addEntity(entity); + return entity; + } + + public int CheckRemainGadgetCountByGroupId(LuaTable table){ + logger.debug("[LUA] Call CheckRemainGadgetCountByGroupId with {}", + printTable(table)); + var groupId = table.get("group_id").toint(); + + var count = getSceneScriptManager().getScene().getEntities().values().stream() + .filter(g -> g instanceof EntityGadget entityGadget && entityGadget.getGroupId() == groupId) + .count(); + return (int)count; + } + + public int GetGadgetStateByConfigId(int groupId, int configId){ + logger.debug("[LUA] Call GetGadgetStateByConfigId with {},{}", + groupId, configId); + + val scene = getSceneScriptManager().getScene(); + val gadget = groupId == 0 ? scene.getEntityByConfigId(configId) : scene.getEntityByConfigId(configId, groupId); + if(!(gadget instanceof EntityGadget)){ + return -1; + } + return ((EntityGadget)gadget).getState(); + } + + public int MarkPlayerAction(int var1, int var2, int var3){ + logger.debug("[LUA] Call MarkPlayerAction with {},{},{}", + var1, var2,var3); + + return 0; + } + + public int AddQuestProgress(String var1){ + logger.debug("[LUA] Call AddQuestProgress with {}", + var1); + + for(var player : getSceneScriptManager().getScene().getPlayers()){ + player.getQuestManager().queueEvent(QuestCond.QUEST_COND_LUA_NOTIFY, var1); + player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_LUA_NOTIFY, var1); + } + + return 0; + } + + /** + * change the state of gadget + */ + public int ChangeGroupGadget(LuaTable table){ + logger.debug("[LUA] Call ChangeGroupGadget with {}", + printTable(table)); + var configId = table.get("config_id").toint(); + var state = table.get("state").toint(); + + var entity = getSceneScriptManager().getScene().getEntityByConfigId(configId); + if(entity == null){ + return 1; + } + + if (entity instanceof EntityGadget entityGadget) { + entityGadget.updateState(state); + return 0; + } + + return 1; + } + + public int GetEntityType(int entityId){ + var entity = getSceneScriptManager().getScene().getEntityById(entityId); + if(entity == null){ + return EntityType.None.getValue(); + } + + return entity.getEntityType(); + } + + public int GetQuestState(int entityId, int questId){ + val player = getSceneScriptManager().getScene().getWorld().getHost(); + + val quest = player.getQuestManager().getQuestById(questId); + if(quest == null){ + return QuestState.QUEST_STATE_NONE.getValue(); + } + + return quest.getState().getValue(); + } + + public int ShowReminder(int reminderId){ + getSceneScriptManager().getScene().broadcastPacket(new PacketDungeonShowReminderNotify(reminderId)); + return 0; + } + + public int RemoveEntityByConfigId(int groupId, int entityType, int configId){ + logger.debug("[LUA] Call RemoveEntityByConfigId"); + + val entity = getSceneScriptManager().getScene().getEntityByConfigId(configId, groupId); + + if(entity == null || entity.getEntityType() != entityType){ + return 1; + } + + getSceneScriptManager().getScene().removeEntity(entity); + + return 0; + } + + public int CreateGroupTimerEvent(int groupID, String source, double time) { + return sceneScriptManager.get().createGroupTimerEvent(groupID, source, time); + } + + public int CancelGroupTimerEvent(int groupID, String source) { + return sceneScriptManager.get().cancelGroupTimerEvent(groupID, source); + } + + public int GetGroupSuite(int groupId) { + //logger.warn("[LUA] Call GetGroupSuite with {}", groupID); + var instance = getSceneScriptManager().getGroupInstanceById(groupId); + if(instance != null) return instance.getActiveSuiteId(); + return 0; + } + public int SetGroupReplaceable(int groupId, boolean value) { + logger.warn("[LUA] Call SetGroupReplaceable with {} {}", groupId, value); + + var group = getSceneScriptManager().getGroupById(groupId); + if(group != null && group.is_replaceable != null) { + group.is_replaceable.value = value; + return 0; + } + return 1; + } + + public LuaTable GetSceneUidList(){ + logger.warn("[LUA] Call unchecked GetSceneUidList"); + //TODO check + var scriptManager = sceneScriptManager.getIfExists(); + if(scriptManager == null){ + return new LuaTable(); + } + var players = scriptManager.getScene().getPlayers(); + var result = new LuaTable(); + for(int i = 0; i< players.size(); i++){ + result.set(Integer.toString(i+1), players.get(i).getUid()); + } + return result; + } + + public int GetSeaLampActivityPhase(){ + logger.warn("[LUA] Call unimplemented GetSeaLampActivityPhase"); + //TODO implement + return 0; + } + public int GadgetPlayUidOp(int groupId, int gadget_crucible, int var3, int var4, String var5, int var6 ){ + logger.warn("[LUA] Call unimplemented GadgetPlayUidOp with {}, {}, {}, {}, {}, {}", groupId, gadget_crucible, var3, var4, var5, var6); + //TODO implement + return 0; + } + public long GetServerTime(){ + logger.debug("[LUA] Call GetServerTime"); + //TODO check + return new Date().getTime(); + } + public long GetServerTimeByWeek(){ + logger.debug("[LUA] Call GetServerTimeByWeek"); + return Calendar.getInstance().get(Calendar.DAY_OF_WEEK); + } + public int GetCurTriggerCount(){ + logger.debug("[LUA] Call GetCurTriggerCount"); + //TODO check + return getSceneScriptManager().getTriggerCount(); + } + public int GetChannellerSlabLoopDungeonLimitTime(){ + logger.warn("[LUA] Call unimplemented GetChannellerSlabLoopDungeonLimitTime"); + //TODO implement + return 0; + } + public int IsPlayerAllAvatarDie(int sceneUid){ + logger.warn("[LUA] Call unimplemented IsPlayerAllAvatarDie {}", sceneUid); + var playerEntities = getSceneScriptManager().getScene().getEntities().values().stream().filter(e -> e.getEntityType() == EntityType.Avatar.getValue()).toList(); + for (GameEntity p : playerEntities){ + var player = (EntityAvatar)p; + if(player.isAlive()){ + return 0; + } + } + //TODO check + return 1; + } + + public int sendShowCommonTipsToClient(String title, String content, int closeTime) { + logger.debug("[LUA] Call sendShowCommonTipsToClient with {}, {}, {}", title, content, closeTime); + sceneScriptManager.get().getScene().broadcastPacket(new PacketShowCommonTipsNotify(title, content, closeTime)); + return 0; + } + + public int sendCloseCommonTipsToClient(){ + logger.debug("[LUA] Call unimplemented sendCloseCommonTipsToClient"); + sceneScriptManager.get().getScene().broadcastPacket(new PacketCloseCommonTipsNotify()); + return 0; + } + + public int CreateFatherChallenge(int var1, int var2, int var3, LuaTable var4){ + logger.warn("[LUA] Call unimplemented CreateFatherChallenge with {} {} {} {}", var1, var2, var3, var4); + //TODO implement var4 object has int success, int fail, bool fail_on_wipe + return 0; + } + public int StartFatherChallenge(int var1){ + logger.warn("[LUA] Call unimplemented StartFatherChallenge with {}", var1); + //TODO implement + return 0; + } + public int ModifyFatherChallengeProperty(int challengeId, int propertyTypeIndex, int value){ + val propertyType = FatherChallengeProperty.values()[propertyTypeIndex]; + logger.warn("[LUA] Call unimplemented ModifyFatherChallengeProperty with {} {} {}", challengeId, propertyType.name(), value); + //TODO implement + return 0; + } + public int AttachChildChallenge(int var1, int var2, int var3, int[] var4, LuaTable var5, LuaTable var6){ + logger.warn("[LUA] Call unimplemented AttachChildChallenge with {} {} {} {} {} {}", var1, var2, var3, var4, var5, var6); + //TODO implement var6 object has int success, int fail, bool fail_on_wipe + return 0; + } + public int CreateEffigyChallengeMonster(int var1, int[] var2){ + logger.warn("[LUA] Call unimplemented CreateEffigyChallengeMonster with {} {}", var1, var2); + //TODO implement + return 0; + } + public int GetEffigyChallengeMonsterLevel(){ + logger.warn("[LUA] Call unimplemented CreateEffigyChallengeMonster"); + //TODO implement + return 0; + } + public int AddTeamEntityGlobalFloatValue(int[] sceneUidList, String var2, int var3){ + logger.warn("[LUA] Call unimplemented AddTeamEntityGlobalFloatValue with {} {} {}", sceneUidList, var2, var3); + //TODO implement + return 0; + } + public int CreateBlossomChestByGroupId(int groupId, int var2){ + logger.warn("[LUA] Call unimplemented SetBlossomScheduleStateByGroupId with {} {}", groupId, var2); + //TODO implement + return 0; + } + public int SetBlossomScheduleStateByGroupId(int groupId, int scene){ + logger.warn("[LUA] Call unimplemented SetBlossomScheduleStateByGroupId with {} {}", groupId, scene); + //TODO implement scene is guessed + return 0; + } + public int RefreshBlossomGroup(LuaTable var1){ + logger.warn("[LUA] Call unimplemented RefreshBlossomGroup with {}", printTable(var1)); + //TODO implement var3 has int group_id, int suite, bool exclude_prev + return 0; + } + public int RefreshBlossomDropRewardByGroupId(int groupId){ + logger.warn("[LUA] Call unimplemented RefreshBlossomDropRewardByGroupId with {}", groupId); + //TODO implement + return 0; + } + public int AddBlossomScheduleProgressByGroupId(int groupId){ + logger.warn("[LUA] Call unimplemented AddBlossomScheduleProgressByGroupId with {}", groupId); + //TODO implement + return 0; + } + public int RefreshHuntingClueGroup(){ + logger.warn("[LUA] Call unimplemented RefreshHuntingClueGroup"); //TODO: Much many calls o this garbages the log + //TODO implement + return 0; + } + public int GetHuntingMonsterExtraSuiteIndexVec(){ + logger.warn("[LUA] Call unimplemented GetHuntingMonsterExtraSuiteIndexVec"); + //TODO implement + return 0; + } + public int SetGroupTempValue(String name, int value, LuaTable var3){ + logger.warn("[LUA] Call unimplemented SetGroupTempValue with {} {} {}", name, value, printTable(var3)); + //TODO implement var3 has int group_id + return 0; + } + public int GetGroupTempValue(String name, LuaTable var2){ + logger.warn("[LUA] Call unimplemented GetGroupTempValue with {} {}", name, printTable(var2)); + //TODO implement var3 has int group_id + return 0; + } + + public int FinishExpeditionChallenge(){ + logger.warn("[LUA] unimplemented Call FinishExpeditionChallenge"); + //TODO implement + return 0; + } + public int ExpeditionChallengeEnterRegion(boolean var1){ + logger.warn("[LUA] unimplemented Call ExpeditionChallengeEnterRegion with {}", var1); + //TODO implement + return 0; + } + + public int StartSealBattle(int gadgetId, LuaTable var2){ + logger.warn("[LUA] unimplemented Call StartSealBattle with {} {}", gadgetId, printTable(var2)); + //TODO implement var2 containt int radius, int battle_time, int monster_group_id, int default_kill_charge, int auto_charge, int auto_decline, int max_energy, SealBattleType battleType + // for type KILL_MONSTER watch group monster_group_id and afterwards trigger EVENT_SEAL_BATTLE_END with the result in param2 + return 0; + } + + public int InitTimeAxis(String var1, int[] var2, boolean var3){ + logger.warn("[LUA] Call unimplemented InitTimeAxis with {} {} {}", var1, var2, var3); + //TODO implement var1 == name? var2 == delay? var3 == should loop? + return 0; + } + public int EndTimeAxis(String var1){ + logger.warn("[LUA] Call unimplemented EndTimeAxis with {} {} {}", var1); + //TODO implement var1 == name? + return 0; + } + + public int SetTeamEntityGlobalFloatValue(int[] sceneUidList, String var2, int var3){ + logger.warn("[LUA] Call unimplemented SetTeamEntityGlobalFloatValue with {} {} {}", sceneUidList, var2, var3); + //TODO implement + return 0; + } + + public int SetTeamServerGlobalValue(int sceneUid, String var2, int var3){ + logger.warn("[LUA] Call unimplemented SetTeamServerGlobalValue with {} {} {}", sceneUid, var2, var3); + //TODO implement + return 0; + } + + public int GetLanternRiteValue(){ + logger.warn("[LUA] Call unimplemented GetLanternRiteValue"); + //TODO implement + return 0; + } + + public int CreateMonsterFaceAvatar(LuaTable var1){ + logger.warn("[LUA] Call unimplemented CreateMonsterFaceAvatar with {}", printTable(var1)); + //TODO implement var1 contains int entity_id, int[] monsters (cfgIds), int[] ranges, int angle + return 0; + } + + public int ChangeToTargetLevelTag(int var1){ + logger.warn("[LUA] Call unimplemented ChangeToTargetLevelTag with {}", var1); + //TODO implement + return 0; + } + + public int AddSceneTag(int sceneId, int sceneTagId){ + logger.warn("[LUA] Call unimplemented AddSceneTag with {}, {}", sceneId, sceneTagId); + //TODO implement + return 0; + } + + public int DelSceneTag(int sceneId, int sceneTagId){ + logger.warn("[LUA] Call unimplemented DelSceneTag with {}, {}", sceneId, sceneTagId); + //TODO implement + return 0; + } + + public boolean CheckSceneTag(int sceneId, int sceneTagId){ + logger.warn("[LUA] Call unimplemented CheckSceneTag with {}, {}", sceneId, sceneTagId); + //TODO implement + return false; + } + public int StartHomeGallery(int galleryId, int uid){ + logger.warn("[LUA] Call unimplemented StartHomeGallery with {} {}", galleryId, uid); + //TODO implement + return 0; + } + + public int StopGallery(int galleryId, boolean var2){ + logger.warn("[LUA] Call unimplemented StopGallery with {} {}", galleryId, var2); + //TODO implement + return 0; + } + public int StartGallery(int galleryId){ + logger.warn("[LUA] Call unimplemented StartGallery with {}", galleryId); + //TODO implement + return 0; + } + + public int UpdatePlayerGalleryScore(int galleryId, LuaTable var2){ + logger.warn("[LUA] Call unimplemented UpdatePlayerGalleryScore with {} {}", galleryId, printTable(var2)); + //TODO implement var2 contains int uid + return 0; + } + public int SetHandballGalleryBallPosAndRot(int galleryId, LuaTable position, LuaTable rotation){ + logger.warn("[LUA] Call unimplemented SetHandballGalleryBallPosAndRot with {} {} {}", galleryId, printTable(position), printTable(rotation)); + //TODO implement + return 0; + } + + public int UnlockFloatSignal(int groupId, int gadgetSignalId){ + logger.warn("[LUA] Call unimplemented UnlockFloatSignal with {} {}", groupId, gadgetSignalId); + //TODO implement + return 0; + } + + public int SendServerMessageByLuaKey(String var1, int[] var2){ + logger.warn("[LUA] Call unimplemented SendServerMessageByLuaKey with {} {}", var1, var2); + //TODO implement + return 0; + } + + public int TryReallocateEntityAuthority(int uid, int endConfig, int var3){ + logger.warn("[LUA] Call unimplemented TryReallocateEntityAuthority with {} {} {}", uid, endConfig, var3); + //TODO implement check var3 type + return 0; + } + + public int ForceRefreshAuthorityByConfigId(int var1, int uid){ + logger.warn("[LUA] Call unimplemented ForceRefreshAuthorityByConfigId with {} {}", var1, uid); + //TODO implement check var3 type + return 0; + } + + public int AddPlayerGroupVisionType(int[] uids, int[] var2){ + logger.warn("[LUA] Call unimplemented AddPlayerGroupVisionType with {} {}", uids, var2); + //TODO implement + return 0; + } + + public int DelPlayerGroupVisionType(int[] uids, int[] var2){ + logger.warn("[LUA] Call unimplemented DelPlayerGroupVisionType with {} {}", uids, var2); + //TODO implement + return 0; + } + + public int MoveAvatarByPointArray(int uid, int targetId, LuaTable var3, String var4){ + logger.warn("[LUA] Call unimplemented MoveAvatarByPointArray with {} {} {} {}", uid, targetId, printTable(var3), var4); + //TODO implement var3 contains int speed, var4 is a json string + return 0; + } + + public int MovePlayerToPos(LuaTable var1){ + logger.warn("[LUA] Call unchecked MovePlayerToPos with {}", printTable(var1)); + //TODO implement var1 contains int[] uid_list, Position pos, int radius, Position rot + return TransPlayerToPos(var1); // todo this is probably not a full scene reload + } + + public int TransPlayerToPos(LuaTable var1){ + logger.warn("[LUA] Call unchecked TransPlayerToPos with {}", printTable(var1)); + //TODO implement var1 contains int[] uid_list, Position pos, int radius, Position rot + var targetsTable = var1.get("uid_list"); + var pos = var1.get("pos"); + var rot = var1.get("rot"); + var radius = var1.get("radius"); + if(targetsTable.isnil() || !targetsTable.istable() || targetsTable.length()==0 || pos.isnil()){ + return 1; + } + ArrayList targets = new ArrayList<>(targetsTable.length()); + for (int i = 1; i <= targetsTable.length(); i++) { + targets.add(targetsTable.get(i).optint(-1)); + } + + var x = pos.get("x"); + var y = pos.get("y"); + var z = pos.get("z"); + + var scriptManager = sceneScriptManager.getIfExists(); + if(scriptManager==null || !x.isnumber() || !y.isnumber() || !z.isnumber()){ + return 2; + } + + var targetPos = new Position(x.toint(), y.toint(), z.toint()); + + var scene = scriptManager.getScene(); + scene.getPlayers().stream().filter(p -> targets.contains(p.getUid())).forEach(p -> { + scene.removePlayer(p); + scene.addPlayer(p); + p.getPosition().set(targetPos); + + // Teleport packet + p.sendPacket(new PacketPlayerEnterSceneNotify(p, EnterTypeOuterClass.EnterType.ENTER_TYPE_GOTO, Lua, scene.getId(), targetPos)); + }); + return 0; + } + + public int PlayCutScene(int cutsceneId, int var2){ + logger.warn("[LUA] Call unimplemented PlayCutScene with {} {}", cutsceneId, var2); + sceneScriptManager.get().getScene().broadcastPacket(new PacketCutsceneBeginNotify(cutsceneId)); + //TODO implement + return 0; + } + + public int PlayCutSceneWithParam(int cutsceneId, int var2, LuaTable var3){ + logger.warn("[LUA] Call unimplemented PlayCutScene with {} {}", cutsceneId, var2, var3); + //TODO implement + return 0; + } + + public int ScenePlaySound(LuaTable soundInfo){ + logger.debug("[LUA] Call unimplemented ScenePlaySound with {}", printTable(soundInfo)); + + val luaSoundName = soundInfo.get("sound_name"); + val luaIsBroadcast = soundInfo.get("is_broadcast"); + val luaPlayPosition = soundInfo.get("play_pos"); + val luaPlayType = soundInfo.get("play_type"); + + val soundName = luaSoundName.optjstring(null); + val isBroadcast = luaIsBroadcast.optboolean(true); + val playPosition = luaToPos(luaPlayPosition); + val playType = luaPlayType.optint(0); // TODO + sceneScriptManager.get().getScene().broadcastPacket(new PacketScenePlayerSoundNotify(playPosition, soundName, playType)); + return 0; + } + + public int BeginCameraSceneLook(LuaTable sceneLookParams){ + logger.debug("[LUA] Call BeginCameraSceneLook with {}", printTable(sceneLookParams)); +// var luaLookPos = sceneLookParams.get("look_pos"); +// var luaFollowPos = sceneLookParams.get("follow_pos"); +// var luaDuration = sceneLookParams.get("duration"); +// var luaIsForce = sceneLookParams.get("is_force"); +// var luaIsBroadcast = sceneLookParams.get("is_broadcast"); +// var luaAllowInput = sceneLookParams.get("is_allow_input"); +// var luaSetFollowPos = sceneLookParams.get("is_set_follow_pos"); +// var luaIsForceWalk = sceneLookParams.get("is_force_walk"); +// var luaIsChangePlayMode = sceneLookParams.get("is_change_play_mode"); +// var luaScreenX = sceneLookParams.get("screen_x"); +// var luaScreenY = sceneLookParams.get("screen_y"); +// +// var cameraParams = new PacketBeginCameraSceneLookNotify.CameraSceneLookNotify(); +// cameraParams.setLookPos(luaToPos(luaLookPos)); +// cameraParams.setFollowPos(luaToPos(luaFollowPos)); +// if (luaDuration.isnumber()) { +// cameraParams.setDuration(luaDuration.tofloat()); +// } +// if (luaScreenX.isnumber()) { +// cameraParams.setScreenX(luaScreenX.tofloat()); +// } +// if (luaScreenY.isnumber()) { +// cameraParams.setScreenY(luaScreenY.tofloat()); +// } +// if (luaIsForce.isboolean()) { +// cameraParams.setForce(luaIsForce.toboolean()); +// } +// if (luaAllowInput.isboolean()) { +// cameraParams.setAllowInput(luaAllowInput.toboolean()); +// } +// if (luaSetFollowPos.isboolean()) { +// cameraParams.setSetFollowPos(luaSetFollowPos.toboolean()); +// } +// if (luaIsForceWalk.isboolean()) { +// cameraParams.setForceWalk(luaIsForceWalk.toboolean()); +// } +// if (luaIsChangePlayMode.isboolean()) { +// cameraParams.setChangePlayMode(luaIsChangePlayMode.toboolean()); +// } +// if(luaIsBroadcast.isboolean()) { } // TODO +// +// sceneScriptManager.get().getScene().broadcastPacket(new PacketBeginCameraSceneLookNotify(cameraParams)); + return 0; + } + + public int ShowReminderRadius(int var1, LuaTable var2, int var3){ + logger.warn("[LUA] Call unimplemented ShowReminderRadius with {} {} {}", var1, printTable(var2), var3); + //TODO implement var2 is a postion + return 0; + } + public int ShowClientGuide(String guideName){ + logger.debug("[LUA] Call unimplemented ShowClientGuide with {}", guideName); + if (GameData.getGuideTriggerDataStringMap().get(guideName) != null) { + // if should handle by open state, dont send packet here + // not entirely sure what return value is about + // probably not needing this check statement here since the value comes from + // the lua script + return 1; + } + sceneScriptManager.get().getScene().broadcastPacket(new PacketShowClientGuideNotify(guideName)); + return 0; + } + + public int ActivateDungeonCheckPoint(int var1){ + logger.warn("[LUA] Call untested ActivateDungeonCheckPoint with {}", var1); + var dungeonManager = getSceneScriptManager().getScene().getDungeonManager(); + if(dungeonManager == null){ + return 1; + } + return dungeonManager.activateRespawnPoint(var1) ? 0:2; + } + + //TODO check + public int SetWeatherAreaState(int var1, int var2){ + logger.warn("[LUA] Call unimplemented SetWeatherAreaState with {} {}", var1, var2); + getSceneScriptManager().getScene().getPlayers().forEach(p -> p.setWeather(var1, ClimateType.getTypeByValue(var2))); + return 0; + } + + //TODO check + public boolean CheckIsInMpMode(){ + logger.debug("[LUA] Call CheckIsInMpMode"); + return getSceneScriptManager().getScene().getWorld().isMultiplayer(); + } + + /** + * TODO properly implement + * var3 might contain the next point, sometimes is a single int, sometimes multiple ints as array + * var4 has RouteType route_type, bool turn_mode + */ + public int SetPlatformPointArray(int entityConfigId, int pointArrayId, int[] var3, LuaTable var4){ + logger.warn("[LUA] Call unimplemented SetPlatformPointArray with {} {} {} {}", entityConfigId, pointArrayId, var3, printTable(var4)); + + val entity = getSceneScriptManager().getScene().getEntityByConfigId(entityConfigId); + if(entity == null){ + return 1; + } + if(!(entity instanceof EntityGadget entityGadget)){ + return 2; //todo maybe also check the gadget type? + } + + var routeConfig = entityGadget.getRouteConfig(); + if(!(routeConfig instanceof PointArrayRoute)){ + routeConfig = new PointArrayRoute((entityGadget).getMetaGadget()); + entityGadget.setRouteConfig(routeConfig); + } + + val configRoute = (PointArrayRoute) routeConfig; + //TODO also check targetPoint/targetPoints + if(configRoute.getPointArrayId() == pointArrayId){ + return -1; + } + + configRoute.setPointArrayId(pointArrayId); + //TODO also set targetPoint/targetPoints + sceneScriptManager.get().getScene().broadcastPacket(new PacketPlatformChangeRouteNotify(entityGadget)); + + return -1; + } + + //TODO check + public int SetPlatformRouteId(int entityConfigId, int routeId){ + logger.info("[LUA] Call SetPlatformRouteId {} {}", entityConfigId, routeId); + + val entity = getSceneScriptManager().getScene().getEntityByConfigId(entityConfigId); + if(entity == null){ + return 1; + } + if(!(entity instanceof EntityGadget entityGadget)){ + return 2; //todo maybe also check the gadget type? + } + + var routeConfig = entityGadget.getRouteConfig(); + if(!(routeConfig instanceof ConfigRoute)){ + routeConfig = new ConfigRoute((entityGadget).getMetaGadget()); + entityGadget.setRouteConfig(routeConfig); + } + + val configRoute = (ConfigRoute) routeConfig; + if(configRoute.getRouteId() == routeId){ + return 0; + } + + configRoute.setRouteId(routeId); + sceneScriptManager.get().getScene().broadcastPacket(new PacketPlatformChangeRouteNotify(entityGadget)); + return 0; + } + + //TODO check + public int StartPlatform(int configId){ + logger.info("[LUA] Call StartPlatform {} ", configId); + + val entity = sceneScriptManager.get().getScene().getEntityByConfigId(configId); + + if(!(entity instanceof EntityGadget entityGadget)) { + return 1; + } + + return entityGadget.startPlatform() ? 0 : 2; + } + + //TODO check + public int StopPlatform(int configId){ + logger.info("[LUA] Call StopPlatform {} ", configId); + val entity = sceneScriptManager.get().getScene().getEntityByConfigId(configId); + if(!(entity instanceof EntityGadget entityGadget)) { + return 1; + } + + return entityGadget.stopPlatform() ? 0 : 2; + } + + public int CreateChannellerSlabCampRewardGadget(int configId){ + logger.warn("[LUA] Call unimplemented CreateChannellerSlabCampRewardGadget {}", configId); + var group = currentGroup.getIfExists(); + if(group == null){ + return 1; + } + createGadget(configId, group); + //TODO implement fully + return 0; + } + + public int AssignPlayerShowTemplateReminder(int var1, LuaTable var2){ + logger.warn("[LUA] Call unimplemented AssignPlayerShowTemplateReminder {} {}", var1, var2); + //TODO implement var2 contains LuaTable param_uid_vec, LuaTable param_vec int[] uid_vec + return 0; + } + + public int RevokePlayerShowTemplateReminder(int var1, LuaValue var2){ + logger.warn("[LUA] Call unimplemented AssignPlayerShowTemplateReminder {} {}", var1, var2); + //TODO implement + return 0; + } + + public int UnlockForce(int force){ + logger.debug("[LUA] Call UnlockForce {}", force); + getSceneScriptManager().getScene().unlockForce(force); + return 0; + } + + public int LockForce(int force){ + logger.debug("[LUA] Call LockForce {}", force); + getSceneScriptManager().getScene().lockForce(force); + return 0; + } + + public int KillGroupEntity(LuaTable var1){ + logger.debug("[LUA] Call KillGroupEntity with {}", printTable(var1)); + //TODO check + var sceneManager = sceneScriptManager.getIfExists(); + var groupId = var1.get("group_id").optint(-1); + var killPolicyId = var1.get("kill_policy").optint(-1); + var gadgetList = var1.get("gadgets"); + if(groupId == -1 || sceneManager == null){ + return 1; + } + + + var group = sceneManager.getGroupById(groupId); + if (group == null) { + return 2; + } + + if(killPolicyId!=-1 ){ + var killPolicy = GroupKillPolicy.values()[killPolicyId]; + return killGroupEntityWithPolicy(sceneManager, group, killPolicy); + } + + return killGroupEntityWithTable(sceneManager, group, var1); + } + + private int killGroupEntityWithTable(SceneScriptManager sceneScriptManager, SceneGroup group, LuaTable lists){ + // get targets + var monsterList = lists.get("monsters"); + var gadgetList = lists.get("gadgets"); + + int[] targets = new int[monsterList.length()+gadgetList.length()]; + int targetsIndex = 0; + for (int i = 1; i<=monsterList.length(); i++, targetsIndex++){ + targets[targetsIndex] = monsterList.get(i).optint(-1); + } + for (int i = 1; i<=gadgetList.length(); i++, targetsIndex++){ + targets[targetsIndex] = gadgetList.get(i).optint(-1); + } + + // kill targets if exists + for(int cfgId : targets){ + var entity = getSceneScriptManager().getScene().getEntityByConfigId(cfgId); + if (entity == null || cfgId == 0) { + continue; + } + getSceneScriptManager().getScene().killEntity(entity, 0); + } + return 0; + } + + private int killGroupEntityWithPolicy(SceneScriptManager sceneScriptManager,SceneGroup group, GroupKillPolicy killPolicy){ + // get targets + var targets = new ArrayList(); + if(killPolicy==GROUP_KILL_MONSTER || killPolicy == GROUP_KILL_ALL){ + targets.addAll(group.monsters.values()); + } + if(killPolicy == GROUP_KILL_GADGET || killPolicy == GROUP_KILL_ALL) { + targets.addAll(group.gadgets.values()); + } + + // kill targets if exists + targets.forEach(o -> { + var entity = getSceneScriptManager().getScene().getEntityByConfigId(o.config_id); + if (entity == null) { + return; + } + getSceneScriptManager().getScene().killEntity(entity, 0); + }); + return 0; + } + + public int GetGadgetIdByEntityId(int entityId){ + var entity = getSceneScriptManager().getScene().getEntityById(entityId); + if(!(entity instanceof EntityBaseGadget)){ + return 0; + } + return ((EntityBaseGadget) entity).getGadgetId(); + } + public int GetMonsterIdByEntityId(int entityId){ + var entity = getSceneScriptManager().getScene().getEntityById(entityId); + if(!(entity instanceof EntityMonster)){ + return 0; + } + return ((EntityMonster) entity).getMonsterData().getId(); + } + public int GetMonsterID(int var1){ + //TODO implement var1 type + return 0; + } + public int GetEntityIdByConfigId(int configId){ + logger.warn("[LUA] Call GetEntityIdByConfigId with {}", configId); + //TODO check + var entity = getSceneScriptManager().getScene().getEntityByConfigId(configId); + return entity != null ? entity.getId() : 0; + } + public int GetAvatarEntityIdByUid(int uid){ + logger.warn("[LUA] Call unchecked GetAvatarEntityIdByUid with {}", uid); + //TODO check + var entity = getSceneScriptManager().getScene().getEntities().values().stream() + .filter(e -> e instanceof EntityAvatar && ((EntityAvatar)e).getPlayer().getUid() == uid) + .findFirst(); + return entity.map(GameEntity::getId).orElse(0); + } + + + + public LuaTable GetPosByEntityId(int entityId){ + logger.warn("[LUA] Call unchecked GetPosByEntityId with {}", entityId); + //TODO check + var entity = getSceneScriptManager().getScene().getEntityById(entityId); + return posToLua(entity != null? entity.getPosition() : null); + } + + public LuaTable GetRotationByEntityId(int entityId){ + logger.debug("[LUA] Call unchecked GetRotationByEntityId with {}", entityId); + //TODO check + var entity = getSceneScriptManager().getScene().getEntityById(entityId); + return posToLua(entity != null? entity.getRotation() : null); + } + + public LuaTable GetActivityOpenAndCloseTimeByScheduleId(int scheduleId){ + logger.debug("[LUA] Call GetActivityOpenAndCloseTimeByScheduleId with {}", scheduleId); + + var result = new LuaTable(); + var activityConfig = ActivityManager.getScheduleActivityConfigMap().get(scheduleId); + + if(activityConfig != null){ + result.set(1, LuaValue.valueOf(activityConfig.getBeginTime().getTime())); + result.set(2, LuaValue.valueOf(activityConfig.getEndTime().getTime())); + } + + return result; + } + + public int GetGameHour(){ + return getSceneScriptManager().getScene().getWorld().getGameTimeHours(); + } + + /** + * Methods used in EntityControllers + */ + + @Nullable + public EntityGadget getCurrentEntityGadget(){ + val entity = currentEntity.getIfExists(); + if(entity instanceof EntityGadget){ + return (EntityGadget) entity; + } + return null; + } + + public int SetGadgetState(int gadgetState) { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + gadget.updateState(gadgetState); + return 0; + } + + public int GetGadgetState(int gadgetState) { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + return gadget.getState(); + } + + public int ResetGadgetState(int gadgetState) { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + gadget.getPosition().set(gadget.getBornPos()); + gadget.getRotation().set(gadget.getBornRot()); + gadget.setStartValue(0); + gadget.setStopValue(0); + gadget.updateState(gadgetState); + return 0; + } + + public int SetGearStartValue(int startValue) { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + gadget.setStartValue(startValue); + return 0; + } + + public int GetGearStartValue() { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + return gadget.getStartValue(); + } + + public int SetGearStopValue(int startValue) { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + gadget.setStopValue(startValue); + return 0; + } + + public int GetGearStopValue() { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + return gadget.getStopValue(); + } + + public int GetGadgetStateBeginTime() { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + return gadget.getTicksSinceChange(); + } + + public int GetContextGadgetConfigId() { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + return gadget.getConfigId(); + } + + public int GetContextGroupId() { + EntityGadget gadget = getCurrentEntityGadget(); + if(gadget == null) return -1; + + return gadget.getGroupId(); + } + + public int[] GetGatherConfigIdList() { + EntityGadget gadget = getCurrentEntityGadget(); + + GameEntity[] children = (GameEntity[]) gadget.getChildren().toArray(); + + int[] configIds = new int[children.length + 1]; + for(int i = 0; i < children.length; i++) { + configIds[i] = children[i].getConfigId(); + } + + return configIds; + } +} diff --git a/src/main/java/emu/grasscutter/scripts/ScriptUtils.java b/src/main/java/emu/grasscutter/scripts/ScriptUtils.java index d646cf59a..01961379f 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptUtils.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptUtils.java @@ -1,26 +1,66 @@ -package emu.grasscutter.scripts; - -import emu.grasscutter.Grasscutter; -import java.util.HashMap; -import org.luaj.vm2.LuaTable; -import org.luaj.vm2.LuaValue; - -public class ScriptUtils { - - public static HashMap toMap(LuaTable table) { - HashMap map = new HashMap<>(); - LuaValue[] rootKeys = table.keys(); - for (LuaValue k : rootKeys) { - if (table.get(k).istable()) { - map.put(k, toMap(table.get(k).checktable())); - } else { - map.put(k, table.get(k)); - } - } - return map; - } - - public static void print(LuaTable table) { - Grasscutter.getLogger().info(toMap(table).toString()); - } -} +package emu.grasscutter.scripts; + +import emu.grasscutter.Grasscutter; +import java.util.HashMap; + +import emu.grasscutter.utils.Position; +import lombok.val; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; + +public interface ScriptUtils { + static HashMap toMap(LuaTable table) { + HashMap map = new HashMap<>(); + LuaValue[] rootKeys = table.keys(); + for (LuaValue k : rootKeys) { + if (table.get(k).istable()) { + map.put(k, toMap(table.get(k).checktable())); + } else { + map.put(k, table.get(k)); + } + } + return map; + } + + static void print(LuaTable table) { + Grasscutter.getLogger().info(toMap(table).toString()); + } + + /** + * Converts a position object into a Lua table. + * + * @param position The position object to convert. + * @return The Lua table. + */ + static LuaTable posToLua(Position position) { + var result = new LuaTable(); + if (position != null) { + result.set("x", position.getX()); + result.set("y", position.getY()); + result.set("z", position.getZ()); + } else { + result.set("x", 0); + result.set("y", 0); + result.set("z", 0); + } + + return result; + } + + /** + * Converts a Lua table into a position object. + * + * @param position The Lua table to convert. + * @return The position object. + */ + static Position luaToPos(LuaValue position) { + var result = new Position(); + if (position != null && !position.isnil()) { + result.setX(position.get("x").optint(0)); + result.setY(position.get("y").optint(0)); + result.setZ(position.get("z").optint(0)); + } + + return result; + } +} diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneConfig.java b/src/main/java/emu/grasscutter/scripts/data/SceneConfig.java index 8fe47c63f..f92534e2c 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneConfig.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneConfig.java @@ -1,15 +1,16 @@ -package emu.grasscutter.scripts.data; - -import emu.grasscutter.utils.Position; -import lombok.Setter; -import lombok.ToString; - -@ToString -@Setter -public class SceneConfig { - public Position vision_anchor; - public Position born_pos; - public Position born_rot; - public Position begin_pos; - public Position size; -} +package emu.grasscutter.scripts.data; + +import emu.grasscutter.utils.Position; +import lombok.Setter; +import lombok.ToString; + +@ToString +@Setter +public class SceneConfig { + public Position vision_anchor; + public Position born_pos; + public Position born_rot; + public Position begin_pos; + public Position size; + public float die_y; +} diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java index 330ef80b1..3bf330e50 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java @@ -1,170 +1,183 @@ -package emu.grasscutter.scripts.data; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.scripts.ScriptLoader; -import emu.grasscutter.utils.Position; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import javax.script.Bindings; -import javax.script.CompiledScript; -import javax.script.ScriptException; -import lombok.Setter; -import lombok.ToString; -import org.luaj.vm2.LuaValue; - -@ToString -@Setter -public class SceneGroup { - public transient int - block_id; // Not an actual variable in the scripts but we will keep it here for reference - - public int id; - public int refresh_id; - public Position pos; - - public Map monsters; // - public Map gadgets; // - public Map triggers; - public Map regions; - public List suites; - public List variables; - - public SceneBusiness business; - public SceneGarbage garbages; - public SceneInitConfig init_config; - - private transient boolean loaded; // Not an actual variable in the scripts either - private transient CompiledScript script; - private transient Bindings bindings; - - public static SceneGroup of(int groupId) { - var group = new SceneGroup(); - group.id = groupId; - return group; - } - - public boolean isLoaded() { - return this.loaded; - } - - public void setLoaded(boolean loaded) { - this.loaded = loaded; - } - - public int getBusinessType() { - return this.business == null ? 0 : this.business.type; - } - - public List getGarbageGadgets() { - return this.garbages == null ? null : this.garbages.gadgets; - } - - public CompiledScript getScript() { - return this.script; - } - - public SceneSuite getSuiteByIndex(int index) { - return this.suites.get(index - 1); - } - - public Bindings getBindings() { - return this.bindings; - } - - public synchronized SceneGroup load(int sceneId) { - if (this.loaded) { - return this; - } - // Set flag here so if there is no script, we don't call this function over and over again. - this.setLoaded(true); - - this.bindings = ScriptLoader.getEngine().createBindings(); - - CompiledScript cs = - ScriptLoader.getScript( - "Scene/" + sceneId + "/scene" + sceneId + "_group" + this.id + ".lua"); - - if (cs == null) { - return this; - } - - this.script = cs; - - // Eval script - try { - cs.eval(this.bindings); - - // Set - this.monsters = - ScriptLoader.getSerializer() - .toList(SceneMonster.class, this.bindings.get("monsters")) - .stream() - .collect(Collectors.toMap(x -> x.config_id, y -> y, (a, b) -> a)); - this.monsters.values().forEach(m -> m.group = this); - - this.gadgets = - ScriptLoader.getSerializer() - .toList(SceneGadget.class, this.bindings.get("gadgets")) - .stream() - .collect(Collectors.toMap(x -> x.config_id, y -> y, (a, b) -> a)); - this.gadgets.values().forEach(m -> m.group = this); - - this.triggers = - ScriptLoader.getSerializer() - .toList(SceneTrigger.class, this.bindings.get("triggers")) - .stream() - .collect(Collectors.toMap(x -> x.name, y -> y, (a, b) -> a)); - this.triggers.values().forEach(t -> t.currentGroup = this); - - this.suites = - ScriptLoader.getSerializer().toList(SceneSuite.class, this.bindings.get("suites")); - this.regions = - ScriptLoader.getSerializer() - .toList(SceneRegion.class, this.bindings.get("regions")) - .stream() - .collect(Collectors.toMap(x -> x.config_id, y -> y, (a, b) -> a)); - this.regions.values().forEach(m -> m.group = this); - - this.init_config = - ScriptLoader.getSerializer() - .toObject(SceneInitConfig.class, this.bindings.get("init_config")); - - // Garbages // TODO: fix properly later - Object garbagesValue = this.bindings.get("garbages"); - if (garbagesValue instanceof LuaValue garbagesTable) { - this.garbages = new SceneGarbage(); - if (garbagesTable.checktable().get("gadgets") != LuaValue.NIL) { - this.garbages.gadgets = - ScriptLoader.getSerializer() - .toList( - SceneGadget.class, garbagesTable.checktable().get("gadgets").checktable()); - this.garbages.gadgets.forEach(m -> m.group = this); - } - } - - // Add variables to suite - this.variables = - ScriptLoader.getSerializer().toList(SceneVar.class, this.bindings.get("variables")); - - // Add monsters and gadgets to suite - this.suites.forEach(i -> i.init(this)); - - } catch (ScriptException e) { - Grasscutter.getLogger() - .error( - "An error occurred while loading group " + this.id + " in scene " + sceneId + ".", e); - } - - Grasscutter.getLogger().debug("Successfully loaded group {} in scene {}.", this.id, sceneId); - return this; - } - - public Optional searchBossChestInGroup() { - return this.gadgets.values().stream() - .filter(g -> g.boss_chest != null && g.boss_chest.monster_config_id > 0) - .map(g -> g.boss_chest) - .findFirst(); - } -} +package emu.grasscutter.scripts.data; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.scripts.ScriptLoader; +import emu.grasscutter.utils.Position; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.luaj.vm2.LuaValue; + +import javax.script.Bindings; +import javax.script.CompiledScript; +import javax.script.ScriptException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.stream.Collectors; + +@ToString +@Setter +public final class SceneGroup { + public transient int block_id; // Not an actual variable in the scripts but we will keep it here for reference + + public int id; + public int refresh_id; + public Position pos; + + public Map monsters; // + public Map npcs; // + public Map gadgets; // + public Map triggers; + public Map regions; + public List suites; + public List variables; + + public SceneBusiness business; + public SceneGarbage garbages; + public SceneInitConfig init_config; + @Getter public boolean dynamic_load = false; + + public SceneReplaceable is_replaceable; + + private transient boolean loaded; // Not an actual variable in the scripts either + private transient CompiledScript script; + private transient Bindings bindings; + public static SceneGroup of(int groupId) { + var group = new SceneGroup(); + group.id = groupId; + return group; + } + + public boolean isLoaded() { + return this.loaded; + } + + public void setLoaded(boolean loaded) { + this.loaded = loaded; + } + + public int getBusinessType() { + return this.business == null ? 0 : this.business.type; + } + + public List getGarbageGadgets() { + return this.garbages == null ? null : this.garbages.gadgets; + } + + public CompiledScript getScript() { + return this.script; + } + + public SceneSuite getSuiteByIndex(int index) { + if(index < 1 || index > suites.size()) { + return null; + } + return this.suites.get(index - 1); + } + + public Bindings getBindings() { + return this.bindings; + } + + public synchronized SceneGroup load(int sceneId) { + if (this.loaded) { + return this; + } + // Set flag here so if there is no script, we don't call this function over and over again. + this.setLoaded(true); + + this.bindings = ScriptLoader.getEngine().createBindings(); + + CompiledScript cs = ScriptLoader.getScript("Scene/" + sceneId + "/scene" + sceneId + "_group" + this.id + ".lua"); + + if (cs == null) { + return this; + } + + this.script = cs; + + // Eval script + try { + cs.eval(this.bindings); + + // Set + this.monsters = ScriptLoader.getSerializer().toList(SceneMonster.class, this.bindings.get("monsters")).stream() + .collect(Collectors.toMap(x -> x.config_id, y -> y, (a, b) -> a)); + this.monsters.values().forEach(m -> m.group = this); + + this.npcs = ScriptLoader.getSerializer().toList(SceneNPC.class, this.bindings.get("npcs")).stream() + .collect(Collectors.toMap(x -> x.config_id, y -> y, (a, b) -> a)); + this.npcs.values().forEach(m -> m.group = this); + + this.gadgets = ScriptLoader.getSerializer().toList(SceneGadget.class, this.bindings.get("gadgets")).stream() + .collect(Collectors.toMap(x -> x.config_id, y -> y, (a, b) -> a)); + this.gadgets.values().forEach(m -> m.group = this); + + this.triggers = ScriptLoader.getSerializer().toList(SceneTrigger.class, this.bindings.get("triggers")).stream() + .collect(Collectors.toMap(SceneTrigger::getName, y -> y, (a, b) -> a)); + this.triggers.values().forEach(t -> t.currentGroup = this); + + this.suites = ScriptLoader.getSerializer().toList(SceneSuite.class, this.bindings.get("suites")); + this.regions = ScriptLoader.getSerializer().toList(SceneRegion.class, this.bindings.get("regions")).stream() + .collect(Collectors.toMap(x -> x.config_id, y -> y, (a, b) -> a)); + this.regions.values().forEach(m -> m.group = this); + + this.init_config = ScriptLoader.getSerializer().toObject(SceneInitConfig.class, this.bindings.get("init_config")); + + // Garbages // TODO: fix properly later + Object garbagesValue = this.bindings.get("garbages"); + if (garbagesValue instanceof LuaValue garbagesTable) { + this.garbages = new SceneGarbage(); + if (garbagesTable.checktable().get("gadgets") != LuaValue.NIL) { + this.garbages.gadgets = ScriptLoader.getSerializer().toList(SceneGadget.class, garbagesTable.checktable().get("gadgets").checktable()); + this.garbages.gadgets.forEach(m -> m.group = this); + } + } + + // Add variables to suite + this.variables = ScriptLoader.getSerializer().toList(SceneVar.class, this.bindings.get("variables")); + + // Add monsters and gadgets to suite + this.suites.forEach(i -> i.init(this)); + + } catch (ScriptException e) { + Grasscutter.getLogger().error("An error occurred while loading group " + this.id + " in scene " + sceneId + ".", e); + } + + Grasscutter.getLogger().debug("Successfully loaded group {} in scene {}.", this.id, sceneId); + return this; + } + + public int findInitSuiteIndex(int exclude_index) { //TODO: Investigate end index + if (init_config == null) return 1; + if (init_config.io_type == 1) return init_config.suite; //IO TYPE FLOW + if (init_config.rand_suite) { + if (suites.size() == 1) { + return init_config.suite; + } else { + List randSuiteList = new ArrayList<>(); + for (int i = 0; i < suites.size(); i++) { + if (i == exclude_index) continue; + + var suite = suites.get(i); + for(int j = 0; j < suite.rand_weight; j++) randSuiteList.add(i); + } + + return randSuiteList.get(new Random().nextInt(randSuiteList.size())); + } + } + return init_config.suite; + } + + public Optional searchBossChestInGroup() { + return this.gadgets.values().stream() + .filter(g -> g.boss_chest != null && g.boss_chest.monster_config_id > 0) + .map(g -> g.boss_chest) + .findFirst(); + } + +} diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneInitConfig.java b/src/main/java/emu/grasscutter/scripts/data/SceneInitConfig.java index 22632ed50..95f6216dd 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneInitConfig.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneInitConfig.java @@ -1,12 +1,13 @@ -package emu.grasscutter.scripts.data; - -import lombok.Setter; -import lombok.ToString; - -@ToString -@Setter -public class SceneInitConfig { - public int suite; - public int end_suite; - public boolean rand_suite; -} +package emu.grasscutter.scripts.data; + +import lombok.Setter; +import lombok.ToString; + +@ToString +@Setter +public final class SceneInitConfig { + public int suite; + public int end_suite; + public int io_type; + public boolean rand_suite; +} diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneMonster.java b/src/main/java/emu/grasscutter/scripts/data/SceneMonster.java index 260dd3996..dc84c6690 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneMonster.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneMonster.java @@ -1,19 +1,15 @@ -package emu.grasscutter.scripts.data; - -import lombok.Setter; -import lombok.ToString; - -@ToString -@Setter -public class SceneMonster extends SceneObject { - public int monster_id; - public int pose_id; - public int drop_id; - public int special_name_id; - public String drop_tag; - public int climate_area_id; - public boolean disableWander; - public int title_id; - public int[] affix; - public int mark_flag; -} +package emu.grasscutter.scripts.data; + +import lombok.Setter; +import lombok.ToString; + +@ToString +@Setter +public class SceneMonster extends SceneObject{ + public int monster_id; + public int pose_id; + public int drop_id; + public boolean disableWander; + public int title_id; + public int special_name_id; +} diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneObject.java b/src/main/java/emu/grasscutter/scripts/data/SceneObject.java index 011dc2c52..dc2561ff6 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneObject.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneObject.java @@ -1,18 +1,19 @@ -package emu.grasscutter.scripts.data; - -import emu.grasscutter.utils.Position; -import lombok.Setter; -import lombok.ToString; - -@ToString -@Setter -public class SceneObject { - public int level; - public int config_id; - public int area_id; - - public Position pos; - public Position rot; - /** not set by lua */ - public transient SceneGroup group; -} +package emu.grasscutter.scripts.data; + +import emu.grasscutter.utils.Position; +import lombok.Setter; +import lombok.ToString; + +@ToString +@Setter +public abstract class SceneObject { + public int level; + public int config_id; + public int area_id; + public int vision_level = 0; + + public Position pos; + public Position rot; + /** not set by lua */ + public transient SceneGroup group; +} diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneSuite.java b/src/main/java/emu/grasscutter/scripts/data/SceneSuite.java index 5ef52db2d..14d71b00f 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneSuite.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneSuite.java @@ -1,60 +1,63 @@ -package emu.grasscutter.scripts.data; - -import java.util.ArrayList; -import java.util.List; -import lombok.Setter; -import lombok.ToString; - -@ToString -@Setter -public class SceneSuite { - // make it refer the default empty list to avoid NPE caused by some group - public List monsters = List.of(); - public List gadgets = List.of(); - public List triggers = List.of(); - public List regions = List.of(); - public int rand_weight; - public int[] npcs; - - public transient List sceneMonsters = List.of(); - public transient List sceneGadgets = List.of(); - public transient List sceneTriggers = List.of(); - public transient List sceneRegions = List.of(); - - public void init(SceneGroup sceneGroup) { - if (sceneGroup.monsters != null && this.monsters != null) { - this.sceneMonsters = - new ArrayList<>( - this.monsters.stream() - .filter(sceneGroup.monsters::containsKey) - .map(sceneGroup.monsters::get) - .toList()); - } - - if (sceneGroup.gadgets != null && this.gadgets != null) { - this.sceneGadgets = - new ArrayList<>( - this.gadgets.stream() - .filter(sceneGroup.gadgets::containsKey) - .map(sceneGroup.gadgets::get) - .toList()); - } - - if (sceneGroup.triggers != null && this.triggers != null) { - this.sceneTriggers = - new ArrayList<>( - this.triggers.stream() - .filter(sceneGroup.triggers::containsKey) - .map(sceneGroup.triggers::get) - .toList()); - } - if (sceneGroup.regions != null && this.regions != null) { - this.sceneRegions = - new ArrayList<>( - this.regions.stream() - .filter(sceneGroup.regions::containsKey) - .map(sceneGroup.regions::get) - .toList()); - } - } -} +package emu.grasscutter.scripts.data; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Setter; +import lombok.ToString; + +@ToString +@Setter +public class SceneSuite { + // make it refer the default empty list to avoid NPE caused by some group + public List monsters = List.of(); + public List gadgets = List.of(); + public List triggers = List.of(); + public List regions = List.of(); + public int rand_weight; + + public boolean ban_refresh = false; + + public transient List sceneMonsters = List.of(); + public transient List sceneGadgets = List.of(); + public transient List sceneTriggers = List.of(); + public transient List sceneRegions = List.of(); + + public void init(SceneGroup sceneGroup) { + if(sceneGroup.monsters != null){ + this.sceneMonsters = new ArrayList<>( + this.monsters.stream() + .filter(sceneGroup.monsters::containsKey) + .map(sceneGroup.monsters::get) + .toList() + ); + } + + if(sceneGroup.gadgets != null){ + this.sceneGadgets = new ArrayList<>( + this.gadgets.stream() + .filter(sceneGroup.gadgets::containsKey) + .map(sceneGroup.gadgets::get) + .toList() + ); + } + + if(sceneGroup.triggers != null) { + this.sceneTriggers = new ArrayList<>( + this.triggers.stream() + .filter(sceneGroup.triggers::containsKey) + .map(sceneGroup.triggers::get) + .toList() + ); + } + if(sceneGroup.regions != null) { + this.sceneRegions = new ArrayList<>( + this.regions.stream() + .filter(sceneGroup.regions::containsKey) + .map(sceneGroup.regions::get) + .toList() + ); + } + + } +} diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java b/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java index 1e477b13b..2fc63cbd8 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneTrigger.java @@ -1,59 +1,45 @@ -package emu.grasscutter.scripts.data; - -import lombok.Setter; - -@Setter -public class SceneTrigger { - public String name; - public int config_id; - public int event; - public String source; - public String condition; - public String action; - public boolean forbid_guest; - public int trigger_count; - public String tlog_tag; - - public transient SceneGroup currentGroup; - - @Override - public boolean equals(Object obj) { - if (obj instanceof SceneTrigger sceneTrigger) { - return this.name.equals(sceneTrigger.name); - } - return super.equals(obj); - } - - @Override - public int hashCode() { - return name.hashCode(); - } - - @Override - public String toString() { - return "SceneTrigger{" - + "name='" - + name - + '\'' - + ", config_id=" - + config_id - + ", event=" - + event - + ", source='" - + source - + '\'' - + ", condition='" - + condition - + '\'' - + ", action='" - + action - + '\'' - + ", forbid_guest='" - + forbid_guest - + '\'' - + ", trigger_count='" - + trigger_count - + '\'' - + '}'; - } -} +package emu.grasscutter.scripts.data; + +import lombok.*; + +@Setter +@Getter +@NoArgsConstructor +// todo find way to deserialize from lua with final fields, maybe with the help of Builder? +public final class SceneTrigger { + private String name; + private int config_id; + private int event; + private int trigger_count = 1; + private String source; + private String condition; + private String action; + private String tag; + + public transient SceneGroup currentGroup; + + @Override + public boolean equals(Object obj) { + if (obj instanceof SceneTrigger sceneTrigger){ + return this.name.equals(sceneTrigger.name); + } else return super.equals(obj); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "SceneTrigger{" + + "name='" + name + '\'' + + ", config_id=" + config_id + + ", event=" + event + + ", source='" + source + '\'' + + ", condition='" + condition + '\'' + + ", action='" + action + '\'' + + ", trigger_count='" + trigger_count + '\'' + + '}'; + } +} diff --git a/src/main/java/emu/grasscutter/scripts/data/ScriptArgs.java b/src/main/java/emu/grasscutter/scripts/data/ScriptArgs.java index 0c7903a2d..a072c32ec 100644 --- a/src/main/java/emu/grasscutter/scripts/data/ScriptArgs.java +++ b/src/main/java/emu/grasscutter/scripts/data/ScriptArgs.java @@ -1,65 +1,90 @@ -package emu.grasscutter.scripts.data; - -public class ScriptArgs { - public int param1; - public int param2; - public int param3; - public int source_eid; // Source entity - public int target_eid; - - public ScriptArgs() {} - - public ScriptArgs(int param1) { - this.param1 = param1; - } - - public ScriptArgs(int param1, int param2) { - this.param1 = param1; - this.param2 = param2; - } - - public int getParam1() { - return param1; - } - - public ScriptArgs setParam1(int param1) { - this.param1 = param1; - return this; - } - - public int getParam2() { - return param2; - } - - public ScriptArgs setParam2(int param2) { - this.param2 = param2; - return this; - } - - public int getParam3() { - return param3; - } - - public ScriptArgs setParam3(int param3) { - this.param3 = param3; - return this; - } - - public int getSourceEntityId() { - return source_eid; - } - - public ScriptArgs setSourceEntityId(int source_eid) { - this.source_eid = source_eid; - return this; - } - - public int getTargetEntityId() { - return target_eid; - } - - public ScriptArgs setTargetEntityId(int target_eid) { - this.target_eid = target_eid; - return this; - } -} +package emu.grasscutter.scripts.data; + +public class ScriptArgs { + public int param1; + public int param2; + public int param3; + public int source_eid; // Source entity + public int target_eid; + public int group_id; + public String source; // source string, used for timers + public int type; // lua event type, used by scripts and the ScriptManager + + public ScriptArgs(int groupId, int eventType) { + this(groupId, eventType, 0,0); + } + + public ScriptArgs(int groupId, int eventType, int param1) { + this(groupId, eventType, param1,0); + } + + public ScriptArgs(int groupId, int eventType, int param1, int param2) { + this.type = eventType; + this.param1 = param1; + this.param2 = param2; + this.group_id = groupId; + } + + public int getParam1() { + return param1; + } + + public ScriptArgs setParam1(int param1) { + this.param1 = param1; + return this; + } + + public int getParam2() { + return param2; + } + + public ScriptArgs setParam2(int param2) { + this.param2 = param2; + return this; + } + + public int getParam3() { + return param3; + } + + public ScriptArgs setParam3(int param3) { + this.param3 = param3; + return this; + } + + public int getSourceEntityId() { + return source_eid; + } + + public ScriptArgs setSourceEntityId(int source_eid) { + this.source_eid = source_eid; + return this; + } + + public int getTargetEntityId() { + return target_eid; + } + + public ScriptArgs setTargetEntityId(int target_eid) { + this.target_eid = target_eid; + return this; + } + + public String getEventSource() { + return source; + } + + public ScriptArgs setEventSource(String source) { + this.source = source; + return this; + } + + public int getGroupId() { + return group_id; + } + + public ScriptArgs setGroupId(int group_id) { + this.group_id = group_id; + return this; + } +} diff --git a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java index ba04d6cb9..d1d180dfd 100644 --- a/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java +++ b/src/main/java/emu/grasscutter/scripts/service/ScriptMonsterTideService.java @@ -1,99 +1,92 @@ -package emu.grasscutter.scripts.service; - -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.scripts.SceneScriptManager; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.scripts.data.SceneMonster; -import emu.grasscutter.scripts.data.ScriptArgs; -import emu.grasscutter.scripts.listener.ScriptMonsterListener; -import java.util.List; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicInteger; - -public class ScriptMonsterTideService { - private final SceneScriptManager sceneScriptManager; - private final SceneGroup currentGroup; - private final AtomicInteger monsterAlive; - private final AtomicInteger monsterTideCount; - private final AtomicInteger monsterKillCount; - private final int monsterSceneLimit; - private final ConcurrentLinkedQueue monsterConfigOrders; - private final OnMonsterCreated onMonsterCreated = new OnMonsterCreated(); - private final OnMonsterDead onMonsterDead = new OnMonsterDead(); - - public ScriptMonsterTideService( - SceneScriptManager sceneScriptManager, - SceneGroup group, - int tideCount, - int monsterSceneLimit, - Integer[] ordersConfigId) { - this.sceneScriptManager = sceneScriptManager; - this.currentGroup = group; - this.monsterSceneLimit = monsterSceneLimit; - this.monsterTideCount = new AtomicInteger(tideCount); - this.monsterKillCount = new AtomicInteger(0); - this.monsterAlive = new AtomicInteger(0); - this.monsterConfigOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId)); - - this.sceneScriptManager - .getScriptMonsterSpawnService() - .addMonsterCreatedListener(onMonsterCreated); - this.sceneScriptManager.getScriptMonsterSpawnService().addMonsterDeadListener(onMonsterDead); - // spawn the first turn - for (int i = 0; i < this.monsterSceneLimit; i++) { - sceneScriptManager.addEntity( - this.sceneScriptManager.createMonster(group.id, group.block_id, getNextMonster())); - } - } - - public SceneMonster getNextMonster() { - var nextId = this.monsterConfigOrders.poll(); - if (currentGroup.monsters.containsKey(nextId)) { - return currentGroup.monsters.get(nextId); - } - // TODO some monster config_id do not exist in groups, so temporarily set it to the first - return currentGroup.monsters.values().stream().findFirst().orElse(null); - } - - public void unload() { - this.sceneScriptManager - .getScriptMonsterSpawnService() - .removeMonsterCreatedListener(onMonsterCreated); - this.sceneScriptManager.getScriptMonsterSpawnService().removeMonsterDeadListener(onMonsterDead); - } - - public class OnMonsterCreated implements ScriptMonsterListener { - @Override - public void onNotify(EntityMonster sceneMonster) { - if (monsterSceneLimit > 0) { - monsterAlive.incrementAndGet(); - monsterTideCount.decrementAndGet(); - } - } - } - - public class OnMonsterDead implements ScriptMonsterListener { - @Override - public void onNotify(EntityMonster sceneMonster) { - if (monsterSceneLimit <= 0) { - return; - } - if (monsterAlive.decrementAndGet() >= monsterSceneLimit) { - // maybe not happen - return; - } - monsterKillCount.incrementAndGet(); - if (monsterTideCount.get() > 0) { - // add more - sceneScriptManager.addEntity( - sceneScriptManager.createMonster( - currentGroup.id, currentGroup.block_id, getNextMonster())); - } - // spawn the last turn of monsters - // fix the 5-2 - sceneScriptManager.callEvent( - EventType.EVENT_MONSTER_TIDE_DIE, new ScriptArgs(monsterKillCount.get())); - } - } -} +package emu.grasscutter.scripts.service; + +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.scripts.SceneScriptManager; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.scripts.data.SceneMonster; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.scripts.listener.ScriptMonsterListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +public final class ScriptMonsterTideService { + private final SceneScriptManager sceneScriptManager; + private final SceneGroup currentGroup; + private final AtomicInteger monsterAlive; + private final AtomicInteger monsterTideCount; + private final AtomicInteger monsterKillCount; + private final int monsterSceneLimit; + private final ConcurrentLinkedQueue monsterConfigOrders; + private final List monsterConfigIds; + private final OnMonsterCreated onMonsterCreated= new OnMonsterCreated(); + private final OnMonsterDead onMonsterDead= new OnMonsterDead(); + + public ScriptMonsterTideService(SceneScriptManager sceneScriptManager, + SceneGroup group, int tideCount, int monsterSceneLimit, Integer[] ordersConfigId){ + this.sceneScriptManager = sceneScriptManager; + this.currentGroup = group; + this.monsterSceneLimit = monsterSceneLimit; + this.monsterTideCount = new AtomicInteger(tideCount); + this.monsterKillCount = new AtomicInteger(0); + this.monsterAlive = new AtomicInteger(0); + this.monsterConfigOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId)); + this.monsterConfigIds = List.of(ordersConfigId); + + this.sceneScriptManager.getScriptMonsterSpawnService().addMonsterCreatedListener(onMonsterCreated); + this.sceneScriptManager.getScriptMonsterSpawnService().addMonsterDeadListener(onMonsterDead); + // spawn the first turn + for (int i = 0; i < this.monsterSceneLimit; i++) { + sceneScriptManager.addEntity(this.sceneScriptManager.createMonster(group.id, group.block_id, getNextMonster())); + } + } + + public class OnMonsterCreated implements ScriptMonsterListener{ + @Override + public void onNotify(EntityMonster sceneMonster) { + if(monsterConfigIds.contains(sceneMonster.getConfigId()) && monsterSceneLimit > 0){ + monsterAlive.incrementAndGet(); + monsterTideCount.decrementAndGet(); + } + } + } + + public SceneMonster getNextMonster(){ + var nextId = this.monsterConfigOrders.poll(); + if(currentGroup.monsters.containsKey(nextId)){ + return currentGroup.monsters.get(nextId); + } + // TODO some monster config_id do not exist in groups, so temporarily set it to the first + return currentGroup.monsters.values().stream().findFirst().orElse(null); + } + + public class OnMonsterDead implements ScriptMonsterListener { + @Override + public void onNotify(EntityMonster sceneMonster) { + if (monsterSceneLimit <= 0) { + return; + } + if (monsterAlive.decrementAndGet() >= monsterSceneLimit) { + // maybe not happen + return; + } + monsterKillCount.incrementAndGet(); + if (monsterTideCount.get() > 0) { + // add more + sceneScriptManager.addEntity(sceneScriptManager.createMonster(currentGroup.id, currentGroup.block_id, getNextMonster())); + } + // spawn the last turn of monsters + // fix the 5-2 + sceneScriptManager.callEvent(new ScriptArgs(currentGroup.id, EventType.EVENT_MONSTER_TIDE_DIE, monsterKillCount.get())); + } + + } + + public void unload(){ + this.sceneScriptManager.getScriptMonsterSpawnService().removeMonsterCreatedListener(onMonsterCreated); + this.sceneScriptManager.getScriptMonsterSpawnService().removeMonsterDeadListener(onMonsterDead); + } +} diff --git a/src/main/java/emu/grasscutter/server/event/entity/EntityDamageEvent.java b/src/main/java/emu/grasscutter/server/event/entity/EntityDamageEvent.java index a27586afc..3aabbd8c2 100644 --- a/src/main/java/emu/grasscutter/server/event/entity/EntityDamageEvent.java +++ b/src/main/java/emu/grasscutter/server/event/entity/EntityDamageEvent.java @@ -1,30 +1,24 @@ -package emu.grasscutter.server.event.entity; - -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.server.event.Cancellable; -import emu.grasscutter.server.event.types.EntityEvent; -import javax.annotation.Nullable; - -public final class EntityDamageEvent extends EntityEvent implements Cancellable { - @Nullable private final GameEntity damager; - private float damage; - - public EntityDamageEvent(GameEntity entity, float damage, @Nullable GameEntity damager) { - super(entity); - - this.damage = damage; - this.damager = damager; - } - - public float getDamage() { - return this.damage; - } - - public void setDamage(float damage) { - this.damage = damage; - } - - @Nullable public GameEntity getDamager() { - return this.damager; - } -} +package emu.grasscutter.server.event.entity; + +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.props.ElementType; +import emu.grasscutter.server.event.Cancellable; +import emu.grasscutter.server.event.types.EntityEvent; +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +public final class EntityDamageEvent extends EntityEvent implements Cancellable { + @Getter @Setter private float damage; + @Getter @Setter private ElementType attackElementType; + @Getter @Nullable private final GameEntity damager; + + public EntityDamageEvent(GameEntity entity, float damage, ElementType attackElementType, @Nullable GameEntity damager) { + super(entity); + + this.damage = damage; + this.attackElementType = attackElementType; + this.damager = damager; + } +} diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index 8bd5ce48f..05acfdff7 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -1,281 +1,279 @@ -package emu.grasscutter.server.game; - -import static emu.grasscutter.config.Configuration.GAME_INFO; -import static emu.grasscutter.utils.Language.translate; - -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.Account; -import emu.grasscutter.game.battlepass.BattlePassSystem; -import emu.grasscutter.game.chat.ChatSystem; -import emu.grasscutter.game.chat.ChatSystemHandler; -import emu.grasscutter.game.combine.CombineManger; -import emu.grasscutter.game.drop.DropSystem; -import emu.grasscutter.game.dungeons.DungeonSystem; -import emu.grasscutter.game.dungeons.challenge.DungeonChallenge; -import emu.grasscutter.game.expedition.ExpeditionSystem; -import emu.grasscutter.game.gacha.GachaSystem; -import emu.grasscutter.game.managers.cooking.CookingCompoundManager; -import emu.grasscutter.game.managers.cooking.CookingManager; -import emu.grasscutter.game.managers.energy.EnergyManager; -import emu.grasscutter.game.managers.stamina.StaminaManager; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.quest.QuestSystem; -import emu.grasscutter.game.shop.ShopSystem; -import emu.grasscutter.game.systems.AnnouncementSystem; -import emu.grasscutter.game.systems.InventorySystem; -import emu.grasscutter.game.systems.MultiplayerSystem; -import emu.grasscutter.game.tower.TowerSystem; -import emu.grasscutter.game.world.World; -import emu.grasscutter.game.world.WorldDataSystem; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; -import emu.grasscutter.server.event.game.ServerTickEvent; -import emu.grasscutter.server.event.internal.ServerStartEvent; -import emu.grasscutter.server.event.internal.ServerStopEvent; -import emu.grasscutter.server.event.types.ServerEvent; -import emu.grasscutter.server.scheduler.ServerTaskScheduler; -import emu.grasscutter.task.TaskMap; -import java.net.InetSocketAddress; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import kcp.highway.ChannelConfig; -import kcp.highway.KcpServer; -import lombok.Getter; - -@Getter -public final class GameServer extends KcpServer { - // Game server base - private final InetSocketAddress address; - private final GameServerPacketHandler packetHandler; - private final Map players; - private final Set worlds; - - // Server systems - private final InventorySystem inventorySystem; - private final GachaSystem gachaSystem; - private final ShopSystem shopSystem; - private final MultiplayerSystem multiplayerSystem; - private final DungeonSystem dungeonSystem; - private final ExpeditionSystem expeditionSystem; - private final DropSystem dropSystem; - private final WorldDataSystem worldDataSystem; - private final BattlePassSystem battlePassSystem; - private final CombineManger combineSystem; - private final TowerSystem towerSystem; - private final AnnouncementSystem announcementSystem; - private final QuestSystem questSystem; - - // Extra - private final ServerTaskScheduler scheduler; - private final TaskMap taskMap; - - private ChatSystemHandler chatManager; - - public GameServer() { - this(getAdapterInetSocketAddress()); - } - - public GameServer(InetSocketAddress address) { - ChannelConfig channelConfig = new ChannelConfig(); - channelConfig.nodelay(true, GAME_INFO.kcpInterval, 2, true); - channelConfig.setMtu(1400); - channelConfig.setSndwnd(256); - channelConfig.setRcvwnd(256); - channelConfig.setTimeoutMillis(30 * 1000); // 30s - channelConfig.setUseConvChannel(true); - channelConfig.setAckNoDelay(false); - - this.init(GameSessionManager.getListener(), channelConfig, address); - - DungeonChallenge.initialize(); - EnergyManager.initialize(); - StaminaManager.initialize(); - CookingManager.initialize(); - CookingCompoundManager.initialize(); - CombineManger.initialize(); - - // Game Server base - this.address = address; - this.packetHandler = new GameServerPacketHandler(PacketHandler.class); - this.players = new ConcurrentHashMap<>(); - this.worlds = Collections.synchronizedSet(new HashSet<>()); - - // Extra - this.scheduler = new ServerTaskScheduler(); - this.taskMap = new TaskMap(true); - - // Create game systems - this.inventorySystem = new InventorySystem(this); - this.gachaSystem = new GachaSystem(this); - this.shopSystem = new ShopSystem(this); - this.multiplayerSystem = new MultiplayerSystem(this); - this.dungeonSystem = new DungeonSystem(this); - this.dropSystem = new DropSystem(this); - this.expeditionSystem = new ExpeditionSystem(this); - this.combineSystem = new CombineManger(this); - this.towerSystem = new TowerSystem(this); - this.worldDataSystem = new WorldDataSystem(this); - this.battlePassSystem = new BattlePassSystem(this); - this.announcementSystem = new AnnouncementSystem(this); - this.questSystem = new QuestSystem(this); - - // Chata manager - this.chatManager = new ChatSystem(this); - - // Hook into shutdown event. - Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); - } - - private static InetSocketAddress getAdapterInetSocketAddress() { - InetSocketAddress inetSocketAddress; - if (GAME_INFO.bindAddress.equals("")) { - inetSocketAddress = new InetSocketAddress(GAME_INFO.bindPort); - } else { - inetSocketAddress = new InetSocketAddress(GAME_INFO.bindAddress, GAME_INFO.bindPort); - } - return inetSocketAddress; - } - - @Deprecated - public ChatSystemHandler getChatManager() { - return chatManager; - } - - @Deprecated - public void setChatManager(ChatSystemHandler chatManager) { - this.chatManager = chatManager; - } - - public ChatSystemHandler getChatSystem() { - return chatManager; - } - - public void setChatSystem(ChatSystemHandler chatManager) { - this.chatManager = chatManager; - } - - public void registerPlayer(Player player) { - getPlayers().put(player.getUid(), player); - } - - public Player getPlayerByUid(int id) { - return this.getPlayerByUid(id, false); - } - - public Player getPlayerByUid(int id, boolean allowOfflinePlayers) { - // Console check - if (id == GameConstants.SERVER_CONSOLE_UID) { - return null; - } - - // Get from online players - Player player = this.getPlayers().get(id); - - if (!allowOfflinePlayers) { - return player; - } - - // Check database if character isnt here - if (player == null) { - player = DatabaseHelper.getPlayerByUid(id); - } - - return player; - } - - public Player getPlayerByAccountId(String accountId) { - Optional playerOpt = - getPlayers().values().stream() - .filter(player -> player.getAccount().getId().equals(accountId)) - .findFirst(); - return playerOpt.orElse(null); - } - - public SocialDetail.Builder getSocialDetailByUid(int id) { - // Get from online players - Player player = this.getPlayerByUid(id, true); - - if (player == null) { - return null; - } - - return player.getSocialDetail(); - } - - public Account getAccountByName(String username) { - Optional playerOpt = - getPlayers().values().stream() - .filter(player -> player.getAccount().getUsername().equals(username)) - .findFirst(); - if (playerOpt.isPresent()) { - return playerOpt.get().getAccount(); - } - return DatabaseHelper.getAccountByName(username); - } - - public synchronized void onTick() { - var tickStart = Instant.now(); - - // Tick worlds. - this.worlds.removeIf(World::onTick); - - // Tick players. - this.players.values().forEach(Player::onTick); - - // Tick scheduler. - this.getScheduler().runTasks(); - - // Call server tick event. - ServerTickEvent event = new ServerTickEvent(tickStart, Instant.now()); - event.call(); - } - - public void registerWorld(World world) { - this.getWorlds().add(world); - } - - public void deregisterWorld(World world) { - // TODO Auto-generated method stub - - } - - public void start() { - // Schedule game loop. - Timer gameLoop = new Timer(); - gameLoop.scheduleAtFixedRate( - new TimerTask() { - @Override - public void run() { - try { - onTick(); - } catch (Exception e) { - Grasscutter.getLogger().error(translate("messages.game.game_update_error"), e); - } - } - }, - new Date(), - 1000L); - Grasscutter.getLogger().info(translate("messages.status.free_software")); - Grasscutter.getLogger() - .info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort())); - ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); - event.call(); - } - - public void onServerShutdown() { - ServerStopEvent event = new ServerStopEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); - event.call(); - - // Kick and save all players - List list = new ArrayList<>(this.getPlayers().size()); - list.addAll(this.getPlayers().values()); - - for (Player player : list) { - player.getSession().close(); - } - } -} +package emu.grasscutter.server.game; + +import static emu.grasscutter.config.Configuration.GAME_INFO; +import static emu.grasscutter.utils.Language.translate; + +import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.game.battlepass.BattlePassSystem; +import emu.grasscutter.game.chat.ChatSystem; +import emu.grasscutter.game.chat.ChatSystemHandler; +import emu.grasscutter.game.combine.CombineManger; +import emu.grasscutter.game.drop.DropSystem; +import emu.grasscutter.game.dungeons.DungeonSystem; +import emu.grasscutter.game.expedition.ExpeditionSystem; +import emu.grasscutter.game.gacha.GachaSystem; +import emu.grasscutter.game.managers.cooking.CookingCompoundManager; +import emu.grasscutter.game.managers.cooking.CookingManager; +import emu.grasscutter.game.managers.energy.EnergyManager; +import emu.grasscutter.game.managers.stamina.StaminaManager; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.QuestSystem; +import emu.grasscutter.game.shop.ShopSystem; +import emu.grasscutter.game.systems.AnnouncementSystem; +import emu.grasscutter.game.systems.InventorySystem; +import emu.grasscutter.game.systems.MultiplayerSystem; +import emu.grasscutter.game.tower.TowerSystem; +import emu.grasscutter.game.world.World; +import emu.grasscutter.game.world.WorldDataSystem; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; +import emu.grasscutter.server.event.game.ServerTickEvent; +import emu.grasscutter.server.event.internal.ServerStartEvent; +import emu.grasscutter.server.event.internal.ServerStopEvent; +import emu.grasscutter.server.event.types.ServerEvent; +import emu.grasscutter.server.scheduler.ServerTaskScheduler; +import emu.grasscutter.task.TaskMap; +import java.net.InetSocketAddress; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import kcp.highway.ChannelConfig; +import kcp.highway.KcpServer; +import lombok.Getter; + +@Getter +public final class GameServer extends KcpServer { + // Game server base + private final InetSocketAddress address; + private final GameServerPacketHandler packetHandler; + private final Map players; + private final Set worlds; + + // Server systems + private final InventorySystem inventorySystem; + private final GachaSystem gachaSystem; + private final ShopSystem shopSystem; + private final MultiplayerSystem multiplayerSystem; + private final DungeonSystem dungeonSystem; + private final ExpeditionSystem expeditionSystem; + private final DropSystem dropSystem; + private final WorldDataSystem worldDataSystem; + private final BattlePassSystem battlePassSystem; + private final CombineManger combineSystem; + private final TowerSystem towerSystem; + private final AnnouncementSystem announcementSystem; + private final QuestSystem questSystem; + + // Extra + private final ServerTaskScheduler scheduler; + private final TaskMap taskMap; + + private ChatSystemHandler chatManager; + + public GameServer() { + this(getAdapterInetSocketAddress()); + } + + public GameServer(InetSocketAddress address) { + ChannelConfig channelConfig = new ChannelConfig(); + channelConfig.nodelay(true, GAME_INFO.kcpInterval, 2, true); + channelConfig.setMtu(1400); + channelConfig.setSndwnd(256); + channelConfig.setRcvwnd(256); + channelConfig.setTimeoutMillis(30 * 1000); // 30s + channelConfig.setUseConvChannel(true); + channelConfig.setAckNoDelay(false); + + this.init(GameSessionManager.getListener(), channelConfig, address); + + EnergyManager.initialize(); + StaminaManager.initialize(); + CookingManager.initialize(); + CookingCompoundManager.initialize(); + CombineManger.initialize(); + + // Game Server base + this.address = address; + this.packetHandler = new GameServerPacketHandler(PacketHandler.class); + this.players = new ConcurrentHashMap<>(); + this.worlds = Collections.synchronizedSet(new HashSet<>()); + + // Extra + this.scheduler = new ServerTaskScheduler(); + this.taskMap = new TaskMap(true); + + // Create game systems + this.inventorySystem = new InventorySystem(this); + this.gachaSystem = new GachaSystem(this); + this.shopSystem = new ShopSystem(this); + this.multiplayerSystem = new MultiplayerSystem(this); + this.dungeonSystem = new DungeonSystem(this); + this.dropSystem = new DropSystem(this); + this.expeditionSystem = new ExpeditionSystem(this); + this.combineSystem = new CombineManger(this); + this.towerSystem = new TowerSystem(this); + this.worldDataSystem = new WorldDataSystem(this); + this.battlePassSystem = new BattlePassSystem(this); + this.announcementSystem = new AnnouncementSystem(this); + this.questSystem = new QuestSystem(this); + + // Chata manager + this.chatManager = new ChatSystem(this); + + // Hook into shutdown event. + Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); + } + + private static InetSocketAddress getAdapterInetSocketAddress() { + InetSocketAddress inetSocketAddress; + if (GAME_INFO.bindAddress.equals("")) { + inetSocketAddress = new InetSocketAddress(GAME_INFO.bindPort); + } else { + inetSocketAddress = new InetSocketAddress(GAME_INFO.bindAddress, GAME_INFO.bindPort); + } + return inetSocketAddress; + } + + @Deprecated + public ChatSystemHandler getChatManager() { + return chatManager; + } + + @Deprecated + public void setChatManager(ChatSystemHandler chatManager) { + this.chatManager = chatManager; + } + + public ChatSystemHandler getChatSystem() { + return chatManager; + } + + public void setChatSystem(ChatSystemHandler chatManager) { + this.chatManager = chatManager; + } + + public void registerPlayer(Player player) { + getPlayers().put(player.getUid(), player); + } + + public Player getPlayerByUid(int id) { + return this.getPlayerByUid(id, false); + } + + public Player getPlayerByUid(int id, boolean allowOfflinePlayers) { + // Console check + if (id == GameConstants.SERVER_CONSOLE_UID) { + return null; + } + + // Get from online players + Player player = this.getPlayers().get(id); + + if (!allowOfflinePlayers) { + return player; + } + + // Check database if character isnt here + if (player == null) { + player = DatabaseHelper.getPlayerByUid(id); + } + + return player; + } + + public Player getPlayerByAccountId(String accountId) { + Optional playerOpt = + getPlayers().values().stream() + .filter(player -> player.getAccount().getId().equals(accountId)) + .findFirst(); + return playerOpt.orElse(null); + } + + public SocialDetail.Builder getSocialDetailByUid(int id) { + // Get from online players + Player player = this.getPlayerByUid(id, true); + + if (player == null) { + return null; + } + + return player.getSocialDetail(); + } + + public Account getAccountByName(String username) { + Optional playerOpt = + getPlayers().values().stream() + .filter(player -> player.getAccount().getUsername().equals(username)) + .findFirst(); + if (playerOpt.isPresent()) { + return playerOpt.get().getAccount(); + } + return DatabaseHelper.getAccountByName(username); + } + + public synchronized void onTick() { + var tickStart = Instant.now(); + + // Tick worlds. + this.worlds.removeIf(World::onTick); + + // Tick players. + this.players.values().forEach(Player::onTick); + + // Tick scheduler. + this.getScheduler().runTasks(); + + // Call server tick event. + ServerTickEvent event = new ServerTickEvent(tickStart, Instant.now()); + event.call(); + } + + public void registerWorld(World world) { + this.getWorlds().add(world); + } + + public void deregisterWorld(World world) { + // TODO Auto-generated method stub + + } + + public void start() { + // Schedule game loop. + Timer gameLoop = new Timer(); + gameLoop.scheduleAtFixedRate( + new TimerTask() { + @Override + public void run() { + try { + onTick(); + } catch (Exception e) { + Grasscutter.getLogger().error(translate("messages.game.game_update_error"), e); + } + } + }, + new Date(), + 1000L); + Grasscutter.getLogger().info(translate("messages.status.free_software")); + Grasscutter.getLogger() + .info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort())); + ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); + event.call(); + } + + public void onServerShutdown() { + ServerStopEvent event = new ServerStopEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); + event.call(); + + // Kick and save all players + List list = new ArrayList<>(this.getPlayers().size()); + list.addAll(this.getPlayers().values()); + + for (Player player : list) { + player.getSession().close(); + } + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAddQuestContentProgressReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAddQuestContentProgressReq.java index d32623e3f..fd87e595f 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAddQuestContentProgressReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAddQuestContentProgressReq.java @@ -1,41 +1,26 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.QuestData.QuestCondition; -import emu.grasscutter.game.quest.enums.QuestTrigger; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.AddQuestContentProgressReqOuterClass; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketAddQuestContentProgressRsp; -import java.util.List; -import java.util.stream.Stream; - -@Opcodes(PacketOpcodes.AddQuestContentProgressReq) -public class HandlerAddQuestContentProgressReq extends PacketHandler { - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - var req = AddQuestContentProgressReqOuterClass.AddQuestContentProgressReq.parseFrom(payload); - // Find all conditions in quest that are the same as the given one - Stream finishCond = - GameData.getQuestDataMap().get(req.getParam()).getFinishCond().stream(); - Stream acceptCond = - GameData.getQuestDataMap().get(req.getParam()).getAcceptCond().stream(); - Stream failCond = - GameData.getQuestDataMap().get(req.getParam()).getFailCond().stream(); - List allCondMatch = - Stream.concat(Stream.concat(acceptCond, failCond), finishCond) - .filter(p -> p.getType().getValue() == req.getContentType()) - .toList(); - for (QuestCondition cond : allCondMatch) { - session - .getPlayer() - .getQuestManager() - .triggerEvent( - QuestTrigger.getContentTriggerByValue(req.getContentType()), cond.getParam()); - } - session.send(new PacketAddQuestContentProgressRsp(req.getContentType())); - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AddQuestContentProgressReqOuterClass.AddQuestContentProgressReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketAddQuestContentProgressRsp; + +@Opcodes(PacketOpcodes.AddQuestContentProgressReq) +public class HandlerAddQuestContentProgressReq extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + var req = AddQuestContentProgressReq.parseFrom(payload); + + // Find all conditions in quest that are the same as the given one + var type = QuestContent.getContentTriggerByValue(req.getContentType()); + if(type != null) { + session.getPlayer().getQuestManager().queueEvent(type, req.getParam()); + } + + session.send(new PacketAddQuestContentProgressRsp(req.getContentType())); + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarChangeElementTypeReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarChangeElementTypeReq.java index 6baa9313e..e3853c95f 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarChangeElementTypeReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarChangeElementTypeReq.java @@ -1,69 +1,39 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.GameConstants; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; -import emu.grasscutter.data.excels.world.WorldAreaData; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.AvatarChangeElementTypeReqOuterClass.AvatarChangeElementTypeReq; -import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketAbilityChangeNotify; -import emu.grasscutter.server.packet.send.PacketAvatarChangeElementTypeRsp; -import emu.grasscutter.server.packet.send.PacketAvatarFightPropNotify; -import emu.grasscutter.server.packet.send.PacketAvatarSkillDepotChangeNotify; - -@Opcodes(PacketOpcodes.AvatarChangeElementTypeReq) -public class HandlerAvatarChangeElementTypeReq extends PacketHandler { - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - AvatarChangeElementTypeReq req = AvatarChangeElementTypeReq.parseFrom(payload); - - WorldAreaData area = GameData.getWorldAreaDataMap().get(req.getAreaId()); - - if (area == null - || area.getElementType() == null - || area.getElementType().getDepotValue() <= 0) { - session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE)); - return; - } - - // Get current avatar, should be one of the main characters - EntityAvatar mainCharacterEntity = - session.getPlayer().getTeamManager().getCurrentAvatarEntity(); - Avatar mainCharacter = mainCharacterEntity.getAvatar(); - - int skillDepotId = area.getElementType().getDepotValue(); - switch (mainCharacter.getAvatarId()) { - case GameConstants.MAIN_CHARACTER_MALE -> skillDepotId += 500; - case GameConstants.MAIN_CHARACTER_FEMALE -> skillDepotId += 700; - default -> { - session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE)); - return; - } - } - - // Sanity checks for skill depots - AvatarSkillDepotData skillDepot = GameData.getAvatarSkillDepotDataMap().get(skillDepotId); - if (skillDepot == null || skillDepot.getId() == mainCharacter.getSkillDepotId()) { - session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE)); - return; - } - - // Set skill depot - mainCharacter.setSkillDepotData(skillDepot); - - // Success - session.send(new PacketAvatarChangeElementTypeRsp()); - - // Ability change packet - session.send(new PacketAvatarSkillDepotChangeNotify(mainCharacter)); - session.send(new PacketAbilityChangeNotify(mainCharacterEntity)); - session.send(new PacketAvatarFightPropNotify(mainCharacter)); - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AvatarChangeElementTypeReqOuterClass.AvatarChangeElementTypeReq; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketAvatarChangeElementTypeRsp; +import lombok.val; + +/** + * Changes the currently active avatars Element if possible + */ +@Opcodes(PacketOpcodes.AvatarChangeElementTypeReq) +public class HandlerAvatarChangeElementTypeReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + var req = AvatarChangeElementTypeReq.parseFrom(payload); + var area = GameData.getWorldAreaDataMap().get(req.getAreaId()); + + if (area == null || area.getElementType() == null || area.getElementType().getDepotIndex() <= 0) { + session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE)); + return; + } + + val avatar = session.getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(); + if (!avatar.changeElement(area.getElementType())) { + session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE)); + return; + } + + // Success + session.send(new PacketAvatarChangeElementTypeRsp()); + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java index 1bbddceed..4b4ab7139 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java @@ -1,191 +1,191 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; -import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify; -import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; -import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo; -import emu.grasscutter.net.proto.EvtAnimatorParameterInfoOuterClass.EvtAnimatorParameterInfo; -import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; -import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; -import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; -import emu.grasscutter.net.proto.PlayerDieTypeOuterClass; -import emu.grasscutter.server.event.entity.EntityMoveEvent; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import emu.grasscutter.utils.Position; - -@Opcodes(PacketOpcodes.CombatInvocationsNotify) -public class HandlerCombatInvocationsNotify extends PacketHandler { - - private float cachedLandingSpeed = 0; - private long cachedLandingTimeMillisecond = 0; - private boolean monitorLandingEvent = false; - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - CombatInvocationsNotify notif = CombatInvocationsNotify.parseFrom(payload); - for (CombatInvokeEntry entry : notif.getInvokeListList()) { - // Handle combat invoke - switch (entry.getArgumentType()) { - case COMBAT_TYPE_ARGUMENT_EVT_BEING_HIT -> { - EvtBeingHitInfo hitInfo = EvtBeingHitInfo.parseFrom(entry.getCombatData()); - AttackResult attackResult = hitInfo.getAttackResult(); - Player player = session.getPlayer(); - - // Check if the player is invulnerable. - if (attackResult.getAttackerId() - != player.getTeamManager().getCurrentAvatarEntity().getId() - && player.getAbilityManager().isAbilityInvulnerable()) break; - - // Handle damage - player.getAttackResults().add(attackResult); - player.getEnergyManager().handleAttackHit(hitInfo); - } - case COMBAT_TYPE_ARGUMENT_ENTITY_MOVE -> { - // Handle movement - EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData()); - GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId()); - if (entity != null) { - // Move player - MotionInfo motionInfo = moveInfo.getMotionInfo(); - MotionState motionState = motionInfo.getState(); - - // Call entity move event. - EntityMoveEvent event = - new EntityMoveEvent( - entity, - new Position(motionInfo.getPos()), - new Position(motionInfo.getRot()), - motionState); - event.call(); - - entity.move(event.getPosition(), event.getRotation()); - entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime()); - entity.setLastMoveReliableSeq(moveInfo.getReliableSeq()); - entity.setMotionState(motionState); - - session - .getPlayer() - .getStaminaManager() - .handleCombatInvocationsNotify(session, moveInfo, entity); - - // TODO: handle MOTION_FIGHT landing which has a different damage factor - // Also, for plunge attacks, LAND_SPEED is always -30 and is not useful. - // May need the height when starting plunge attack. - - // MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packets. - // Cache land speed for later use. - if (motionState == MotionState.MOTION_STATE_LAND_SPEED) { - cachedLandingSpeed = motionInfo.getSpeed().getY(); - cachedLandingTimeMillisecond = System.currentTimeMillis(); - monitorLandingEvent = true; - } - if (monitorLandingEvent) { - if (motionState == MotionState.MOTION_STATE_FALL_ON_GROUND) { - monitorLandingEvent = false; - handleFallOnGround(session, entity, motionState); - } - } - - // MOTION_STATE_NOTIFY = Dont send to other players - if (motionState == MotionState.MOTION_STATE_NOTIFY) { - continue; - } - } - } - case COMBAT_TYPE_ARGUMENT_ANIMATOR_PARAMETER_CHANGED -> { - EvtAnimatorParameterInfo paramInfo = - EvtAnimatorParameterInfo.parseFrom(entry.getCombatData()); - if (paramInfo.getIsServerCache()) { - paramInfo = paramInfo.toBuilder().setIsServerCache(false).build(); - entry = entry.toBuilder().setCombatData(paramInfo.toByteString()).build(); - } - } - default -> {} - } - - session.getPlayer().getCombatInvokeHandler().addEntry(entry.getForwardType(), entry); - } - } - - private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) { - if (session.getPlayer().inGodmode()) { - return; - } - // People have reported that after plunge attack (client sends a FIGHT instead of - // FALL_ON_GROUND) they will die - // if they talk to an NPC (this is when the client sends a FALL_ON_GROUND) without jumping - // again. - // A dirty patch: if not received immediately after MOTION_LAND_SPEED, discard this packet. - // 200ms seems to be a reasonable delay. - int maxDelay = 200; - long actualDelay = System.currentTimeMillis() - cachedLandingTimeMillisecond; - Grasscutter.getLogger() - .trace( - "MOTION_FALL_ON_GROUND received after " - + actualDelay - + "/" - + maxDelay - + "ms." - + (actualDelay > maxDelay ? " Discard" : "")); - if (actualDelay > maxDelay) { - return; - } - float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - float damageFactor = 0; - if (cachedLandingSpeed < -23.5) { - damageFactor = 0.33f; - } - if (cachedLandingSpeed < -25) { - damageFactor = 0.5f; - } - if (cachedLandingSpeed < -26.5) { - damageFactor = 0.66f; - } - if (cachedLandingSpeed < -28) { - damageFactor = 1f; - } - float damage = maxHP * damageFactor; - float newHP = currentHP - damage; - if (newHP < 0) { - newHP = 0; - } - if (damageFactor > 0) { - Grasscutter.getLogger() - .debug( - currentHP - + "/" - + maxHP - + "\tLandingSpeed: " - + cachedLandingSpeed - + "\tDamageFactor: " - + damageFactor - + "\tDamage: " - + damage - + "\tNewHP: " - + newHP); - } else { - Grasscutter.getLogger().trace(currentHP + "/" + maxHP + "\tLandingSpeed: 0\tNo damage"); - } - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); - entity - .getWorld() - .broadcastPacket( - new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); - if (newHP == 0) { - session - .getPlayer() - .getStaminaManager() - .killAvatar(session, entity, PlayerDieTypeOuterClass.PlayerDieType.PLAYER_DIE_TYPE_FALL); - } - cachedLandingSpeed = 0; - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; +import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify; +import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; +import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo; +import emu.grasscutter.net.proto.EvtAnimatorParameterInfoOuterClass.EvtAnimatorParameterInfo; +import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; +import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; +import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; +import emu.grasscutter.net.proto.PlayerDieTypeOuterClass; +import emu.grasscutter.server.event.entity.EntityMoveEvent; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.utils.Position; + +@Opcodes(PacketOpcodes.CombatInvocationsNotify) +public class HandlerCombatInvocationsNotify extends PacketHandler { + + private float cachedLandingSpeed = 0; + private long cachedLandingTimeMillisecond = 0; + private boolean monitorLandingEvent = false; + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + CombatInvocationsNotify notif = CombatInvocationsNotify.parseFrom(payload); + for (CombatInvokeEntry entry : notif.getInvokeListList()) { + // Handle combat invoke + switch (entry.getArgumentType()) { + case COMBAT_TYPE_ARGUMENT_EVT_BEING_HIT -> { + EvtBeingHitInfo hitInfo = EvtBeingHitInfo.parseFrom(entry.getCombatData()); + AttackResult attackResult = hitInfo.getAttackResult(); + Player player = session.getPlayer(); + + // Check if the player is invulnerable. + if (attackResult.getAttackerId() + != player.getTeamManager().getCurrentAvatarEntity().getId() + && player.getAbilityManager().isAbilityInvulnerable()) break; + + // Handle damage + player.getAttackResults().add(attackResult); + player.getEnergyManager().handleAttackHit(hitInfo); + } + case COMBAT_TYPE_ARGUMENT_ENTITY_MOVE -> { + // Handle movement + EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData()); + GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId()); + if (entity != null) { + // Move player + MotionInfo motionInfo = moveInfo.getMotionInfo(); + MotionState motionState = motionInfo.getState(); + + // Call entity move event. + EntityMoveEvent event = + new EntityMoveEvent( + entity, + new Position(motionInfo.getPos()), + new Position(motionInfo.getRot()), + motionState); + event.call(); + + entity.move(event.getPosition(), event.getRotation()); + entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime()); + entity.setLastMoveReliableSeq(moveInfo.getReliableSeq()); + entity.setMotionState(motionState); + + session + .getPlayer() + .getStaminaManager() + .handleCombatInvocationsNotify(session, moveInfo, entity); + + // TODO: handle MOTION_FIGHT landing which has a different damage factor + // Also, for plunge attacks, LAND_SPEED is always -30 and is not useful. + // May need the height when starting plunge attack. + + // MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packets. + // Cache land speed for later use. + if (motionState == MotionState.MOTION_STATE_LAND_SPEED) { + cachedLandingSpeed = motionInfo.getSpeed().getY(); + cachedLandingTimeMillisecond = System.currentTimeMillis(); + monitorLandingEvent = true; + } + if (monitorLandingEvent) { + if (motionState == MotionState.MOTION_STATE_FALL_ON_GROUND) { + monitorLandingEvent = false; + handleFallOnGround(session, entity, motionState); + } + } + + // MOTION_STATE_NOTIFY = Dont send to other players + if (motionState == MotionState.MOTION_STATE_NOTIFY) { + continue; + } + } + } + case COMBAT_TYPE_ARGUMENT_ANIMATOR_PARAMETER_CHANGED -> { + EvtAnimatorParameterInfo paramInfo = + EvtAnimatorParameterInfo.parseFrom(entry.getCombatData()); + if (paramInfo.getIsServerCache()) { + paramInfo = paramInfo.toBuilder().setIsServerCache(false).build(); + entry = entry.toBuilder().setCombatData(paramInfo.toByteString()).build(); + } + } + default -> {} + } + + session.getPlayer().getCombatInvokeHandler().addEntry(entry.getForwardType(), entry); + } + } + + private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) { + if (session.getPlayer().isInGodMode()) { + return; + } + // People have reported that after plunge attack (client sends a FIGHT instead of + // FALL_ON_GROUND) they will die + // if they talk to an NPC (this is when the client sends a FALL_ON_GROUND) without jumping + // again. + // A dirty patch: if not received immediately after MOTION_LAND_SPEED, discard this packet. + // 200ms seems to be a reasonable delay. + int maxDelay = 200; + long actualDelay = System.currentTimeMillis() - cachedLandingTimeMillisecond; + Grasscutter.getLogger() + .trace( + "MOTION_FALL_ON_GROUND received after " + + actualDelay + + "/" + + maxDelay + + "ms." + + (actualDelay > maxDelay ? " Discard" : "")); + if (actualDelay > maxDelay) { + return; + } + float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + float damageFactor = 0; + if (cachedLandingSpeed < -23.5) { + damageFactor = 0.33f; + } + if (cachedLandingSpeed < -25) { + damageFactor = 0.5f; + } + if (cachedLandingSpeed < -26.5) { + damageFactor = 0.66f; + } + if (cachedLandingSpeed < -28) { + damageFactor = 1f; + } + float damage = maxHP * damageFactor; + float newHP = currentHP - damage; + if (newHP < 0) { + newHP = 0; + } + if (damageFactor > 0) { + Grasscutter.getLogger() + .debug( + currentHP + + "/" + + maxHP + + "\tLandingSpeed: " + + cachedLandingSpeed + + "\tDamageFactor: " + + damageFactor + + "\tDamage: " + + damage + + "\tNewHP: " + + newHP); + } else { + Grasscutter.getLogger().trace(currentHP + "/" + maxHP + "\tLandingSpeed: 0\tNo damage"); + } + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); + entity + .getWorld() + .broadcastPacket( + new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); + if (newHP == 0) { + session + .getPlayer() + .getStaminaManager() + .killAvatar(session, entity, PlayerDieTypeOuterClass.PlayerDieType.PLAYER_DIE_TYPE_FALL); + } + cachedLandingSpeed = 0; + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java index 29ceb50a7..b1c13d419 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java @@ -1,28 +1,29 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify; -import emu.grasscutter.server.game.GameSession; - -@Opcodes(PacketOpcodes.EvtDoSkillSuccNotify) -public class HandlerEvtDoSkillSuccNotify extends PacketHandler { - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload); - - var player = session.getPlayer(); - int skillId = notify.getSkillId(); - int casterId = notify.getCasterId(); - - // Call skill perform in the player's ability manager. - player.getAbilityManager().onSkillStart(session.getPlayer(), skillId, casterId); - - // Handle skill notify in other managers. - player.getStaminaManager().handleEvtDoSkillSuccNotify(session, skillId, casterId); - player.getEnergyManager().handleEvtDoSkillSuccNotify(session, skillId, casterId); - player.getQuestManager().triggerEvent(QuestContent.QUEST_CONTENT_SKILL, skillId, 0); - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.EvtDoSkillSuccNotify) +public class HandlerEvtDoSkillSuccNotify extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload); + + var player = session.getPlayer(); + int skillId = notify.getSkillId(); + int casterId = notify.getCasterId(); + + // Call skill perform in the player's ability manager. + player.getAbilityManager().onSkillStart(session.getPlayer(), skillId, casterId); + + // Handle skill notify in other managers. + player.getStaminaManager().handleEvtDoSkillSuccNotify(session, skillId, casterId); + player.getEnergyManager().handleEvtDoSkillSuccNotify(session, skillId, casterId); + player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_SKILL, skillId, 0); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerExecuteGadgetLuaReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerExecuteGadgetLuaReq.java index 1ef70e3a1..a18f87630 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerExecuteGadgetLuaReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerExecuteGadgetLuaReq.java @@ -1,29 +1,29 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.ExecuteGadgetLuaReqOuterClass.ExecuteGadgetLuaReq; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketExecuteGadgetLuaRsp; - -@Opcodes(PacketOpcodes.ExecuteGadgetLuaReq) -public class HandlerExecuteGadgetLuaReq extends PacketHandler { - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - ExecuteGadgetLuaReq req = ExecuteGadgetLuaReq.parseFrom(payload); - - Player player = session.getPlayer(); - GameEntity entity = player.getScene().getEntities().get(req.getSourceEntityId()); - - int result = 1; - if (entity instanceof EntityGadget gadget) - result = gadget.onClientExecuteRequest(req.getParam1(), req.getParam2(), req.getParam3()); - - player.sendPacket(new PacketExecuteGadgetLuaRsp(result)); - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.ExecuteGadgetLuaReqOuterClass.ExecuteGadgetLuaReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketExecuteGadgetLuaRsp; + +@Opcodes(PacketOpcodes.ExecuteGadgetLuaReq) +public class HandlerExecuteGadgetLuaReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + ExecuteGadgetLuaReq req = ExecuteGadgetLuaReq.parseFrom(payload); + + Player player = session.getPlayer(); + GameEntity entity = player.getScene().getEntities().get(req.getSourceEntityId()); + + int result = 1; + if (entity instanceof EntityGadget gadget) + result = gadget.onClientExecuteRequest(req.getParam1(), req.getParam2(), req.getParam3()); + + player.sendPacket(new PacketExecuteGadgetLuaRsp(result)); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGadgetInteractReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGadgetInteractReq.java index 10c6040f7..8759be3c5 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGadgetInteractReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGadgetInteractReq.java @@ -1,22 +1,23 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.server.game.GameSession; - -@Opcodes(PacketOpcodes.GadgetInteractReq) -public class HandlerGadgetInteractReq extends PacketHandler { - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - GadgetInteractReq req = GadgetInteractReq.parseFrom(payload); - - session - .getPlayer() - .getQuestManager() - .triggerEvent(QuestContent.QUEST_CONTENT_INTERACT_GADGET, req.getGadgetId()); - session.getPlayer().interactWith(req.getGadgetEntityId(), req); - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.GadgetInteractReq) +public class HandlerGadgetInteractReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + GadgetInteractReq req = GadgetInteractReq.parseFrom(payload); + + session + .getPlayer() + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_INTERACT_GADGET, req.getGadgetId()); + session.getPlayer().interactWith(req.getGadgetEntityId(), req); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameSettleReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameSettleReq.java index 460d6dfbe..651a3dbc3 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameSettleReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameSettleReq.java @@ -1,67 +1,66 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.game.activity.musicgame.MusicGameActivityHandler; -import emu.grasscutter.game.activity.musicgame.MusicGamePlayerData; -import emu.grasscutter.game.props.ActivityType; -import emu.grasscutter.game.props.WatcherTriggerType; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.MusicGameSettleReqOuterClass; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketActivityInfoNotify; -import emu.grasscutter.server.packet.send.PacketMusicGameSettleRsp; - -@Opcodes(PacketOpcodes.MusicGameSettleReq) -public class HandlerMusicGameSettleReq extends PacketHandler { - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - var req = MusicGameSettleReqOuterClass.MusicGameSettleReq.parseFrom(payload); - - var playerData = - session - .getPlayer() - .getActivityManager() - .getPlayerActivityDataByActivityType(ActivityType.NEW_ACTIVITY_MUSIC_GAME); - if (playerData.isEmpty()) { - return; - } - var handler = (MusicGameActivityHandler) playerData.get().getActivityHandler(); - boolean isNewRecord = false; - // check if custom beatmap - - if (req.getUgcGuid() == 0) { - session - .getPlayer() - .getActivityManager() - .triggerWatcher( - WatcherTriggerType.TRIGGER_FLEUR_FAIR_MUSIC_GAME_REACH_SCORE, - String.valueOf(req.getMusicBasicId()), - String.valueOf(req.getScore())); - - isNewRecord = - handler.setMusicGameRecord( - playerData.get(), - MusicGamePlayerData.MusicGameRecord.of() - .musicId(req.getMusicBasicId()) - .maxCombo(req.getMaxCombo()) - .maxScore(req.getScore()) - .build()); - - // update activity info - session.send(new PacketActivityInfoNotify(handler.toProto(playerData.get()))); - } else { - handler.setMusicGameCustomBeatmapRecord( - playerData.get(), - MusicGamePlayerData.CustomBeatmapRecord.of() - .musicShareId(req.getUgcGuid()) - .score(req.getMaxCombo()) - // .settle(req.getSuccess()) - .build()); - } - - session.send( - new PacketMusicGameSettleRsp(req.getMusicBasicId(), req.getUgcGuid(), isNewRecord)); - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.activity.musicgame.MusicGameActivityHandler; +import emu.grasscutter.game.activity.musicgame.MusicGamePlayerData; +import emu.grasscutter.game.props.ActivityType; +import emu.grasscutter.game.props.WatcherTriggerType; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.MusicGameSettleReqOuterClass.MusicGameSettleReq; +import emu.grasscutter.net.proto.RetcodeOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketActivityInfoNotify; +import emu.grasscutter.server.packet.send.PacketMusicGameSettleRsp; +import lombok.val; + +@Opcodes(PacketOpcodes.MusicGameSettleReq) +public class HandlerMusicGameSettleReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + val req = MusicGameSettleReq.parseFrom(payload); + + val activityManager = session.getPlayer().getActivityManager(); + + val playerDataOpt = activityManager.getPlayerActivityDataByActivityType(ActivityType.NEW_ACTIVITY_MUSIC_GAME); + if (playerDataOpt.isEmpty()) { + session.send(new PacketMusicGameSettleRsp(RetcodeOuterClass.Retcode.RET_MUSIC_GAME_LEVEL_CONFIG_NOT_FOUND, req)); + return; + } + + val playerData = playerDataOpt.get(); + val handler = (MusicGameActivityHandler) playerData.getActivityHandler(); + boolean isNewRecord = false; + + // check if custom beatmap + if (req.getUgcGuid() == 0) { + session.getPlayer().getActivityManager().triggerWatcher( + WatcherTriggerType.TRIGGER_FLEUR_FAIR_MUSIC_GAME_REACH_SCORE, + String.valueOf(req.getMusicBasicId()), + String.valueOf(req.getScore()) + ); + + isNewRecord = handler.setMusicGameRecord(playerData, + MusicGamePlayerData.MusicGameRecord.of() + .musicId(req.getMusicBasicId()) + .maxCombo(req.getMaxCombo()) + .maxScore(req.getScore()) + .build()); + + // update activity info + session.send(new PacketActivityInfoNotify(handler.toProto(playerData, activityManager.getConditionExecutor()))); + } else { + handler.setMusicGameCustomBeatmapRecord(playerData, + MusicGamePlayerData.CustomBeatmapRecord.of() + .musicShareId(req.getUgcGuid()) + .score(req.getMaxCombo()) + .settle(req.getIsSaveScore()) + .build()); + } + + + session.send(new PacketMusicGameSettleRsp(req.getMusicBasicId(), req.getUgcGuid(), isNewRecord)); + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java index 5ba4bd6bd..64928e8b2 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java @@ -1,66 +1,67 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.MainQuestData; -import emu.grasscutter.data.binout.MainQuestData.TalkData; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.NpcTalkReqOuterClass.NpcTalkReq; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketNpcTalkRsp; - -@Opcodes(PacketOpcodes.NpcTalkReq) -public class HandlerNpcTalkReq extends PacketHandler { - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - NpcTalkReq req = NpcTalkReq.parseFrom(payload); - - // Check if mainQuest exists - // remove last 2 digits to get a mainQuestId - int talkId = req.getTalkId(); - int mainQuestId = talkId / 100; - MainQuestData mainQuestData = GameData.getMainQuestDataMap().get(mainQuestId); - - if (mainQuestData != null) { - // This talk is associated with a quest. Handle it. - // If the quest has no talk data defined on it, create one. - TalkData talkForQuest = new TalkData(talkId, ""); - if (mainQuestData.getTalks() != null) { - var talks = mainQuestData.getTalks().stream().filter(p -> p.getId() == talkId).toList(); - - if (talks.size() > 0) { - talkForQuest = talks.get(0); - } - } - - // Add to the list of done talks for this quest. - var mainQuest = session.getPlayer().getQuestManager().getMainQuestById(mainQuestId); - if (mainQuest != null) { - session - .getPlayer() - .getQuestManager() - .getMainQuestById(mainQuestId) - .getTalks() - .put(talkId, talkForQuest); - } - - // Fire quest triggers. - session - .getPlayer() - .getQuestManager() - .triggerEvent( - QuestContent.QUEST_CONTENT_COMPLETE_ANY_TALK, String.valueOf(req.getTalkId()), 0, 0); - session - .getPlayer() - .getQuestManager() - .triggerEvent(QuestContent.QUEST_CONTENT_COMPLETE_TALK, req.getTalkId(), 0); - session - .getPlayer() - .getQuestManager() - .triggerEvent(QuestContent.QUEST_CONTENT_FINISH_PLOT, req.getTalkId(), 0); - } - - session.send(new PacketNpcTalkRsp(req.getNpcEntityId(), req.getTalkId(), req.getEntityId())); - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.MainQuestData; +import emu.grasscutter.data.binout.MainQuestData.TalkData; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.NpcTalkReqOuterClass.NpcTalkReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketNpcTalkRsp; + +@Opcodes(PacketOpcodes.NpcTalkReq) +public class HandlerNpcTalkReq extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + NpcTalkReq req = NpcTalkReq.parseFrom(payload); + + // Check if mainQuest exists + // remove last 2 digits to get a mainQuestId + int talkId = req.getTalkId(); + int mainQuestId = talkId / 100; + MainQuestData mainQuestData = GameData.getMainQuestDataMap().get(mainQuestId); + + if (mainQuestData != null) { + // This talk is associated with a quest. Handle it. + // If the quest has no talk data defined on it, create one. + TalkData talkForQuest = new TalkData(talkId, ""); + if (mainQuestData.getTalks() != null) { + var talks = mainQuestData.getTalks().stream().filter(p -> p.getId() == talkId).toList(); + + if (talks.size() > 0) { + talkForQuest = talks.get(0); + } + } + + // Add to the list of done talks for this quest. + var mainQuest = session.getPlayer().getQuestManager().getMainQuestById(mainQuestId); + if (mainQuest != null) { + session + .getPlayer() + .getQuestManager() + .getMainQuestById(mainQuestId) + .getTalks() + .put(talkId, talkForQuest); + } + + // Fire quest triggers. + session + .getPlayer() + .getQuestManager() + .queueEvent( + QuestContent.QUEST_CONTENT_COMPLETE_ANY_TALK, String.valueOf(req.getTalkId()), 0, 0); + session + .getPlayer() + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_COMPLETE_TALK, req.getTalkId(), 0); + session + .getPlayer() + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_FINISH_PLOT, req.getTalkId(), 0); + } + + session.send(new PacketNpcTalkRsp(req.getNpcEntityId(), req.getTalkId(), req.getEntityId())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerPostEnterSceneReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerPostEnterSceneReq.java index b1f1f7482..a75a9bd3a 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerPostEnterSceneReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerPostEnterSceneReq.java @@ -1,24 +1,25 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.game.props.SceneType; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketPostEnterSceneRsp; - -@Opcodes(PacketOpcodes.PostEnterSceneReq) -public class HandlerPostEnterSceneReq extends PacketHandler { - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - if (session.getPlayer().getScene().getSceneType() == SceneType.SCENE_ROOM) { - session - .getPlayer() - .getQuestManager() - .triggerEvent(QuestContent.QUEST_CONTENT_ENTER_ROOM, session.getPlayer().getSceneId(), 0); - } - - session.send(new PacketPostEnterSceneRsp(session.getPlayer())); - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.props.SceneType; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketPostEnterSceneRsp; + +@Opcodes(PacketOpcodes.PostEnterSceneReq) +public class HandlerPostEnterSceneReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + if (session.getPlayer().getScene().getSceneType() == SceneType.SCENE_ROOM) { + session + .getPlayer() + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_ENTER_ROOM, session.getPlayer().getSceneId(), 0); + } + + session.send(new PacketPostEnterSceneRsp(session.getPlayer())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSelectWorktopOptionReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSelectWorktopOptionReq.java index e0137841b..a8191a6ca 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSelectWorktopOptionReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSelectWorktopOptionReq.java @@ -1,40 +1,39 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.data.ScriptArgs; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketSelectWorktopOptionRsp; - -@Opcodes(PacketOpcodes.SelectWorktopOptionReq) -public class HandlerSelectWorktopOptionReq extends PacketHandler { - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - SelectWorktopOptionReq req = SelectWorktopOptionReq.parseFrom(payload); - - try { - GameEntity entity = session.getPlayer().getScene().getEntityById(req.getGadgetEntityId()); - - if (!(entity instanceof EntityGadget)) { - return; - } - session.getPlayer().getScene().selectWorktopOptionWith(req); - session - .getPlayer() - .getScene() - .getScriptManager() - .callEvent( - EventType.EVENT_SELECT_OPTION, - new ScriptArgs(entity.getConfigId(), req.getOptionId())); - } finally { - // Always send packet - session.send(new PacketSelectWorktopOptionRsp(req.getGadgetEntityId(), req.getOptionId())); - } - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketSelectWorktopOptionRsp; + +@Opcodes(PacketOpcodes.SelectWorktopOptionReq) +public class HandlerSelectWorktopOptionReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + SelectWorktopOptionReq req = SelectWorktopOptionReq.parseFrom(payload); + + try { + GameEntity entity = session.getPlayer().getScene().getEntityById(req.getGadgetEntityId()); + + if (!(entity instanceof EntityGadget)) { + return; + } + session.getPlayer().getScene().selectWorktopOptionWith(req); + session.getPlayer().getScene().getScriptManager().callEvent( + new ScriptArgs(entity.getGroupId(), EventType.EVENT_SELECT_OPTION, entity.getConfigId(), req.getOptionId()) + ); + session.getPlayer().getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_WORKTOP_SELECT, entity.getConfigId(), req.getOptionId()); + } finally { + // Always send packet + session.send(new PacketSelectWorktopOptionRsp(req.getGadgetEntityId(), req.getOptionId())); + } + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSkipPlayerGameTimeReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSkipPlayerGameTimeReq.java index 437261df1..398a76583 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSkipPlayerGameTimeReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSkipPlayerGameTimeReq.java @@ -14,7 +14,7 @@ public class HandlerSkipPlayerGameTimeReq extends PacketHandler { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { var req = SkipPlayerGameTimeReqOuterClass.SkipPlayerGameTimeReq.parseFrom(payload); var player = session.getPlayer(); - player.getScene().setTime(req.getGameTime()); + player.updatePlayerGameTime(req.getGameTime()); player.getScene().broadcastPacket(new PacketPlayerGameTimeNotify(player)); player.sendPacket(new PacketSkipPlayerGameTimeRsp(req)); } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerUpdateAbilityCreatedMovingPlatformNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerUpdateAbilityCreatedMovingPlatformNotify.java index 0b69fe145..cf5e1ebd5 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerUpdateAbilityCreatedMovingPlatformNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerUpdateAbilityCreatedMovingPlatformNotify.java @@ -1,34 +1,27 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.game.entity.platform.EntityPlatform; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.UpdateAbilityCreatedMovingPlatformNotifyOuterClass; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketPlatformStartRouteNotify; -import emu.grasscutter.server.packet.send.PacketPlatformStopRouteNotify; - -@Opcodes(PacketOpcodes.UpdateAbilityCreatedMovingPlatformNotify) -public class HandlerUpdateAbilityCreatedMovingPlatformNotify extends PacketHandler { - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - var notify = - UpdateAbilityCreatedMovingPlatformNotifyOuterClass.UpdateAbilityCreatedMovingPlatformNotify - .parseFrom(payload); - var entity = session.getPlayer().getScene().getEntityById(notify.getEntityId()); - - if (!(entity instanceof EntityPlatform)) { - return; - } - - var scene = ((EntityPlatform) entity).getOwner().getScene(); - - switch (notify.getOpType()) { - case OP_TYPE_ACTIVATE -> scene.broadcastPacket( - new PacketPlatformStartRouteNotify((EntityPlatform) entity, scene)); - case OP_TYPE_DEACTIVATE -> scene.broadcastPacket( - new PacketPlatformStopRouteNotify((EntityPlatform) entity, scene)); - } - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.gadget.platform.AbilityRoute; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.UpdateAbilityCreatedMovingPlatformNotifyOuterClass.UpdateAbilityCreatedMovingPlatformNotify; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.UpdateAbilityCreatedMovingPlatformNotify) +public class HandlerUpdateAbilityCreatedMovingPlatformNotify extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + var notify = UpdateAbilityCreatedMovingPlatformNotify.parseFrom(payload); + var entity = session.getPlayer().getScene().getEntityById(notify.getEntityId()); + + if (!(entity instanceof EntityGadget entityGadget) || !(entityGadget.getRouteConfig() instanceof AbilityRoute)) { + return; + } + + switch (notify.getOpType()) { + case OP_TYPE_ACTIVATE -> entityGadget.startPlatform(); + case OP_TYPE_DEACTIVATE -> entityGadget.stopPlatform(); + } + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketChangeGameTimeRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketChangeGameTimeRsp.java index fe69b62f4..62766db57 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketChangeGameTimeRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketChangeGameTimeRsp.java @@ -1,18 +1,18 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.player.Player; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.ChangeGameTimeRspOuterClass.ChangeGameTimeRsp; - -public class PacketChangeGameTimeRsp extends BasePacket { - - public PacketChangeGameTimeRsp(Player player) { - super(PacketOpcodes.ChangeGameTimeRsp); - - ChangeGameTimeRsp proto = - ChangeGameTimeRsp.newBuilder().setCurGameTime(player.getScene().getTime()).build(); - - this.setData(proto); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.ChangeGameTimeRspOuterClass.ChangeGameTimeRsp; + +public class PacketChangeGameTimeRsp extends BasePacket { + + public PacketChangeGameTimeRsp(Player player) { + super(PacketOpcodes.ChangeGameTimeRsp); + + ChangeGameTimeRsp proto = + ChangeGameTimeRsp.newBuilder().setCurGameTime(player.getWorld().getGameTime()).build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java index 31dd3c9f2..5439c8922 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSettleNotify.java @@ -1,64 +1,13 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.DungeonSettleNotifyOuterClass.DungeonSettleNotify; -import emu.grasscutter.net.proto.ItemParamOuterClass; -import emu.grasscutter.net.proto.TowerLevelEndNotifyOuterClass.TowerLevelEndNotify; - -public class PacketDungeonSettleNotify extends BasePacket { - - public PacketDungeonSettleNotify(WorldChallenge challenge) { - super(PacketOpcodes.DungeonSettleNotify); - - DungeonSettleNotify proto = - DungeonSettleNotify.newBuilder() - .setDungeonId(challenge.getScene().getDungeonData().getId()) - .setIsSuccess(challenge.isSuccess()) - .setCloseTime(challenge.getScene().getAutoCloseTime()) - .setResult(challenge.isSuccess() ? 1 : 0) - .build(); - - this.setData(proto); - } - - public PacketDungeonSettleNotify( - WorldChallenge challenge, boolean canJump, boolean hasNextLevel, int nextFloorId) { - super(PacketOpcodes.DungeonSettleNotify); - - var continueStatus = - TowerLevelEndNotify.ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE; - if (challenge.isSuccess() && canJump) { - continueStatus = - hasNextLevel - ? TowerLevelEndNotify.ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE - : TowerLevelEndNotify.ContinueStateType - .CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE; - } - - var towerLevelEndNotify = - TowerLevelEndNotify.newBuilder() - .setIsSuccess(challenge.isSuccess()) - .setContinueState(continueStatus) - .addFinishedStarCondList(1) - .addFinishedStarCondList(2) - .addFinishedStarCondList(3) - .addRewardItemList( - ItemParamOuterClass.ItemParam.newBuilder().setItemId(201).setCount(1000).build()); - if (nextFloorId > 0 && canJump) { - towerLevelEndNotify.setNextFloorId(nextFloorId); - } - - DungeonSettleNotify proto = - DungeonSettleNotify.newBuilder() - .setDungeonId(challenge.getScene().getDungeonData().getId()) - .setIsSuccess(challenge.isSuccess()) - .setCloseTime(challenge.getScene().getAutoCloseTime()) - .setResult(challenge.isSuccess() ? 1 : 0) - .setTowerLevelEndNotify(towerLevelEndNotify.build()) - .build(); - - this.setData(proto); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; + +public class PacketDungeonSettleNotify extends BasePacket { + public PacketDungeonSettleNotify(BaseDungeonResult result) { + super(PacketOpcodes.DungeonSettleNotify); + + this.setData(result.getProto()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketEntityFightPropUpdateNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketEntityFightPropUpdateNotify.java index bcb4e4b76..c86521fe7 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketEntityFightPropUpdateNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketEntityFightPropUpdateNotify.java @@ -1,22 +1,33 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.EntityFightPropUpdateNotifyOuterClass.EntityFightPropUpdateNotify; - -public class PacketEntityFightPropUpdateNotify extends BasePacket { - - public PacketEntityFightPropUpdateNotify(GameEntity entity, FightProperty prop) { - super(PacketOpcodes.EntityFightPropUpdateNotify); - - EntityFightPropUpdateNotify proto = - EntityFightPropUpdateNotify.newBuilder() - .setEntityId(entity.getId()) - .putFightPropMap(prop.getId(), entity.getFightProperty(prop)) - .build(); - - this.setData(proto); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.EntityFightPropUpdateNotifyOuterClass.EntityFightPropUpdateNotify; + +import java.util.Collection; + +public class PacketEntityFightPropUpdateNotify extends BasePacket { + public PacketEntityFightPropUpdateNotify(GameEntity entity, FightProperty prop) { + super(PacketOpcodes.EntityFightPropUpdateNotify); + + EntityFightPropUpdateNotify proto = + EntityFightPropUpdateNotify.newBuilder() + .setEntityId(entity.getId()) + .putFightPropMap(prop.getId(), entity.getFightProperty(prop)) + .build(); + + this.setData(proto); + } + + public PacketEntityFightPropUpdateNotify(GameEntity entity, Collection props) { + super(PacketOpcodes.EntityFightPropUpdateNotify); + + var protoBuilder = EntityFightPropUpdateNotify.newBuilder() + .setEntityId(entity.getId()); + props.forEach(p -> protoBuilder.putFightPropMap(p.getId(), entity.getFightProperty(p))); + + this.setData(protoBuilder); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestNotify.java index 30a8ede32..6da35e3fc 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestNotify.java @@ -1,26 +1,26 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.quest.GameMainQuest; -import emu.grasscutter.game.quest.enums.ParentQuestState; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.FinishedParentQuestNotifyOuterClass.FinishedParentQuestNotify; - -public class PacketFinishedParentQuestNotify extends BasePacket { - - public PacketFinishedParentQuestNotify(Player player) { - super(PacketOpcodes.FinishedParentQuestNotify, true); - - FinishedParentQuestNotify.Builder proto = FinishedParentQuestNotify.newBuilder(); - - for (GameMainQuest mainQuest : player.getQuestManager().getMainQuests().values()) { - // Canceled Quests do not appear in this packet - if (mainQuest.getState() != ParentQuestState.PARENT_QUEST_STATE_CANCELED) { - proto.addParentQuestList(mainQuest.toProto()); - } - } - - this.setData(proto); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.game.quest.enums.ParentQuestState; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.FinishedParentQuestNotifyOuterClass.FinishedParentQuestNotify; + +public class PacketFinishedParentQuestNotify extends BasePacket { + + public PacketFinishedParentQuestNotify(Player player) { + super(PacketOpcodes.FinishedParentQuestNotify, true); + + FinishedParentQuestNotify.Builder proto = FinishedParentQuestNotify.newBuilder(); + + for (GameMainQuest mainQuest : player.getQuestManager().getMainQuests().values()) { + //Canceled Quests do not appear in this packet + if (mainQuest.getState() != ParentQuestState.PARENT_QUEST_STATE_CANCELED) { + proto.addParentQuestList(mainQuest.toProto(false)); + } + } + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestUpdateNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestUpdateNotify.java index db571e003..d3e4542f4 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestUpdateNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketFinishedParentQuestUpdateNotify.java @@ -1,31 +1,33 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.quest.GameMainQuest; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.FinishedParentQuestUpdateNotifyOuterClass.FinishedParentQuestUpdateNotify; -import java.util.List; - -public class PacketFinishedParentQuestUpdateNotify extends BasePacket { - - public PacketFinishedParentQuestUpdateNotify(GameMainQuest quest) { - super(PacketOpcodes.FinishedParentQuestUpdateNotify); - - FinishedParentQuestUpdateNotify proto = - FinishedParentQuestUpdateNotify.newBuilder().addParentQuestList(quest.toProto()).build(); - - this.setData(proto); - } - - public PacketFinishedParentQuestUpdateNotify(List quests) { - super(PacketOpcodes.FinishedParentQuestUpdateNotify); - - var proto = FinishedParentQuestUpdateNotify.newBuilder(); - - for (GameMainQuest mainQuest : quests) { - proto.addParentQuestList(mainQuest.toProto()); - } - proto.build(); - this.setData(proto); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.quest.GameMainQuest; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.FinishedParentQuestUpdateNotifyOuterClass.FinishedParentQuestUpdateNotify; + +import java.util.List; + +public class PacketFinishedParentQuestUpdateNotify extends BasePacket { + + public PacketFinishedParentQuestUpdateNotify(GameMainQuest quest) { + super(PacketOpcodes.FinishedParentQuestUpdateNotify); + + FinishedParentQuestUpdateNotify proto = FinishedParentQuestUpdateNotify.newBuilder() + .addParentQuestList(quest.toProto(true)) + .build(); + + this.setData(proto); + } + + public PacketFinishedParentQuestUpdateNotify(List quests) { + super(PacketOpcodes.FinishedParentQuestUpdateNotify); + + var proto = FinishedParentQuestUpdateNotify.newBuilder(); + + for (GameMainQuest mainQuest : quests) { + proto.addParentQuestList(mainQuest.toProto(true)); + } + proto.build(); + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown2Rsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown2Rsp.java index 31cef2efb..0d74182ce 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown2Rsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown2Rsp.java @@ -1,11 +1,11 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; - -public class PacketHomeUnknown2Rsp extends BasePacket { - - public PacketHomeUnknown2Rsp() { - super(PacketOpcodes.Unk2700_KIIOGMKFNNP_ServerRsp); - } -} +//package emu.grasscutter.server.packet.send; +// +//import emu.grasscutter.net.packet.BasePacket; +//import emu.grasscutter.net.packet.PacketOpcodes; +// +//public class PacketHomeUnknown2Rsp extends BasePacket { +// +// public PacketHomeUnknown2Rsp() { +// super(PacketOpcodes.Unk2700_KIIOGMKFNNP_ServerRsp); +// } +//} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameSettleRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameSettleRsp.java index 403be8b98..5651851bc 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameSettleRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameSettleRsp.java @@ -1,18 +1,32 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.MusicGameSettleRspOuterClass; - -public class PacketMusicGameSettleRsp extends BasePacket { - - public PacketMusicGameSettleRsp(int musicBasicId, long musicShareId, boolean isNewRecord) { - super(PacketOpcodes.MusicGameSettleRsp); - - var proto = MusicGameSettleRspOuterClass.MusicGameSettleRsp.newBuilder(); - - proto.setMusicBasicId(musicBasicId).setUgcGuid(musicShareId).setIsNewRecord(isNewRecord); - - this.setData(proto); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.MusicGameSettleReqOuterClass; +import emu.grasscutter.net.proto.MusicGameSettleRspOuterClass; +import emu.grasscutter.net.proto.RetcodeOuterClass; + +public class PacketMusicGameSettleRsp extends BasePacket { + + public PacketMusicGameSettleRsp(int musicBasicId, long musicShareId, boolean isNewRecord) { + super(PacketOpcodes.MusicGameSettleRsp); + + var proto = MusicGameSettleRspOuterClass.MusicGameSettleRsp.newBuilder(); + + proto.setMusicBasicId(musicBasicId).setUgcGuid(musicShareId).setIsNewRecord(isNewRecord); + + this.setData(proto); + } + + public PacketMusicGameSettleRsp(RetcodeOuterClass.Retcode errorCode, MusicGameSettleReqOuterClass.MusicGameSettleReq req) { + super(PacketOpcodes.MusicGameSettleRsp); + + var proto = MusicGameSettleRspOuterClass.MusicGameSettleRsp.newBuilder() + .setRetcode(errorCode.getNumber()) + .setMusicBasicId(req.getMusicBasicId()) + .setUgcGuid(req.getUgcGuid()); + + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformStartRouteNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformStartRouteNotify.java index c9ad31dc6..7414feb5b 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformStartRouteNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformStartRouteNotify.java @@ -1,22 +1,20 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.entity.platform.EntityPlatform; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.PlatformStartRouteNotifyOuterClass; - -public class PacketPlatformStartRouteNotify extends BasePacket { - public PacketPlatformStartRouteNotify(EntityPlatform entity, Scene scene) { - super(PacketOpcodes.PlatformStartRouteNotify); - - var notify = - PlatformStartRouteNotifyOuterClass.PlatformStartRouteNotify.newBuilder() - .setEntityId(entity.getId()) - .setSceneTime(scene.getSceneTime()) - .setPlatform(entity.onStartRoute()) - .build(); - - this.setData(notify); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.PlatformStartRouteNotifyOuterClass.PlatformStartRouteNotify; +import lombok.val; + +public class PacketPlatformStartRouteNotify extends BasePacket { + public PacketPlatformStartRouteNotify(EntityGadget gadgetEntity) { + super(PacketOpcodes.PlatformStartRouteNotify); + + val notify = PlatformStartRouteNotify.newBuilder() + .setEntityId(gadgetEntity.getId()) + .setSceneTime(gadgetEntity.getScene().getSceneTime()) + .setPlatform(gadgetEntity.getPlatformInfo()); + + this.setData(notify); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformStopRouteNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformStopRouteNotify.java index 6dec5b466..08cd2c64b 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformStopRouteNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformStopRouteNotify.java @@ -1,22 +1,20 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.entity.platform.EntityPlatform; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.PlatformStopRouteNotifyOuterClass; - -public class PacketPlatformStopRouteNotify extends BasePacket { - public PacketPlatformStopRouteNotify(EntityPlatform entity, Scene scene) { - super(PacketOpcodes.PlatformStopRouteNotify); - - var notify = - PlatformStopRouteNotifyOuterClass.PlatformStopRouteNotify.newBuilder() - .setPlatform(entity.onStopRoute()) - .setSceneTime(scene.getSceneTime()) - .setEntityId(entity.getId()) - .build(); - - this.setData(notify); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.PlatformStopRouteNotifyOuterClass; + +public class PacketPlatformStopRouteNotify extends BasePacket { + public PacketPlatformStopRouteNotify(EntityGadget gadgetEntity) { + super(PacketOpcodes.PlatformStopRouteNotify); + + var notify = PlatformStopRouteNotifyOuterClass.PlatformStopRouteNotify.newBuilder() + .setPlatform(gadgetEntity.getPlatformInfo()) + .setSceneTime(gadgetEntity.getScene().getSceneTime()) + .setEntityId(gadgetEntity.getId()) + .build(); + + this.setData(notify); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerGameTimeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerGameTimeNotify.java index 8dd589ef8..6c0f0ca78 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerGameTimeNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerGameTimeNotify.java @@ -1,21 +1,21 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.player.Player; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.PlayerGameTimeNotifyOuterClass.PlayerGameTimeNotify; - -public class PacketPlayerGameTimeNotify extends BasePacket { - - public PacketPlayerGameTimeNotify(Player player) { - super(PacketOpcodes.PlayerGameTimeNotify); - - PlayerGameTimeNotify proto = - PlayerGameTimeNotify.newBuilder() - .setGameTime(player.getScene().getTime()) - .setUid(player.getUid()) - .build(); - - this.setData(proto); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.PlayerGameTimeNotifyOuterClass.PlayerGameTimeNotify; + +public class PacketPlayerGameTimeNotify extends BasePacket { + + public PacketPlayerGameTimeNotify(Player player) { + super(PacketOpcodes.PlayerGameTimeNotify); + + PlayerGameTimeNotify proto = + PlayerGameTimeNotify.newBuilder() + .setGameTime(player.getWorld().getGameTime()) + .setUid(player.getUid()) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index 3ace8f010..94600ec5a 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -1,513 +1,513 @@ -package emu.grasscutter.utils; - -import static emu.grasscutter.config.Configuration.FALLBACK_LANGUAGE; -import static emu.grasscutter.utils.FileUtils.getResourcePath; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.ResourceLoader; -import emu.grasscutter.data.excels.achievement.AchievementData; -import emu.grasscutter.game.player.Player; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import lombok.EqualsAndHashCode; - -public final class Language { - private static final Map cachedLanguages = new ConcurrentHashMap<>(); - private static final int TEXTMAP_CACHE_VERSION = 0x9CCACE04; - private static final Pattern textMapKeyValueRegex = Pattern.compile("\"(\\d+)\": \"(.+)\""); - private static final Path TEXTMAP_CACHE_PATH = - Path.of(Utils.toFilePath("cache/TextMapCache.bin")); - private static boolean scannedTextmaps = - false; // Ensure that we don't infinitely rescan on cache misses that don't exist - private static Int2ObjectMap textMapStrings; - private final String languageCode; - private final Map translations = new ConcurrentHashMap<>(); - - /** Reads a file and creates a language instance. */ - private Language(LanguageStreamDescription description) { - languageCode = description.getLanguageCode(); - - try { - var object = - JsonUtils.decode( - Utils.readFromInputStream(description.getLanguageFile()), JsonObject.class); - object - .entrySet() - .forEach(entry -> putFlattenedKey(translations, entry.getKey(), entry.getValue())); - } catch (Exception exception) { - Grasscutter.getLogger() - .warn("Failed to load language file: " + description.getLanguageCode(), exception); - } - } - - /** - * Creates a language instance from a code. - * - * @param langCode The language code. - * @return A language instance. - */ - public static Language getLanguage(String langCode) { - if (cachedLanguages.containsKey(langCode)) { - return cachedLanguages.get(langCode); - } - - var fallbackLanguageCode = Utils.getLanguageCode(FALLBACK_LANGUAGE); - var description = getLanguageFileDescription(langCode, fallbackLanguageCode); - var actualLanguageCode = description.getLanguageCode(); - - Language languageInst; - if (description.getLanguageFile() != null) { - languageInst = new Language(description); - cachedLanguages.put(actualLanguageCode, languageInst); - } else { - languageInst = cachedLanguages.get(actualLanguageCode); - cachedLanguages.put(langCode, languageInst); - } - - return languageInst; - } - - /** - * Returns the translated value from the key while substituting arguments. - * - * @param key The key of the translated value to return. - * @param args The arguments to substitute. - * @return A translated value with arguments substituted. - */ - public static String translate(String key, Object... args) { - String translated = Grasscutter.getLanguage().get(key); - - for (int i = 0; i < args.length; i++) { - args[i] = - switch (args[i].getClass().getSimpleName()) { - case "String" -> args[i]; - case "TextStrings" -> ((TextStrings) args[i]) - .get(0) - .replace("\\\\n", "\\n"); // TODO: Change this to server language - default -> args[i].toString(); - }; - } - - try { - return translated.formatted(args); - } catch (Exception exception) { - Grasscutter.getLogger().error("Failed to format string: " + key, exception); - return translated; - } - } - - /** - * Returns the translated value from the key while substituting arguments. - * - * @param player Target player - * @param key The key of the translated value to return. - * @param args The arguments to substitute. - * @return A translated value with arguments substituted. - */ - public static String translate(Player player, String key, Object... args) { - if (player == null) { - return translate(key, args); - } - - var langCode = Utils.getLanguageCode(player.getAccount().getLocale()); - String translated = getLanguage(langCode).get(key); - - for (int i = 0; i < args.length; i++) { - args[i] = - switch (args[i].getClass().getSimpleName()) { - case "String" -> args[i]; - case "TextStrings" -> ((TextStrings) args[i]) - .getGC(langCode) - .replace("\\\\n", "\n"); // Note that we don't unescape \n for server console - default -> args[i].toString(); - }; - } - - try { - return translated.formatted(args); - } catch (Exception exception) { - Grasscutter.getLogger().error("Failed to format string: " + key, exception); - return translated; - } - } - - /** - * Recursive helper function to flatten a Json tree Converts input like {"foo": {"bar": "baz"}} to - * {"foo.bar": "baz"} - * - * @param map The map to insert the keys into - * @param key The flattened key of the current element - * @param element The current element - */ - private static void putFlattenedKey(Map map, String key, JsonElement element) { - if (element.isJsonObject()) { - element - .getAsJsonObject() - .entrySet() - .forEach( - entry -> { - String keyPrefix = key.isEmpty() ? "" : key + "."; - putFlattenedKey(map, keyPrefix + entry.getKey(), entry.getValue()); - }); - } else { - map.put(key, element.getAsString()); - } - } - - /** - * create a LanguageStreamDescription - * - * @param languageCode The name of the language code. - * @param fallbackLanguageCode The name of the fallback language code. - */ - private static LanguageStreamDescription getLanguageFileDescription( - String languageCode, String fallbackLanguageCode) { - var fileName = languageCode + ".json"; - var fallback = fallbackLanguageCode + ".json"; - - String actualLanguageCode = languageCode; - InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); - - if (file == null) { // Provided fallback language. - Grasscutter.getLogger() - .warn("Failed to load language file: " + fileName + ", falling back to: " + fallback); - actualLanguageCode = fallbackLanguageCode; - if (cachedLanguages.containsKey(actualLanguageCode)) { - return new LanguageStreamDescription(actualLanguageCode, null); - } - - file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); - } - - if (file == null) { // Fallback the fallback language. - Grasscutter.getLogger() - .warn("Failed to load language file: " + fallback + ", falling back to: en-US.json"); - actualLanguageCode = "en-US"; - if (cachedLanguages.containsKey(actualLanguageCode)) { - return new LanguageStreamDescription(actualLanguageCode, null); - } - - file = Grasscutter.class.getResourceAsStream("/languages/en-US.json"); - } - - if (file == null) - throw new RuntimeException( - "Unable to load the primary, fallback, and 'en-US' language files."); - - return new LanguageStreamDescription(actualLanguageCode, file); - } - - private static Int2ObjectMap loadTextMapFile(String language, IntSet nameHashes) { - Int2ObjectMap output = new Int2ObjectOpenHashMap<>(); - try (BufferedReader file = - Files.newBufferedReader( - getResourcePath("TextMap/TextMap" + language + ".json"), StandardCharsets.UTF_8)) { - Matcher matcher = textMapKeyValueRegex.matcher(""); - return new Int2ObjectOpenHashMap<>( - file.lines() - .sequential() - .map(matcher::reset) // Side effects, but it's faster than making a new one - .filter(Matcher::find) - .filter( - m -> - nameHashes.contains( - (int) Long.parseLong(m.group(1)))) // TODO: Cache this parse somehow - .collect( - Collectors.toMap( - m -> (int) Long.parseLong(m.group(1)), - m -> m.group(2).replace("\\\"", "\"")))); - } catch (Exception e) { - Grasscutter.getLogger().error("Error loading textmap: " + language); - Grasscutter.getLogger().error(e.toString()); - } - return output; - } - - private static Int2ObjectMap loadTextMapFiles(IntSet nameHashes) { - Map> - mapLanguageMaps = // Separate step to process the textmaps in parallel - TextStrings.LIST_LANGUAGES.parallelStream() - .collect( - Collectors.toConcurrentMap( - s -> TextStrings.MAP_LANGUAGES.getInt(s), - s -> loadTextMapFile(s, nameHashes))); - List> languageMaps = - IntStream.range(0, TextStrings.NUM_LANGUAGES) - .mapToObj(i -> mapLanguageMaps.get(i)) - .collect(Collectors.toList()); - - Map canonicalTextStrings = new HashMap<>(); - return new Int2ObjectOpenHashMap( - nameHashes - .intStream() - .boxed() - .collect( - Collectors.toMap( - key -> key, - key -> { - TextStrings t = - new TextStrings( - IntStream.range(0, TextStrings.NUM_LANGUAGES) - .mapToObj(i -> languageMaps.get(i).get((int) key)) - .collect(Collectors.toList()), - key); - return canonicalTextStrings.computeIfAbsent(t, x -> t); - }))); - } - - @SuppressWarnings("unchecked") - private static Int2ObjectMap loadTextMapsCache() throws Exception { - try (ObjectInputStream file = - new ObjectInputStream( - new BufferedInputStream(Files.newInputStream(TEXTMAP_CACHE_PATH), 0x100000))) { - final int fileVersion = file.readInt(); - if (fileVersion != TEXTMAP_CACHE_VERSION) throw new Exception("Invalid cache version"); - return (Int2ObjectMap) file.readObject(); - } - } - - private static void saveTextMapsCache(Int2ObjectMap input) throws IOException { - try { - Files.createDirectory(Path.of("cache")); - } catch (FileAlreadyExistsException ignored) { - } - try (ObjectOutputStream file = - new ObjectOutputStream( - new BufferedOutputStream( - Files.newOutputStream(TEXTMAP_CACHE_PATH, StandardOpenOption.CREATE), 0x100000))) { - file.writeInt(TEXTMAP_CACHE_VERSION); - file.writeObject(input); - } - } - - @Deprecated(forRemoval = true) - public static Int2ObjectMap getTextMapStrings() { - if (textMapStrings == null) loadTextMaps(); - return textMapStrings; - } - - public static TextStrings getTextMapKey(int key) { - if ((textMapStrings == null) || (!scannedTextmaps && !textMapStrings.containsKey(key))) - loadTextMaps(); - return textMapStrings.get(key); - } - - public static TextStrings getTextMapKey(long hash) { - return getTextMapKey((int) hash); - } - - public static void loadTextMaps() { - // Check system timestamps on cache and resources - try { - long cacheModified = Files.getLastModifiedTime(TEXTMAP_CACHE_PATH).toMillis(); - - long textmapsModified = - Files.list(getResourcePath("TextMap")) - .filter(path -> path.toString().endsWith(".json")) - .map( - path -> { - try { - return Files.getLastModifiedTime(path).toMillis(); - } catch (Exception ignored) { - Grasscutter.getLogger() - .debug("Exception while checking modified time: ", path); - return Long.MAX_VALUE; // Don't use cache, something has gone wrong - } - }) - .max(Long::compare) - .get(); - - Grasscutter.getLogger() - .debug( - "Cache modified %d, textmap modified %d".formatted(cacheModified, textmapsModified)); - if (textmapsModified < cacheModified) { - // Try loading from cache - Grasscutter.getLogger().info("Loading cached 'TextMaps'..."); - textMapStrings = loadTextMapsCache(); - return; - } - } catch (Exception e) { - Grasscutter.getLogger().debug("Exception while checking cache: ", e); - } - - // Regenerate cache - Grasscutter.getLogger().debug("Generating TextMaps cache"); - ResourceLoader.loadAll(); - IntSet usedHashes = new IntOpenHashSet(); - GameData.getAchievementDataMap().values().stream() - .filter(AchievementData::isUsed) - .forEach( - a -> { - usedHashes.add((int) a.getTitleTextMapHash()); - usedHashes.add((int) a.getDescTextMapHash()); - }); - GameData.getAvatarDataMap().forEach((k, v) -> usedHashes.add((int) v.getNameTextMapHash())); - GameData.getAvatarSkillDataMap() - .forEach( - (k, v) -> { - usedHashes.add((int) v.getNameTextMapHash()); - usedHashes.add((int) v.getDescTextMapHash()); - }); - GameData.getItemDataMap().forEach((k, v) -> usedHashes.add((int) v.getNameTextMapHash())); - GameData.getHomeWorldBgmDataMap() - .forEach((k, v) -> usedHashes.add((int) v.getBgmNameTextMapHash())); - GameData.getMonsterDataMap().forEach((k, v) -> usedHashes.add((int) v.getNameTextMapHash())); - GameData.getMainQuestDataMap().forEach((k, v) -> usedHashes.add((int) v.getTitleTextMapHash())); - GameData.getQuestDataMap().forEach((k, v) -> usedHashes.add((int) v.getDescTextMapHash())); - // Incidental strings - usedHashes.add((int) 4233146695L); // Character - usedHashes.add((int) 4231343903L); // Weapon - usedHashes.add((int) 332935371L); // Standard Wish - usedHashes.add((int) 2272170627L); // Character Event Wish - usedHashes.add((int) 3352513147L); // Character Event Wish-2 - usedHashes.add((int) 2864268523L); // Weapon Event Wish - - textMapStrings = loadTextMapFiles(usedHashes); - scannedTextmaps = true; - try { - saveTextMapsCache(textMapStrings); - } catch (IOException e) { - Grasscutter.getLogger().error("Failed to save TextMap cache: ", e); - } - } - - /** get language code */ - public String getLanguageCode() { - return languageCode; - } - - /** - * Returns the value (as a string) from a nested key. - * - * @param key The key to look for. - * @return The value (as a string) from a nested key. - */ - public String get(String key) { - if (translations.containsKey(key)) return translations.get(key); - String valueNotFoundPattern = "This value does not exist. Please report this to the Discord: "; - String result = valueNotFoundPattern + key; - if (!languageCode.equals("en-US")) { - String englishValue = getLanguage("en-US").get(key); - if (!englishValue.contains(valueNotFoundPattern)) { - result += "\nhere is english version:\n" + englishValue; - } - } - return result; - } - - private static class LanguageStreamDescription { - private final String languageCode; - private final InputStream languageFile; - - public LanguageStreamDescription(String languageCode, InputStream languageFile) { - this.languageCode = languageCode; - this.languageFile = languageFile; - } - - public String getLanguageCode() { - return languageCode; - } - - public InputStream getLanguageFile() { - return languageFile; - } - } - - @EqualsAndHashCode - public static class TextStrings implements Serializable { - public static final String[] ARR_LANGUAGES = { - "EN", "CHS", "CHT", "JP", "KR", "DE", "ES", "FR", "ID", "PT", "RU", "TH", "VI" - }; - public static final String[] ARR_GC_LANGUAGES = { - "en-US", "zh-CN", "zh-TW", "ja-JP", "ko-KR", "en-US", "es-ES", "fr-FR", "en-US", "en-US", - "ru-RU", "en-US", "en-US" - }; // TODO: Update the placeholder en-US entries if we ever add GC translations for the missing - // client languages - public static final int NUM_LANGUAGES = ARR_LANGUAGES.length; - public static final List LIST_LANGUAGES = Arrays.asList(ARR_LANGUAGES); - public static final Object2IntMap - MAP_LANGUAGES = // Map "EN": 0, "CHS": 1, ..., "VI": 12 - new Object2IntOpenHashMap<>( - IntStream.range(0, ARR_LANGUAGES.length) - .boxed() - .collect(Collectors.toMap(i -> ARR_LANGUAGES[i], i -> i))); - public static final Object2IntMap MAP_GC_LANGUAGES = // Map "en-US": 0, "zh-CN": 1, ... - new Object2IntOpenHashMap<>( - IntStream.range(0, ARR_GC_LANGUAGES.length) - .boxed() - .collect( - Collectors.toMap( - i -> ARR_GC_LANGUAGES[i], - i -> i, - (i1, i2) -> i1))); // Have to handle duplicates referring back to the first - public String[] strings = new String[ARR_LANGUAGES.length]; - - public TextStrings() {} - - public TextStrings(String init) { - for (int i = 0; i < NUM_LANGUAGES; i++) this.strings[i] = init; - } - - public TextStrings(List strings, int key) { - // Some hashes don't have strings for some languages :( - String nullReplacement = "[N/A] %d".formatted((long) key & 0xFFFFFFFFL); - for (int i = 0; i < NUM_LANGUAGES; i++) { // Find first non-null if there is any - String s = strings.get(i); - if (s != null) { - nullReplacement = "[%s] - %s".formatted(ARR_LANGUAGES[i], s); - break; - } - } - for (int i = 0; i < NUM_LANGUAGES; i++) { - String s = strings.get(i); - if (s != null) this.strings[i] = s; - else this.strings[i] = nullReplacement; - } - } - - public static List getLanguages() { - return Arrays.stream(ARR_GC_LANGUAGES).map(Language::getLanguage).toList(); - } - - public String get(int languageIndex) { - return strings[languageIndex]; - } - - public String get(String languageCode) { - return strings[MAP_LANGUAGES.getOrDefault(languageCode, 0)]; - } - - public String getGC(String languageCode) { - return strings[MAP_GC_LANGUAGES.getOrDefault(languageCode, 0)]; - } - - public boolean set(String languageCode, String string) { - int index = MAP_LANGUAGES.getOrDefault(languageCode, -1); - if (index < 0) return false; - strings[index] = string; - return true; - } - } -} +package emu.grasscutter.utils; + +import static emu.grasscutter.config.Configuration.FALLBACK_LANGUAGE; +import static emu.grasscutter.utils.FileUtils.getResourcePath; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.ResourceLoader; +import emu.grasscutter.data.excels.achievement.AchievementData; +import emu.grasscutter.game.player.Player; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.EqualsAndHashCode; + +public final class Language { + private static final Map cachedLanguages = new ConcurrentHashMap<>(); + private static final int TEXTMAP_CACHE_VERSION = 0x9CCACE04; + private static final Pattern textMapKeyValueRegex = Pattern.compile("\"(\\d+)\": \"(.+)\""); + private static final Path TEXTMAP_CACHE_PATH = + Path.of(Utils.toFilePath("cache/TextMapCache.bin")); + private static boolean scannedTextmaps = + false; // Ensure that we don't infinitely rescan on cache misses that don't exist + private static Int2ObjectMap textMapStrings; + private final String languageCode; + private final Map translations = new ConcurrentHashMap<>(); + + /** Reads a file and creates a language instance. */ + private Language(LanguageStreamDescription description) { + languageCode = description.getLanguageCode(); + + try { + var object = + JsonUtils.decode( + Utils.readFromInputStream(description.getLanguageFile()), JsonObject.class); + object + .entrySet() + .forEach(entry -> putFlattenedKey(translations, entry.getKey(), entry.getValue())); + } catch (Exception exception) { + Grasscutter.getLogger() + .warn("Failed to load language file: " + description.getLanguageCode(), exception); + } + } + + /** + * Creates a language instance from a code. + * + * @param langCode The language code. + * @return A language instance. + */ + public static Language getLanguage(String langCode) { + if (cachedLanguages.containsKey(langCode)) { + return cachedLanguages.get(langCode); + } + + var fallbackLanguageCode = Utils.getLanguageCode(FALLBACK_LANGUAGE); + var description = getLanguageFileDescription(langCode, fallbackLanguageCode); + var actualLanguageCode = description.getLanguageCode(); + + Language languageInst; + if (description.getLanguageFile() != null) { + languageInst = new Language(description); + cachedLanguages.put(actualLanguageCode, languageInst); + } else { + languageInst = cachedLanguages.get(actualLanguageCode); + cachedLanguages.put(langCode, languageInst); + } + + return languageInst; + } + + /** + * Returns the translated value from the key while substituting arguments. + * + * @param key The key of the translated value to return. + * @param args The arguments to substitute. + * @return A translated value with arguments substituted. + */ + public static String translate(String key, Object... args) { + String translated = Grasscutter.getLanguage().get(key); + + for (int i = 0; i < args.length; i++) { + args[i] = + switch (args[i].getClass().getSimpleName()) { + case "String" -> args[i]; + case "TextStrings" -> ((TextStrings) args[i]) + .get(0) + .replace("\\\\n", "\\n"); // TODO: Change this to server language + default -> args[i].toString(); + }; + } + + try { + return translated.formatted(args); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to format string: " + key, exception); + return translated; + } + } + + /** + * Returns the translated value from the key while substituting arguments. + * + * @param player Target player + * @param key The key of the translated value to return. + * @param args The arguments to substitute. + * @return A translated value with arguments substituted. + */ + public static String translate(Player player, String key, Object... args) { + if (player == null) { + return translate(key, args); + } + + var langCode = Utils.getLanguageCode(player.getAccount().getLocale()); + String translated = getLanguage(langCode).get(key); + + for (int i = 0; i < args.length; i++) { + args[i] = + switch (args[i].getClass().getSimpleName()) { + case "String" -> args[i]; + case "TextStrings" -> ((TextStrings) args[i]) + .getGC(langCode) + .replace("\\\\n", "\n"); // Note that we don't unescape \n for server console + default -> args[i].toString(); + }; + } + + try { + return translated.formatted(args); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to format string: " + key, exception); + return translated; + } + } + + /** + * Recursive helper function to flatten a Json tree Converts input like {"foo": {"bar": "baz"}} to + * {"foo.bar": "baz"} + * + * @param map The map to insert the keys into + * @param key The flattened key of the current element + * @param element The current element + */ + private static void putFlattenedKey(Map map, String key, JsonElement element) { + if (element.isJsonObject()) { + element + .getAsJsonObject() + .entrySet() + .forEach( + entry -> { + String keyPrefix = key.isEmpty() ? "" : key + "."; + putFlattenedKey(map, keyPrefix + entry.getKey(), entry.getValue()); + }); + } else { + map.put(key, element.getAsString()); + } + } + + /** + * create a LanguageStreamDescription + * + * @param languageCode The name of the language code. + * @param fallbackLanguageCode The name of the fallback language code. + */ + private static LanguageStreamDescription getLanguageFileDescription( + String languageCode, String fallbackLanguageCode) { + var fileName = languageCode + ".json"; + var fallback = fallbackLanguageCode + ".json"; + + String actualLanguageCode = languageCode; + InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); + + if (file == null) { // Provided fallback language. + Grasscutter.getLogger() + .warn("Failed to load language file: " + fileName + ", falling back to: " + fallback); + actualLanguageCode = fallbackLanguageCode; + if (cachedLanguages.containsKey(actualLanguageCode)) { + return new LanguageStreamDescription(actualLanguageCode, null); + } + + file = Grasscutter.class.getResourceAsStream("/languages/" + fallback); + } + + if (file == null) { // Fallback the fallback language. + Grasscutter.getLogger() + .warn("Failed to load language file: " + fallback + ", falling back to: en-US.json"); + actualLanguageCode = "en-US"; + if (cachedLanguages.containsKey(actualLanguageCode)) { + return new LanguageStreamDescription(actualLanguageCode, null); + } + + file = Grasscutter.class.getResourceAsStream("/languages/en-US.json"); + } + + if (file == null) + throw new RuntimeException( + "Unable to load the primary, fallback, and 'en-US' language files."); + + return new LanguageStreamDescription(actualLanguageCode, file); + } + + private static Int2ObjectMap loadTextMapFile(String language, IntSet nameHashes) { + Int2ObjectMap output = new Int2ObjectOpenHashMap<>(); + try (BufferedReader file = + Files.newBufferedReader( + getResourcePath("TextMap/TextMap" + language + ".json"), StandardCharsets.UTF_8)) { + Matcher matcher = textMapKeyValueRegex.matcher(""); + return new Int2ObjectOpenHashMap<>( + file.lines() + .sequential() + .map(matcher::reset) // Side effects, but it's faster than making a new one + .filter(Matcher::find) + .filter( + m -> + nameHashes.contains( + (int) Long.parseLong(m.group(1)))) // TODO: Cache this parse somehow + .collect( + Collectors.toMap( + m -> (int) Long.parseLong(m.group(1)), + m -> m.group(2).replace("\\\"", "\"")))); + } catch (Exception e) { + Grasscutter.getLogger().error("Error loading textmap: " + language); + Grasscutter.getLogger().error(e.toString()); + } + return output; + } + + private static Int2ObjectMap loadTextMapFiles(IntSet nameHashes) { + Map> + mapLanguageMaps = // Separate step to process the textmaps in parallel + TextStrings.LIST_LANGUAGES.parallelStream() + .collect( + Collectors.toConcurrentMap( + s -> TextStrings.MAP_LANGUAGES.getInt(s), + s -> loadTextMapFile(s, nameHashes))); + List> languageMaps = + IntStream.range(0, TextStrings.NUM_LANGUAGES) + .mapToObj(i -> mapLanguageMaps.get(i)) + .collect(Collectors.toList()); + + Map canonicalTextStrings = new HashMap<>(); + return new Int2ObjectOpenHashMap( + nameHashes + .intStream() + .boxed() + .collect( + Collectors.toMap( + key -> key, + key -> { + TextStrings t = + new TextStrings( + IntStream.range(0, TextStrings.NUM_LANGUAGES) + .mapToObj(i -> languageMaps.get(i).get((int) key)) + .collect(Collectors.toList()), + key); + return canonicalTextStrings.computeIfAbsent(t, x -> t); + }))); + } + + @SuppressWarnings("unchecked") + private static Int2ObjectMap loadTextMapsCache() throws Exception { + try (ObjectInputStream file = + new ObjectInputStream( + new BufferedInputStream(Files.newInputStream(TEXTMAP_CACHE_PATH), 0x100000))) { + final int fileVersion = file.readInt(); + if (fileVersion != TEXTMAP_CACHE_VERSION) throw new Exception("Invalid cache version"); + return (Int2ObjectMap) file.readObject(); + } + } + + private static void saveTextMapsCache(Int2ObjectMap input) throws IOException { + try { + Files.createDirectory(Path.of("cache")); + } catch (FileAlreadyExistsException ignored) { + } + try (ObjectOutputStream file = + new ObjectOutputStream( + new BufferedOutputStream( + Files.newOutputStream(TEXTMAP_CACHE_PATH, StandardOpenOption.CREATE), 0x100000))) { + file.writeInt(TEXTMAP_CACHE_VERSION); + file.writeObject(input); + } + } + + @Deprecated(forRemoval = true) + public static Int2ObjectMap getTextMapStrings() { + if (textMapStrings == null) loadTextMaps(); + return textMapStrings; + } + + public static TextStrings getTextMapKey(int key) { + if ((textMapStrings == null) || (!scannedTextmaps && !textMapStrings.containsKey(key))) + loadTextMaps(); + return textMapStrings.get(key); + } + + public static TextStrings getTextMapKey(long hash) { + return getTextMapKey((int) hash); + } + + public static void loadTextMaps() { + // Check system timestamps on cache and resources + try { + long cacheModified = Files.getLastModifiedTime(TEXTMAP_CACHE_PATH).toMillis(); + + long textmapsModified = + Files.list(getResourcePath("TextMap")) + .filter(path -> path.toString().endsWith(".json")) + .map( + path -> { + try { + return Files.getLastModifiedTime(path).toMillis(); + } catch (Exception ignored) { + Grasscutter.getLogger() + .debug("Exception while checking modified time: ", path); + return Long.MAX_VALUE; // Don't use cache, something has gone wrong + } + }) + .max(Long::compare) + .get(); + + Grasscutter.getLogger() + .debug( + "Cache modified %d, textmap modified %d".formatted(cacheModified, textmapsModified)); + if (textmapsModified < cacheModified) { + // Try loading from cache + Grasscutter.getLogger().debug("Loading cached 'TextMaps'..."); + textMapStrings = loadTextMapsCache(); + return; + } + } catch (Exception e) { + Grasscutter.getLogger().debug("Exception while checking cache: ", e); + } + + // Regenerate cache + Grasscutter.getLogger().debug("Generating TextMaps cache"); + ResourceLoader.loadAll(); + IntSet usedHashes = new IntOpenHashSet(); + GameData.getAchievementDataMap().values().stream() + .filter(AchievementData::isUsed) + .forEach( + a -> { + usedHashes.add((int) a.getTitleTextMapHash()); + usedHashes.add((int) a.getDescTextMapHash()); + }); + GameData.getAvatarDataMap().forEach((k, v) -> usedHashes.add((int) v.getNameTextMapHash())); + GameData.getAvatarSkillDataMap() + .forEach( + (k, v) -> { + usedHashes.add((int) v.getNameTextMapHash()); + usedHashes.add((int) v.getDescTextMapHash()); + }); + GameData.getItemDataMap().forEach((k, v) -> usedHashes.add((int) v.getNameTextMapHash())); + GameData.getHomeWorldBgmDataMap() + .forEach((k, v) -> usedHashes.add((int) v.getBgmNameTextMapHash())); + GameData.getMonsterDataMap().forEach((k, v) -> usedHashes.add((int) v.getNameTextMapHash())); + GameData.getMainQuestDataMap().forEach((k, v) -> usedHashes.add((int) v.getTitleTextMapHash())); + GameData.getQuestDataMap().forEach((k, v) -> usedHashes.add((int) v.getDescTextMapHash())); + // Incidental strings + usedHashes.add((int) 4233146695L); // Character + usedHashes.add((int) 4231343903L); // Weapon + usedHashes.add((int) 332935371L); // Standard Wish + usedHashes.add((int) 2272170627L); // Character Event Wish + usedHashes.add((int) 3352513147L); // Character Event Wish-2 + usedHashes.add((int) 2864268523L); // Weapon Event Wish + + textMapStrings = loadTextMapFiles(usedHashes); + scannedTextmaps = true; + try { + saveTextMapsCache(textMapStrings); + } catch (IOException e) { + Grasscutter.getLogger().error("Failed to save TextMap cache: ", e); + } + } + + /** get language code */ + public String getLanguageCode() { + return languageCode; + } + + /** + * Returns the value (as a string) from a nested key. + * + * @param key The key to look for. + * @return The value (as a string) from a nested key. + */ + public String get(String key) { + if (translations.containsKey(key)) return translations.get(key); + String valueNotFoundPattern = "This value does not exist. Please report this to the Discord: "; + String result = valueNotFoundPattern + key; + if (!languageCode.equals("en-US")) { + String englishValue = getLanguage("en-US").get(key); + if (!englishValue.contains(valueNotFoundPattern)) { + result += "\nhere is english version:\n" + englishValue; + } + } + return result; + } + + private static class LanguageStreamDescription { + private final String languageCode; + private final InputStream languageFile; + + public LanguageStreamDescription(String languageCode, InputStream languageFile) { + this.languageCode = languageCode; + this.languageFile = languageFile; + } + + public String getLanguageCode() { + return languageCode; + } + + public InputStream getLanguageFile() { + return languageFile; + } + } + + @EqualsAndHashCode + public static class TextStrings implements Serializable { + public static final String[] ARR_LANGUAGES = { + "EN", "CHS", "CHT", "JP", "KR", "DE", "ES", "FR", "ID", "PT", "RU", "TH", "VI" + }; + public static final String[] ARR_GC_LANGUAGES = { + "en-US", "zh-CN", "zh-TW", "ja-JP", "ko-KR", "en-US", "es-ES", "fr-FR", "en-US", "en-US", + "ru-RU", "en-US", "en-US" + }; // TODO: Update the placeholder en-US entries if we ever add GC translations for the missing + // client languages + public static final int NUM_LANGUAGES = ARR_LANGUAGES.length; + public static final List LIST_LANGUAGES = Arrays.asList(ARR_LANGUAGES); + public static final Object2IntMap + MAP_LANGUAGES = // Map "EN": 0, "CHS": 1, ..., "VI": 12 + new Object2IntOpenHashMap<>( + IntStream.range(0, ARR_LANGUAGES.length) + .boxed() + .collect(Collectors.toMap(i -> ARR_LANGUAGES[i], i -> i))); + public static final Object2IntMap MAP_GC_LANGUAGES = // Map "en-US": 0, "zh-CN": 1, ... + new Object2IntOpenHashMap<>( + IntStream.range(0, ARR_GC_LANGUAGES.length) + .boxed() + .collect( + Collectors.toMap( + i -> ARR_GC_LANGUAGES[i], + i -> i, + (i1, i2) -> i1))); // Have to handle duplicates referring back to the first + public String[] strings = new String[ARR_LANGUAGES.length]; + + public TextStrings() {} + + public TextStrings(String init) { + for (int i = 0; i < NUM_LANGUAGES; i++) this.strings[i] = init; + } + + public TextStrings(List strings, int key) { + // Some hashes don't have strings for some languages :( + String nullReplacement = "[N/A] %d".formatted((long) key & 0xFFFFFFFFL); + for (int i = 0; i < NUM_LANGUAGES; i++) { // Find first non-null if there is any + String s = strings.get(i); + if (s != null) { + nullReplacement = "[%s] - %s".formatted(ARR_LANGUAGES[i], s); + break; + } + } + for (int i = 0; i < NUM_LANGUAGES; i++) { + String s = strings.get(i); + if (s != null) this.strings[i] = s; + else this.strings[i] = nullReplacement; + } + } + + public static List getLanguages() { + return Arrays.stream(ARR_GC_LANGUAGES).map(Language::getLanguage).toList(); + } + + public String get(int languageIndex) { + return strings[languageIndex]; + } + + public String get(String languageCode) { + return strings[MAP_LANGUAGES.getOrDefault(languageCode, 0)]; + } + + public String getGC(String languageCode) { + return strings[MAP_GC_LANGUAGES.getOrDefault(languageCode, 0)]; + } + + public boolean set(String languageCode, String string) { + int index = MAP_LANGUAGES.getOrDefault(languageCode, -1); + if (index < 0) return false; + strings[index] = string; + return true; + } + } +}