diff --git a/src/main/java/emu/grasscutter/command/CommandHelpers.java b/src/main/java/emu/grasscutter/command/CommandHelpers.java index aa1f68840..1748c9a0f 100644 --- a/src/main/java/emu/grasscutter/command/CommandHelpers.java +++ b/src/main/java/emu/grasscutter/command/CommandHelpers.java @@ -1,57 +1,59 @@ -package emu.grasscutter.command; - -import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nonnull; - -public class CommandHelpers { - public static final Pattern lvlRegex = - Pattern.compile("(? List parseIntParameters( - List args, @Nonnull T params, Map> map) { - args.removeIf( - arg -> { - var argL = arg.toLowerCase(); - boolean deleteArg = false; - for (var entry : map.entrySet()) { - int argNum = matchIntOrNeg(entry.getKey(), argL); - if (argNum != -1) { - entry.getValue().accept(params, argNum); - deleteArg = true; - } - } - return deleteArg; - }); - return args; - } -} +package emu.grasscutter.command; + +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; + +public class CommandHelpers { + public static final Pattern lvlRegex = + Pattern.compile("(? List parseIntParameters( + List args, @Nonnull T params, Map> map) { + args.removeIf( + arg -> { + var argL = arg.toLowerCase(); + boolean deleteArg = false; + for (var entry : map.entrySet()) { + int argNum = matchIntOrNeg(entry.getKey(), argL); + if (argNum != -1) { + entry.getValue().accept(params, argNum); + deleteArg = true; + } + } + return deleteArg; + }); + return args; + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/CutsceneCommand.java b/src/main/java/emu/grasscutter/command/commands/CutsceneCommand.java new file mode 100644 index 000000000..6acc73963 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/CutsceneCommand.java @@ -0,0 +1,27 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.packet.send.PacketCutsceneBeginNotify; +import java.util.List; +import lombok.val; + +@Command( + label = "cutscene", + aliases = {"c"}, + usage = {"[]"}, + permission = "player.group", + permissionTargeted = "player.group.others") +public final class CutsceneCommand implements CommandHandler { + + @Override + public void execute(Player sender, Player targetPlayer, List args) { + if (args.isEmpty()) { + sendUsageMessage(sender); + return; + } + val cutSceneId = Integer.parseInt(args.get(0)); + targetPlayer.sendPacket(new PacketCutsceneBeginNotify(cutSceneId)); + } +} diff --git a/src/main/java/emu/grasscutter/command/commands/SoundCommand.java b/src/main/java/emu/grasscutter/command/commands/SoundCommand.java new file mode 100644 index 000000000..41fcfb382 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/SoundCommand.java @@ -0,0 +1,48 @@ +package emu.grasscutter.command.commands; + +import static emu.grasscutter.utils.Language.translate; + +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.packet.send.PacketScenePlayerSoundNotify; +import emu.grasscutter.utils.Position; +import java.util.List; +import lombok.val; + +@Command( + label = "sound", + aliases = {"s", "audio"}, + usage = {"[] []"}, + permission = "player.group", + permissionTargeted = "player.group.others") +public final class SoundCommand implements CommandHandler { + + @Override + public void execute(Player sender, Player targetPlayer, List args) { + if (args.isEmpty()) { + sendUsageMessage(sender); + return; + } + val soundName = args.get(0); + var playPosition = targetPlayer.getPosition(); + if (args.size() == 4) { + try { + float x, y, z; + x = Float.parseFloat(args.get(1)); + y = Float.parseFloat(args.get(2)); + z = Float.parseFloat(args.get(3)); + playPosition = new Position(x, y, z); + } catch (NumberFormatException ignored) { + CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error")); + return; + } + } else if (args.size() > 1) { + sendUsageMessage(sender); + return; + } + targetPlayer + .getScene() + .broadcastPacket(new PacketScenePlayerSoundNotify(playPosition, soundName, 1)); + } +} diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index 5b581d077..f72576002 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -1,618 +1,624 @@ -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; - } -} +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.quest.enums.QuestCond; +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); + } + + @Nullable public static List getQuestDataByConditions( + QuestCond questCond, int param0, String questStr) { + return beginCondQuestMap.get(QuestData.questConditionKey(questCond, param0, questStr)); + } + + public static Int2ObjectMap getAchievementDataMap() { + AchievementData.divideIntoGroups(); + return achievementDataMap; + } +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/InTimeTrigger.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/InTimeTrigger.java index 649a8665a..99b74fabc 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/InTimeTrigger.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/InTimeTrigger.java @@ -1,13 +1,13 @@ -package emu.grasscutter.game.dungeons.challenge.trigger; - -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; - -public class InTimeTrigger extends ChallengeTrigger { - @Override - public void onCheckTimeout(WorldChallenge challenge) { - var current = System.currentTimeMillis(); - if (current - challenge.getStartedAt() > challenge.getTimeLimit() * 1000L) { - challenge.fail(); - } - } -} +package emu.grasscutter.game.dungeons.challenge.trigger; + +import emu.grasscutter.game.dungeons.challenge.WorldChallenge; + +public class InTimeTrigger extends ChallengeTrigger { + @Override + public void onCheckTimeout(WorldChallenge challenge) { + var current = challenge.getScene().getSceneTimeSeconds(); + if (current - challenge.getStartedAt() > challenge.getTimeLimit()) { + challenge.fail(); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index df09240c2..31cbb72e4 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -15,6 +15,7 @@ import emu.grasscutter.game.activity.ActivityManager; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.AvatarStorage; import emu.grasscutter.game.battlepass.BattlePassManager; +import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.expedition.ExpeditionInfo; import emu.grasscutter.game.friends.FriendsList; @@ -44,6 +45,7 @@ import emu.grasscutter.game.props.ClimateType; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.props.WatcherTriggerType; import emu.grasscutter.game.quest.QuestManager; +import emu.grasscutter.game.quest.enums.QuestCond; import emu.grasscutter.game.quest.enums.QuestContent; import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.tower.TowerData; @@ -51,20 +53,18 @@ import emu.grasscutter.game.tower.TowerManager; import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.World; import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.proto.*; import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; import emu.grasscutter.net.proto.MpSettingTypeOuterClass.MpSettingType; import emu.grasscutter.net.proto.OnlinePlayerInfoOuterClass.OnlinePlayerInfo; -import emu.grasscutter.net.proto.PlayerApplyEnterMpResultNotifyOuterClass; import emu.grasscutter.net.proto.PlayerLocationInfoOuterClass.PlayerLocationInfo; -import emu.grasscutter.net.proto.PlayerWorldLocationInfoOuterClass; import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; -import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; -import emu.grasscutter.net.proto.SocialShowAvatarInfoOuterClass; +import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGrantRecord.GrantReason; import emu.grasscutter.scripts.data.SceneRegion; import emu.grasscutter.server.event.player.PlayerJoinEvent; import emu.grasscutter.server.event.player.PlayerQuitEvent; @@ -88,6 +88,7 @@ import java.time.ZoneId; import java.util.*; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.LinkedBlockingQueue; +import java.util.stream.Stream; import static emu.grasscutter.config.Configuration.GAME_OPTIONS; @@ -472,6 +473,8 @@ public class Player { // Handle open state unlocks from level-up. this.getProgressManager().tryUnlockOpenStates(); + this.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_PLAYER_LEVEL_UP, level); + this.getQuestManager().queueEvent(QuestCond.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER, level); return true; } @@ -613,7 +616,7 @@ public class Player { public void onEnterRegion(SceneRegion region) { getQuestManager().forEachActiveQuest(quest -> { - if (quest.getTriggers().containsKey("ENTER_REGION_" + region.config_id)) { + if (quest.getTriggerData() != null && quest.getTriggers().containsKey("ENTER_REGION_"+ region.config_id)) { // If trigger hasn't been fired yet if (!Boolean.TRUE.equals(quest.getTriggers().put("ENTER_REGION_" + region.config_id, true))) { //getSession().send(new PacketServerCondMeetQuestListUpdateNotify()); @@ -824,6 +827,85 @@ public class Player { addAvatar(avatar, true); } + public void addAvatar(int avatarId) { + // I dont see why we cant do this lolz + addAvatar(new Avatar(avatarId), true); + } + + 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(); + + var trialCustomParams = GameData.getTrialAvatarCustomData().get(trialAvatarId).getTrialAvatarParamList(); + return trialCustomParams.isEmpty() ? List.of() : Stream.of(trialCustomParams.get(0).split(";")).map(Integer::parseInt).toList(); + } + + public boolean addTrialAvatar(int trialAvatarId, GrantReason reason, int questMainId){ + List trialAvatarBasicParam = getTrialAvatarParam(trialAvatarId); + if (trialAvatarBasicParam.isEmpty()) return false; + + Avatar avatar = new Avatar(trialAvatarBasicParam.get(0)); + if (avatar.getAvatarData() == null || !hasSentLoginPackets()) return false; + + avatar.setOwner(this); + // Add trial weapons and relics + avatar.setTrialAvatarInfo(trialAvatarBasicParam.get(1), trialAvatarId, reason, questMainId); + avatar.equipTrialItems(); + // Recalc stats + avatar.recalcStats(); + + // Packet, mimic official server behaviour, add to player's bag but not saving to db + sendPacket(new PacketAvatarAddNotify(avatar, false)); + // add to avatar to temporary trial team + getTeamManager().addAvatarToTrialTeam(avatar); + return true; + } + + public boolean addTrialAvatarForQuest(int trialAvatarId, int questMainId) { + // TODO: Find method for 'setupTrialAvatarTeamForQuest'. + getTeamManager().setupTrialAvatars(true); + if (!addTrialAvatar( + trialAvatarId, + GrantReason.GRANT_REASON_BY_QUEST, + questMainId)) return false; + getTeamManager().trialAvatarTeamPostUpdate(); + // Packet, mimic official server behaviour, neccessary to stop player from modifying team + sendPacket(new PacketAvatarTeamUpdateNotify(this)); + return true; + } + + public void addTrialAvatarsForActivity(List trialAvatarIds) { + getTeamManager().setupTrialAvatars(false); + trialAvatarIds.forEach(trialAvatarId -> addTrialAvatar( + trialAvatarId, + GrantReason.GRANT_REASON_BY_TRIAL_AVATAR_ACTIVITY, + 0)); + getTeamManager().trialAvatarTeamPostUpdate(0); + } + + public boolean removeTrialAvatarForQuest(int trialAvatarId) { + if (!getTeamManager().isUsingTrialTeam()) return false; + + sendPacket(new PacketAvatarDelNotify(List.of(getTeamManager().getTrialAvatarGuid(trialAvatarId)))); + getTeamManager().removeTrialAvatarTeam(trialAvatarId); + sendPacket(new PacketAvatarTeamUpdateNotify()); + return true; + } + + public void removeTrialAvatarForActivity() { + if (!getTeamManager().isUsingTrialTeam()) return; + + sendPacket(new PacketAvatarDelNotify(getTeamManager().getActiveTeam().stream() + .map(x -> x.getAvatar().getGuid()).toList())); + getTeamManager().removeTrialAvatarTeam(); + } + public void addFlycloak(int flycloakId) { this.getFlyCloakList().add(flycloakId); this.sendPacket(new PacketAvatarGainFlycloakNotify(flycloakId)); @@ -834,6 +916,11 @@ public class Player { this.sendPacket(new PacketAvatarGainCostumeNotify(costumeId)); } + public void addPersonalLine(int personalLineId) { + this.getPersonalLineList().add(personalLineId); + session.getPlayer().getQuestManager().queueEvent(QuestCond.QUEST_COND_PERSONAL_LINE_UNLOCK, personalLineId); + } + public void addNameCard(int nameCardId) { this.getNameCardList().add(nameCardId); this.sendPacket(new PacketUnlockNameCardNotify(nameCardId)); @@ -863,6 +950,46 @@ public class Player { this.getServer().getChatSystem().sendPrivateMessageFromServer(getUid(), message.toString()); } + public void setAvatarsAbilityForScene(Scene scene) { + try { + var levelEntityConfig = scene.getSceneData().getLevelEntityConfig(); + var config = GameData.getConfigLevelEntityDataMap().get(levelEntityConfig); + if (config == null){ + return; + } + + List avatarIds = scene.getSceneData().getSpecifiedAvatarList(); + List specifiedAvatarList = getTeamManager().getActiveTeam(); + + if (avatarIds != null && avatarIds.size() > 0){ + // certain scene could limit specifc avatars' entry + specifiedAvatarList.clear(); + for (int id : avatarIds){ + var avatar = getAvatars().getAvatarById(id); + if (avatar == null){ + continue; + } + specifiedAvatarList.add(new EntityAvatar(scene, avatar)); + } + } + + for (EntityAvatar entityAvatar : specifiedAvatarList){ + var avatarData = entityAvatar.getAvatar().getAvatarData(); + if (avatarData == null){ + continue; + } + avatarData.buildEmbryo(); + if (config.getAvatarAbilities() == null){ + continue; // continue and not break because has to rebuild ability for the next avatar if any + } + for (var 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); + } + } /** * Sends a message to another player. * @@ -982,7 +1109,7 @@ public class Player { } } - SocialDetail.Builder social = SocialDetail.newBuilder() + return SocialDetail.newBuilder() .setUid(this.getUid()) .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(this.getHeadImage())) .setNickname(this.getNickname()) @@ -996,7 +1123,6 @@ public class Player { .addAllShowNameCardIdList(this.getShowNameCardInfoList()) .setFinishAchievementNum(this.getFinishedAchievementNum()) .setFriendEnterHomeOptionValue(this.getHome() == null ? 0 : this.getHome().getEnterHomeOption()); - return social; } public int getFinishedAchievementNum() { @@ -1135,6 +1261,8 @@ public class Player { // Home resources this.getHome().updateHourlyResources(this); + + this.getQuestManager().onTick(); } private synchronized void doDailyReset() { @@ -1200,12 +1328,17 @@ public class Player { this.achievements = Achievements.getByPlayer(this); this.getAvatars().loadFromDatabase(); this.getInventory().loadFromDatabase(); - this.loadBattlePassManager(); // Call before avatar postLoad to avoid null pointer - this.getAvatars().postLoad(); // Needs to be called after inventory is handled this.getFriendsList().loadFromDatabase(); this.getMailHandler().loadFromDatabase(); this.getQuestManager().loadFromDatabase(); + + this.loadBattlePassManager(); + this.getAvatars().postLoad(); // Needs to be called after inventory is handled + } + + public void onPlayerBorn() { + getQuestManager().onPlayerBorn(); } public void onLogin() { diff --git a/src/main/java/emu/grasscutter/game/props/EntityType.java b/src/main/java/emu/grasscutter/game/props/EntityType.java index 7456f72bf..cf06ebae5 100644 --- a/src/main/java/emu/grasscutter/game/props/EntityType.java +++ b/src/main/java/emu/grasscutter/game/props/EntityType.java @@ -1,108 +1,109 @@ -package emu.grasscutter.game.props; - -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; - -public enum EntityType { - None(0), - Avatar(1), - Monster(2), - Bullet(3), - AttackPhyisicalUnit(4), - AOE(5), - Camera(6), - EnviroArea(7), - Equip(8), - MonsterEquip(9), - Grass(10), - Level(11), - NPC(12), - TransPointFirst(13), - TransPointFirstGadget(14), - TransPointSecond(15), - TransPointSecondGadget(16), - DropItem(17), - Field(18), - Gadget(19), - Water(20), - GatherPoint(21), - GatherObject(22), - AirflowField(23), - SpeedupField(24), - Gear(25), - Chest(26), - EnergyBall(27), - ElemCrystal(28), - Timeline(29), - Worktop(30), - Team(31), - Platform(32), - AmberWind(33), - EnvAnimal(34), - SealGadget(35), - Tree(36), - Bush(37), - QuestGadget(38), - Lightning(39), - RewardPoint(40), - RewardStatue(41), - MPLevel(42), - WindSeed(43), - MpPlayRewardPoint(44), - ViewPoint(45), - RemoteAvatar(46), - GeneralRewardPoint(47), - PlayTeam(48), - OfferingGadget(49), - EyePoint(50), - MiracleRing(51), - Foundation(52), - WidgetGadget(53), - Vehicle(54), - SubEquip(55), - FishRod(56), - CustomTile(57), - FishPool(58), - CustomGadget(59), - BlackMud(60), - RoguelikeOperatorGadget(61), - NightCrowGadget(62), - Projector(63), - Screen(64), - EchoShell(65), - UIInteractGadget(66), - PlaceHolder(99); - - 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); - }); - } - - private final int value; - - EntityType(int value) { - this.value = value; - } - - public static EntityType getTypeByValue(int value) { - return map.getOrDefault(value, None); - } - - public static EntityType getTypeByName(String name) { - return stringMap.getOrDefault(name, None); - } - - public int getValue() { - return value; - } -} +package emu.grasscutter.game.props; + +import emu.grasscutter.scripts.constants.IntValueEnum; +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; + +public enum EntityType implements IntValueEnum { + None(0), + Avatar(1), + Monster(2), + Bullet(3), + AttackPhyisicalUnit(4), + AOE(5), + Camera(6), + EnviroArea(7), + Equip(8), + MonsterEquip(9), + Grass(10), + Level(11), + NPC(12), + TransPointFirst(13), + TransPointFirstGadget(14), + TransPointSecond(15), + TransPointSecondGadget(16), + DropItem(17), + Field(18), + Gadget(19), + Water(20), + GatherPoint(21), + GatherObject(22), + AirflowField(23), + SpeedupField(24), + Gear(25), + Chest(26), + EnergyBall(27), + ElemCrystal(28), + Timeline(29), + Worktop(30), + Team(31), + Platform(32), + AmberWind(33), + EnvAnimal(34), + SealGadget(35), + Tree(36), + Bush(37), + QuestGadget(38), + Lightning(39), + RewardPoint(40), + RewardStatue(41), + MPLevel(42), + WindSeed(43), + MpPlayRewardPoint(44), + ViewPoint(45), + RemoteAvatar(46), + GeneralRewardPoint(47), + PlayTeam(48), + OfferingGadget(49), + EyePoint(50), + MiracleRing(51), + Foundation(52), + WidgetGadget(53), + Vehicle(54), + SubEquip(55), + FishRod(56), + CustomTile(57), + FishPool(58), + CustomGadget(59), + BlackMud(60), + RoguelikeOperatorGadget(61), + NightCrowGadget(62), + Projector(63), + Screen(64), + EchoShell(65), + UIInteractGadget(66), + PlaceHolder(99); + + 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); + }); + } + + private final int value; + + EntityType(int value) { + this.value = value; + } + + public static EntityType getTypeByValue(int value) { + return map.getOrDefault(value, None); + } + + public static EntityType getTypeByName(String name) { + return stringMap.getOrDefault(name, None); + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java index ddc7b509c..0b01ff494 100644 --- a/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java +++ b/src/main/java/emu/grasscutter/game/quest/GameMainQuest.java @@ -16,7 +16,6 @@ import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.quest.enums.*; -import emu.grasscutter.game.world.World; import emu.grasscutter.net.proto.ChildQuestOuterClass.ChildQuest; import emu.grasscutter.net.proto.ParentQuestOuterClass.ParentQuest; import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify; @@ -41,7 +40,7 @@ public class GameMainQuest { @Getter private int[] questVars; @Getter private long[] timeVar; //QuestUpdateQuestVarReq is sent in two stages... - @Getter private List questVarsUpdate; + private List questVarsUpdate; @Getter private ParentQuestState state; @Getter private boolean isFinished; @Getter List questGroupSuites; @@ -67,6 +66,13 @@ public class GameMainQuest { addAllChildQuests(); } + public List getQuestVarsUpdate() { + if(questVarsUpdate == null){ + questVarsUpdate = new ArrayList<>(); + } + return questVarsUpdate; + } + private void addAllChildQuests() { List subQuestIds = Arrays.stream(GameData.getMainQuestDataMap().get(this.parentQuestId).getSubQuests()).map(SubQuestData::getSubId).toList(); for (Integer subQuestId : subQuestIds) { @@ -166,10 +172,10 @@ public class GameMainQuest { } // handoff main quest - if (mainQuestData.getSuggestTrackMainQuestList() != null) { - Arrays.stream(mainQuestData.getSuggestTrackMainQuestList()) - .forEach(getQuestManager()::startMainQuest); - } + // if (mainQuestData.getSuggestTrackMainQuestList() != null) { + // Arrays.stream(mainQuestData.getSuggestTrackMainQuestList()) + // .forEach(getQuestManager()::startMainQuest); + // } } //TODO public void fail() {} @@ -181,9 +187,9 @@ public class GameMainQuest { return null; } - /*if(rewindPositions.isEmpty()){ - addRewindPoints(); - }*/ + // if(rewindPositions.isEmpty()){ + // this.addRewindPoints(); + // } List posAndRot = new ArrayList<>(); if(hasRewindPosition(targetQuest.getSubQuestId(), posAndRot)){ @@ -198,8 +204,8 @@ public class GameMainQuest { if (hasRewindPosition(quest.getSubQuestId(), posAndRot)) { return posAndRot; } - } + return null; } diff --git a/src/main/java/emu/grasscutter/game/quest/GameQuest.java b/src/main/java/emu/grasscutter/game/quest/GameQuest.java index fb92013bc..363e618d8 100644 --- a/src/main/java/emu/grasscutter/game/quest/GameQuest.java +++ b/src/main/java/emu/grasscutter/game/quest/GameQuest.java @@ -1,308 +1,312 @@ -package emu.grasscutter.game.quest; - -import dev.morphia.annotations.Entity; -import dev.morphia.annotations.Transient; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.ChapterData; -import emu.grasscutter.data.excels.QuestData; -import emu.grasscutter.data.excels.TriggerExcelConfigData; -import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; -import emu.grasscutter.game.player.Player; -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.ChapterStateOuterClass; -import emu.grasscutter.net.proto.QuestOuterClass.Quest; -import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.server.packet.send.PacketChapterStateNotify; -import emu.grasscutter.server.packet.send.PacketDelQuestNotify; -import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify; -import emu.grasscutter.utils.Utils; -import java.util.HashMap; -import java.util.Map; -import javax.script.Bindings; -import lombok.Getter; -import lombok.Setter; -import lombok.val; - -@Entity -public class GameQuest { - @Transient @Getter @Setter private GameMainQuest mainQuest; - @Transient @Getter private QuestData questData; - - @Getter private int subQuestId; - @Getter private int mainQuestId; - @Getter @Setter public QuestState state; - - @Getter @Setter private int startTime; - @Getter @Setter private int acceptTime; - @Getter @Setter private int finishTime; - - @Getter @Setter private long startGameDay; - - @Getter private int[] finishProgressList; - @Getter private int[] failProgressList; - @Transient @Getter private Map triggerData; - @Getter private Map triggers; - private transient Bindings bindings; - - @Deprecated // Morphia only. Do not use. - public GameQuest() {} - - public GameQuest(GameMainQuest mainQuest, QuestData questData) { - this.mainQuest = mainQuest; - this.subQuestId = questData.getId(); - this.mainQuestId = questData.getMainId(); - this.questData = questData; - this.state = QuestState.QUEST_STATE_UNSTARTED; - this.triggerData = new HashMap<>(); - this.triggers = new HashMap<>(); - } - - public void start() { - clearProgress(false); - this.acceptTime = Utils.getCurrentSeconds(); - this.startTime = this.acceptTime; - this.startGameDay = getOwner().getWorld().getTotalGameTimeDays(); - this.state = QuestState.QUEST_STATE_UNFINISHED; - val triggerCond = - questData.getFinishCond().stream() - .filter(p -> p.getType() == QuestContent.QUEST_CONTENT_TRIGGER_FIRE) - .toList(); - if (triggerCond.size() > 0) { - for (val cond : triggerCond) { - TriggerExcelConfigData newTrigger = - GameData.getTriggerExcelConfigDataMap().get(cond.getParam()[0]); - if (newTrigger != null) { - if (this.triggerData == null) { - this.triggerData = new HashMap<>(); - } - triggerData.put(newTrigger.getTriggerName(), newTrigger); - triggers.put(newTrigger.getTriggerName(), false); - SceneGroup group = SceneGroup.of(newTrigger.getGroupId()).load(newTrigger.getSceneId()); - getOwner() - .getWorld() - .getSceneById(newTrigger.getSceneId()) - .loadTriggerFromGroup(group, newTrigger.getTriggerName()); - } - } - } - - getOwner().sendPacket(new PacketQuestListUpdateNotify(this)); - - if (ChapterData.beginQuestChapterMap.containsKey(subQuestId)) { - getOwner() - .sendPacket( - new PacketChapterStateNotify( - ChapterData.beginQuestChapterMap.get(subQuestId).getId(), - ChapterStateOuterClass.ChapterState.CHAPTER_STATE_BEGIN)); - } - - // Some subQuests and talks become active when some other subQuests are unfinished (even from - // different MainQuests) - getOwner() - .getQuestManager() - .queueEvent( - QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL, - getSubQuestId(), - getState().getValue(), - 0, - 0, - 0); - getOwner() - .getQuestManager() - .queueEvent( - QuestCond.QUEST_COND_STATE_EQUAL, getSubQuestId(), getState().getValue(), 0, 0, 0); - - getQuestData() - .getBeginExec() - .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); - getOwner().getQuestManager().checkQuestAlreadyFullfilled(this); - - Grasscutter.getLogger().debug("Quest {} is started", subQuestId); - } - - public String getTriggerNameById(int id) { - TriggerExcelConfigData trigger = GameData.getTriggerExcelConfigDataMap().get(id); - if (trigger != null) { - String triggerName = trigger.getTriggerName(); - return triggerName; - } - // return empty string if can't find trigger - return ""; - } - - public Player getOwner() { - return this.getMainQuest().getOwner(); - } - - public void setConfig(QuestData config) { - if (config == null || getSubQuestId() != config.getId()) return; - this.questData = config; - } - - public void setFinishProgress(int index, int value) { - finishProgressList[index] = value; - } - - public void setFailProgress(int index, int value) { - failProgressList[index] = value; - } - - public boolean clearProgress(boolean notifyDelete) { - // TODO improve - var oldState = state; - if (questData.getFinishCond() != null && questData.getFinishCond().size() != 0) { - this.finishProgressList = new int[questData.getFinishCond().size()]; - } - - if (questData.getFailCond() != null && questData.getFailCond().size() != 0) { - this.failProgressList = new int[questData.getFailCond().size()]; - } - setState(QuestState.QUEST_STATE_UNSTARTED); - finishTime = 0; - acceptTime = 0; - startTime = 0; - if (oldState == QuestState.QUEST_STATE_UNSTARTED) { - return false; - } - if (notifyDelete) { - getOwner().sendPacket(new PacketDelQuestNotify(getSubQuestId())); - } - save(); - return true; - } - - public void finish() { - this.state = QuestState.QUEST_STATE_FINISHED; - this.finishTime = Utils.getCurrentSeconds(); - - getOwner().sendPacket(new PacketQuestListUpdateNotify(this)); - - if (getQuestData().isFinishParent()) { - // This quest finishes the questline - the main quest will also save the quest to db, so we - // don't have to call save() here - getMainQuest().finish(); - } - - getQuestData() - .getFinishExec() - .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); - // Some subQuests have conditions that subQuests are finished (even from different MainQuests) - getOwner() - .getQuestManager() - .queueEvent( - QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL, - this.subQuestId, - this.state.getValue(), - 0, - 0, - 0); - getOwner() - .getQuestManager() - .queueEvent(QuestContent.QUEST_CONTENT_FINISH_PLOT, this.subQuestId, 0); - getOwner() - .getQuestManager() - .queueEvent( - QuestCond.QUEST_COND_STATE_EQUAL, this.subQuestId, this.state.getValue(), 0, 0, 0); - getOwner() - .getScene() - .triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_FINISH_QUEST, getSubQuestId()); - - getOwner().getProgressManager().tryUnlockOpenStates(); - - if (ChapterData.endQuestChapterMap.containsKey(subQuestId)) { - mainQuest - .getOwner() - .sendPacket( - new PacketChapterStateNotify( - ChapterData.endQuestChapterMap.get(subQuestId).getId(), - ChapterStateOuterClass.ChapterState.CHAPTER_STATE_END)); - } - - // hard coding to give amber - if (getQuestData().getSubId() == 35402) { - getOwner().getInventory().addItem(1021, 1, ActionReason.QuestItem); // amber item id - } - Grasscutter.getLogger().debug("Quest {} is finished", subQuestId); - } - - // TODO - public void fail() { - this.state = QuestState.QUEST_STATE_FAILED; - this.finishTime = Utils.getCurrentSeconds(); - - this.getOwner().sendPacket(new PacketQuestListUpdateNotify(this)); - - // Some subQuests have conditions that subQuests fail (even from different MainQuests) - this.getOwner() - .getQuestManager() - .queueEvent( - QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL, - this.subQuestId, - this.state.getValue(), - 0, - 0, - 0); - this.getOwner() - .getQuestManager() - .queueEvent( - QuestCond.QUEST_COND_STATE_EQUAL, this.subQuestId, this.state.getValue(), 0, 0, 0); - - this.getQuestData() - .getFailExec() - .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); - - if (this.getQuestData().getTrialAvatarList() != null) { - this.getQuestData() - .getTrialAvatarList() - .forEach(t -> this.getOwner().getTeamManager().removeTrialAvatar(t)); - } - - Grasscutter.getLogger().debug("Quest {} is failed", subQuestId); - } - - // Return true if it did the rewind - public boolean rewind(boolean notifyDelete) { - getMainQuest().getChildQuests().values().stream() - .filter(p -> p.getQuestData().getOrder() > this.getQuestData().getOrder()) - .forEach( - q -> { - q.clearProgress(notifyDelete); - }); - clearProgress(notifyDelete); - this.start(); - return true; - } - - public void save() { - getMainQuest().save(); - } - - public Quest toProto() { - Quest.Builder proto = - Quest.newBuilder() - .setQuestId(getSubQuestId()) - .setState(getState().getValue()) - .setParentQuestId(getMainQuestId()) - .setStartTime(getStartTime()) - .setStartGameTime(438) - .setAcceptTime(getAcceptTime()); - - if (getFinishProgressList() != null) { - for (int i : getFinishProgressList()) { - proto.addFinishProgressList(i); - } - } - - if (getFailProgressList() != null) { - for (int i : getFailProgressList()) { - proto.addFailProgressList(i); - } - } - - return proto.build(); - } -} +package emu.grasscutter.game.quest; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Transient; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.ChapterData; +import emu.grasscutter.data.excels.QuestData; +import emu.grasscutter.data.excels.TriggerExcelConfigData; +import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; +import emu.grasscutter.game.player.Player; +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.ChapterStateOuterClass; +import emu.grasscutter.net.proto.QuestOuterClass.Quest; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.server.packet.send.PacketChapterStateNotify; +import emu.grasscutter.server.packet.send.PacketDelQuestNotify; +import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify; +import emu.grasscutter.utils.Utils; +import java.util.HashMap; +import java.util.Map; +import javax.script.Bindings; +import lombok.Getter; +import lombok.Setter; +import lombok.val; + +@Entity +public class GameQuest { + @Transient @Getter @Setter private GameMainQuest mainQuest; + @Transient @Getter private QuestData questData; + + @Getter private int subQuestId; + @Getter private int mainQuestId; + @Getter @Setter public QuestState state; + + @Getter @Setter private int startTime; + @Getter @Setter private int acceptTime; + @Getter @Setter private int finishTime; + + @Getter @Setter private long startGameDay; + + @Getter private int[] finishProgressList; + @Getter private int[] failProgressList; + @Transient @Getter private Map triggerData; + @Getter private Map triggers; + private transient Bindings bindings; + + @Deprecated // Morphia only. Do not use. + public GameQuest() {} + + public GameQuest(GameMainQuest mainQuest, QuestData questData) { + this.mainQuest = mainQuest; + this.subQuestId = questData.getId(); + this.mainQuestId = questData.getMainId(); + this.questData = questData; + this.state = QuestState.QUEST_STATE_UNSTARTED; + this.triggerData = new HashMap<>(); + this.triggers = new HashMap<>(); + } + + public void start() { + clearProgress(false); + this.acceptTime = Utils.getCurrentSeconds(); + this.startTime = this.acceptTime; + this.startGameDay = getOwner().getWorld().getTotalGameTimeDays(); + this.state = QuestState.QUEST_STATE_UNFINISHED; + val triggerCond = + questData.getFinishCond().stream() + .filter(p -> p.getType() == QuestContent.QUEST_CONTENT_TRIGGER_FIRE) + .toList(); + if (triggerCond.size() > 0) { + for (val cond : triggerCond) { + TriggerExcelConfigData newTrigger = + GameData.getTriggerExcelConfigDataMap().get(cond.getParam()[0]); + if (newTrigger != null) { + if (this.triggerData == null) { + this.triggerData = new HashMap<>(); + } + triggerData.put(newTrigger.getTriggerName(), newTrigger); + triggers.put(newTrigger.getTriggerName(), false); + SceneGroup group = SceneGroup.of(newTrigger.getGroupId()).load(newTrigger.getSceneId()); + getOwner() + .getWorld() + .getSceneById(newTrigger.getSceneId()) + .loadTriggerFromGroup(group, newTrigger.getTriggerName()); + } + } + } + + getOwner().sendPacket(new PacketQuestListUpdateNotify(this)); + + if (ChapterData.beginQuestChapterMap.containsKey(subQuestId)) { + getOwner() + .sendPacket( + new PacketChapterStateNotify( + ChapterData.beginQuestChapterMap.get(subQuestId).getId(), + ChapterStateOuterClass.ChapterState.CHAPTER_STATE_BEGIN)); + } + + // Some subQuests and talks become active when some other subQuests are unfinished (even from + // different MainQuests) + getOwner() + .getQuestManager() + .queueEvent( + QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL, + getSubQuestId(), + getState().getValue(), + 0, + 0, + 0); + getOwner() + .getQuestManager() + .queueEvent( + QuestCond.QUEST_COND_STATE_EQUAL, getSubQuestId(), getState().getValue(), 0, 0, 0); + + getQuestData() + .getBeginExec() + .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); + getOwner().getQuestManager().checkQuestAlreadyFullfilled(this); + + Grasscutter.getLogger().debug("Quest {} is started", subQuestId); + this.save(); + } + + public String getTriggerNameById(int id) { + TriggerExcelConfigData trigger = GameData.getTriggerExcelConfigDataMap().get(id); + if (trigger != null) { + String triggerName = trigger.getTriggerName(); + return triggerName; + } + // return empty string if can't find trigger + return ""; + } + + public Player getOwner() { + return this.getMainQuest().getOwner(); + } + + public void setConfig(QuestData config) { + if (config == null || getSubQuestId() != config.getId()) return; + this.questData = config; + } + + public void setFinishProgress(int index, int value) { + finishProgressList[index] = value; + } + + public void setFailProgress(int index, int value) { + failProgressList[index] = value; + } + + public boolean clearProgress(boolean notifyDelete) { + // TODO improve + var oldState = state; + if (questData.getFinishCond() != null && questData.getFinishCond().size() != 0) { + this.finishProgressList = new int[questData.getFinishCond().size()]; + } + + if (questData.getFailCond() != null && questData.getFailCond().size() != 0) { + this.failProgressList = new int[questData.getFailCond().size()]; + } + setState(QuestState.QUEST_STATE_UNSTARTED); + finishTime = 0; + acceptTime = 0; + startTime = 0; + if (oldState == QuestState.QUEST_STATE_UNSTARTED) { + return false; + } + if (notifyDelete) { + getOwner().sendPacket(new PacketDelQuestNotify(getSubQuestId())); + } + save(); + return true; + } + + public void finish() { + this.state = QuestState.QUEST_STATE_FINISHED; + this.finishTime = Utils.getCurrentSeconds(); + + getOwner().sendPacket(new PacketQuestListUpdateNotify(this)); + + if (getQuestData().isFinishParent()) { + // This quest finishes the questline - the main quest will also save the quest to db, so we + // don't have to call save() here + getMainQuest().finish(); + } + + getQuestData() + .getFinishExec() + .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); + // Some subQuests have conditions that subQuests are finished (even from different MainQuests) + getOwner() + .getQuestManager() + .queueEvent( + QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL, + this.subQuestId, + this.state.getValue(), + 0, + 0, + 0); + getOwner() + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_FINISH_PLOT, this.subQuestId, 0); + getOwner() + .getQuestManager() + .queueEvent( + QuestCond.QUEST_COND_STATE_EQUAL, this.subQuestId, this.state.getValue(), 0, 0, 0); + getOwner() + .getScene() + .triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_FINISH_QUEST, getSubQuestId()); + + getOwner().getProgressManager().tryUnlockOpenStates(); + + if (ChapterData.endQuestChapterMap.containsKey(subQuestId)) { + mainQuest + .getOwner() + .sendPacket( + new PacketChapterStateNotify( + ChapterData.endQuestChapterMap.get(subQuestId).getId(), + ChapterStateOuterClass.ChapterState.CHAPTER_STATE_END)); + } + + // hard coding to give amber + if (getQuestData().getSubId() == 35402) { + getOwner().getInventory().addItem(1021, 1, ActionReason.QuestItem); // amber item id + } + + this.save(); + + Grasscutter.getLogger().debug("Quest {} is finished", subQuestId); + } + + // TODO + public void fail() { + this.state = QuestState.QUEST_STATE_FAILED; + this.finishTime = Utils.getCurrentSeconds(); + + this.getOwner().sendPacket(new PacketQuestListUpdateNotify(this)); + + // Some subQuests have conditions that subQuests fail (even from different MainQuests) + this.getOwner() + .getQuestManager() + .queueEvent( + QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL, + this.subQuestId, + this.state.getValue(), + 0, + 0, + 0); + this.getOwner() + .getQuestManager() + .queueEvent( + QuestCond.QUEST_COND_STATE_EQUAL, this.subQuestId, this.state.getValue(), 0, 0, 0); + + this.getQuestData() + .getFailExec() + .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); + + if (this.getQuestData().getTrialAvatarList() != null) { + this.getQuestData() + .getTrialAvatarList() + .forEach(t -> this.getOwner().getTeamManager().removeTrialAvatar(t)); + } + + Grasscutter.getLogger().debug("Quest {} is failed", subQuestId); + } + + // Return true if it did the rewind + public boolean rewind(boolean notifyDelete) { + getMainQuest().getChildQuests().values().stream() + .filter(p -> p.getQuestData().getOrder() > this.getQuestData().getOrder()) + .forEach( + q -> { + q.clearProgress(notifyDelete); + }); + clearProgress(notifyDelete); + this.start(); + return true; + } + + public void save() { + getMainQuest().save(); + } + + public Quest toProto() { + Quest.Builder proto = + Quest.newBuilder() + .setQuestId(getSubQuestId()) + .setState(getState().getValue()) + .setParentQuestId(getMainQuestId()) + .setStartTime(getStartTime()) + .setStartGameTime(438) + .setAcceptTime(getAcceptTime()); + + if (getFinishProgressList() != null) { + for (int i : getFinishProgressList()) { + proto.addFinishProgressList(i); + } + } + + if (getFailProgressList() != null) { + for (int i : getFailProgressList()) { + proto.addFailProgressList(i); + } + } + + return proto.build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/QuestManager.java b/src/main/java/emu/grasscutter/game/quest/QuestManager.java index 23215e74c..365be7a4c 100644 --- a/src/main/java/emu/grasscutter/game/quest/QuestManager.java +++ b/src/main/java/emu/grasscutter/game/quest/QuestManager.java @@ -7,10 +7,7 @@ import emu.grasscutter.data.excels.QuestData; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.player.BasePlayerManager; import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.quest.enums.ParentQuestState; -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.quest.enums.*; import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify; import emu.grasscutter.utils.Position; import io.netty.util.concurrent.FastThreadLocalThread; @@ -19,6 +16,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; import lombok.val; +import javax.annotation.Nonnull; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingDeque; @@ -86,13 +84,17 @@ public class QuestManager extends BasePlayerManager { this.mainQuests = new Int2ObjectOpenHashMap<>(); } + // TODO store user value set on enable + public boolean isQuestingEnabled() { + return Grasscutter.getConfig().server.game.gameOptions.questing; + } + public void onPlayerBorn() { // TODO scan the quest and start the quest with acceptCond fulfilled // The off send 3 request in that order: 1. FinishedParentQuestNotify, 2. QuestListNotify, 3. ServerCondMeetQuestListUpdateNotify - List newQuests = this.addMultMainQuests(newPlayerMainQuests); - for (GameMainQuest mainQuest : newQuests) { - startMainQuest(mainQuest.getParentQuestId()); + if(this.isQuestingEnabled()) { + this.enableQuests(); } //getPlayer().sendPacket(new PacketFinishedParentQuestUpdateNotify(newQuests)); @@ -117,6 +119,8 @@ public class QuestManager extends BasePlayerManager { } quest.checkProgress(); } + + player.getActivityManager().triggerActivityConditions(); } public void onTick(){ @@ -163,7 +167,8 @@ public class QuestManager extends BasePlayerManager { } public void enableQuests() { - onPlayerBorn(); + this.triggerEvent(QuestCond.QUEST_COND_NONE, null, 0); + this.triggerEvent(QuestCond.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER, null, 1); } /* @@ -256,6 +261,11 @@ public class QuestManager extends BasePlayerManager { return null; } + return addQuest(questConfig); + } + + public GameQuest addQuest(@Nonnull QuestData questConfig) { + // Main quest GameMainQuest mainQuest = this.getMainQuestById(questConfig.getMainId()); @@ -265,7 +275,7 @@ public class QuestManager extends BasePlayerManager { } // Sub quest - GameQuest quest = mainQuest.getChildQuestById(questId); + GameQuest quest = mainQuest.getChildQuestById(questConfig.getSubId()); // Forcefully start quest.start(); @@ -286,13 +296,12 @@ public class QuestManager extends BasePlayerManager { .map(MainQuestData.SubQuestData::getSubId) .ifPresent(this::addQuest); //TODO find a better way then hardcoding to detect needed required quests - if(mainQuestId == 355){ - startMainQuest(361); - startMainQuest(418); - startMainQuest(423); - startMainQuest(20509); - - } + // if (mainQuestId == 355){ + // startMainQuest(361); + // startMainQuest(418); + // startMainQuest(423); + // startMainQuest(20509); + // } } public void queueEvent(QuestCond condType, int... params) { queueEvent(condType, "", params); @@ -312,13 +321,42 @@ public class QuestManager extends BasePlayerManager { public void triggerEvent(QuestCond condType, String paramStr, int... params) { Grasscutter.getLogger().debug("Trigger Event {}, {}, {}", condType, paramStr, params); - List checkMainQuests = this.getMainQuests().values().stream() - .filter(i -> i.getState() != ParentQuestState.PARENT_QUEST_STATE_FINISHED) - .toList(); - for (GameMainQuest mainquest : checkMainQuests) { - mainquest.tryAcceptSubQuests(condType, paramStr, params); + var potentialQuests = GameData.getQuestDataByConditions(condType, params[0], paramStr); + if(potentialQuests == null){ + return; } + + var questSystem = getPlayer().getServer().getQuestSystem(); + var owner = getPlayer(); + + potentialQuests.forEach(questData -> { + if(this.wasSubQuestStarted(questData)){ + return; + } + val acceptCond = questData.getAcceptCond(); + int[] accept = new int[acceptCond.size()]; + for (int i = 0; i < acceptCond.size(); i++) { + val condition = acceptCond.get(i); + boolean result = questSystem.triggerCondition(owner, questData, condition, paramStr, params); + accept[i] = result ? 1 : 0; + } + + boolean shouldAccept = LogicType.calculate(questData.getAcceptCondComb(), accept); + + if (shouldAccept){ + GameQuest quest = owner.getQuestManager().addQuest(questData); + Grasscutter.getLogger().debug("Added quest {} result {}", questData.getSubId(), quest !=null); + } + }); } + + public boolean wasSubQuestStarted(QuestData questData) { + var quest = getQuestById(questData.getId()); + if(quest == null) return false; + + return quest.state != QuestState.QUEST_STATE_UNSTARTED; + } + public void triggerEvent(QuestContent condType, String paramStr, int... params) { Grasscutter.getLogger().debug("Trigger Event {}, {}, {}", condType, paramStr, params); List checkMainQuests = this.getMainQuests().values().stream() @@ -332,6 +370,7 @@ public class QuestManager extends BasePlayerManager { /** * TODO maybe trigger them delayed to allow basic communication finish first + * TODO move content checks to use static informations where possible to allow direct already fulfilled checking * @param quest */ public void checkQuestAlreadyFullfilled(GameQuest quest){ @@ -355,6 +394,7 @@ public class QuestManager extends BasePlayerManager { queueEvent(condition.getType(), condition.getParam()[0], condition.getParam()[1]); } } + case QUEST_CONTENT_PLAYER_LEVEL_UP -> queueEvent(condition.getType(), player.getLevel()); } } }, 1); diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentUnlockTransPoint.java b/src/main/java/emu/grasscutter/game/quest/content/ContentUnlockTransPoint.java index 131bece5b..053bd73d6 100644 --- a/src/main/java/emu/grasscutter/game/quest/content/ContentUnlockTransPoint.java +++ b/src/main/java/emu/grasscutter/game/quest/content/ContentUnlockTransPoint.java @@ -1,16 +1,19 @@ -package emu.grasscutter.game.quest.content; - -import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT; - -import emu.grasscutter.data.excels.QuestData; -import emu.grasscutter.game.quest.GameQuest; -import emu.grasscutter.game.quest.QuestValueContent; - -@QuestValueContent(QUEST_CONTENT_UNLOCK_TRANS_POINT) -public class ContentUnlockTransPoint extends BaseContent { - @Override - public boolean execute( - GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) { - return condition.getParam()[0] == params[0] && condition.getParam()[1] == params[1]; - } -} +package emu.grasscutter.game.quest.content; + +import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT; + +import emu.grasscutter.data.excels.QuestData; +import emu.grasscutter.game.quest.GameQuest; +import emu.grasscutter.game.quest.QuestValueContent; + +@QuestValueContent(QUEST_CONTENT_UNLOCK_TRANS_POINT) +public class ContentUnlockTransPoint extends BaseContent { + @Override + public boolean execute( + GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) { + var sceneId = condition.getParam()[0]; + var scenePointId = condition.getParam()[1]; + var scenePoints = quest.getOwner().getUnlockedScenePoints().get(sceneId); + return scenePoints != null && scenePoints.contains(scenePointId); + } +} 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 bd7ff7a15..95ca4a79c 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().refillActiveEnergy(); - } -} +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().debug("Energy refilled"); + return quest.getOwner().getEnergyManager().refillActiveEnergy(); + } +} diff --git a/src/main/java/emu/grasscutter/game/world/World.java b/src/main/java/emu/grasscutter/game/world/World.java index 3bec2cad2..a303891a1 100644 --- a/src/main/java/emu/grasscutter/game/world/World.java +++ b/src/main/java/emu/grasscutter/game/world/World.java @@ -319,12 +319,14 @@ public class World implements Iterable { var newScene = this.getSceneById(teleportProperties.getSceneId()); newScene.addPlayer(player); player.setAvatarsAbilityForScene(newScene); + // Dungeon // Dungeon system is handling this already // if(dungeonData!=null){ // var dungeonManager = new DungeonManager(newScene, dungeonData); // dungeonManager.startDungeon(); // } + SceneConfig config = newScene.getScriptManager().getConfig(); if (teleportProperties.getTeleportTo() == null && config != null) { if (config.born_pos != null) { diff --git a/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java b/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java index ca4b79ba4..9c4968a35 100644 --- a/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java +++ b/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java @@ -1,153 +1,160 @@ -package emu.grasscutter.game.world; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.DataLoader; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.InvestigationMonsterData; -import emu.grasscutter.data.excels.RewardPreviewData; -import emu.grasscutter.data.excels.world.WorldLevelData; -import emu.grasscutter.game.entity.gadget.chest.BossChestInteractHandler; -import emu.grasscutter.game.entity.gadget.chest.ChestInteractHandler; -import emu.grasscutter.game.entity.gadget.chest.NormalChestInteractHandler; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.net.proto.InvestigationMonsterOuterClass; -import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.scripts.data.SceneMonster; -import emu.grasscutter.server.game.BaseGameSystem; -import emu.grasscutter.server.game.GameServer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -public class WorldDataSystem extends BaseGameSystem { - private final Map chestInteractHandlerMap; // chestType-Handler - private final Map sceneInvestigationGroupMap; // - - public WorldDataSystem(GameServer server) { - super(server); - this.chestInteractHandlerMap = new HashMap<>(); - this.sceneInvestigationGroupMap = new ConcurrentHashMap<>(); - - loadChestConfig(); - } - - public synchronized void loadChestConfig() { - // set the special chest first - chestInteractHandlerMap.put("SceneObj_Chest_Flora", new BossChestInteractHandler()); - - try { - DataLoader.loadList("ChestReward.json", ChestReward.class) - .forEach( - reward -> - reward - .getObjNames() - .forEach( - name -> - chestInteractHandlerMap.computeIfAbsent( - name, x -> new NormalChestInteractHandler(reward)))); - } catch (Exception e) { - Grasscutter.getLogger().error("Unable to load chest reward config.", e); - } - } - - public Map getChestInteractHandlerMap() { - return chestInteractHandlerMap; - } - - public RewardPreviewData getRewardByBossId(int monsterId) { - var investigationMonsterData = - GameData.getInvestigationMonsterDataMap().values().parallelStream() - .filter(imd -> imd.getMonsterIdList() != null && !imd.getMonsterIdList().isEmpty()) - .filter(imd -> imd.getMonsterIdList().get(0) == monsterId) - .findFirst(); - - if (investigationMonsterData.isEmpty()) { - return null; - } - return GameData.getRewardPreviewDataMap() - .get(investigationMonsterData.get().getRewardPreviewId()); - } - - private SceneGroup getInvestigationGroup(int sceneId, int groupId) { - var key = sceneId + "_" + groupId; - if (!sceneInvestigationGroupMap.containsKey(key)) { - var group = SceneGroup.of(groupId).load(sceneId); - sceneInvestigationGroupMap.putIfAbsent(key, group); - return group; - } - return sceneInvestigationGroupMap.get(key); - } - - public int getMonsterLevel(SceneMonster monster, World world) { - // Calculate level - int level = monster.level; - WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(world.getWorldLevel()); - - if (worldLevelData != null) { - level = worldLevelData.getMonsterLevel(); - } - return level; - } - - private InvestigationMonsterOuterClass.InvestigationMonster getInvestigationMonster( - Player player, InvestigationMonsterData imd) { - if (imd.getGroupIdList().isEmpty() || imd.getMonsterIdList().isEmpty()) { - return null; - } - - var groupId = imd.getGroupIdList().get(0); - var monsterId = imd.getMonsterIdList().get(0); - var sceneId = imd.getCityData().getSceneId(); - var group = getInvestigationGroup(sceneId, groupId); - - if (group == null || group.monsters == null) { - return null; - } - - var monster = - group.monsters.values().stream().filter(x -> x.monster_id == monsterId).findFirst(); - if (monster.isEmpty()) { - return null; - } - - var builder = InvestigationMonsterOuterClass.InvestigationMonster.newBuilder(); - - builder - .setId(imd.getId()) - .setCityId(imd.getCityId()) - .setSceneId(imd.getCityData().getSceneId()) - .setGroupId(groupId) - .setMonsterId(monsterId) - .setLevel(getMonsterLevel(monster.get(), player.getWorld())) - .setIsAlive(true) - .setNextRefreshTime(Integer.MAX_VALUE) - .setRefreshInterval(Integer.MAX_VALUE) - .setPos(monster.get().pos.toProto()); - - if ("Boss".equals(imd.getMonsterCategory())) { - var bossChest = group.searchBossChestInGroup(); - if (bossChest.isPresent()) { - builder.setResin(bossChest.get().resin); - builder.setMaxBossChestNum(bossChest.get().take_num); - } - } - return builder.build(); - } - - public List getInvestigationMonstersByCityId( - Player player, int cityId) { - var cityData = GameData.getCityDataMap().get(cityId); - if (cityData == null) { - Grasscutter.getLogger().warn("City not exist {}", cityId); - return List.of(); - } - - return GameData.getInvestigationMonsterDataMap().values().parallelStream() - .filter(imd -> imd.getCityId() == cityId) - .map(imd -> this.getInvestigationMonster(player, imd)) - .filter(Objects::nonNull) - .toList(); - } -} +package emu.grasscutter.game.world; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.DataLoader; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.InvestigationMonsterData; +import emu.grasscutter.data.excels.RewardPreviewData; +import emu.grasscutter.data.excels.world.WorldLevelData; +import emu.grasscutter.game.entity.gadget.chest.BossChestInteractHandler; +import emu.grasscutter.game.entity.gadget.chest.ChestInteractHandler; +import emu.grasscutter.game.entity.gadget.chest.NormalChestInteractHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.proto.InvestigationMonsterOuterClass; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.scripts.data.SceneMonster; +import emu.grasscutter.server.game.BaseGameSystem; +import emu.grasscutter.server.game.GameServer; +import org.luaj.vm2.LuaError; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class WorldDataSystem extends BaseGameSystem { + private final Map chestInteractHandlerMap; // chestType-Handler + private final Map sceneInvestigationGroupMap; // + + public WorldDataSystem(GameServer server) { + super(server); + this.chestInteractHandlerMap = new HashMap<>(); + this.sceneInvestigationGroupMap = new ConcurrentHashMap<>(); + + loadChestConfig(); + } + + public synchronized void loadChestConfig() { + // set the special chest first + chestInteractHandlerMap.put("SceneObj_Chest_Flora", new BossChestInteractHandler()); + + try { + DataLoader.loadList("ChestReward.json", ChestReward.class) + .forEach( + reward -> + reward + .getObjNames() + .forEach( + name -> + chestInteractHandlerMap.computeIfAbsent( + name, x -> new NormalChestInteractHandler(reward)))); + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to load chest reward config.", e); + } + } + + public Map getChestInteractHandlerMap() { + return chestInteractHandlerMap; + } + + public RewardPreviewData getRewardByBossId(int monsterId) { + var investigationMonsterData = + GameData.getInvestigationMonsterDataMap().values().parallelStream() + .filter(imd -> imd.getMonsterIdList() != null && !imd.getMonsterIdList().isEmpty()) + .filter(imd -> imd.getMonsterIdList().get(0) == monsterId) + .findFirst(); + + if (investigationMonsterData.isEmpty()) { + return null; + } + return GameData.getRewardPreviewDataMap() + .get(investigationMonsterData.get().getRewardPreviewId()); + } + + private SceneGroup getInvestigationGroup(int sceneId, int groupId) { + var key = sceneId + "_" + groupId; + if (!sceneInvestigationGroupMap.containsKey(key)) { + try { + var group = SceneGroup.of(groupId).load(sceneId); + sceneInvestigationGroupMap.putIfAbsent(key, group); + return group; + } catch (LuaError luaError) { + Grasscutter.getLogger() + .error("failed to get investigationGroup {} in scene{}:", groupId, sceneId, luaError); + } + } + return sceneInvestigationGroupMap.get(key); + } + + public int getMonsterLevel(SceneMonster monster, World world) { + // Calculate level + int level = monster.level; + WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(world.getWorldLevel()); + + if (worldLevelData != null) { + level = worldLevelData.getMonsterLevel(); + } + return level; + } + + private InvestigationMonsterOuterClass.InvestigationMonster getInvestigationMonster( + Player player, InvestigationMonsterData imd) { + if (imd.getGroupIdList().isEmpty() || imd.getMonsterIdList().isEmpty()) { + return null; + } + + var groupId = imd.getGroupIdList().get(0); + var monsterId = imd.getMonsterIdList().get(0); + var sceneId = imd.getCityData().getSceneId(); + var group = getInvestigationGroup(sceneId, groupId); + + if (group == null || group.monsters == null) { + return null; + } + + var monster = + group.monsters.values().stream().filter(x -> x.monster_id == monsterId).findFirst(); + if (monster.isEmpty()) { + return null; + } + + var builder = InvestigationMonsterOuterClass.InvestigationMonster.newBuilder(); + + builder + .setId(imd.getId()) + .setCityId(imd.getCityId()) + .setSceneId(imd.getCityData().getSceneId()) + .setGroupId(groupId) + .setMonsterId(monsterId) + .setLevel(getMonsterLevel(monster.get(), player.getWorld())) + .setIsAlive(true) + .setNextRefreshTime(Integer.MAX_VALUE) + .setRefreshInterval(Integer.MAX_VALUE) + .setPos(monster.get().pos.toProto()); + + if ("Boss".equals(imd.getMonsterCategory())) { + var bossChest = group.searchBossChestInGroup(); + if (bossChest.isPresent()) { + builder.setResin(bossChest.get().resin); + builder.setMaxBossChestNum(bossChest.get().take_num); + } + } + return builder.build(); + } + + public List getInvestigationMonstersByCityId( + Player player, int cityId) { + var cityData = GameData.getCityDataMap().get(cityId); + if (cityData == null) { + Grasscutter.getLogger().warn("City not exist {}", cityId); + return List.of(); + } + + return GameData.getInvestigationMonsterDataMap().values().parallelStream() + .filter(imd -> imd.getCityId() == cityId) + .map(imd -> this.getInvestigationMonster(player, imd)) + .filter(Objects::nonNull) + .toList(); + } +} diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index d2e8caec3..8f4ec1aa6 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -1,1144 +1,1177 @@ -package emu.grasscutter.scripts; - -import static emu.grasscutter.scripts.constants.EventType.*; - -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 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 javax.annotation.Nonnull; -import javax.annotation.Nullable; -import kotlin.Pair; -import lombok.val; -import org.luaj.vm2.LuaError; -import org.luaj.vm2.LuaValue; -import org.luaj.vm2.lib.jse.CoerceJavaToLua; - -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(); - } -} +package emu.grasscutter.scripts; + +import static emu.grasscutter.scripts.constants.EventType.*; + +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 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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import kotlin.Pair; +import lombok.val; +import org.luaj.vm2.LuaError; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.CoerceJavaToLua; + +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) { + 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 + scene.broadcastPacket(new PacketGroupSuiteNotify(groupId, suiteId)); + } + + return true; + } + + public boolean refreshGroupSuite(int groupId, int suiteId, GameQuest quest) { + var result = refreshGroupSuite(groupId, suiteId); + if (suiteId != 0 && quest != null) { + quest + .getMainQuest() + .getQuestGroupSuites() + .add( + QuestGroupSuite.of().scene(getScene().getId()).group(groupId).suite(suiteId).build()); + } + + return result; + } + + 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<>(); + + if (group.monsters != null) { + group + .monsters + .values() + .forEach( + m -> { + addGridPositionToMap( + groupPositions.get(m.vision_level), + group.id, + m.vision_level, + m.pos); + vision_levels.add(m.vision_level); + }); + } else { + Grasscutter.getLogger() + .error("group.monsters null for group {}", group.id); + } + if (group.gadgets != null) { + 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); + }); + } else { + Grasscutter.getLogger() + .error("group.gadgets null for group {}", group.id); + } + + if (group.npcs != null) { + group + .npcs + .values() + .forEach( + n -> + addGridPositionToMap( + groupPositions.get(n.vision_level), + group.id, + n.vision_level, + n.pos)); + } else { + Grasscutter.getLogger().error("group.npcs null for group {}", group.id); + } + + if (group.regions != null) { + group + .regions + .values() + .forEach( + r -> + addGridPositionToMap( + groupPositions.get(0), group.id, 0, r.pos)); + } else { + Grasscutter.getLogger() + .error("group.regions null for group {}", group.id); + } + + 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) { + relevantTriggers = + this.getTriggersByEvent(eventType).stream() + .filter( + t -> + t.getCondition().contains(String.valueOf(params.param1)) + && (t.getSource().isEmpty() + || t.getSource().equals(params.getEventSource()))) + .collect(Collectors.toSet()); + } else { + relevantTriggers = + this.getTriggersByEvent(eventType).stream() + .filter( + t -> params.getGroupId() == 0 || t.getCurrentGroup().id == params.getGroupId()) + .filter( + t -> (t.getSource().isEmpty() || t.getSource().equals(params.getEventSource()))) + .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()); + this.cancelGroupTimerEvent(groupID, source); + 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 : new HashSet<>(groupTimers)) { + if (timer.component1().equals(source)) { + Grasscutter.getGameServer().getScheduler().cancelTask(timer.component2()); + groupTimers.remove(timer); + 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/ScriptLoader.java b/src/main/java/emu/grasscutter/scripts/ScriptLoader.java index d3dd8ca9d..45b052a30 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLoader.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLoader.java @@ -1,152 +1,176 @@ -package emu.grasscutter.scripts; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.props.EntityType; -import emu.grasscutter.game.quest.enums.QuestState; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.constants.ScriptGadgetState; -import emu.grasscutter.scripts.constants.ScriptRegionShape; -import emu.grasscutter.scripts.data.SceneMeta; -import emu.grasscutter.scripts.serializer.LuaSerializer; -import emu.grasscutter.scripts.serializer.Serializer; -import emu.grasscutter.utils.FileUtils; -import java.io.File; -import java.io.FileReader; -import java.lang.ref.SoftReference; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import javax.script.*; -import lombok.Getter; -import org.luaj.vm2.LuaTable; -import org.luaj.vm2.LuaValue; -import org.luaj.vm2.lib.OneArgFunction; -import org.luaj.vm2.lib.jse.CoerceJavaToLua; -import org.luaj.vm2.script.LuajContext; - -public class ScriptLoader { - private static ScriptEngineManager sm; - @Getter private static ScriptEngine engine; - private static ScriptEngineFactory factory; - @Getter private static Serializer serializer; - @Getter private static ScriptLib scriptLib; - @Getter private static LuaValue scriptLibLua; - /** suggest GC to remove it if the memory is less */ - private static final Map> scriptsCache = - new ConcurrentHashMap<>(); - /** sceneId - SceneMeta */ - private static final Map> sceneMetaCache = - new ConcurrentHashMap<>(); - - public static synchronized void init() throws Exception { - if (sm != null) { - throw new Exception("Script loader already initialized"); - } - - // Create script engine - sm = new ScriptEngineManager(); - engine = sm.getEngineByName("luaj"); - factory = getEngine().getFactory(); - - // Lua stuff - serializer = new LuaSerializer(); - - // Set engine to replace require as a temporary fix to missing scripts - LuajContext ctx = (LuajContext) engine.getContext(); - ctx.globals.set( - "require", - new OneArgFunction() { - @Override - public LuaValue call(LuaValue arg0) { - return LuaValue.ZERO; - } - }); - - LuaTable table = new LuaTable(); - Arrays.stream(EntityType.values()) - .forEach(e -> table.set(e.name().toUpperCase(), e.getValue())); - ctx.globals.set("EntityType", table); - - LuaTable table1 = new LuaTable(); - Arrays.stream(QuestState.values()) - .forEach(e -> table1.set(e.name().toUpperCase(), e.getValue())); - ctx.globals.set("QuestState", table1); - - ctx.globals.set( - "EventType", - CoerceJavaToLua.coerce( - new EventType())); // TODO - make static class to avoid instantiating a new class every - // scene - ctx.globals.set("GadgetState", CoerceJavaToLua.coerce(new ScriptGadgetState())); - ctx.globals.set("RegionShape", CoerceJavaToLua.coerce(new ScriptRegionShape())); - - scriptLib = new ScriptLib(); - scriptLibLua = CoerceJavaToLua.coerce(scriptLib); - ctx.globals.set("ScriptLib", scriptLibLua); - } - - public static Optional tryGet(SoftReference softReference) { - try { - return Optional.ofNullable(softReference.get()); - } catch (NullPointerException npe) { - return Optional.empty(); - } - } - - @Deprecated(forRemoval = true) - public static CompiledScript getScriptByPath(String path) { - var sc = tryGet(scriptsCache.get(path)); - if (sc.isPresent()) { - return sc.get(); - } - - Grasscutter.getLogger().debug("Loading script " + path); - - File file = new File(path); - - if (!file.exists()) return null; - - try (FileReader fr = new FileReader(file)) { - var script = ((Compilable) getEngine()).compile(fr); - scriptsCache.put(path, new SoftReference<>(script)); - return script; - } catch (Exception e) { - Grasscutter.getLogger().error("Loading script {} failed!", path, e); - return null; - } - } - - public static CompiledScript getScript(String path) { - var sc = tryGet(scriptsCache.get(path)); - if (sc.isPresent()) { - return sc.get(); - } - - Grasscutter.getLogger().debug("Loading script " + path); - final Path scriptPath = FileUtils.getScriptPath(path); - if (!Files.exists(scriptPath)) return null; - - try { - var script = ((Compilable) getEngine()).compile(Files.newBufferedReader(scriptPath)); - scriptsCache.put(path, new SoftReference<>(script)); - return script; - } catch (Exception e) { - Grasscutter.getLogger() - .error("Loading script {} failed! - {}", path, e.getLocalizedMessage()); - return null; - } - } - - public static SceneMeta getSceneMeta(int sceneId) { - return tryGet(sceneMetaCache.get(sceneId)) - .orElseGet( - () -> { - var instance = SceneMeta.of(sceneId); - sceneMetaCache.put(sceneId, new SoftReference<>(instance)); - return instance; - }); - } -} +package emu.grasscutter.scripts; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.dungeons.challenge.enums.ChallengeEventMarkType; +import emu.grasscutter.game.dungeons.challenge.enums.FatherChallengeProperty; +import emu.grasscutter.game.props.ElementType; +import emu.grasscutter.game.props.EntityType; +import emu.grasscutter.game.quest.enums.QuestState; +import emu.grasscutter.scripts.constants.*; +import emu.grasscutter.scripts.data.SceneMeta; +import emu.grasscutter.scripts.serializer.LuaSerializer; +import emu.grasscutter.scripts.serializer.Serializer; +import emu.grasscutter.utils.FileUtils; +import java.io.File; +import java.io.FileReader; +import java.lang.ref.SoftReference; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import javax.script.*; +import lombok.Getter; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.OneArgFunction; +import org.luaj.vm2.lib.jse.CoerceJavaToLua; +import org.luaj.vm2.script.LuajContext; + +public class ScriptLoader { + private static ScriptEngineManager sm; + @Getter private static ScriptEngine engine; + private static ScriptEngineFactory factory; + @Getter private static Serializer serializer; + @Getter private static ScriptLib scriptLib; + @Getter private static LuaValue scriptLibLua; + /** suggest GC to remove it if the memory is less */ + private static final Map> scriptsCache = + new ConcurrentHashMap<>(); + /** sceneId - SceneMeta */ + private static final Map> sceneMetaCache = + new ConcurrentHashMap<>(); + + public static synchronized void init() throws Exception { + if (sm != null) { + throw new Exception("Script loader already initialized"); + } + + // Create script engine + sm = new ScriptEngineManager(); + engine = sm.getEngineByName("luaj"); + factory = getEngine().getFactory(); + + // Lua stuff + serializer = new LuaSerializer(); + + // Set engine to replace require as a temporary fix to missing scripts + LuajContext ctx = (LuajContext) engine.getContext(); + ctx.globals.set( + "require", + new OneArgFunction() { + @Override + public LuaValue call(LuaValue arg0) { + return LuaValue.ZERO; + } + }); + + addEnumByIntValue(ctx, EntityType.values(), "EntityType"); + addEnumByIntValue(ctx, QuestState.values(), "QuestState"); + addEnumByIntValue(ctx, ElementType.values(), "ElementType"); + + addEnumByOrdinal(ctx, GroupKillPolicy.values(), "GroupKillPolicy"); + addEnumByOrdinal(ctx, SealBattleType.values(), "SealBattleType"); + addEnumByOrdinal(ctx, FatherChallengeProperty.values(), "FatherChallengeProperty"); + addEnumByOrdinal(ctx, ChallengeEventMarkType.values(), "ChallengeEventMarkType"); + + ctx.globals.set( + "EventType", + CoerceJavaToLua.coerce( + new EventType())); // TODO - make static class to avoid instantiating a new class every + // scene + ctx.globals.set("GadgetState", CoerceJavaToLua.coerce(new ScriptGadgetState())); + ctx.globals.set("RegionShape", CoerceJavaToLua.coerce(new ScriptRegionShape())); + + scriptLib = new ScriptLib(); + scriptLibLua = CoerceJavaToLua.coerce(scriptLib); + ctx.globals.set("ScriptLib", scriptLibLua); + } + + private static > void addEnumByOrdinal( + LuajContext ctx, T[] enumArray, String name) { + LuaTable table = new LuaTable(); + Arrays.stream(enumArray) + .forEach( + e -> { + table.set(e.name(), e.ordinal()); + table.set(e.name().toUpperCase(), e.ordinal()); + }); + ctx.globals.set(name, table); + } + + private static & IntValueEnum> void addEnumByIntValue( + LuajContext ctx, T[] enumArray, String name) { + LuaTable table = new LuaTable(); + Arrays.stream(enumArray) + .forEach( + e -> { + table.set(e.name(), e.getValue()); + table.set(e.name().toUpperCase(), e.getValue()); + }); + ctx.globals.set(name, table); + } + + public static Optional tryGet(SoftReference softReference) { + try { + return Optional.ofNullable(softReference.get()); + } catch (NullPointerException npe) { + return Optional.empty(); + } + } + + @Deprecated(forRemoval = true) + public static CompiledScript getScriptByPath(String path) { + var sc = tryGet(scriptsCache.get(path)); + if (sc.isPresent()) { + return sc.get(); + } + + // Grasscutter.getLogger().debug("Loading script " + path); + + File file = new File(path); + + if (!file.exists()) return null; + + try (FileReader fr = new FileReader(file)) { + var script = ((Compilable) getEngine()).compile(fr); + scriptsCache.put(path, new SoftReference<>(script)); + return script; + } catch (Exception e) { + Grasscutter.getLogger().error("Loading script {} failed!", path, e); + return null; + } + } + + public static CompiledScript getScript(String path) { + var sc = tryGet(scriptsCache.get(path)); + if (sc.isPresent()) { + return sc.get(); + } + + // Grasscutter.getLogger().debug("Loading script " + path); + final Path scriptPath = FileUtils.getScriptPath(path); + if (!Files.exists(scriptPath)) return null; + + try { + var script = ((Compilable) getEngine()).compile(Files.newBufferedReader(scriptPath)); + scriptsCache.put(path, new SoftReference<>(script)); + return script; + } catch (Exception e) { + Grasscutter.getLogger() + .error("Loading script {} failed! - {}", path, e.getLocalizedMessage()); + return null; + } + } + + public static SceneMeta getSceneMeta(int sceneId) { + return tryGet(sceneMetaCache.get(sceneId)) + .orElseGet( + () -> { + var instance = SceneMeta.of(sceneId); + sceneMetaCache.put(sceneId, new SoftReference<>(instance)); + return instance; + }); + } +} diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneBlock.java b/src/main/java/emu/grasscutter/scripts/data/SceneBlock.java index 639574ff7..4c4299f98 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneBlock.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneBlock.java @@ -1,86 +1,87 @@ -package emu.grasscutter.scripts.data; - -import com.github.davidmoten.rtreemulti.RTree; -import com.github.davidmoten.rtreemulti.geometry.Geometry; -import com.github.davidmoten.rtreemulti.geometry.Rectangle; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.scripts.SceneIndexManager; -import emu.grasscutter.scripts.ScriptLoader; -import emu.grasscutter.utils.Position; -import java.util.Map; -import java.util.stream.Collectors; -import javax.script.Bindings; -import javax.script.CompiledScript; -import javax.script.ScriptException; -import lombok.Setter; -import lombok.ToString; - -@ToString -@Setter -public class SceneBlock { - public int id; - public Position max; - public Position min; - - public int sceneId; - public Map groups; - public RTree sceneGroupIndex; - - private transient boolean loaded; // Not an actual variable in the scripts either - - public boolean isLoaded() { - return this.loaded; - } - - public void setLoaded(boolean loaded) { - this.loaded = loaded; - } - - public boolean contains(Position pos) { - return pos.getX() <= this.max.getX() - && pos.getX() >= this.min.getX() - && pos.getZ() <= this.max.getZ() - && pos.getZ() >= this.min.getZ(); - } - - public SceneBlock load(int sceneId, Bindings bindings) { - if (this.loaded) { - return this; - } - this.sceneId = sceneId; - this.setLoaded(true); - - CompiledScript cs = - ScriptLoader.getScript( - "Scene/" + sceneId + "/scene" + sceneId + "_block" + this.id + ".lua"); - - if (cs == null) { - return null; - } - - // Eval script - try { - cs.eval(bindings); - - // Set groups - this.groups = - ScriptLoader.getSerializer().toList(SceneGroup.class, bindings.get("groups")).stream() - .collect(Collectors.toMap(x -> x.id, y -> y, (a, b) -> a)); - - this.groups.values().forEach(g -> g.block_id = this.id); - this.sceneGroupIndex = - SceneIndexManager.buildIndex(3, this.groups.values(), g -> g.pos.toPoint()); - } catch (ScriptException exception) { - Grasscutter.getLogger() - .error( - "An error occurred while loading block " + this.id + " in scene " + sceneId, - exception); - } - Grasscutter.getLogger().debug("Successfully loaded block {} in scene {}.", this.id, sceneId); - return this; - } - - public Rectangle toRectangle() { - return Rectangle.create(this.min.toXZDoubleArray(), this.max.toXZDoubleArray()); - } -} +package emu.grasscutter.scripts.data; + +import com.github.davidmoten.rtreemulti.RTree; +import com.github.davidmoten.rtreemulti.geometry.Geometry; +import com.github.davidmoten.rtreemulti.geometry.Rectangle; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.scripts.SceneIndexManager; +import emu.grasscutter.scripts.ScriptLoader; +import emu.grasscutter.utils.Position; +import java.util.Map; +import java.util.stream.Collectors; +import javax.script.Bindings; +import javax.script.CompiledScript; +import javax.script.ScriptException; +import lombok.Setter; +import lombok.ToString; + +@ToString +@Setter +public class SceneBlock { + public int id; + public Position max; + public Position min; + + public int sceneId; + public Map groups; + public RTree sceneGroupIndex; + + private transient boolean loaded; // Not an actual variable in the scripts either + + public boolean isLoaded() { + return this.loaded; + } + + public void setLoaded(boolean loaded) { + this.loaded = loaded; + } + + public boolean contains(Position pos) { + int range = Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange; + return pos.getX() <= (this.max.getX() + range) + && pos.getX() >= (this.min.getX() - range) + && pos.getZ() <= (this.max.getZ() + range) + && pos.getZ() >= (this.min.getZ() - range); + } + + public SceneBlock load(int sceneId, Bindings bindings) { + if (this.loaded) { + return this; + } + this.sceneId = sceneId; + this.setLoaded(true); + + CompiledScript cs = + ScriptLoader.getScript( + "Scene/" + sceneId + "/scene" + sceneId + "_block" + this.id + ".lua"); + + if (cs == null) { + return null; + } + + // Eval script + try { + cs.eval(bindings); + + // Set groups + this.groups = + ScriptLoader.getSerializer().toList(SceneGroup.class, bindings.get("groups")).stream() + .collect(Collectors.toMap(x -> x.id, y -> y, (a, b) -> a)); + + this.groups.values().forEach(g -> g.block_id = this.id); + this.sceneGroupIndex = + SceneIndexManager.buildIndex(3, this.groups.values(), g -> g.pos.toPoint()); + } catch (ScriptException exception) { + Grasscutter.getLogger() + .error( + "An error occurred while loading block " + this.id + " in scene " + sceneId, + exception); + } + Grasscutter.getLogger().debug("Successfully loaded block {} in scene {}.", this.id, sceneId); + return this; + } + + public Rectangle toRectangle() { + return Rectangle.create(this.min.toXZDoubleArray(), this.max.toXZDoubleArray()); + } +} diff --git a/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java b/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java index 36df2380c..e8d6c2646 100644 --- a/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java +++ b/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java @@ -1,246 +1,268 @@ -package emu.grasscutter.scripts.serializer; - -import com.esotericsoftware.reflectasm.ConstructorAccess; -import com.esotericsoftware.reflectasm.MethodAccess; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.scripts.ScriptUtils; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.experimental.FieldDefaults; -import org.luaj.vm2.LuaTable; -import org.luaj.vm2.LuaValue; - -public class LuaSerializer implements Serializer { - - private static final Map, MethodAccess> methodAccessCache = new ConcurrentHashMap<>(); - private static final Map, ConstructorAccess> constructorCache = - new ConcurrentHashMap<>(); - private static final Map, Map> fieldMetaCache = - new ConcurrentHashMap<>(); - - @Override - public List toList(Class type, Object obj) { - return serializeList(type, (LuaTable) obj); - } - - @Override - public T toObject(Class type, Object obj) { - return serialize(type, (LuaTable) obj); - } - - @Override - public Map toMap(Class type, Object obj) { - return serializeMap(type, (LuaTable) obj); - } - - private Map serializeMap(Class type, LuaTable table) { - Map map = new HashMap<>(); - - if (table == null) { - return map; - } - - try { - LuaValue[] keys = table.keys(); - for (LuaValue k : keys) { - try { - LuaValue keyValue = table.get(k); - - T object = null; - - if (keyValue.istable()) { - object = serialize(type, keyValue.checktable()); - } else if (keyValue.isint()) { - object = (T) (Integer) keyValue.toint(); - } else if (keyValue.isnumber()) { - object = (T) (Float) keyValue.tofloat(); // terrible... - } else if (keyValue.isstring()) { - object = (T) keyValue.tojstring(); - } else if (keyValue.isboolean()) { - object = (T) (Boolean) keyValue.toboolean(); - } else { - object = (T) keyValue; - } - - if (object != null) { - map.put(String.valueOf(k), object); - } - } catch (Exception ex) { - - } - } - } catch (Exception e) { - e.printStackTrace(); - } - - return map; - } - - public List serializeList(Class type, LuaTable table) { - List list = new ArrayList<>(); - - if (table == null) { - return list; - } - - try { - LuaValue[] keys = table.keys(); - for (LuaValue k : keys) { - try { - LuaValue keyValue = table.get(k); - - T object = null; - - if (keyValue.istable()) { - object = serialize(type, keyValue.checktable()); - } else if (keyValue.isint()) { - object = (T) (Integer) keyValue.toint(); - } else if (keyValue.isnumber()) { - object = (T) (Float) keyValue.tofloat(); // terrible... - } else if (keyValue.isstring()) { - object = (T) keyValue.tojstring(); - } else if (keyValue.isboolean()) { - object = (T) (Boolean) keyValue.toboolean(); - } else { - object = (T) keyValue; - } - - if (object != null) { - list.add(object); - } - } catch (Exception ex) { - - } - } - } catch (Exception e) { - e.printStackTrace(); - } - - return list; - } - - public T serialize(Class type, LuaTable table) { - T object = null; - - if (type == List.class) { - try { - Class listType = (Class) type.getTypeParameters()[0].getClass(); - return (T) serializeList(listType, table); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - try { - if (!methodAccessCache.containsKey(type)) { - cacheType(type); - } - var methodAccess = methodAccessCache.get(type); - var fieldMetaMap = fieldMetaCache.get(type); - - object = (T) constructorCache.get(type).newInstance(); - - if (table == null) { - return object; - } - - LuaValue[] keys = table.keys(); - for (LuaValue k : keys) { - try { - var keyName = k.checkjstring(); - if (!fieldMetaMap.containsKey(keyName)) { - continue; - } - var fieldMeta = fieldMetaMap.get(keyName); - LuaValue keyValue = table.get(k); - - if (keyValue.istable()) { - methodAccess.invoke( - object, fieldMeta.index, serialize(fieldMeta.getType(), keyValue.checktable())); - } else if (fieldMeta.getType().equals(float.class)) { - methodAccess.invoke(object, fieldMeta.index, keyValue.tofloat()); - } else if (fieldMeta.getType().equals(int.class)) { - methodAccess.invoke(object, fieldMeta.index, keyValue.toint()); - } else if (fieldMeta.getType().equals(String.class)) { - methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring()); - } else if (fieldMeta.getType().equals(boolean.class)) { - methodAccess.invoke(object, fieldMeta.index, keyValue.toboolean()); - } else { - methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring()); - } - } catch (Exception ex) { - // ex.printStackTrace(); - continue; - } - } - } catch (Exception e) { - Grasscutter.getLogger().debug(ScriptUtils.toMap(table).toString()); - e.printStackTrace(); - } - - return object; - } - - public Map cacheType(Class type) { - if (fieldMetaCache.containsKey(type)) { - return fieldMetaCache.get(type); - } - if (!constructorCache.containsKey(type)) { - constructorCache.putIfAbsent(type, ConstructorAccess.get(type)); - } - var methodAccess = - Optional.ofNullable(methodAccessCache.get(type)).orElse(MethodAccess.get(type)); - methodAccessCache.putIfAbsent(type, methodAccess); - - var fieldMetaMap = new HashMap(); - var methodNameSet = new HashSet<>(Arrays.stream(methodAccess.getMethodNames()).toList()); - - Arrays.stream(type.getDeclaredFields()) - .filter(field -> methodNameSet.contains(getSetterName(field.getName()))) - .forEach( - field -> { - var setter = getSetterName(field.getName()); - var index = methodAccess.getIndex(setter); - fieldMetaMap.put( - field.getName(), new FieldMeta(field.getName(), setter, index, field.getType())); - }); - - Arrays.stream(type.getFields()) - .filter(field -> !fieldMetaMap.containsKey(field.getName())) - .filter(field -> methodNameSet.contains(getSetterName(field.getName()))) - .forEach( - field -> { - var setter = getSetterName(field.getName()); - var index = methodAccess.getIndex(setter); - fieldMetaMap.put( - field.getName(), new FieldMeta(field.getName(), setter, index, field.getType())); - }); - - fieldMetaCache.put(type, fieldMetaMap); - return fieldMetaMap; - } - - public String getSetterName(String fieldName) { - if (fieldName == null || fieldName.length() == 0) { - return null; - } - if (fieldName.length() == 1) { - return "set" + fieldName.toUpperCase(); - } - return "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); - } - - @Data - @AllArgsConstructor - @FieldDefaults(level = AccessLevel.PRIVATE) - static class FieldMeta { - String name; - String setter; - int index; - Class type; - } -} +package emu.grasscutter.scripts.serializer; + +import com.esotericsoftware.reflectasm.ConstructorAccess; +import com.esotericsoftware.reflectasm.MethodAccess; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.scripts.ScriptUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import org.jetbrains.annotations.Nullable; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; + +public class LuaSerializer implements Serializer { + + private static final Map, MethodAccess> methodAccessCache = new ConcurrentHashMap<>(); + private static final Map, ConstructorAccess> constructorCache = + new ConcurrentHashMap<>(); + private static final Map, Map> fieldMetaCache = + new ConcurrentHashMap<>(); + + @Override + public List toList(Class type, Object obj) { + return serializeList(type, (LuaTable) obj); + } + + @Override + public T toObject(Class type, Object obj) { + return serialize(type, null, (LuaTable) obj); + } + + @Override + public Map toMap(Class type, Object obj) { + return serializeMap(type, (LuaTable) obj); + } + + private Map serializeMap(Class type, LuaTable table) { + Map map = new HashMap<>(); + + if (table == null) { + return map; + } + + try { + LuaValue[] keys = table.keys(); + for (LuaValue k : keys) { + try { + LuaValue keyValue = table.get(k); + + T object = null; + + if (keyValue.istable()) { + object = serialize(type, null, keyValue.checktable()); + } else if (keyValue.isint()) { + object = (T) (Integer) keyValue.toint(); + } else if (keyValue.isnumber()) { + object = (T) (Float) keyValue.tofloat(); // terrible... + } else if (keyValue.isstring()) { + object = (T) keyValue.tojstring(); + } else if (keyValue.isboolean()) { + object = (T) (Boolean) keyValue.toboolean(); + } else { + object = (T) keyValue; + } + + if (object != null) { + map.put(String.valueOf(k), object); + } + } catch (Exception ex) { + + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + return map; + } + + public List serializeList(Class type, LuaTable table) { + List list = new ArrayList<>(); + + if (table == null) { + return list; + } + + try { + LuaValue[] keys = table.keys(); + for (LuaValue k : keys) { + try { + LuaValue keyValue = table.get(k); + + T object = null; + + if (keyValue.istable()) { + object = serialize(type, null, keyValue.checktable()); + } else if (keyValue.isint()) { + object = (T) (Integer) keyValue.toint(); + } else if (keyValue.isnumber()) { + object = (T) (Float) keyValue.tofloat(); // terrible... + } else if (keyValue.isstring()) { + object = (T) keyValue.tojstring(); + } else if (keyValue.isboolean()) { + object = (T) (Boolean) keyValue.toboolean(); + } else { + object = (T) keyValue; + } + + if (object != null) { + list.add(object); + } + } catch (Exception ex) { + + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + return list; + } + + private Class getListType(Class type, @Nullable Field field) { + if (field == null) { + return type.getTypeParameters()[0].getClass(); + } + Type fieldType = field.getGenericType(); + if (fieldType instanceof ParameterizedType) { + return (Class) ((ParameterizedType) fieldType).getActualTypeArguments()[0]; + } + + return null; + } + + public T serialize(Class type, @Nullable Field field, LuaTable table) { + T object = null; + + if (type == List.class) { + try { + Class listType = getListType(type, field); + return (T) serializeList(listType, table); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + try { + if (!methodAccessCache.containsKey(type)) { + cacheType(type); + } + var methodAccess = methodAccessCache.get(type); + var fieldMetaMap = fieldMetaCache.get(type); + + object = (T) constructorCache.get(type).newInstance(); + + if (table == null) { + return object; + } + + LuaValue[] keys = table.keys(); + for (LuaValue k : keys) { + try { + var keyName = k.checkjstring(); + if (!fieldMetaMap.containsKey(keyName)) { + continue; + } + var fieldMeta = fieldMetaMap.get(keyName); + LuaValue keyValue = table.get(k); + + if (keyValue.istable()) { + methodAccess.invoke( + object, + fieldMeta.index, + serialize(fieldMeta.getType(), fieldMeta.getField(), keyValue.checktable())); + } else if (fieldMeta.getType().equals(float.class)) { + methodAccess.invoke(object, fieldMeta.index, keyValue.tofloat()); + } else if (fieldMeta.getType().equals(int.class)) { + methodAccess.invoke(object, fieldMeta.index, keyValue.toint()); + } else if (fieldMeta.getType().equals(String.class)) { + methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring()); + } else if (fieldMeta.getType().equals(boolean.class)) { + methodAccess.invoke(object, fieldMeta.index, keyValue.toboolean()); + } else { + methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring()); + } + } catch (Exception ex) { + // ex.printStackTrace(); + continue; + } + } + } catch (Exception e) { + Grasscutter.getLogger().debug(ScriptUtils.toMap(table).toString()); + e.printStackTrace(); + } + + return object; + } + + public Map cacheType(Class type) { + if (fieldMetaCache.containsKey(type)) { + return fieldMetaCache.get(type); + } + if (!constructorCache.containsKey(type)) { + constructorCache.putIfAbsent(type, ConstructorAccess.get(type)); + } + var methodAccess = + Optional.ofNullable(methodAccessCache.get(type)).orElse(MethodAccess.get(type)); + methodAccessCache.putIfAbsent(type, methodAccess); + + var fieldMetaMap = new HashMap(); + var methodNameSet = new HashSet<>(Arrays.stream(methodAccess.getMethodNames()).toList()); + + Arrays.stream(type.getDeclaredFields()) + .filter(field -> methodNameSet.contains(getSetterName(field.getName()))) + .forEach( + field -> { + var setter = getSetterName(field.getName()); + var index = methodAccess.getIndex(setter); + fieldMetaMap.put( + field.getName(), + new FieldMeta(field.getName(), setter, index, field.getType(), field)); + }); + + Arrays.stream(type.getFields()) + .filter(field -> !fieldMetaMap.containsKey(field.getName())) + .filter(field -> methodNameSet.contains(getSetterName(field.getName()))) + .forEach( + field -> { + var setter = getSetterName(field.getName()); + var index = methodAccess.getIndex(setter); + fieldMetaMap.put( + field.getName(), + new FieldMeta(field.getName(), setter, index, field.getType(), field)); + }); + + fieldMetaCache.put(type, fieldMetaMap); + return fieldMetaMap; + } + + public String getSetterName(String fieldName) { + if (fieldName == null || fieldName.length() == 0) { + return null; + } + if (fieldName.length() == 1) { + return "set" + fieldName.toUpperCase(); + } + return "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); + } + + @Data + @AllArgsConstructor + @FieldDefaults(level = AccessLevel.PRIVATE) + static class FieldMeta { + String name; + String setter; + int index; + Class type; + @Nullable Field field; + } +} diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index 5fa3ee5b9..95e9365e2 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -1,279 +1,281 @@ -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(); - } - } -} +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 + world.save(); //Save the player's world + } + + 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(); + } + + getWorlds().forEach(World::save); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestUpdateQuestVarReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestUpdateQuestVarReq.java index f3cc1068c..d37649032 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestUpdateQuestVarReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestUpdateQuestVarReq.java @@ -1,45 +1,61 @@ -package emu.grasscutter.server.packet.recv; - -import emu.grasscutter.game.quest.GameMainQuest; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketHandler; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.QuestUpdateQuestVarReqOuterClass; -import emu.grasscutter.net.proto.QuestVarOpOuterClass; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.server.packet.send.PacketQuestUpdateQuestVarRsp; -import java.util.List; - -@Opcodes(PacketOpcodes.QuestUpdateQuestVarReq) -public class HandlerQuestUpdateQuestVarReq extends PacketHandler { - - @Override - public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { - // Client sends packets. One with the value, and one with the index and the new value to - // set/inc/dec - var req = QuestUpdateQuestVarReqOuterClass.QuestUpdateQuestVarReq.parseFrom(payload); - GameMainQuest mainQuest = - session.getPlayer().getQuestManager().getMainQuestById(req.getQuestId() / 100); - List questVars = req.getQuestVarOpListList(); - if (mainQuest.getQuestVarsUpdate().size() == 0) { - for (QuestVarOpOuterClass.QuestVarOp questVar : questVars) { - mainQuest.getQuestVarsUpdate().add(questVar.getValue()); - } - } else { - for (QuestVarOpOuterClass.QuestVarOp questVar : questVars) { - if (questVar.getIsAdd()) { - if (questVar.getValue() >= 0) { - mainQuest.incQuestVar(questVar.getIndex(), questVar.getValue()); - } else { - mainQuest.decQuestVar(questVar.getIndex(), questVar.getValue()); - } - } else { - mainQuest.setQuestVar(questVar.getIndex(), mainQuest.getQuestVarsUpdate().get(0)); - } - // remove the first element from the update list - mainQuest.getQuestVarsUpdate().remove(0); - } - } - session.send(new PacketQuestUpdateQuestVarRsp(req.getQuestId())); - } -} +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.QuestUpdateQuestVarReqOuterClass.QuestUpdateQuestVarReq; +import emu.grasscutter.net.proto.QuestVarOpOuterClass.QuestVarOp; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketQuestUpdateQuestVarRsp; +import java.util.List; + +@Opcodes(PacketOpcodes.QuestUpdateQuestVarReq) +public class HandlerQuestUpdateQuestVarReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + // Client sends packets. One with the value, and one with the index and the new value to + // set/inc/dec + var req = QuestUpdateQuestVarReq.parseFrom(payload); + var questManager = session.getPlayer().getQuestManager(); + var subQuest = questManager.getQuestById(req.getQuestId()); + var mainQuest = questManager.getMainQuestById(req.getParentQuestId()); + if (mainQuest == null && subQuest != null) { + mainQuest = subQuest.getMainQuest(); + } + + if (mainQuest == null) { + session.send(new PacketQuestUpdateQuestVarRsp(req, Retcode.RET_QUEST_NOT_EXIST)); + Grasscutter.getLogger() + .debug( + "trying to update QuestVar for non existing quest s{} m{}", + req.getQuestId(), + req.getParentQuestId()); + return; + } + List questVars = req.getQuestVarOpListList(); + var questVarUpdate = mainQuest.getQuestVarsUpdate(); + if (questVarUpdate.size() == 0) { + for (var questVar : questVars) { + questVarUpdate.add(questVar.getValue()); + } + } else { + for (QuestVarOp questVar : questVars) { + if (questVar.getIsAdd()) { + if (questVar.getValue() >= 0) { + mainQuest.incQuestVar(questVar.getIndex(), questVar.getValue()); + } else { + mainQuest.decQuestVar(questVar.getIndex(), questVar.getValue()); + } + } else { + mainQuest.setQuestVar(questVar.getIndex(), questVarUpdate.get(0)); + } + // remove the first element from the update list + questVarUpdate.remove(0); + } + } + session.send(new PacketQuestUpdateQuestVarRsp(req)); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerEnterDungeonRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerEnterDungeonRsp.java index 4475b550c..3e7bd354e 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerEnterDungeonRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerEnterDungeonRsp.java @@ -1,17 +1,25 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.PlayerEnterDungeonRspOuterClass.PlayerEnterDungeonRsp; - -public class PacketPlayerEnterDungeonRsp extends BasePacket { - - public PacketPlayerEnterDungeonRsp(int pointId, int dungeonId) { - super(PacketOpcodes.PlayerEnterDungeonRsp); - - PlayerEnterDungeonRsp proto = - PlayerEnterDungeonRsp.newBuilder().setPointId(pointId).setDungeonId(dungeonId).build(); - - 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.PlayerEnterDungeonRspOuterClass.PlayerEnterDungeonRsp; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; + +public class PacketPlayerEnterDungeonRsp extends BasePacket { + + public PacketPlayerEnterDungeonRsp(int pointId, int dungeonId, boolean success) { + super(PacketOpcodes.PlayerEnterDungeonRsp); + + PlayerEnterDungeonRsp proto = + PlayerEnterDungeonRsp.newBuilder() + .setPointId(pointId) + .setDungeonId(dungeonId) + .setRetcode( + success + ? Retcode.RET_SUCC_VALUE + : Retcode.RET_FAIL_VALUE) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerHomeCompInfoNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerHomeCompInfoNotify.java index 5cacdf836..0584e5a1d 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerHomeCompInfoNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerHomeCompInfoNotify.java @@ -1,32 +1,33 @@ -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.PlayerHomeCompInfoNotifyOuterClass; -import emu.grasscutter.net.proto.PlayerHomeCompInfoOuterClass; - -public class PacketPlayerHomeCompInfoNotify extends BasePacket { - - public PacketPlayerHomeCompInfoNotify(Player player) { - super(PacketOpcodes.PlayerHomeCompInfoNotify); - - if (player.getRealmList() == null) { - // Do not send - return; - } - - PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify proto = - PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify.newBuilder() - .setCompInfo( - PlayerHomeCompInfoOuterClass.PlayerHomeCompInfo.newBuilder() - .addAllUnlockedModuleIdList(player.getRealmList()) - .addAllSeenModuleIdList(player.getSeenRealmList()) - .addAllLevelupRewardGotLevelList(player.getHomeRewardedLevels()) - .setFriendEnterHomeOptionValue(player.getHome().getEnterHomeOption()) - .build()) - .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.PlayerHomeCompInfoNotifyOuterClass; +import emu.grasscutter.net.proto.PlayerHomeCompInfoOuterClass; + +import java.util.List; + +public class PacketPlayerHomeCompInfoNotify extends BasePacket { + + public PacketPlayerHomeCompInfoNotify(Player player) { + super(PacketOpcodes.PlayerHomeCompInfoNotify); + + if (player.getRealmList() == null) { + // Do not send + return; + } + + PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify proto = + PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify.newBuilder() + .setCompInfo( + PlayerHomeCompInfoOuterClass.PlayerHomeCompInfo.newBuilder() + .addAllUnlockedModuleIdList(player.getRealmList()) + .addAllLevelupRewardGotLevelList(List.of(1)) // Hardcoded + .setFriendEnterHomeOptionValue(player.getHome().getEnterHomeOption()) + .build()) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerSetPauseRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerSetPauseRsp.java index fdf37f696..b09032ed6 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerSetPauseRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerSetPauseRsp.java @@ -1,13 +1,16 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; - -public class PacketPlayerSetPauseRsp extends BasePacket { - - public PacketPlayerSetPauseRsp(int clientSequence) { - super(PacketOpcodes.PlayerSetPauseRsp); - - this.buildHeader(clientSequence); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.PlayerSetPauseRspOuterClass.PlayerSetPauseRsp; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; + +public class PacketPlayerSetPauseRsp extends BasePacket { + + public PacketPlayerSetPauseRsp() { + super(PacketOpcodes.PlayerSetPauseRsp); + + this.setData(PlayerSetPauseRsp.newBuilder() + .setRetcode(Retcode.RET_SUCC_VALUE)); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketQuestUpdateQuestVarRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestUpdateQuestVarRsp.java index 3324f5149..e077351ee 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketQuestUpdateQuestVarRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestUpdateQuestVarRsp.java @@ -1,19 +1,27 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.Opcodes; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.QuestUpdateQuestVarRspOuterClass; - -@Opcodes(PacketOpcodes.QuestUpdateQuestVarReq) -public class PacketQuestUpdateQuestVarRsp extends BasePacket { - - public PacketQuestUpdateQuestVarRsp(int questId) { - super(PacketOpcodes.QuestUpdateQuestVarRsp); - var rsp = - QuestUpdateQuestVarRspOuterClass.QuestUpdateQuestVarRsp.newBuilder() - .setQuestId(questId) - .build(); - this.setData(rsp); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.QuestUpdateQuestVarReqOuterClass.QuestUpdateQuestVarReq; +import emu.grasscutter.net.proto.QuestUpdateQuestVarRspOuterClass; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; + +@Opcodes(PacketOpcodes.QuestUpdateQuestVarReq) +public class PacketQuestUpdateQuestVarRsp extends BasePacket { + + public PacketQuestUpdateQuestVarRsp(QuestUpdateQuestVarReq req) { + this(req, Retcode.RET_SUCC); + } + + public PacketQuestUpdateQuestVarRsp(QuestUpdateQuestVarReq req, Retcode retcode) { + super(PacketOpcodes.QuestUpdateQuestVarRsp); + var rsp = + QuestUpdateQuestVarRspOuterClass.QuestUpdateQuestVarRsp.newBuilder() + .setQuestId(req.getQuestId()) + .setParentQuestId(req.getParentQuestId()) + .setRetcode(retcode.getNumber()) + .build(); + this.setData(rsp); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketScenePointUnlockNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketScenePointUnlockNotify.java index 3dba4a4f1..bcd55d777 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketScenePointUnlockNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketScenePointUnlockNotify.java @@ -1,31 +1,29 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.ScenePointUnlockNotifyOuterClass.ScenePointUnlockNotify; - -public class PacketScenePointUnlockNotify extends BasePacket { - public PacketScenePointUnlockNotify(int sceneId, int pointId) { - super(PacketOpcodes.ScenePointUnlockNotify); - - ScenePointUnlockNotify.Builder p = - ScenePointUnlockNotify.newBuilder() - .setSceneId(sceneId) - .addPointList(pointId) - .addUnhidePointList(pointId); - - this.setData(p); - } - - public PacketScenePointUnlockNotify(int sceneId, Iterable pointIds) { - super(PacketOpcodes.ScenePointUnlockNotify); - - ScenePointUnlockNotify.Builder p = - ScenePointUnlockNotify.newBuilder() - .setSceneId(sceneId) - .addAllPointList(pointIds) - .addAllUnhidePointList(pointIds); - - this.setData(p); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.ScenePointUnlockNotifyOuterClass.ScenePointUnlockNotify; + +public class PacketScenePointUnlockNotify extends BasePacket { + public PacketScenePointUnlockNotify(int sceneId, int pointId) { + super(PacketOpcodes.ScenePointUnlockNotify); + + ScenePointUnlockNotify.Builder p = + ScenePointUnlockNotify.newBuilder() + .setSceneId(sceneId) + .addPointList(pointId); + + this.setData(p); + } + + public PacketScenePointUnlockNotify(int sceneId, Iterable pointIds) { + super(PacketOpcodes.ScenePointUnlockNotify); + + ScenePointUnlockNotify.Builder p = + ScenePointUnlockNotify.newBuilder() + .setSceneId(sceneId) + .addAllPointList(pointIds); + + this.setData(p); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketSceneTimeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneTimeNotify.java index 490dfc641..d018e6fca 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketSceneTimeNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneTimeNotify.java @@ -1,36 +1,27 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.SceneTimeNotifyOuterClass.SceneTimeNotify; - -public class PacketSceneTimeNotify extends BasePacket { - - public PacketSceneTimeNotify(Player player) { - super(PacketOpcodes.SceneTimeNotify); - - var proto = - SceneTimeNotify.newBuilder() - .setIsPaused(player.isPaused()) - .setSceneId(player.getSceneId()) - .setSceneTime(player.getScene().getSceneTime()) - .build(); - - this.setData(proto); - } - - public PacketSceneTimeNotify(Scene scene) { - super(PacketOpcodes.SceneTimeNotify); - - var proto = - SceneTimeNotify.newBuilder() - .setSceneId(scene.getId()) - .setSceneTime(scene.getSceneTime()) - .setIsPaused(scene.isPaused()) - .build(); - - this.setData(proto); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.SceneTimeNotifyOuterClass.SceneTimeNotify; + +public class PacketSceneTimeNotify extends BasePacket { + + public PacketSceneTimeNotify(Player player) { + this(player.getScene()); + } + + public PacketSceneTimeNotify(Scene scene) { + super(PacketOpcodes.SceneTimeNotify); + + var proto = + SceneTimeNotify.newBuilder() + .setSceneId(scene.getId()) + .setSceneTime(scene.getSceneTime()) + .setIsPaused(scene.isPaused()) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockPersonalLineRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockPersonalLineRsp.java index cd9ca0c88..f5f205e53 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockPersonalLineRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockPersonalLineRsp.java @@ -1,18 +1,29 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.UnlockPersonalLineRspOuterClass; - -public class PacketUnlockPersonalLineRsp extends BasePacket { - - public PacketUnlockPersonalLineRsp(int id, int level, int chapterId) { - super(PacketOpcodes.UnlockPersonalLineRsp); - - var proto = UnlockPersonalLineRspOuterClass.UnlockPersonalLineRsp.newBuilder(); - - proto.setPersonalLineId(id).setLevel(level).setChapterId(chapterId); - - 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.RetcodeOuterClass.Retcode; +import emu.grasscutter.net.proto.UnlockPersonalLineRspOuterClass; + +public class PacketUnlockPersonalLineRsp extends BasePacket { + + public PacketUnlockPersonalLineRsp(int id, int level, int chapterId) { + super(PacketOpcodes.UnlockPersonalLineRsp); + + var proto = UnlockPersonalLineRspOuterClass.UnlockPersonalLineRsp.newBuilder(); + + proto.setPersonalLineId(id).setLevel(level).setChapterId(chapterId); + + this.setData(proto); + } + + public PacketUnlockPersonalLineRsp(int id, Retcode retCode) { + super(PacketOpcodes.UnlockPersonalLineRsp); + + var proto = UnlockPersonalLineRspOuterClass.UnlockPersonalLineRsp.newBuilder(); + + proto.setPersonalLineId(id).setRetcode(retCode.getNumber()); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketVehicleInteractRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketVehicleInteractRsp.java index dcc37b460..dfcfb0576 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketVehicleInteractRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketVehicleInteractRsp.java @@ -1,67 +1,73 @@ -package emu.grasscutter.server.packet.send; - -import emu.grasscutter.game.entity.EntityVehicle; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.VehicleInteractRspOuterClass.VehicleInteractRsp; -import emu.grasscutter.net.proto.VehicleInteractTypeOuterClass.VehicleInteractType; -import emu.grasscutter.net.proto.VehicleMemberOuterClass.VehicleMember; - -public class PacketVehicleInteractRsp extends BasePacket { - - public PacketVehicleInteractRsp(Player player, int entityId, VehicleInteractType interactType) { - super(PacketOpcodes.VehicleInteractRsp); - VehicleInteractRsp.Builder proto = VehicleInteractRsp.newBuilder(); - - GameEntity vehicle = player.getScene().getEntityById(entityId); - - if (vehicle instanceof EntityVehicle) { - proto.setEntityId(vehicle.getId()); - - VehicleMember vehicleMember = - VehicleMember.newBuilder() - .setUid(player.getUid()) - .setAvatarGuid(player.getTeamManager().getCurrentCharacterGuid()) - .build(); - - proto.setInteractType(interactType); - proto.setMember(vehicleMember); - - switch (interactType) { - case VEHICLE_INTERACT_TYPE_IN -> { - ((EntityVehicle) vehicle).getVehicleMembers().add(vehicleMember); - } - case VEHICLE_INTERACT_TYPE_OUT -> { - ((EntityVehicle) vehicle).getVehicleMembers().remove(vehicleMember); - } - default -> {} - } - } - this.setData(proto.build()); - } - - public PacketVehicleInteractRsp( - EntityVehicle vehicle, VehicleMember vehicleMember, VehicleInteractType interactType) { - super(PacketOpcodes.VehicleInteractRsp); - VehicleInteractRsp.Builder proto = VehicleInteractRsp.newBuilder(); - - if (vehicle != null) { - proto.setEntityId(vehicle.getId()); - proto.setInteractType(interactType); - proto.setMember(vehicleMember); - - switch (interactType) { - case VEHICLE_INTERACT_TYPE_IN -> { - vehicle.getVehicleMembers().add(vehicleMember); - } - case VEHICLE_INTERACT_TYPE_OUT -> { - vehicle.getVehicleMembers().remove(vehicleMember); - } - default -> {} - } - } - this.setData(proto.build()); - } -} +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.entity.EntityVehicle; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.VehicleInteractRspOuterClass.VehicleInteractRsp; +import emu.grasscutter.net.proto.VehicleInteractTypeOuterClass.VehicleInteractType; +import emu.grasscutter.net.proto.VehicleMemberOuterClass.VehicleMember; + +public class PacketVehicleInteractRsp extends BasePacket { + + public PacketVehicleInteractRsp(Player player, int entityId, VehicleInteractType interactType) { + super(PacketOpcodes.VehicleInteractRsp); + VehicleInteractRsp.Builder proto = VehicleInteractRsp.newBuilder(); + + GameEntity vehicle = player.getScene().getEntityById(entityId); + + if (vehicle instanceof EntityVehicle) { + proto.setEntityId(vehicle.getId()); + + VehicleMember vehicleMember = + VehicleMember.newBuilder() + .setUid(player.getUid()) + .setAvatarGuid(player.getTeamManager().getCurrentCharacterGuid()) + .build(); + + proto.setInteractType(interactType); + proto.setMember(vehicleMember); + + switch (interactType) { + case VEHICLE_INTERACT_TYPE_IN -> { + ((EntityVehicle) vehicle).getVehicleMembers().add(vehicleMember); + player + .getQuestManager() + .queueEvent( + QuestContent.QUEST_CONTENT_ENTER_VEHICLE, + ((EntityVehicle) vehicle).getGadgetId()); + } + case VEHICLE_INTERACT_TYPE_OUT -> { + ((EntityVehicle) vehicle).getVehicleMembers().remove(vehicleMember); + } + default -> {} + } + } + this.setData(proto.build()); + } + + public PacketVehicleInteractRsp( + EntityVehicle vehicle, VehicleMember vehicleMember, VehicleInteractType interactType) { + super(PacketOpcodes.VehicleInteractRsp); + VehicleInteractRsp.Builder proto = VehicleInteractRsp.newBuilder(); + + if (vehicle != null) { + proto.setEntityId(vehicle.getId()); + proto.setInteractType(interactType); + proto.setMember(vehicleMember); + + switch (interactType) { + case VEHICLE_INTERACT_TYPE_IN -> { + vehicle.getVehicleMembers().add(vehicleMember); + } + case VEHICLE_INTERACT_TYPE_OUT -> { + vehicle.getVehicleMembers().remove(vehicleMember); + } + default -> {} + } + } + this.setData(proto.build()); + } +} diff --git a/src/main/java/emu/grasscutter/server/scheduler/ServerTask.java b/src/main/java/emu/grasscutter/server/scheduler/ServerTask.java index 95fdda484..a6b5c2053 100644 --- a/src/main/java/emu/grasscutter/server/scheduler/ServerTask.java +++ b/src/main/java/emu/grasscutter/server/scheduler/ServerTask.java @@ -1,62 +1,66 @@ -package emu.grasscutter.server.scheduler; - -import emu.grasscutter.Grasscutter; -import lombok.Getter; - -/** This class works the same as a runnable, except with more information. */ -public final class ServerTask implements Runnable { - /* The runnable to run. */ - private final Runnable runnable; - /* This ID is assigned by the scheduler. */ - @Getter private final int taskId; - /* The period at which the task should be run. */ - /* The delay between the first execute. */ - private final int period, delay; - /* The amount of times the task has been run. */ - @Getter private int ticks = 0; - /* Should the check consider delay? */ - private boolean considerDelay = true; - - public ServerTask(Runnable runnable, int taskId, int period, int delay) { - this.runnable = runnable; - this.taskId = taskId; - this.period = period; - this.delay = delay; - } - - /** Cancels the task from running the next time. */ - public void cancel() { - Grasscutter.getGameServer().getScheduler().cancelTask(this.taskId); - } - - /** - * Checks if the task should run at the current tick. - * - * @return True if the task should run, false otherwise. - */ - public boolean shouldRun() { - if (this.delay != -1 && this.considerDelay) { - this.considerDelay = false; - return this.ticks == this.delay; - } else if (this.period != -1) return this.ticks % this.period == 0; - else return true; - } - - /** - * Checks if the task should be canceled. - * - * @return True if the task should be canceled, false otherwise. - */ - public boolean shouldCancel() { - return this.period == -1; - } - - /** Runs the task. */ - @Override - public void run() { - // Run the runnable. - this.runnable.run(); - // Increase tick count. - this.ticks++; - } -} +package emu.grasscutter.server.scheduler; + +import emu.grasscutter.Grasscutter; +import lombok.Getter; + +/** This class works the same as a runnable, except with more information. */ +public final class ServerTask implements Runnable { + /* The runnable to run. */ + private final Runnable runnable; + /* This ID is assigned by the scheduler. */ + @Getter private final int taskId; + /* The period at which the task should be run. */ + /* The delay between the first execute. */ + private final int period, delay; + /* The amount of times the task has been run. */ + @Getter private int ticks = 0; + /* Should the check consider delay? */ + private boolean considerDelay = true; + + public ServerTask(Runnable runnable, int taskId, int period, int delay) { + this.runnable = runnable; + this.taskId = taskId; + this.period = period; + this.delay = delay; + } + + /** Cancels the task from running the next time. */ + public void cancel() { + Grasscutter.getGameServer().getScheduler().cancelTask(this.taskId); + } + + /** + * Checks if the task should run at the current tick. + * + * @return True if the task should run, false otherwise. + */ + public boolean shouldRun() { + // Increase tick count. + var ticks = this.ticks++; + if (this.delay != -1 && this.considerDelay) { + this.considerDelay = false; + return ticks == this.delay; + } else if (this.period != -1) return ticks % this.period == 0; + else return true; + } + + /** + * Checks if the task should be canceled. + * + * @return True if the task should be canceled, false otherwise. + */ + public boolean shouldCancel() { + return this.period == -1 && ticks >= delay; + } + + /** Runs the task. */ + @Override + public void run() { + // Run the runnable. + try { + this.runnable.run(); + } catch (Exception ex) { + Grasscutter.getLogger().error("Exception during task: ", ex); + } + } +} diff --git a/src/main/java/emu/grasscutter/task/TaskHandler.java b/src/main/java/emu/grasscutter/task/TaskHandler.java index 6ff2a9327..a340722fb 100644 --- a/src/main/java/emu/grasscutter/task/TaskHandler.java +++ b/src/main/java/emu/grasscutter/task/TaskHandler.java @@ -1,16 +1,14 @@ -package emu.grasscutter.task; - -import org.quartz.Job; -import org.quartz.JobExecutionException; -import org.quartz.PersistJobDataAfterExecution; - -@PersistJobDataAfterExecution -public abstract class TaskHandler implements Job { - public void restartExecute() throws JobExecutionException { - execute(null); - } - - public abstract void onEnable(); - - public abstract void onDisable(); -} +package emu.grasscutter.task; + +import org.quartz.*; + +@PersistJobDataAfterExecution +public abstract class TaskHandler implements Job { + public void restartExecute() throws JobExecutionException { + execute(null); + } + + public abstract void onEnable(); + + public abstract void onDisable(); +} diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index c1e1a49eb..9000e8410 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -1,391 +1,394 @@ -package emu.grasscutter.tools; - -import static emu.grasscutter.utils.FileUtils.getResourcePath; -import static emu.grasscutter.utils.Language.getTextMapKey; - -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.command.CommandHandler; -import emu.grasscutter.command.CommandMap; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.ResourceLoader; -import emu.grasscutter.data.excels.ItemData; -import emu.grasscutter.data.excels.achievement.AchievementData; -import emu.grasscutter.data.excels.avatar.AvatarData; -import emu.grasscutter.game.inventory.MaterialType; -import emu.grasscutter.utils.Language; -import emu.grasscutter.utils.Language.TextStrings; -import it.unimi.dsi.fastutil.ints.Int2IntRBTreeMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.LongStream; -import lombok.val; - -public final class Tools { - /** - * This generates the GM handbooks with a message by default. - * - * @throws Exception If an error occurs while generating the handbooks. - */ - public static void createGmHandbooks() throws Exception { - Tools.createGmHandbooks(true); - } - - /** - * Generates a GM handbook for each language. - * - * @param message Should a message be printed to the console? - * @throws Exception If an error occurs while generating the handbooks. - */ - public static void createGmHandbooks(boolean message) throws Exception { - val languages = Language.TextStrings.getLanguages(); - - ResourceLoader.loadAll(); - val mainQuestTitles = - new Int2IntRBTreeMap( - GameData.getMainQuestDataMap().int2ObjectEntrySet().stream() - .collect( - Collectors.toMap( - e -> e.getIntKey(), e -> (int) e.getValue().getTitleTextMapHash()))); - // val questDescs = new - // Int2IntRBTreeMap(GameData.getQuestDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getDescTextMapHash()))); - - val avatarDataMap = new Int2ObjectRBTreeMap<>(GameData.getAvatarDataMap()); - val itemDataMap = new Int2ObjectRBTreeMap<>(GameData.getItemDataMap()); - val monsterDataMap = new Int2ObjectRBTreeMap<>(GameData.getMonsterDataMap()); - val sceneDataMap = new Int2ObjectRBTreeMap<>(GameData.getSceneDataMap()); - val questDataMap = new Int2ObjectRBTreeMap<>(GameData.getQuestDataMap()); - val achievementDataMap = new Int2ObjectRBTreeMap<>(GameData.getAchievementDataMap()); - - Function, String> getPad = m -> "%" + m.lastKey().toString().length() + "s : "; - - // Create builders and helper functions - val handbookBuilders = - IntStream.range(0, TextStrings.NUM_LANGUAGES).mapToObj(i -> new StringBuilder()).toList(); - var h = - new Object() { - void newLine(String line) { - handbookBuilders.forEach(b -> b.append(line + "\n")); - } - - void newSection(String title) { - newLine("\n\n// " + title); - } - - void newTranslatedLine(String template, TextStrings... textstrings) { - for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { - String s = template; - for (int j = 0; j < textstrings.length; j++) - s = s.replace("{" + j + "}", textstrings[j].strings[i]); - handbookBuilders.get(i).append(s + "\n"); - } - } - - void newTranslatedLine(String template, long... hashes) { - newTranslatedLine( - template, - LongStream.of(hashes) - .mapToObj(hash -> getTextMapKey(hash)) - .toArray(TextStrings[]::new)); - } - }; - - // Preamble - h.newLine("// Grasscutter " + GameConstants.VERSION + " GM Handbook"); - h.newLine( - "// Created " - + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now())); - - // Commands - h.newSection("Commands"); - final List cmdList = CommandMap.getInstance().getHandlersAsList(); - final String padCmdLabel = - "%" - + cmdList.stream() - .map(CommandHandler::getLabel) - .map(String::length) - .max(Integer::compare) - .get() - + "s : "; - for (CommandHandler cmd : cmdList) { - final String label = padCmdLabel.formatted(cmd.getLabel()); - final String descKey = cmd.getDescriptionKey(); - for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { - String desc = - languages.get(i).get(descKey).replace("\n", "\n\t\t\t\t").replace("\t", " "); - handbookBuilders.get(i).append(label + desc + "\n"); - } - } - // Avatars - h.newSection("Avatars"); - val avatarPre = getPad.apply(avatarDataMap); - avatarDataMap.forEach( - (id, data) -> - h.newTranslatedLine(avatarPre.formatted(id) + "{0}", data.getNameTextMapHash())); - // Items - h.newSection("Items"); - val itemPre = getPad.apply(itemDataMap); - itemDataMap.forEach( - (id, data) -> { - val name = getTextMapKey(data.getNameTextMapHash()); - if (Objects.requireNonNull(data.getMaterialType()) == MaterialType.MATERIAL_BGM) { - val bgmName = - Optional.ofNullable(data.getItemUse()) - .map(u -> u.get(0)) - .map(u -> u.getUseParam()) - .filter(u -> u.length > 0) - .map(u -> Integer.parseInt(u[0])) - .map(bgmId -> GameData.getHomeWorldBgmDataMap().get(bgmId)) - .map(bgm -> bgm.getBgmNameTextMapHash()) - .map(hash -> getTextMapKey(hash)); - if (bgmName.isPresent()) { - h.newTranslatedLine(itemPre.formatted(id) + "{0} - {1}", name, bgmName.get()); - return; - } // Fall-through - } - h.newTranslatedLine(itemPre.formatted(id) + "{0}", name); - }); - // Monsters - h.newSection("Monsters"); - val monsterPre = getPad.apply(monsterDataMap); - monsterDataMap.forEach( - (id, data) -> - h.newTranslatedLine( - monsterPre.formatted(id) + data.getMonsterName() + " - {0}", - data.getNameTextMapHash())); - // Scenes - no translations - h.newSection("Scenes"); - val padSceneId = getPad.apply(sceneDataMap); - sceneDataMap.forEach((id, data) -> h.newLine(padSceneId.formatted(id) + data.getScriptData())); - // Quests - h.newSection("Quests"); - val padQuestId = getPad.apply(questDataMap); - questDataMap.forEach( - (id, data) -> - h.newTranslatedLine( - padQuestId.formatted(id) + "{0} - {1}", - mainQuestTitles.get(data.getMainId()), - data.getDescTextMapHash())); - // Achievements - h.newSection("Achievements"); - val padAchievementId = getPad.apply(achievementDataMap); - achievementDataMap.values().stream() - .filter(AchievementData::isUsed) - .forEach( - data -> { - h.newTranslatedLine( - padAchievementId.formatted(data.getId()) + "{0} - {1}", - data.getTitleTextMapHash(), - data.getDescTextMapHash()); - }); - - // Write txt files - for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { - File GMHandbookOutputpath = new File("./GM Handbook"); - GMHandbookOutputpath.mkdir(); - final String fileName = - "./GM Handbook/GM Handbook - %s.txt".formatted(TextStrings.ARR_LANGUAGES[i]); - try (PrintWriter writer = - new PrintWriter( - new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8), - false)) { - writer.write(handbookBuilders.get(i).toString()); - } - } - - if (message) Grasscutter.getLogger().info("GM Handbooks generated!"); - } - - public static List createGachaMappingJsons() { - final int NUM_LANGUAGES = Language.TextStrings.NUM_LANGUAGES; - final Language.TextStrings CHARACTER = Language.getTextMapKey(4233146695L); // "Character" in EN - final Language.TextStrings WEAPON = Language.getTextMapKey(4231343903L); // "Weapon" in EN - final Language.TextStrings STANDARD_WISH = - Language.getTextMapKey(332935371L); // "Standard Wish" in EN - final Language.TextStrings CHARACTER_EVENT_WISH = - Language.getTextMapKey(2272170627L); // "Character Event Wish" in EN - final Language.TextStrings CHARACTER_EVENT_WISH_2 = - Language.getTextMapKey(3352513147L); // "Character Event Wish-2" in EN - final Language.TextStrings WEAPON_EVENT_WISH = - Language.getTextMapKey(2864268523L); // "Weapon Event Wish" in EN - final List sbs = new ArrayList<>(NUM_LANGUAGES); - for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) - sbs.add(new StringBuilder("{\n")); // Web requests should never need Windows line endings - - // Avatars - GameData.getAvatarDataMap() - .keySet() - .intStream() - .sorted() - .forEach( - id -> { - AvatarData data = GameData.getAvatarDataMap().get(id); - int avatarID = data.getId(); - if (avatarID >= 11000000) { // skip test avatar - return; - } - String color = - switch (data.getQualityType()) { - case "QUALITY_PURPLE" -> "purple"; - case "QUALITY_ORANGE" -> "yellow"; - case "QUALITY_BLUE" -> "blue"; - default -> ""; - }; - Language.TextStrings avatarName = Language.getTextMapKey(data.getNameTextMapHash()); - for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) { - sbs.get(langIdx) - .append("\t\"") - .append(avatarID % 1000 + 1000) - .append("\": [\"") - .append(avatarName.get(langIdx)) - .append(" (") - .append(CHARACTER.get(langIdx)) - .append(")\", \"") - .append(color) - .append("\"],\n"); - } - }); - - // Weapons - GameData.getItemDataMap() - .keySet() - .intStream() - .sorted() - .forEach( - id -> { - ItemData data = GameData.getItemDataMap().get(id); - if (data.getId() <= 11101 || data.getId() >= 20000) { - return; // skip non weapon items - } - String color = - switch (data.getRankLevel()) { - case 3 -> "blue"; - case 4 -> "purple"; - case 5 -> "yellow"; - default -> null; - }; - if (color == null) return; // skip unnecessary entries - Language.TextStrings weaponName = Language.getTextMapKey(data.getNameTextMapHash()); - for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) { - sbs.get(langIdx) - .append("\t\"") - .append(data.getId()) - .append("\": [\"") - .append(weaponName.get(langIdx).replaceAll("\"", "\\\\\"")) - .append(" (") - .append(WEAPON.get(langIdx)) - .append(")\", \"") - .append(color) - .append("\"],\n"); - } - }); - - for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) { - sbs.get(langIdx) - .append("\t\"200\": \"") - .append(STANDARD_WISH.get(langIdx)) - .append("\",\n\t\"301\": \"") - .append(CHARACTER_EVENT_WISH.get(langIdx)) - .append("\",\n\t\"400\": \"") - .append(CHARACTER_EVENT_WISH_2.get(langIdx)) - .append("\",\n\t\"302\": \"") - .append(WEAPON_EVENT_WISH.get(langIdx)) - .append("\"\n}"); - } - return sbs.stream().map(StringBuilder::toString).toList(); - } - - public static void createGachaMappings(Path location) throws IOException { - ResourceLoader.loadResources(); - List jsons = createGachaMappingJsons(); - var usedLocales = new HashSet(); - StringBuilder sb = new StringBuilder("mappings = {\n"); - for (int i = 0; i < Language.TextStrings.NUM_LANGUAGES; i++) { - String locale = - Language.TextStrings.ARR_GC_LANGUAGES[i] - .toLowerCase(); // TODO: change the templates to not use lowercased locale codes - if (usedLocales.add( - locale)) { // Some locales fallback to en-us, we don't want to redefine en-us with - // vietnamese strings - sb.append("\t\"%s\": ".formatted(locale)); - sb.append(jsons.get(i).replace("\n", "\n\t") + ",\n"); - } - } - sb.setLength(sb.length() - 2); // Delete trailing ",\n" - sb.append("\n}"); - - Files.createDirectories(location.getParent()); - Files.writeString(location, sb); - Grasscutter.getLogger().debug("Mappings generated to " + location); - } - - public static List getAvailableLanguage() { - List availableLangList = new ArrayList<>(); - try { - Files.newDirectoryStream(getResourcePath("TextMap"), "TextMap*.json") - .forEach( - path -> { - availableLangList.add( - path.getFileName() - .toString() - .replace("TextMap", "") - .replace(".json", "") - .toLowerCase()); - }); - } catch (IOException e) { - Grasscutter.getLogger().error("Failed to get available languages:", e); - } - return availableLangList; - } - - @Deprecated(forRemoval = true, since = "1.2.3") - public static String getLanguageOption() { - List availableLangList = getAvailableLanguage(); - - // Use system out for better format - if (availableLangList.size() == 1) { - return availableLangList.get(0).toUpperCase(); - } - StringBuilder stagedMessage = new StringBuilder(); - stagedMessage.append( - "The following languages mappings are available, please select one: [default: EN] \n"); - - StringBuilder groupedLangList = new StringBuilder(">\t"); - String input; - int groupedLangCount = 0; - - for (String availableLanguage : availableLangList) { - groupedLangCount++; - groupedLangList.append(availableLanguage).append("\t"); - - if (groupedLangCount == 6) { - stagedMessage.append(groupedLangList).append("\n"); - groupedLangCount = 0; - groupedLangList = new StringBuilder(">\t"); - } - } - - if (groupedLangCount > 0) { - stagedMessage.append(groupedLangList).append("\n"); - } - - stagedMessage.append("\nYour choice: [EN] "); - - input = Grasscutter.getConsole().readLine(stagedMessage.toString()); - if (availableLangList.contains(input.toLowerCase())) { - return input.toUpperCase(); - } - - Grasscutter.getLogger().info("Invalid option. Will use EN (English) as fallback."); - return "EN"; - } -} +package emu.grasscutter.tools; + +import static emu.grasscutter.utils.FileUtils.getResourcePath; +import static emu.grasscutter.utils.Language.getTextMapKey; + +import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.command.CommandMap; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.ResourceLoader; +import emu.grasscutter.data.excels.ItemData; +import emu.grasscutter.data.excels.achievement.AchievementData; +import emu.grasscutter.data.excels.avatar.AvatarData; +import emu.grasscutter.game.inventory.MaterialType; +import emu.grasscutter.utils.Language; +import emu.grasscutter.utils.Language.TextStrings; +import it.unimi.dsi.fastutil.ints.Int2IntRBTreeMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import lombok.val; + +public final class Tools { + /** + * This generates the GM handbooks with a message by default. + * + * @throws Exception If an error occurs while generating the handbooks. + */ + public static void createGmHandbooks() throws Exception { + Tools.createGmHandbooks(true); + } + + /** + * Generates a GM handbook for each language. + * + * @param message Should a message be printed to the console? + * @throws Exception If an error occurs while generating the handbooks. + */ + public static void createGmHandbooks(boolean message) throws Exception { + val languages = Language.TextStrings.getLanguages(); + + ResourceLoader.loadAll(); + val mainQuestTitles = + new Int2IntRBTreeMap( + GameData.getMainQuestDataMap().int2ObjectEntrySet().stream() + .collect( + Collectors.toMap( + e -> e.getIntKey(), e -> (int) e.getValue().getTitleTextMapHash()))); + // val questDescs = new + // Int2IntRBTreeMap(GameData.getQuestDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getDescTextMapHash()))); + + val avatarDataMap = new Int2ObjectRBTreeMap<>(GameData.getAvatarDataMap()); + val itemDataMap = new Int2ObjectRBTreeMap<>(GameData.getItemDataMap()); + val monsterDataMap = new Int2ObjectRBTreeMap<>(GameData.getMonsterDataMap()); + val sceneDataMap = new Int2ObjectRBTreeMap<>(GameData.getSceneDataMap()); + val questDataMap = new Int2ObjectRBTreeMap<>(GameData.getQuestDataMap()); + val achievementDataMap = new Int2ObjectRBTreeMap<>(GameData.getAchievementDataMap()); + + Function, String> getPad = m -> "%" + m.lastKey().toString().length() + "s : "; + + // Create builders and helper functions + val handbookBuilders = + IntStream.range(0, TextStrings.NUM_LANGUAGES).mapToObj(i -> new StringBuilder()).toList(); + var h = + new Object() { + void newLine(String line) { + handbookBuilders.forEach(b -> b.append(line + "\n")); + } + + void newSection(String title) { + newLine("\n\n// " + title); + } + + void newTranslatedLine(String template, TextStrings... textstrings) { + for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { + String s = template; + for (int j = 0; j < textstrings.length; j++) + s = s.replace("{" + j + "}", textstrings[j].strings[i]); + handbookBuilders.get(i).append(s + "\n"); + } + } + + void newTranslatedLine(String template, long... hashes) { + newTranslatedLine( + template, + LongStream.of(hashes) + .mapToObj(hash -> getTextMapKey(hash)) + .toArray(TextStrings[]::new)); + } + }; + + // Preamble + h.newLine("// Grasscutter " + GameConstants.VERSION + " GM Handbook"); + h.newLine( + "// Created " + + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now())); + + // Commands + h.newSection("Commands"); + final List cmdList = CommandMap.getInstance().getHandlersAsList(); + final String padCmdLabel = + "%" + + cmdList.stream() + .map(CommandHandler::getLabel) + .map(String::length) + .max(Integer::compare) + .get() + + "s : "; + for (CommandHandler cmd : cmdList) { + final String label = padCmdLabel.formatted(cmd.getLabel()); + final String descKey = cmd.getDescriptionKey(); + for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { + String desc = + languages.get(i).get(descKey).replace("\n", "\n\t\t\t\t").replace("\t", " "); + handbookBuilders.get(i).append(label + desc + "\n"); + } + } + // Avatars + h.newSection("Avatars"); + val avatarPre = getPad.apply(avatarDataMap); + avatarDataMap.forEach( + (id, data) -> + h.newTranslatedLine(avatarPre.formatted(id) + "{0}", data.getNameTextMapHash())); + // Items + h.newSection("Items"); + val itemPre = getPad.apply(itemDataMap); + itemDataMap.forEach( + (id, data) -> { + val name = getTextMapKey(data.getNameTextMapHash()); + switch (data.getMaterialType()) { + case MATERIAL_BGM: + val bgmName = + Optional.ofNullable(data.getItemUse()) + .map(u -> u.get(0)) + .map(u -> u.getUseParam()) + .filter(u -> u.length > 0) + .map(u -> Integer.parseInt(u[0])) + .map(bgmId -> GameData.getHomeWorldBgmDataMap().get(bgmId)) + .map(bgm -> bgm.getBgmNameTextMapHash()) + .map(hash -> getTextMapKey(hash)); + if (bgmName.isPresent()) { + h.newTranslatedLine(itemPre.formatted(id) + "{0} - {1}", name, bgmName.get()); + return; + } // Fall-through + default: + h.newTranslatedLine(itemPre.formatted(id) + "{0}", name); + return; + } + }); + // Monsters + h.newSection("Monsters"); + val monsterPre = getPad.apply(monsterDataMap); + monsterDataMap.forEach( + (id, data) -> + h.newTranslatedLine( + monsterPre.formatted(id) + data.getMonsterName() + " - {0}", + data.getNameTextMapHash())); + // Scenes - no translations + h.newSection("Scenes"); + val padSceneId = getPad.apply(sceneDataMap); + sceneDataMap.forEach((id, data) -> h.newLine(padSceneId.formatted(id) + data.getScriptData())); + // Quests + h.newSection("Quests"); + val padQuestId = getPad.apply(questDataMap); + questDataMap.forEach( + (id, data) -> + h.newTranslatedLine( + padQuestId.formatted(id) + "{0} - {1}", + mainQuestTitles.get(data.getMainId()), + data.getDescTextMapHash())); + // Achievements + h.newSection("Achievements"); + val padAchievementId = getPad.apply(achievementDataMap); + achievementDataMap.values().stream() + .filter(AchievementData::isUsed) + .forEach( + data -> { + h.newTranslatedLine( + padAchievementId.formatted(data.getId()) + "{0} - {1}", + data.getTitleTextMapHash(), + data.getDescTextMapHash()); + }); + + // Write txt files + for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { + File GMHandbookOutputpath = new File("./GM Handbook"); + GMHandbookOutputpath.mkdir(); + final String fileName = + "./GM Handbook/GM Handbook - %s.txt".formatted(TextStrings.ARR_LANGUAGES[i]); + try (PrintWriter writer = + new PrintWriter( + new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8), + false)) { + writer.write(handbookBuilders.get(i).toString()); + } + } + + if (message) Grasscutter.getLogger().info("GM Handbooks generated!"); + } + + public static List createGachaMappingJsons() { + final int NUM_LANGUAGES = Language.TextStrings.NUM_LANGUAGES; + final Language.TextStrings CHARACTER = Language.getTextMapKey(4233146695L); // "Character" in EN + final Language.TextStrings WEAPON = Language.getTextMapKey(4231343903L); // "Weapon" in EN + final Language.TextStrings STANDARD_WISH = + Language.getTextMapKey(332935371L); // "Standard Wish" in EN + final Language.TextStrings CHARACTER_EVENT_WISH = + Language.getTextMapKey(2272170627L); // "Character Event Wish" in EN + final Language.TextStrings CHARACTER_EVENT_WISH_2 = + Language.getTextMapKey(3352513147L); // "Character Event Wish-2" in EN + final Language.TextStrings WEAPON_EVENT_WISH = + Language.getTextMapKey(2864268523L); // "Weapon Event Wish" in EN + final List sbs = new ArrayList<>(NUM_LANGUAGES); + for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) + sbs.add(new StringBuilder("{\n")); // Web requests should never need Windows line endings + + // Avatars + GameData.getAvatarDataMap() + .keySet() + .intStream() + .sorted() + .forEach( + id -> { + AvatarData data = GameData.getAvatarDataMap().get(id); + int avatarID = data.getId(); + if (avatarID >= 11000000) { // skip test avatar + return; + } + String color = + switch (data.getQualityType()) { + case "QUALITY_PURPLE" -> "purple"; + case "QUALITY_ORANGE" -> "yellow"; + case "QUALITY_BLUE" -> "blue"; + default -> ""; + }; + Language.TextStrings avatarName = Language.getTextMapKey(data.getNameTextMapHash()); + for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) { + sbs.get(langIdx) + .append("\t\"") + .append(avatarID % 1000 + 1000) + .append("\": [\"") + .append(avatarName.get(langIdx)) + .append(" (") + .append(CHARACTER.get(langIdx)) + .append(")\", \"") + .append(color) + .append("\"],\n"); + } + }); + + // Weapons + GameData.getItemDataMap() + .keySet() + .intStream() + .sorted() + .forEach( + id -> { + ItemData data = GameData.getItemDataMap().get(id); + if (data.getId() <= 11101 || data.getId() >= 20000) { + return; // skip non weapon items + } + String color = + switch (data.getRankLevel()) { + case 3 -> "blue"; + case 4 -> "purple"; + case 5 -> "yellow"; + default -> null; + }; + if (color == null) return; // skip unnecessary entries + Language.TextStrings weaponName = Language.getTextMapKey(data.getNameTextMapHash()); + for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) { + sbs.get(langIdx) + .append("\t\"") + .append(data.getId()) + .append("\": [\"") + .append(weaponName.get(langIdx).replaceAll("\"", "\\\\\"")) + .append(" (") + .append(WEAPON.get(langIdx)) + .append(")\", \"") + .append(color) + .append("\"],\n"); + } + }); + + for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) { + sbs.get(langIdx) + .append("\t\"200\": \"") + .append(STANDARD_WISH.get(langIdx)) + .append("\",\n\t\"301\": \"") + .append(CHARACTER_EVENT_WISH.get(langIdx)) + .append("\",\n\t\"400\": \"") + .append(CHARACTER_EVENT_WISH_2.get(langIdx)) + .append("\",\n\t\"302\": \"") + .append(WEAPON_EVENT_WISH.get(langIdx)) + .append("\"\n}"); + } + return sbs.stream().map(StringBuilder::toString).toList(); + } + + public static void createGachaMappings(Path location) throws IOException { + ResourceLoader.loadResources(); + List jsons = createGachaMappingJsons(); + var usedLocales = new HashSet(); + StringBuilder sb = new StringBuilder("mappings = {\n"); + for (int i = 0; i < Language.TextStrings.NUM_LANGUAGES; i++) { + String locale = + Language.TextStrings.ARR_GC_LANGUAGES[i] + .toLowerCase(); // TODO: change the templates to not use lowercased locale codes + if (usedLocales.add( + locale)) { // Some locales fallback to en-us, we don't want to redefine en-us with + // vietnamese strings + sb.append("\t\"%s\": ".formatted(locale)); + sb.append(jsons.get(i).replace("\n", "\n\t") + ",\n"); + } + } + sb.setLength(sb.length() - 2); // Delete trailing ",\n" + sb.append("\n}"); + + Files.createDirectories(location.getParent()); + Files.writeString(location, sb); + Grasscutter.getLogger().debug("Mappings generated to " + location); + } + + public static List getAvailableLanguage() { + List availableLangList = new ArrayList<>(); + try { + Files.newDirectoryStream(getResourcePath("TextMap"), "TextMap*.json") + .forEach( + path -> { + availableLangList.add( + path.getFileName() + .toString() + .replace("TextMap", "") + .replace(".json", "") + .toLowerCase()); + }); + } catch (IOException e) { + Grasscutter.getLogger().error("Failed to get available languages:", e); + } + return availableLangList; + } + + @Deprecated(forRemoval = true, since = "1.2.3") + public static String getLanguageOption() { + List availableLangList = getAvailableLanguage(); + + // Use system out for better format + if (availableLangList.size() == 1) { + return availableLangList.get(0).toUpperCase(); + } + StringBuilder stagedMessage = new StringBuilder(); + stagedMessage.append( + "The following languages mappings are available, please select one: [default: EN] \n"); + + StringBuilder groupedLangList = new StringBuilder(">\t"); + String input; + int groupedLangCount = 0; + + for (String availableLanguage : availableLangList) { + groupedLangCount++; + groupedLangList.append(availableLanguage).append("\t"); + + if (groupedLangCount == 6) { + stagedMessage.append(groupedLangList).append("\n"); + groupedLangCount = 0; + groupedLangList = new StringBuilder(">\t"); + } + } + + if (groupedLangCount > 0) { + stagedMessage.append(groupedLangList).append("\n"); + } + + stagedMessage.append("\nYour choice: [EN] "); + + input = Grasscutter.getConsole().readLine(stagedMessage.toString()); + if (availableLangList.contains(input.toLowerCase())) { + return input.toUpperCase(); + } + + Grasscutter.getLogger().info("Invalid option. Will use EN (English) as fallback."); + return "EN"; + } +}