Continue merging quests (pt. 2)

This commit is contained in:
KingRainbow44 2023-04-09 14:35:45 -04:00
parent 97ee71bcf4
commit 644f1b3ab9
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
31 changed files with 4392 additions and 3984 deletions

View File

@ -1,57 +1,59 @@
package emu.grasscutter.command; package emu.grasscutter.command;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
public class CommandHelpers { public class CommandHelpers {
public static final Pattern lvlRegex = public static final Pattern lvlRegex =
Pattern.compile("(?<!\\w)l(?:vl?)?(\\d+)"); // Java doesn't have raw string literals :( Pattern.compile("(?<!\\w)l(?:vl?)?(\\d+)"); // Java doesn't have raw string literals :(
public static final Pattern amountRegex = public static final Pattern amountRegex =
Pattern.compile("((?<=(?<!\\w)x)\\d+|\\d+(?=x)(?!x\\d))"); Pattern.compile("((?<=(?<!\\w)x)\\d+|\\d+(?=x)(?!x\\d))");
public static final Pattern refineRegex = Pattern.compile("(?<!\\w)r(\\d+)"); public static final Pattern refineRegex = Pattern.compile("(?<!\\w)r(\\d+)");
public static final Pattern rankRegex = Pattern.compile("(\\d+)\\*"); public static final Pattern rankRegex = Pattern.compile("(\\d+)\\*");
public static final Pattern constellationRegex = Pattern.compile("(?<!\\w)c(\\d+)"); public static final Pattern constellationRegex = Pattern.compile("(?<!\\w)c(\\d+)");
public static final Pattern skillLevelRegex = Pattern.compile("sl(\\d+)"); public static final Pattern skillLevelRegex = Pattern.compile("sl(\\d+)");
public static final Pattern stateRegex = Pattern.compile("state(\\d+)"); public static final Pattern stateRegex = Pattern.compile("state(\\d+)");
public static final Pattern blockRegex = Pattern.compile("blk(\\d+)"); public static final Pattern blockRegex = Pattern.compile("blk(\\d+)");
public static final Pattern groupRegex = Pattern.compile("grp(\\d+)"); public static final Pattern groupRegex = Pattern.compile("grp(\\d+)");
public static final Pattern configRegex = Pattern.compile("cfg(\\d+)"); public static final Pattern configRegex = Pattern.compile("cfg(\\d+)");
public static final Pattern hpRegex = Pattern.compile("(?<!\\w)hp(\\d+)"); public static final Pattern hpRegex = Pattern.compile("(?<!\\w)hp(\\d+)");
public static final Pattern maxHPRegex = Pattern.compile("maxhp(\\d+)"); public static final Pattern maxHPRegex = Pattern.compile("maxhp(\\d+)");
public static final Pattern atkRegex = Pattern.compile("atk(\\d+)"); public static final Pattern atkRegex = Pattern.compile("atk(\\d+)");
public static final Pattern defRegex = Pattern.compile("def(\\d+)"); public static final Pattern defRegex = Pattern.compile("def(\\d+)");
public static final Pattern aiRegex = Pattern.compile("ai(\\d+)"); public static final Pattern aiRegex = Pattern.compile("ai(\\d+)");
public static final Pattern sceneRegex = Pattern.compile("scene(\\d+)");
public static int matchIntOrNeg(Pattern pattern, String arg) { public static final Pattern suiteRegex = Pattern.compile("suite(\\d+)");
Matcher match = pattern.matcher(arg);
if (match.find()) { public static int matchIntOrNeg(Pattern pattern, String arg) {
return Integer.parseInt( Matcher match = pattern.matcher(arg);
match.group( if (match.find()) {
1)); // This should be exception-safe as only \d+ can be passed to it (i.e. non-empty return Integer.parseInt(
// string of pure digits) match.group(
} 1)); // This should be exception-safe as only \d+ can be passed to it (i.e. non-empty
return -1; // string of pure digits)
} }
return -1;
public static <T> List<String> parseIntParameters( }
List<String> args, @Nonnull T params, Map<Pattern, BiConsumer<T, Integer>> map) {
args.removeIf( public static <T> List<String> parseIntParameters(
arg -> { List<String> args, @Nonnull T params, Map<Pattern, BiConsumer<T, Integer>> map) {
var argL = arg.toLowerCase(); args.removeIf(
boolean deleteArg = false; arg -> {
for (var entry : map.entrySet()) { var argL = arg.toLowerCase();
int argNum = matchIntOrNeg(entry.getKey(), argL); boolean deleteArg = false;
if (argNum != -1) { for (var entry : map.entrySet()) {
entry.getValue().accept(params, argNum); int argNum = matchIntOrNeg(entry.getKey(), argL);
deleteArg = true; if (argNum != -1) {
} entry.getValue().accept(params, argNum);
} deleteArg = true;
return deleteArg; }
}); }
return args; return deleteArg;
} });
} return args;
}
}

View File

@ -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 = {"[<cutsceneId>]"},
permission = "player.group",
permissionTargeted = "player.group.others")
public final class CutsceneCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (args.isEmpty()) {
sendUsageMessage(sender);
return;
}
val cutSceneId = Integer.parseInt(args.get(0));
targetPlayer.sendPacket(new PacketCutsceneBeginNotify(cutSceneId));
}
}

View File

@ -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 = {"[<audioname>] [<x><y><z>]"},
permission = "player.group",
permissionTargeted = "player.group.others")
public final class SoundCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> 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));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
package emu.grasscutter.game.dungeons.challenge.trigger; package emu.grasscutter.game.dungeons.challenge.trigger;
import emu.grasscutter.game.dungeons.challenge.WorldChallenge; import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
public class InTimeTrigger extends ChallengeTrigger { public class InTimeTrigger extends ChallengeTrigger {
@Override @Override
public void onCheckTimeout(WorldChallenge challenge) { public void onCheckTimeout(WorldChallenge challenge) {
var current = System.currentTimeMillis(); var current = challenge.getScene().getSceneTimeSeconds();
if (current - challenge.getStartedAt() > challenge.getTimeLimit() * 1000L) { if (current - challenge.getStartedAt() > challenge.getTimeLimit()) {
challenge.fail(); challenge.fail();
} }
} }
} }

View File

@ -15,6 +15,7 @@ import emu.grasscutter.game.activity.ActivityManager;
import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.avatar.AvatarStorage; import emu.grasscutter.game.avatar.AvatarStorage;
import emu.grasscutter.game.battlepass.BattlePassManager; import emu.grasscutter.game.battlepass.BattlePassManager;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.expedition.ExpeditionInfo; import emu.grasscutter.game.expedition.ExpeditionInfo;
import emu.grasscutter.game.friends.FriendsList; 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.PlayerProperty;
import emu.grasscutter.game.props.WatcherTriggerType; import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.game.quest.QuestManager; import emu.grasscutter.game.quest.QuestManager;
import emu.grasscutter.game.quest.enums.QuestCond;
import emu.grasscutter.game.quest.enums.QuestContent; import emu.grasscutter.game.quest.enums.QuestContent;
import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.shop.ShopLimit;
import emu.grasscutter.game.tower.TowerData; 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.Scene;
import emu.grasscutter.game.world.World; import emu.grasscutter.game.world.World;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.proto.*;
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult;
import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry;
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
import emu.grasscutter.net.proto.MpSettingTypeOuterClass.MpSettingType; import emu.grasscutter.net.proto.MpSettingTypeOuterClass.MpSettingType;
import emu.grasscutter.net.proto.OnlinePlayerInfoOuterClass.OnlinePlayerInfo; 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.PlayerLocationInfoOuterClass.PlayerLocationInfo;
import emu.grasscutter.net.proto.PlayerWorldLocationInfoOuterClass;
import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture;
import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; 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.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.scripts.data.SceneRegion;
import emu.grasscutter.server.event.player.PlayerJoinEvent; import emu.grasscutter.server.event.player.PlayerJoinEvent;
import emu.grasscutter.server.event.player.PlayerQuitEvent; import emu.grasscutter.server.event.player.PlayerQuitEvent;
@ -88,6 +88,7 @@ import java.time.ZoneId;
import java.util.*; import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Stream;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS; import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
@ -472,6 +473,8 @@ public class Player {
// Handle open state unlocks from level-up. // Handle open state unlocks from level-up.
this.getProgressManager().tryUnlockOpenStates(); 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; return true;
} }
@ -613,7 +616,7 @@ public class Player {
public void onEnterRegion(SceneRegion region) { public void onEnterRegion(SceneRegion region) {
getQuestManager().forEachActiveQuest(quest -> { 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 trigger hasn't been fired yet
if (!Boolean.TRUE.equals(quest.getTriggers().put("ENTER_REGION_" + region.config_id, true))) { if (!Boolean.TRUE.equals(quest.getTriggers().put("ENTER_REGION_" + region.config_id, true))) {
//getSession().send(new PacketServerCondMeetQuestListUpdateNotify()); //getSession().send(new PacketServerCondMeetQuestListUpdateNotify());
@ -824,6 +827,85 @@ public class Player {
addAvatar(avatar, true); addAvatar(avatar, true);
} }
public void addAvatar(int avatarId) {
// I dont see why we cant do this lolz
addAvatar(new Avatar(avatarId), true);
}
public List<Integer> 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<Integer> 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<Integer> 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) { public void addFlycloak(int flycloakId) {
this.getFlyCloakList().add(flycloakId); this.getFlyCloakList().add(flycloakId);
this.sendPacket(new PacketAvatarGainFlycloakNotify(flycloakId)); this.sendPacket(new PacketAvatarGainFlycloakNotify(flycloakId));
@ -834,6 +916,11 @@ public class Player {
this.sendPacket(new PacketAvatarGainCostumeNotify(costumeId)); 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) { public void addNameCard(int nameCardId) {
this.getNameCardList().add(nameCardId); this.getNameCardList().add(nameCardId);
this.sendPacket(new PacketUnlockNameCardNotify(nameCardId)); this.sendPacket(new PacketUnlockNameCardNotify(nameCardId));
@ -863,6 +950,46 @@ public class Player {
this.getServer().getChatSystem().sendPrivateMessageFromServer(getUid(), message.toString()); 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<Integer> avatarIds = scene.getSceneData().getSpecifiedAvatarList();
List<EntityAvatar> 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. * Sends a message to another player.
* *
@ -982,7 +1109,7 @@ public class Player {
} }
} }
SocialDetail.Builder social = SocialDetail.newBuilder() return SocialDetail.newBuilder()
.setUid(this.getUid()) .setUid(this.getUid())
.setProfilePicture(ProfilePicture.newBuilder().setAvatarId(this.getHeadImage())) .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(this.getHeadImage()))
.setNickname(this.getNickname()) .setNickname(this.getNickname())
@ -996,7 +1123,6 @@ public class Player {
.addAllShowNameCardIdList(this.getShowNameCardInfoList()) .addAllShowNameCardIdList(this.getShowNameCardInfoList())
.setFinishAchievementNum(this.getFinishedAchievementNum()) .setFinishAchievementNum(this.getFinishedAchievementNum())
.setFriendEnterHomeOptionValue(this.getHome() == null ? 0 : this.getHome().getEnterHomeOption()); .setFriendEnterHomeOptionValue(this.getHome() == null ? 0 : this.getHome().getEnterHomeOption());
return social;
} }
public int getFinishedAchievementNum() { public int getFinishedAchievementNum() {
@ -1135,6 +1261,8 @@ public class Player {
// Home resources // Home resources
this.getHome().updateHourlyResources(this); this.getHome().updateHourlyResources(this);
this.getQuestManager().onTick();
} }
private synchronized void doDailyReset() { private synchronized void doDailyReset() {
@ -1200,12 +1328,17 @@ public class Player {
this.achievements = Achievements.getByPlayer(this); this.achievements = Achievements.getByPlayer(this);
this.getAvatars().loadFromDatabase(); this.getAvatars().loadFromDatabase();
this.getInventory().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.getFriendsList().loadFromDatabase();
this.getMailHandler().loadFromDatabase(); this.getMailHandler().loadFromDatabase();
this.getQuestManager().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() { public void onLogin() {

View File

@ -1,108 +1,109 @@
package emu.grasscutter.game.props; package emu.grasscutter.game.props;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import emu.grasscutter.scripts.constants.IntValueEnum;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import java.util.HashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.Map; import java.util.HashMap;
import java.util.stream.Stream; import java.util.Map;
import java.util.stream.Stream;
public enum EntityType {
None(0), public enum EntityType implements IntValueEnum {
Avatar(1), None(0),
Monster(2), Avatar(1),
Bullet(3), Monster(2),
AttackPhyisicalUnit(4), Bullet(3),
AOE(5), AttackPhyisicalUnit(4),
Camera(6), AOE(5),
EnviroArea(7), Camera(6),
Equip(8), EnviroArea(7),
MonsterEquip(9), Equip(8),
Grass(10), MonsterEquip(9),
Level(11), Grass(10),
NPC(12), Level(11),
TransPointFirst(13), NPC(12),
TransPointFirstGadget(14), TransPointFirst(13),
TransPointSecond(15), TransPointFirstGadget(14),
TransPointSecondGadget(16), TransPointSecond(15),
DropItem(17), TransPointSecondGadget(16),
Field(18), DropItem(17),
Gadget(19), Field(18),
Water(20), Gadget(19),
GatherPoint(21), Water(20),
GatherObject(22), GatherPoint(21),
AirflowField(23), GatherObject(22),
SpeedupField(24), AirflowField(23),
Gear(25), SpeedupField(24),
Chest(26), Gear(25),
EnergyBall(27), Chest(26),
ElemCrystal(28), EnergyBall(27),
Timeline(29), ElemCrystal(28),
Worktop(30), Timeline(29),
Team(31), Worktop(30),
Platform(32), Team(31),
AmberWind(33), Platform(32),
EnvAnimal(34), AmberWind(33),
SealGadget(35), EnvAnimal(34),
Tree(36), SealGadget(35),
Bush(37), Tree(36),
QuestGadget(38), Bush(37),
Lightning(39), QuestGadget(38),
RewardPoint(40), Lightning(39),
RewardStatue(41), RewardPoint(40),
MPLevel(42), RewardStatue(41),
WindSeed(43), MPLevel(42),
MpPlayRewardPoint(44), WindSeed(43),
ViewPoint(45), MpPlayRewardPoint(44),
RemoteAvatar(46), ViewPoint(45),
GeneralRewardPoint(47), RemoteAvatar(46),
PlayTeam(48), GeneralRewardPoint(47),
OfferingGadget(49), PlayTeam(48),
EyePoint(50), OfferingGadget(49),
MiracleRing(51), EyePoint(50),
Foundation(52), MiracleRing(51),
WidgetGadget(53), Foundation(52),
Vehicle(54), WidgetGadget(53),
SubEquip(55), Vehicle(54),
FishRod(56), SubEquip(55),
CustomTile(57), FishRod(56),
FishPool(58), CustomTile(57),
CustomGadget(59), FishPool(58),
BlackMud(60), CustomGadget(59),
RoguelikeOperatorGadget(61), BlackMud(60),
NightCrowGadget(62), RoguelikeOperatorGadget(61),
Projector(63), NightCrowGadget(62),
Screen(64), Projector(63),
EchoShell(65), Screen(64),
UIInteractGadget(66), EchoShell(65),
PlaceHolder(99); UIInteractGadget(66),
PlaceHolder(99);
private static final Int2ObjectMap<EntityType> map = new Int2ObjectOpenHashMap<>();
private static final Map<String, EntityType> stringMap = new HashMap<>(); private static final Int2ObjectMap<EntityType> map = new Int2ObjectOpenHashMap<>();
private static final Map<String, EntityType> stringMap = new HashMap<>();
static {
Stream.of(values()) static {
.forEach( Stream.of(values())
e -> { .forEach(
map.put(e.getValue(), e); e -> {
stringMap.put(e.name(), e); map.put(e.getValue(), e);
}); stringMap.put(e.name(), e);
} });
}
private final int value;
private final int value;
EntityType(int value) {
this.value = value; EntityType(int value) {
} this.value = value;
}
public static EntityType getTypeByValue(int value) {
return map.getOrDefault(value, None); public static EntityType getTypeByValue(int value) {
} return map.getOrDefault(value, None);
}
public static EntityType getTypeByName(String name) {
return stringMap.getOrDefault(name, None); public static EntityType getTypeByName(String name) {
} return stringMap.getOrDefault(name, None);
}
public int getValue() {
return value; public int getValue() {
} return value;
} }
}

View File

@ -16,7 +16,6 @@ import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.quest.enums.*; import emu.grasscutter.game.quest.enums.*;
import emu.grasscutter.game.world.World;
import emu.grasscutter.net.proto.ChildQuestOuterClass.ChildQuest; import emu.grasscutter.net.proto.ChildQuestOuterClass.ChildQuest;
import emu.grasscutter.net.proto.ParentQuestOuterClass.ParentQuest; import emu.grasscutter.net.proto.ParentQuestOuterClass.ParentQuest;
import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify; import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify;
@ -41,7 +40,7 @@ public class GameMainQuest {
@Getter private int[] questVars; @Getter private int[] questVars;
@Getter private long[] timeVar; @Getter private long[] timeVar;
//QuestUpdateQuestVarReq is sent in two stages... //QuestUpdateQuestVarReq is sent in two stages...
@Getter private List<Integer> questVarsUpdate; private List<Integer> questVarsUpdate;
@Getter private ParentQuestState state; @Getter private ParentQuestState state;
@Getter private boolean isFinished; @Getter private boolean isFinished;
@Getter List<QuestGroupSuite> questGroupSuites; @Getter List<QuestGroupSuite> questGroupSuites;
@ -67,6 +66,13 @@ public class GameMainQuest {
addAllChildQuests(); addAllChildQuests();
} }
public List<Integer> getQuestVarsUpdate() {
if(questVarsUpdate == null){
questVarsUpdate = new ArrayList<>();
}
return questVarsUpdate;
}
private void addAllChildQuests() { private void addAllChildQuests() {
List<Integer> subQuestIds = Arrays.stream(GameData.getMainQuestDataMap().get(this.parentQuestId).getSubQuests()).map(SubQuestData::getSubId).toList(); List<Integer> subQuestIds = Arrays.stream(GameData.getMainQuestDataMap().get(this.parentQuestId).getSubQuests()).map(SubQuestData::getSubId).toList();
for (Integer subQuestId : subQuestIds) { for (Integer subQuestId : subQuestIds) {
@ -166,10 +172,10 @@ public class GameMainQuest {
} }
// handoff main quest // handoff main quest
if (mainQuestData.getSuggestTrackMainQuestList() != null) { // if (mainQuestData.getSuggestTrackMainQuestList() != null) {
Arrays.stream(mainQuestData.getSuggestTrackMainQuestList()) // Arrays.stream(mainQuestData.getSuggestTrackMainQuestList())
.forEach(getQuestManager()::startMainQuest); // .forEach(getQuestManager()::startMainQuest);
} // }
} }
//TODO //TODO
public void fail() {} public void fail() {}
@ -181,9 +187,9 @@ public class GameMainQuest {
return null; return null;
} }
/*if(rewindPositions.isEmpty()){ // if(rewindPositions.isEmpty()){
addRewindPoints(); // this.addRewindPoints();
}*/ // }
List<Position> posAndRot = new ArrayList<>(); List<Position> posAndRot = new ArrayList<>();
if(hasRewindPosition(targetQuest.getSubQuestId(), posAndRot)){ if(hasRewindPosition(targetQuest.getSubQuestId(), posAndRot)){
@ -198,8 +204,8 @@ public class GameMainQuest {
if (hasRewindPosition(quest.getSubQuestId(), posAndRot)) { if (hasRewindPosition(quest.getSubQuestId(), posAndRot)) {
return posAndRot; return posAndRot;
} }
} }
return null; return null;
} }

View File

@ -1,308 +1,312 @@
package emu.grasscutter.game.quest; package emu.grasscutter.game.quest;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Transient; import dev.morphia.annotations.Transient;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.ChapterData; import emu.grasscutter.data.excels.ChapterData;
import emu.grasscutter.data.excels.QuestData; import emu.grasscutter.data.excels.QuestData;
import emu.grasscutter.data.excels.TriggerExcelConfigData; import emu.grasscutter.data.excels.TriggerExcelConfigData;
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.quest.enums.QuestCond; import emu.grasscutter.game.quest.enums.QuestCond;
import emu.grasscutter.game.quest.enums.QuestContent; import emu.grasscutter.game.quest.enums.QuestContent;
import emu.grasscutter.game.quest.enums.QuestState; import emu.grasscutter.game.quest.enums.QuestState;
import emu.grasscutter.net.proto.ChapterStateOuterClass; import emu.grasscutter.net.proto.ChapterStateOuterClass;
import emu.grasscutter.net.proto.QuestOuterClass.Quest; import emu.grasscutter.net.proto.QuestOuterClass.Quest;
import emu.grasscutter.scripts.data.SceneGroup; import emu.grasscutter.scripts.data.SceneGroup;
import emu.grasscutter.server.packet.send.PacketChapterStateNotify; import emu.grasscutter.server.packet.send.PacketChapterStateNotify;
import emu.grasscutter.server.packet.send.PacketDelQuestNotify; import emu.grasscutter.server.packet.send.PacketDelQuestNotify;
import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify; import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.script.Bindings; import javax.script.Bindings;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.val; import lombok.val;
@Entity @Entity
public class GameQuest { public class GameQuest {
@Transient @Getter @Setter private GameMainQuest mainQuest; @Transient @Getter @Setter private GameMainQuest mainQuest;
@Transient @Getter private QuestData questData; @Transient @Getter private QuestData questData;
@Getter private int subQuestId; @Getter private int subQuestId;
@Getter private int mainQuestId; @Getter private int mainQuestId;
@Getter @Setter public QuestState state; @Getter @Setter public QuestState state;
@Getter @Setter private int startTime; @Getter @Setter private int startTime;
@Getter @Setter private int acceptTime; @Getter @Setter private int acceptTime;
@Getter @Setter private int finishTime; @Getter @Setter private int finishTime;
@Getter @Setter private long startGameDay; @Getter @Setter private long startGameDay;
@Getter private int[] finishProgressList; @Getter private int[] finishProgressList;
@Getter private int[] failProgressList; @Getter private int[] failProgressList;
@Transient @Getter private Map<String, TriggerExcelConfigData> triggerData; @Transient @Getter private Map<String, TriggerExcelConfigData> triggerData;
@Getter private Map<String, Boolean> triggers; @Getter private Map<String, Boolean> triggers;
private transient Bindings bindings; private transient Bindings bindings;
@Deprecated // Morphia only. Do not use. @Deprecated // Morphia only. Do not use.
public GameQuest() {} public GameQuest() {}
public GameQuest(GameMainQuest mainQuest, QuestData questData) { public GameQuest(GameMainQuest mainQuest, QuestData questData) {
this.mainQuest = mainQuest; this.mainQuest = mainQuest;
this.subQuestId = questData.getId(); this.subQuestId = questData.getId();
this.mainQuestId = questData.getMainId(); this.mainQuestId = questData.getMainId();
this.questData = questData; this.questData = questData;
this.state = QuestState.QUEST_STATE_UNSTARTED; this.state = QuestState.QUEST_STATE_UNSTARTED;
this.triggerData = new HashMap<>(); this.triggerData = new HashMap<>();
this.triggers = new HashMap<>(); this.triggers = new HashMap<>();
} }
public void start() { public void start() {
clearProgress(false); clearProgress(false);
this.acceptTime = Utils.getCurrentSeconds(); this.acceptTime = Utils.getCurrentSeconds();
this.startTime = this.acceptTime; this.startTime = this.acceptTime;
this.startGameDay = getOwner().getWorld().getTotalGameTimeDays(); this.startGameDay = getOwner().getWorld().getTotalGameTimeDays();
this.state = QuestState.QUEST_STATE_UNFINISHED; this.state = QuestState.QUEST_STATE_UNFINISHED;
val triggerCond = val triggerCond =
questData.getFinishCond().stream() questData.getFinishCond().stream()
.filter(p -> p.getType() == QuestContent.QUEST_CONTENT_TRIGGER_FIRE) .filter(p -> p.getType() == QuestContent.QUEST_CONTENT_TRIGGER_FIRE)
.toList(); .toList();
if (triggerCond.size() > 0) { if (triggerCond.size() > 0) {
for (val cond : triggerCond) { for (val cond : triggerCond) {
TriggerExcelConfigData newTrigger = TriggerExcelConfigData newTrigger =
GameData.getTriggerExcelConfigDataMap().get(cond.getParam()[0]); GameData.getTriggerExcelConfigDataMap().get(cond.getParam()[0]);
if (newTrigger != null) { if (newTrigger != null) {
if (this.triggerData == null) { if (this.triggerData == null) {
this.triggerData = new HashMap<>(); this.triggerData = new HashMap<>();
} }
triggerData.put(newTrigger.getTriggerName(), newTrigger); triggerData.put(newTrigger.getTriggerName(), newTrigger);
triggers.put(newTrigger.getTriggerName(), false); triggers.put(newTrigger.getTriggerName(), false);
SceneGroup group = SceneGroup.of(newTrigger.getGroupId()).load(newTrigger.getSceneId()); SceneGroup group = SceneGroup.of(newTrigger.getGroupId()).load(newTrigger.getSceneId());
getOwner() getOwner()
.getWorld() .getWorld()
.getSceneById(newTrigger.getSceneId()) .getSceneById(newTrigger.getSceneId())
.loadTriggerFromGroup(group, newTrigger.getTriggerName()); .loadTriggerFromGroup(group, newTrigger.getTriggerName());
} }
} }
} }
getOwner().sendPacket(new PacketQuestListUpdateNotify(this)); getOwner().sendPacket(new PacketQuestListUpdateNotify(this));
if (ChapterData.beginQuestChapterMap.containsKey(subQuestId)) { if (ChapterData.beginQuestChapterMap.containsKey(subQuestId)) {
getOwner() getOwner()
.sendPacket( .sendPacket(
new PacketChapterStateNotify( new PacketChapterStateNotify(
ChapterData.beginQuestChapterMap.get(subQuestId).getId(), ChapterData.beginQuestChapterMap.get(subQuestId).getId(),
ChapterStateOuterClass.ChapterState.CHAPTER_STATE_BEGIN)); ChapterStateOuterClass.ChapterState.CHAPTER_STATE_BEGIN));
} }
// Some subQuests and talks become active when some other subQuests are unfinished (even from // Some subQuests and talks become active when some other subQuests are unfinished (even from
// different MainQuests) // different MainQuests)
getOwner() getOwner()
.getQuestManager() .getQuestManager()
.queueEvent( .queueEvent(
QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL, QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL,
getSubQuestId(), getSubQuestId(),
getState().getValue(), getState().getValue(),
0, 0,
0, 0,
0); 0);
getOwner() getOwner()
.getQuestManager() .getQuestManager()
.queueEvent( .queueEvent(
QuestCond.QUEST_COND_STATE_EQUAL, getSubQuestId(), getState().getValue(), 0, 0, 0); QuestCond.QUEST_COND_STATE_EQUAL, getSubQuestId(), getState().getValue(), 0, 0, 0);
getQuestData() getQuestData()
.getBeginExec() .getBeginExec()
.forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam()));
getOwner().getQuestManager().checkQuestAlreadyFullfilled(this); getOwner().getQuestManager().checkQuestAlreadyFullfilled(this);
Grasscutter.getLogger().debug("Quest {} is started", subQuestId); Grasscutter.getLogger().debug("Quest {} is started", subQuestId);
} this.save();
}
public String getTriggerNameById(int id) {
TriggerExcelConfigData trigger = GameData.getTriggerExcelConfigDataMap().get(id); public String getTriggerNameById(int id) {
if (trigger != null) { TriggerExcelConfigData trigger = GameData.getTriggerExcelConfigDataMap().get(id);
String triggerName = trigger.getTriggerName(); if (trigger != null) {
return triggerName; String triggerName = trigger.getTriggerName();
} return triggerName;
// return empty string if can't find trigger }
return ""; // return empty string if can't find trigger
} return "";
}
public Player getOwner() {
return this.getMainQuest().getOwner(); public Player getOwner() {
} return this.getMainQuest().getOwner();
}
public void setConfig(QuestData config) {
if (config == null || getSubQuestId() != config.getId()) return; public void setConfig(QuestData config) {
this.questData = config; if (config == null || getSubQuestId() != config.getId()) return;
} this.questData = config;
}
public void setFinishProgress(int index, int value) {
finishProgressList[index] = value; public void setFinishProgress(int index, int value) {
} finishProgressList[index] = value;
}
public void setFailProgress(int index, int value) {
failProgressList[index] = value; public void setFailProgress(int index, int value) {
} failProgressList[index] = value;
}
public boolean clearProgress(boolean notifyDelete) {
// TODO improve public boolean clearProgress(boolean notifyDelete) {
var oldState = state; // TODO improve
if (questData.getFinishCond() != null && questData.getFinishCond().size() != 0) { var oldState = state;
this.finishProgressList = new int[questData.getFinishCond().size()]; 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()]; if (questData.getFailCond() != null && questData.getFailCond().size() != 0) {
} this.failProgressList = new int[questData.getFailCond().size()];
setState(QuestState.QUEST_STATE_UNSTARTED); }
finishTime = 0; setState(QuestState.QUEST_STATE_UNSTARTED);
acceptTime = 0; finishTime = 0;
startTime = 0; acceptTime = 0;
if (oldState == QuestState.QUEST_STATE_UNSTARTED) { startTime = 0;
return false; if (oldState == QuestState.QUEST_STATE_UNSTARTED) {
} return false;
if (notifyDelete) { }
getOwner().sendPacket(new PacketDelQuestNotify(getSubQuestId())); if (notifyDelete) {
} getOwner().sendPacket(new PacketDelQuestNotify(getSubQuestId()));
save(); }
return true; save();
} return true;
}
public void finish() {
this.state = QuestState.QUEST_STATE_FINISHED; public void finish() {
this.finishTime = Utils.getCurrentSeconds(); this.state = QuestState.QUEST_STATE_FINISHED;
this.finishTime = Utils.getCurrentSeconds();
getOwner().sendPacket(new PacketQuestListUpdateNotify(this));
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 if (getQuestData().isFinishParent()) {
// don't have to call save() here // This quest finishes the questline - the main quest will also save the quest to db, so we
getMainQuest().finish(); // don't have to call save() here
} getMainQuest().finish();
}
getQuestData()
.getFinishExec() getQuestData()
.forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); .getFinishExec()
// Some subQuests have conditions that subQuests are finished (even from different MainQuests) .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam()));
getOwner() // Some subQuests have conditions that subQuests are finished (even from different MainQuests)
.getQuestManager() getOwner()
.queueEvent( .getQuestManager()
QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL, .queueEvent(
this.subQuestId, QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL,
this.state.getValue(), this.subQuestId,
0, this.state.getValue(),
0, 0,
0); 0,
getOwner() 0);
.getQuestManager() getOwner()
.queueEvent(QuestContent.QUEST_CONTENT_FINISH_PLOT, this.subQuestId, 0); .getQuestManager()
getOwner() .queueEvent(QuestContent.QUEST_CONTENT_FINISH_PLOT, this.subQuestId, 0);
.getQuestManager() getOwner()
.queueEvent( .getQuestManager()
QuestCond.QUEST_COND_STATE_EQUAL, this.subQuestId, this.state.getValue(), 0, 0, 0); .queueEvent(
getOwner() QuestCond.QUEST_COND_STATE_EQUAL, this.subQuestId, this.state.getValue(), 0, 0, 0);
.getScene() getOwner()
.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_FINISH_QUEST, getSubQuestId()); .getScene()
.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_FINISH_QUEST, getSubQuestId());
getOwner().getProgressManager().tryUnlockOpenStates();
getOwner().getProgressManager().tryUnlockOpenStates();
if (ChapterData.endQuestChapterMap.containsKey(subQuestId)) {
mainQuest if (ChapterData.endQuestChapterMap.containsKey(subQuestId)) {
.getOwner() mainQuest
.sendPacket( .getOwner()
new PacketChapterStateNotify( .sendPacket(
ChapterData.endQuestChapterMap.get(subQuestId).getId(), new PacketChapterStateNotify(
ChapterStateOuterClass.ChapterState.CHAPTER_STATE_END)); ChapterData.endQuestChapterMap.get(subQuestId).getId(),
} ChapterStateOuterClass.ChapterState.CHAPTER_STATE_END));
}
// hard coding to give amber
if (getQuestData().getSubId() == 35402) { // hard coding to give amber
getOwner().getInventory().addItem(1021, 1, ActionReason.QuestItem); // amber item id if (getQuestData().getSubId() == 35402) {
} getOwner().getInventory().addItem(1021, 1, ActionReason.QuestItem); // amber item id
Grasscutter.getLogger().debug("Quest {} is finished", subQuestId); }
}
this.save();
// TODO
public void fail() { Grasscutter.getLogger().debug("Quest {} is finished", subQuestId);
this.state = QuestState.QUEST_STATE_FAILED; }
this.finishTime = Utils.getCurrentSeconds();
// TODO
this.getOwner().sendPacket(new PacketQuestListUpdateNotify(this)); public void fail() {
this.state = QuestState.QUEST_STATE_FAILED;
// Some subQuests have conditions that subQuests fail (even from different MainQuests) this.finishTime = Utils.getCurrentSeconds();
this.getOwner()
.getQuestManager() this.getOwner().sendPacket(new PacketQuestListUpdateNotify(this));
.queueEvent(
QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL, // Some subQuests have conditions that subQuests fail (even from different MainQuests)
this.subQuestId, this.getOwner()
this.state.getValue(), .getQuestManager()
0, .queueEvent(
0, QuestContent.QUEST_CONTENT_QUEST_STATE_EQUAL,
0); this.subQuestId,
this.getOwner() this.state.getValue(),
.getQuestManager() 0,
.queueEvent( 0,
QuestCond.QUEST_COND_STATE_EQUAL, this.subQuestId, this.state.getValue(), 0, 0, 0); 0);
this.getOwner()
this.getQuestData() .getQuestManager()
.getFailExec() .queueEvent(
.forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); QuestCond.QUEST_COND_STATE_EQUAL, this.subQuestId, this.state.getValue(), 0, 0, 0);
if (this.getQuestData().getTrialAvatarList() != null) { this.getQuestData()
this.getQuestData() .getFailExec()
.getTrialAvatarList() .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam()));
.forEach(t -> this.getOwner().getTeamManager().removeTrialAvatar(t));
} if (this.getQuestData().getTrialAvatarList() != null) {
this.getQuestData()
Grasscutter.getLogger().debug("Quest {} is failed", subQuestId); .getTrialAvatarList()
} .forEach(t -> this.getOwner().getTeamManager().removeTrialAvatar(t));
}
// Return true if it did the rewind
public boolean rewind(boolean notifyDelete) { Grasscutter.getLogger().debug("Quest {} is failed", subQuestId);
getMainQuest().getChildQuests().values().stream() }
.filter(p -> p.getQuestData().getOrder() > this.getQuestData().getOrder())
.forEach( // Return true if it did the rewind
q -> { public boolean rewind(boolean notifyDelete) {
q.clearProgress(notifyDelete); getMainQuest().getChildQuests().values().stream()
}); .filter(p -> p.getQuestData().getOrder() > this.getQuestData().getOrder())
clearProgress(notifyDelete); .forEach(
this.start(); q -> {
return true; q.clearProgress(notifyDelete);
} });
clearProgress(notifyDelete);
public void save() { this.start();
getMainQuest().save(); return true;
} }
public Quest toProto() { public void save() {
Quest.Builder proto = getMainQuest().save();
Quest.newBuilder() }
.setQuestId(getSubQuestId())
.setState(getState().getValue()) public Quest toProto() {
.setParentQuestId(getMainQuestId()) Quest.Builder proto =
.setStartTime(getStartTime()) Quest.newBuilder()
.setStartGameTime(438) .setQuestId(getSubQuestId())
.setAcceptTime(getAcceptTime()); .setState(getState().getValue())
.setParentQuestId(getMainQuestId())
if (getFinishProgressList() != null) { .setStartTime(getStartTime())
for (int i : getFinishProgressList()) { .setStartGameTime(438)
proto.addFinishProgressList(i); .setAcceptTime(getAcceptTime());
}
} if (getFinishProgressList() != null) {
for (int i : getFinishProgressList()) {
if (getFailProgressList() != null) { proto.addFinishProgressList(i);
for (int i : getFailProgressList()) { }
proto.addFailProgressList(i); }
}
} if (getFailProgressList() != null) {
for (int i : getFailProgressList()) {
return proto.build(); proto.addFailProgressList(i);
} }
} }
return proto.build();
}
}

View File

@ -7,10 +7,7 @@ import emu.grasscutter.data.excels.QuestData;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.player.BasePlayerManager; import emu.grasscutter.game.player.BasePlayerManager;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.enums.ParentQuestState; import emu.grasscutter.game.quest.enums.*;
import emu.grasscutter.game.quest.enums.QuestCond;
import emu.grasscutter.game.quest.enums.QuestContent;
import emu.grasscutter.game.quest.enums.QuestState;
import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify; import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
@ -19,6 +16,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter; import lombok.Getter;
import lombok.val; import lombok.val;
import javax.annotation.Nonnull;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingDeque;
@ -86,13 +84,17 @@ public class QuestManager extends BasePlayerManager {
this.mainQuests = new Int2ObjectOpenHashMap<>(); this.mainQuests = new Int2ObjectOpenHashMap<>();
} }
// TODO store user value set on enable
public boolean isQuestingEnabled() {
return Grasscutter.getConfig().server.game.gameOptions.questing;
}
public void onPlayerBorn() { public void onPlayerBorn() {
// TODO scan the quest and start the quest with acceptCond fulfilled // 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 // The off send 3 request in that order: 1. FinishedParentQuestNotify, 2. QuestListNotify, 3. ServerCondMeetQuestListUpdateNotify
List<GameMainQuest> newQuests = this.addMultMainQuests(newPlayerMainQuests); if(this.isQuestingEnabled()) {
for (GameMainQuest mainQuest : newQuests) { this.enableQuests();
startMainQuest(mainQuest.getParentQuestId());
} }
//getPlayer().sendPacket(new PacketFinishedParentQuestUpdateNotify(newQuests)); //getPlayer().sendPacket(new PacketFinishedParentQuestUpdateNotify(newQuests));
@ -117,6 +119,8 @@ public class QuestManager extends BasePlayerManager {
} }
quest.checkProgress(); quest.checkProgress();
} }
player.getActivityManager().triggerActivityConditions();
} }
public void onTick(){ public void onTick(){
@ -163,7 +167,8 @@ public class QuestManager extends BasePlayerManager {
} }
public void enableQuests() { 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 null;
} }
return addQuest(questConfig);
}
public GameQuest addQuest(@Nonnull QuestData questConfig) {
// Main quest // Main quest
GameMainQuest mainQuest = this.getMainQuestById(questConfig.getMainId()); GameMainQuest mainQuest = this.getMainQuestById(questConfig.getMainId());
@ -265,7 +275,7 @@ public class QuestManager extends BasePlayerManager {
} }
// Sub quest // Sub quest
GameQuest quest = mainQuest.getChildQuestById(questId); GameQuest quest = mainQuest.getChildQuestById(questConfig.getSubId());
// Forcefully start // Forcefully start
quest.start(); quest.start();
@ -286,13 +296,12 @@ public class QuestManager extends BasePlayerManager {
.map(MainQuestData.SubQuestData::getSubId) .map(MainQuestData.SubQuestData::getSubId)
.ifPresent(this::addQuest); .ifPresent(this::addQuest);
//TODO find a better way then hardcoding to detect needed required quests //TODO find a better way then hardcoding to detect needed required quests
if(mainQuestId == 355){ // if (mainQuestId == 355){
startMainQuest(361); // startMainQuest(361);
startMainQuest(418); // startMainQuest(418);
startMainQuest(423); // startMainQuest(423);
startMainQuest(20509); // startMainQuest(20509);
// }
}
} }
public void queueEvent(QuestCond condType, int... params) { public void queueEvent(QuestCond condType, int... params) {
queueEvent(condType, "", params); queueEvent(condType, "", params);
@ -312,13 +321,42 @@ public class QuestManager extends BasePlayerManager {
public void triggerEvent(QuestCond condType, String paramStr, int... params) { public void triggerEvent(QuestCond condType, String paramStr, int... params) {
Grasscutter.getLogger().debug("Trigger Event {}, {}, {}", condType, paramStr, params); Grasscutter.getLogger().debug("Trigger Event {}, {}, {}", condType, paramStr, params);
List<GameMainQuest> checkMainQuests = this.getMainQuests().values().stream() var potentialQuests = GameData.getQuestDataByConditions(condType, params[0], paramStr);
.filter(i -> i.getState() != ParentQuestState.PARENT_QUEST_STATE_FINISHED) if(potentialQuests == null){
.toList(); return;
for (GameMainQuest mainquest : checkMainQuests) {
mainquest.tryAcceptSubQuests(condType, paramStr, params);
} }
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) { public void triggerEvent(QuestContent condType, String paramStr, int... params) {
Grasscutter.getLogger().debug("Trigger Event {}, {}, {}", condType, paramStr, params); Grasscutter.getLogger().debug("Trigger Event {}, {}, {}", condType, paramStr, params);
List<GameMainQuest> checkMainQuests = this.getMainQuests().values().stream() List<GameMainQuest> 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 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 * @param quest
*/ */
public void checkQuestAlreadyFullfilled(GameQuest quest){ public void checkQuestAlreadyFullfilled(GameQuest quest){
@ -355,6 +394,7 @@ public class QuestManager extends BasePlayerManager {
queueEvent(condition.getType(), condition.getParam()[0], condition.getParam()[1]); queueEvent(condition.getType(), condition.getParam()[0], condition.getParam()[1]);
} }
} }
case QUEST_CONTENT_PLAYER_LEVEL_UP -> queueEvent(condition.getType(), player.getLevel());
} }
} }
}, 1); }, 1);

View File

@ -1,16 +1,19 @@
package emu.grasscutter.game.quest.content; package emu.grasscutter.game.quest.content;
import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT; import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT;
import emu.grasscutter.data.excels.QuestData; import emu.grasscutter.data.excels.QuestData;
import emu.grasscutter.game.quest.GameQuest; import emu.grasscutter.game.quest.GameQuest;
import emu.grasscutter.game.quest.QuestValueContent; import emu.grasscutter.game.quest.QuestValueContent;
@QuestValueContent(QUEST_CONTENT_UNLOCK_TRANS_POINT) @QuestValueContent(QUEST_CONTENT_UNLOCK_TRANS_POINT)
public class ContentUnlockTransPoint extends BaseContent { public class ContentUnlockTransPoint extends BaseContent {
@Override @Override
public boolean execute( public boolean execute(
GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) { GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
return condition.getParam()[0] == params[0] && condition.getParam()[1] == params[1]; var sceneId = condition.getParam()[0];
} var scenePointId = condition.getParam()[1];
} var scenePoints = quest.getOwner().getUnlockedScenePoints().get(sceneId);
return scenePoints != null && scenePoints.contains(scenePointId);
}
}

View File

@ -1,17 +1,17 @@
package emu.grasscutter.game.quest.exec; package emu.grasscutter.game.quest.exec;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.excels.QuestData; import emu.grasscutter.data.excels.QuestData;
import emu.grasscutter.game.quest.GameQuest; import emu.grasscutter.game.quest.GameQuest;
import emu.grasscutter.game.quest.QuestValueExec; import emu.grasscutter.game.quest.QuestValueExec;
import emu.grasscutter.game.quest.enums.QuestExec; import emu.grasscutter.game.quest.enums.QuestExec;
import emu.grasscutter.game.quest.handlers.QuestExecHandler; import emu.grasscutter.game.quest.handlers.QuestExecHandler;
@QuestValueExec(QuestExec.QUEST_EXEC_ADD_CUR_AVATAR_ENERGY) @QuestValueExec(QuestExec.QUEST_EXEC_ADD_CUR_AVATAR_ENERGY)
public class ExecAddCurAvatarEnergy extends QuestExecHandler { public class ExecAddCurAvatarEnergy extends QuestExecHandler {
@Override @Override
public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) { public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
Grasscutter.getLogger().info("Energy refilled"); Grasscutter.getLogger().debug("Energy refilled");
return quest.getOwner().getEnergyManager().refillActiveEnergy(); return quest.getOwner().getEnergyManager().refillActiveEnergy();
} }
} }

View File

@ -319,12 +319,14 @@ public class World implements Iterable<Player> {
var newScene = this.getSceneById(teleportProperties.getSceneId()); var newScene = this.getSceneById(teleportProperties.getSceneId());
newScene.addPlayer(player); newScene.addPlayer(player);
player.setAvatarsAbilityForScene(newScene); player.setAvatarsAbilityForScene(newScene);
// Dungeon // Dungeon
// Dungeon system is handling this already // Dungeon system is handling this already
// if(dungeonData!=null){ // if(dungeonData!=null){
// var dungeonManager = new DungeonManager(newScene, dungeonData); // var dungeonManager = new DungeonManager(newScene, dungeonData);
// dungeonManager.startDungeon(); // dungeonManager.startDungeon();
// } // }
SceneConfig config = newScene.getScriptManager().getConfig(); SceneConfig config = newScene.getScriptManager().getConfig();
if (teleportProperties.getTeleportTo() == null && config != null) { if (teleportProperties.getTeleportTo() == null && config != null) {
if (config.born_pos != null) { if (config.born_pos != null) {

View File

@ -1,153 +1,160 @@
package emu.grasscutter.game.world; package emu.grasscutter.game.world;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader; import emu.grasscutter.data.DataLoader;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.InvestigationMonsterData; import emu.grasscutter.data.excels.InvestigationMonsterData;
import emu.grasscutter.data.excels.RewardPreviewData; import emu.grasscutter.data.excels.RewardPreviewData;
import emu.grasscutter.data.excels.world.WorldLevelData; import emu.grasscutter.data.excels.world.WorldLevelData;
import emu.grasscutter.game.entity.gadget.chest.BossChestInteractHandler; import emu.grasscutter.game.entity.gadget.chest.BossChestInteractHandler;
import emu.grasscutter.game.entity.gadget.chest.ChestInteractHandler; import emu.grasscutter.game.entity.gadget.chest.ChestInteractHandler;
import emu.grasscutter.game.entity.gadget.chest.NormalChestInteractHandler; import emu.grasscutter.game.entity.gadget.chest.NormalChestInteractHandler;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.InvestigationMonsterOuterClass; import emu.grasscutter.net.proto.InvestigationMonsterOuterClass;
import emu.grasscutter.scripts.data.SceneGroup; import emu.grasscutter.scripts.data.SceneGroup;
import emu.grasscutter.scripts.data.SceneMonster; import emu.grasscutter.scripts.data.SceneMonster;
import emu.grasscutter.server.game.BaseGameSystem; import emu.grasscutter.server.game.BaseGameSystem;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import java.util.HashMap; import org.luaj.vm2.LuaError;
import java.util.List;
import java.util.Map; import java.util.HashMap;
import java.util.Objects; import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.Map;
import java.util.Objects;
public class WorldDataSystem extends BaseGameSystem { import java.util.concurrent.ConcurrentHashMap;
private final Map<String, ChestInteractHandler> chestInteractHandlerMap; // chestType-Handler
private final Map<String, SceneGroup> sceneInvestigationGroupMap; // <sceneId_groupId, Group> public class WorldDataSystem extends BaseGameSystem {
private final Map<String, ChestInteractHandler> chestInteractHandlerMap; // chestType-Handler
public WorldDataSystem(GameServer server) { private final Map<String, SceneGroup> sceneInvestigationGroupMap; // <sceneId_groupId, Group>
super(server);
this.chestInteractHandlerMap = new HashMap<>(); public WorldDataSystem(GameServer server) {
this.sceneInvestigationGroupMap = new ConcurrentHashMap<>(); super(server);
this.chestInteractHandlerMap = new HashMap<>();
loadChestConfig(); this.sceneInvestigationGroupMap = new ConcurrentHashMap<>();
}
loadChestConfig();
public synchronized void loadChestConfig() { }
// set the special chest first
chestInteractHandlerMap.put("SceneObj_Chest_Flora", new BossChestInteractHandler()); public synchronized void loadChestConfig() {
// set the special chest first
try { chestInteractHandlerMap.put("SceneObj_Chest_Flora", new BossChestInteractHandler());
DataLoader.loadList("ChestReward.json", ChestReward.class)
.forEach( try {
reward -> DataLoader.loadList("ChestReward.json", ChestReward.class)
reward .forEach(
.getObjNames() reward ->
.forEach( reward
name -> .getObjNames()
chestInteractHandlerMap.computeIfAbsent( .forEach(
name, x -> new NormalChestInteractHandler(reward)))); name ->
} catch (Exception e) { chestInteractHandlerMap.computeIfAbsent(
Grasscutter.getLogger().error("Unable to load chest reward config.", e); name, x -> new NormalChestInteractHandler(reward))));
} } catch (Exception e) {
} Grasscutter.getLogger().error("Unable to load chest reward config.", e);
}
public Map<String, ChestInteractHandler> getChestInteractHandlerMap() { }
return chestInteractHandlerMap;
} public Map<String, ChestInteractHandler> getChestInteractHandlerMap() {
return chestInteractHandlerMap;
public RewardPreviewData getRewardByBossId(int monsterId) { }
var investigationMonsterData =
GameData.getInvestigationMonsterDataMap().values().parallelStream() public RewardPreviewData getRewardByBossId(int monsterId) {
.filter(imd -> imd.getMonsterIdList() != null && !imd.getMonsterIdList().isEmpty()) var investigationMonsterData =
.filter(imd -> imd.getMonsterIdList().get(0) == monsterId) GameData.getInvestigationMonsterDataMap().values().parallelStream()
.findFirst(); .filter(imd -> imd.getMonsterIdList() != null && !imd.getMonsterIdList().isEmpty())
.filter(imd -> imd.getMonsterIdList().get(0) == monsterId)
if (investigationMonsterData.isEmpty()) { .findFirst();
return null;
} if (investigationMonsterData.isEmpty()) {
return GameData.getRewardPreviewDataMap() return null;
.get(investigationMonsterData.get().getRewardPreviewId()); }
} return GameData.getRewardPreviewDataMap()
.get(investigationMonsterData.get().getRewardPreviewId());
private SceneGroup getInvestigationGroup(int sceneId, int groupId) { }
var key = sceneId + "_" + groupId;
if (!sceneInvestigationGroupMap.containsKey(key)) { private SceneGroup getInvestigationGroup(int sceneId, int groupId) {
var group = SceneGroup.of(groupId).load(sceneId); var key = sceneId + "_" + groupId;
sceneInvestigationGroupMap.putIfAbsent(key, group); if (!sceneInvestigationGroupMap.containsKey(key)) {
return group; try {
} var group = SceneGroup.of(groupId).load(sceneId);
return sceneInvestigationGroupMap.get(key); sceneInvestigationGroupMap.putIfAbsent(key, group);
} return group;
} catch (LuaError luaError) {
public int getMonsterLevel(SceneMonster monster, World world) { Grasscutter.getLogger()
// Calculate level .error("failed to get investigationGroup {} in scene{}:", groupId, sceneId, luaError);
int level = monster.level; }
WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(world.getWorldLevel()); }
return sceneInvestigationGroupMap.get(key);
if (worldLevelData != null) { }
level = worldLevelData.getMonsterLevel();
} public int getMonsterLevel(SceneMonster monster, World world) {
return level; // Calculate level
} int level = monster.level;
WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(world.getWorldLevel());
private InvestigationMonsterOuterClass.InvestigationMonster getInvestigationMonster(
Player player, InvestigationMonsterData imd) { if (worldLevelData != null) {
if (imd.getGroupIdList().isEmpty() || imd.getMonsterIdList().isEmpty()) { level = worldLevelData.getMonsterLevel();
return null; }
} return level;
}
var groupId = imd.getGroupIdList().get(0);
var monsterId = imd.getMonsterIdList().get(0); private InvestigationMonsterOuterClass.InvestigationMonster getInvestigationMonster(
var sceneId = imd.getCityData().getSceneId(); Player player, InvestigationMonsterData imd) {
var group = getInvestigationGroup(sceneId, groupId); if (imd.getGroupIdList().isEmpty() || imd.getMonsterIdList().isEmpty()) {
return null;
if (group == null || group.monsters == null) { }
return null;
} var groupId = imd.getGroupIdList().get(0);
var monsterId = imd.getMonsterIdList().get(0);
var monster = var sceneId = imd.getCityData().getSceneId();
group.monsters.values().stream().filter(x -> x.monster_id == monsterId).findFirst(); var group = getInvestigationGroup(sceneId, groupId);
if (monster.isEmpty()) {
return null; if (group == null || group.monsters == null) {
} return null;
}
var builder = InvestigationMonsterOuterClass.InvestigationMonster.newBuilder();
var monster =
builder group.monsters.values().stream().filter(x -> x.monster_id == monsterId).findFirst();
.setId(imd.getId()) if (monster.isEmpty()) {
.setCityId(imd.getCityId()) return null;
.setSceneId(imd.getCityData().getSceneId()) }
.setGroupId(groupId)
.setMonsterId(monsterId) var builder = InvestigationMonsterOuterClass.InvestigationMonster.newBuilder();
.setLevel(getMonsterLevel(monster.get(), player.getWorld()))
.setIsAlive(true) builder
.setNextRefreshTime(Integer.MAX_VALUE) .setId(imd.getId())
.setRefreshInterval(Integer.MAX_VALUE) .setCityId(imd.getCityId())
.setPos(monster.get().pos.toProto()); .setSceneId(imd.getCityData().getSceneId())
.setGroupId(groupId)
if ("Boss".equals(imd.getMonsterCategory())) { .setMonsterId(monsterId)
var bossChest = group.searchBossChestInGroup(); .setLevel(getMonsterLevel(monster.get(), player.getWorld()))
if (bossChest.isPresent()) { .setIsAlive(true)
builder.setResin(bossChest.get().resin); .setNextRefreshTime(Integer.MAX_VALUE)
builder.setMaxBossChestNum(bossChest.get().take_num); .setRefreshInterval(Integer.MAX_VALUE)
} .setPos(monster.get().pos.toProto());
}
return builder.build(); if ("Boss".equals(imd.getMonsterCategory())) {
} var bossChest = group.searchBossChestInGroup();
if (bossChest.isPresent()) {
public List<InvestigationMonsterOuterClass.InvestigationMonster> getInvestigationMonstersByCityId( builder.setResin(bossChest.get().resin);
Player player, int cityId) { builder.setMaxBossChestNum(bossChest.get().take_num);
var cityData = GameData.getCityDataMap().get(cityId); }
if (cityData == null) { }
Grasscutter.getLogger().warn("City not exist {}", cityId); return builder.build();
return List.of(); }
}
public List<InvestigationMonsterOuterClass.InvestigationMonster> getInvestigationMonstersByCityId(
return GameData.getInvestigationMonsterDataMap().values().parallelStream() Player player, int cityId) {
.filter(imd -> imd.getCityId() == cityId) var cityData = GameData.getCityDataMap().get(cityId);
.map(imd -> this.getInvestigationMonster(player, imd)) if (cityData == null) {
.filter(Objects::nonNull) Grasscutter.getLogger().warn("City not exist {}", cityId);
.toList(); return List.of();
} }
}
return GameData.getInvestigationMonsterDataMap().values().parallelStream()
.filter(imd -> imd.getCityId() == cityId)
.map(imd -> this.getInvestigationMonster(player, imd))
.filter(Objects::nonNull)
.toList();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,152 +1,176 @@
package emu.grasscutter.scripts; package emu.grasscutter.scripts;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.dungeons.challenge.enums.ChallengeEventMarkType;
import emu.grasscutter.game.quest.enums.QuestState; import emu.grasscutter.game.dungeons.challenge.enums.FatherChallengeProperty;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.game.props.ElementType;
import emu.grasscutter.scripts.constants.ScriptGadgetState; import emu.grasscutter.game.props.EntityType;
import emu.grasscutter.scripts.constants.ScriptRegionShape; import emu.grasscutter.game.quest.enums.QuestState;
import emu.grasscutter.scripts.data.SceneMeta; import emu.grasscutter.scripts.constants.*;
import emu.grasscutter.scripts.serializer.LuaSerializer; import emu.grasscutter.scripts.data.SceneMeta;
import emu.grasscutter.scripts.serializer.Serializer; import emu.grasscutter.scripts.serializer.LuaSerializer;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.scripts.serializer.Serializer;
import java.io.File; import emu.grasscutter.utils.FileUtils;
import java.io.FileReader; import java.io.File;
import java.lang.ref.SoftReference; import java.io.FileReader;
import java.nio.file.Files; import java.lang.ref.SoftReference;
import java.nio.file.Path; import java.nio.file.Files;
import java.util.Arrays; import java.nio.file.Path;
import java.util.Map; import java.util.Arrays;
import java.util.Optional; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.Optional;
import javax.script.*; import java.util.concurrent.ConcurrentHashMap;
import lombok.Getter; import javax.script.*;
import org.luaj.vm2.LuaTable; import lombok.Getter;
import org.luaj.vm2.LuaValue; import org.luaj.vm2.LuaTable;
import org.luaj.vm2.lib.OneArgFunction; import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.jse.CoerceJavaToLua; import org.luaj.vm2.lib.OneArgFunction;
import org.luaj.vm2.script.LuajContext; import org.luaj.vm2.lib.jse.CoerceJavaToLua;
import org.luaj.vm2.script.LuajContext;
public class ScriptLoader {
private static ScriptEngineManager sm; public class ScriptLoader {
@Getter private static ScriptEngine engine; private static ScriptEngineManager sm;
private static ScriptEngineFactory factory; @Getter private static ScriptEngine engine;
@Getter private static Serializer serializer; private static ScriptEngineFactory factory;
@Getter private static ScriptLib scriptLib; @Getter private static Serializer serializer;
@Getter private static LuaValue scriptLibLua; @Getter private static ScriptLib scriptLib;
/** suggest GC to remove it if the memory is less */ @Getter private static LuaValue scriptLibLua;
private static final Map<String, SoftReference<CompiledScript>> scriptsCache = /** suggest GC to remove it if the memory is less */
new ConcurrentHashMap<>(); private static final Map<String, SoftReference<CompiledScript>> scriptsCache =
/** sceneId - SceneMeta */ new ConcurrentHashMap<>();
private static final Map<Integer, SoftReference<SceneMeta>> sceneMetaCache = /** sceneId - SceneMeta */
new ConcurrentHashMap<>(); private static final Map<Integer, SoftReference<SceneMeta>> sceneMetaCache =
new ConcurrentHashMap<>();
public static synchronized void init() throws Exception {
if (sm != null) { public static synchronized void init() throws Exception {
throw new Exception("Script loader already initialized"); if (sm != null) {
} throw new Exception("Script loader already initialized");
}
// Create script engine
sm = new ScriptEngineManager(); // Create script engine
engine = sm.getEngineByName("luaj"); sm = new ScriptEngineManager();
factory = getEngine().getFactory(); engine = sm.getEngineByName("luaj");
factory = getEngine().getFactory();
// Lua stuff
serializer = new LuaSerializer(); // Lua stuff
serializer = new LuaSerializer();
// Set engine to replace require as a temporary fix to missing scripts
LuajContext ctx = (LuajContext) engine.getContext(); // Set engine to replace require as a temporary fix to missing scripts
ctx.globals.set( LuajContext ctx = (LuajContext) engine.getContext();
"require", ctx.globals.set(
new OneArgFunction() { "require",
@Override new OneArgFunction() {
public LuaValue call(LuaValue arg0) { @Override
return LuaValue.ZERO; public LuaValue call(LuaValue arg0) {
} return LuaValue.ZERO;
}); }
});
LuaTable table = new LuaTable();
Arrays.stream(EntityType.values()) addEnumByIntValue(ctx, EntityType.values(), "EntityType");
.forEach(e -> table.set(e.name().toUpperCase(), e.getValue())); addEnumByIntValue(ctx, QuestState.values(), "QuestState");
ctx.globals.set("EntityType", table); addEnumByIntValue(ctx, ElementType.values(), "ElementType");
LuaTable table1 = new LuaTable(); addEnumByOrdinal(ctx, GroupKillPolicy.values(), "GroupKillPolicy");
Arrays.stream(QuestState.values()) addEnumByOrdinal(ctx, SealBattleType.values(), "SealBattleType");
.forEach(e -> table1.set(e.name().toUpperCase(), e.getValue())); addEnumByOrdinal(ctx, FatherChallengeProperty.values(), "FatherChallengeProperty");
ctx.globals.set("QuestState", table1); addEnumByOrdinal(ctx, ChallengeEventMarkType.values(), "ChallengeEventMarkType");
ctx.globals.set( ctx.globals.set(
"EventType", "EventType",
CoerceJavaToLua.coerce( CoerceJavaToLua.coerce(
new EventType())); // TODO - make static class to avoid instantiating a new class every new EventType())); // TODO - make static class to avoid instantiating a new class every
// scene // scene
ctx.globals.set("GadgetState", CoerceJavaToLua.coerce(new ScriptGadgetState())); ctx.globals.set("GadgetState", CoerceJavaToLua.coerce(new ScriptGadgetState()));
ctx.globals.set("RegionShape", CoerceJavaToLua.coerce(new ScriptRegionShape())); ctx.globals.set("RegionShape", CoerceJavaToLua.coerce(new ScriptRegionShape()));
scriptLib = new ScriptLib(); scriptLib = new ScriptLib();
scriptLibLua = CoerceJavaToLua.coerce(scriptLib); scriptLibLua = CoerceJavaToLua.coerce(scriptLib);
ctx.globals.set("ScriptLib", scriptLibLua); ctx.globals.set("ScriptLib", scriptLibLua);
} }
public static <T> Optional<T> tryGet(SoftReference<T> softReference) { private static <T extends Enum<T>> void addEnumByOrdinal(
try { LuajContext ctx, T[] enumArray, String name) {
return Optional.ofNullable(softReference.get()); LuaTable table = new LuaTable();
} catch (NullPointerException npe) { Arrays.stream(enumArray)
return Optional.empty(); .forEach(
} e -> {
} table.set(e.name(), e.ordinal());
table.set(e.name().toUpperCase(), e.ordinal());
@Deprecated(forRemoval = true) });
public static CompiledScript getScriptByPath(String path) { ctx.globals.set(name, table);
var sc = tryGet(scriptsCache.get(path)); }
if (sc.isPresent()) {
return sc.get(); private static <T extends Enum<T> & IntValueEnum> void addEnumByIntValue(
} LuajContext ctx, T[] enumArray, String name) {
LuaTable table = new LuaTable();
Grasscutter.getLogger().debug("Loading script " + path); Arrays.stream(enumArray)
.forEach(
File file = new File(path); e -> {
table.set(e.name(), e.getValue());
if (!file.exists()) return null; table.set(e.name().toUpperCase(), e.getValue());
});
try (FileReader fr = new FileReader(file)) { ctx.globals.set(name, table);
var script = ((Compilable) getEngine()).compile(fr); }
scriptsCache.put(path, new SoftReference<>(script));
return script; public static <T> Optional<T> tryGet(SoftReference<T> softReference) {
} catch (Exception e) { try {
Grasscutter.getLogger().error("Loading script {} failed!", path, e); return Optional.ofNullable(softReference.get());
return null; } catch (NullPointerException npe) {
} return Optional.empty();
} }
}
public static CompiledScript getScript(String path) {
var sc = tryGet(scriptsCache.get(path)); @Deprecated(forRemoval = true)
if (sc.isPresent()) { public static CompiledScript getScriptByPath(String path) {
return sc.get(); 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; // Grasscutter.getLogger().debug("Loading script " + path);
try { File file = new File(path);
var script = ((Compilable) getEngine()).compile(Files.newBufferedReader(scriptPath));
scriptsCache.put(path, new SoftReference<>(script)); if (!file.exists()) return null;
return script;
} catch (Exception e) { try (FileReader fr = new FileReader(file)) {
Grasscutter.getLogger() var script = ((Compilable) getEngine()).compile(fr);
.error("Loading script {} failed! - {}", path, e.getLocalizedMessage()); scriptsCache.put(path, new SoftReference<>(script));
return null; return script;
} } catch (Exception e) {
} Grasscutter.getLogger().error("Loading script {} failed!", path, e);
return null;
public static SceneMeta getSceneMeta(int sceneId) { }
return tryGet(sceneMetaCache.get(sceneId)) }
.orElseGet(
() -> { public static CompiledScript getScript(String path) {
var instance = SceneMeta.of(sceneId); var sc = tryGet(scriptsCache.get(path));
sceneMetaCache.put(sceneId, new SoftReference<>(instance)); if (sc.isPresent()) {
return instance; 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;
});
}
}

View File

@ -1,86 +1,87 @@
package emu.grasscutter.scripts.data; package emu.grasscutter.scripts.data;
import com.github.davidmoten.rtreemulti.RTree; import com.github.davidmoten.rtreemulti.RTree;
import com.github.davidmoten.rtreemulti.geometry.Geometry; import com.github.davidmoten.rtreemulti.geometry.Geometry;
import com.github.davidmoten.rtreemulti.geometry.Rectangle; import com.github.davidmoten.rtreemulti.geometry.Rectangle;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.scripts.SceneIndexManager; import emu.grasscutter.scripts.SceneIndexManager;
import emu.grasscutter.scripts.ScriptLoader; import emu.grasscutter.scripts.ScriptLoader;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.script.Bindings; import javax.script.Bindings;
import javax.script.CompiledScript; import javax.script.CompiledScript;
import javax.script.ScriptException; import javax.script.ScriptException;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
@ToString @ToString
@Setter @Setter
public class SceneBlock { public class SceneBlock {
public int id; public int id;
public Position max; public Position max;
public Position min; public Position min;
public int sceneId; public int sceneId;
public Map<Integer, SceneGroup> groups; public Map<Integer, SceneGroup> groups;
public RTree<SceneGroup, Geometry> sceneGroupIndex; public RTree<SceneGroup, Geometry> sceneGroupIndex;
private transient boolean loaded; // Not an actual variable in the scripts either private transient boolean loaded; // Not an actual variable in the scripts either
public boolean isLoaded() { public boolean isLoaded() {
return this.loaded; return this.loaded;
} }
public void setLoaded(boolean loaded) { public void setLoaded(boolean loaded) {
this.loaded = loaded; this.loaded = loaded;
} }
public boolean contains(Position pos) { public boolean contains(Position pos) {
return pos.getX() <= this.max.getX() int range = Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange;
&& pos.getX() >= this.min.getX() return pos.getX() <= (this.max.getX() + range)
&& pos.getZ() <= this.max.getZ() && pos.getX() >= (this.min.getX() - range)
&& pos.getZ() >= this.min.getZ(); && pos.getZ() <= (this.max.getZ() + range)
} && pos.getZ() >= (this.min.getZ() - range);
}
public SceneBlock load(int sceneId, Bindings bindings) {
if (this.loaded) { public SceneBlock load(int sceneId, Bindings bindings) {
return this; if (this.loaded) {
} return this;
this.sceneId = sceneId; }
this.setLoaded(true); this.sceneId = sceneId;
this.setLoaded(true);
CompiledScript cs =
ScriptLoader.getScript( CompiledScript cs =
"Scene/" + sceneId + "/scene" + sceneId + "_block" + this.id + ".lua"); ScriptLoader.getScript(
"Scene/" + sceneId + "/scene" + sceneId + "_block" + this.id + ".lua");
if (cs == null) {
return null; if (cs == null) {
} return null;
}
// Eval script
try { // Eval script
cs.eval(bindings); try {
cs.eval(bindings);
// Set groups
this.groups = // Set groups
ScriptLoader.getSerializer().toList(SceneGroup.class, bindings.get("groups")).stream() this.groups =
.collect(Collectors.toMap(x -> x.id, y -> y, (a, b) -> a)); 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 = this.groups.values().forEach(g -> g.block_id = this.id);
SceneIndexManager.buildIndex(3, this.groups.values(), g -> g.pos.toPoint()); this.sceneGroupIndex =
} catch (ScriptException exception) { SceneIndexManager.buildIndex(3, this.groups.values(), g -> g.pos.toPoint());
Grasscutter.getLogger() } catch (ScriptException exception) {
.error( Grasscutter.getLogger()
"An error occurred while loading block " + this.id + " in scene " + sceneId, .error(
exception); "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; 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()); public Rectangle toRectangle() {
} return Rectangle.create(this.min.toXZDoubleArray(), this.max.toXZDoubleArray());
} }
}

View File

@ -1,246 +1,268 @@
package emu.grasscutter.scripts.serializer; package emu.grasscutter.scripts.serializer;
import com.esotericsoftware.reflectasm.ConstructorAccess; import com.esotericsoftware.reflectasm.ConstructorAccess;
import com.esotericsoftware.reflectasm.MethodAccess; import com.esotericsoftware.reflectasm.MethodAccess;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.scripts.ScriptUtils; import emu.grasscutter.scripts.ScriptUtils;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.lang.reflect.Field;
import lombok.AccessLevel; import java.lang.reflect.ParameterizedType;
import lombok.AllArgsConstructor; import java.lang.reflect.Type;
import lombok.Data; import java.util.*;
import lombok.experimental.FieldDefaults; import java.util.concurrent.ConcurrentHashMap;
import org.luaj.vm2.LuaTable; import lombok.AccessLevel;
import org.luaj.vm2.LuaValue; import lombok.AllArgsConstructor;
import lombok.Data;
public class LuaSerializer implements Serializer { import lombok.experimental.FieldDefaults;
import org.jetbrains.annotations.Nullable;
private static final Map<Class<?>, MethodAccess> methodAccessCache = new ConcurrentHashMap<>(); import org.luaj.vm2.LuaTable;
private static final Map<Class<?>, ConstructorAccess<?>> constructorCache = import org.luaj.vm2.LuaValue;
new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<String, FieldMeta>> fieldMetaCache = public class LuaSerializer implements Serializer {
new ConcurrentHashMap<>();
private static final Map<Class<?>, MethodAccess> methodAccessCache = new ConcurrentHashMap<>();
@Override private static final Map<Class<?>, ConstructorAccess<?>> constructorCache =
public <T> List<T> toList(Class<T> type, Object obj) { new ConcurrentHashMap<>();
return serializeList(type, (LuaTable) obj); private static final Map<Class<?>, Map<String, FieldMeta>> fieldMetaCache =
} new ConcurrentHashMap<>();
@Override @Override
public <T> T toObject(Class<T> type, Object obj) { public <T> List<T> toList(Class<T> type, Object obj) {
return serialize(type, (LuaTable) obj); return serializeList(type, (LuaTable) obj);
} }
@Override @Override
public <T> Map<String, T> toMap(Class<T> type, Object obj) { public <T> T toObject(Class<T> type, Object obj) {
return serializeMap(type, (LuaTable) obj); return serialize(type, null, (LuaTable) obj);
} }
private <T> Map<String, T> serializeMap(Class<T> type, LuaTable table) { @Override
Map<String, T> map = new HashMap<>(); public <T> Map<String, T> toMap(Class<T> type, Object obj) {
return serializeMap(type, (LuaTable) obj);
if (table == null) { }
return map;
} private <T> Map<String, T> serializeMap(Class<T> type, LuaTable table) {
Map<String, T> map = new HashMap<>();
try {
LuaValue[] keys = table.keys(); if (table == null) {
for (LuaValue k : keys) { return map;
try { }
LuaValue keyValue = table.get(k);
try {
T object = null; LuaValue[] keys = table.keys();
for (LuaValue k : keys) {
if (keyValue.istable()) { try {
object = serialize(type, keyValue.checktable()); LuaValue keyValue = table.get(k);
} else if (keyValue.isint()) {
object = (T) (Integer) keyValue.toint(); T object = null;
} else if (keyValue.isnumber()) {
object = (T) (Float) keyValue.tofloat(); // terrible... if (keyValue.istable()) {
} else if (keyValue.isstring()) { object = serialize(type, null, keyValue.checktable());
object = (T) keyValue.tojstring(); } else if (keyValue.isint()) {
} else if (keyValue.isboolean()) { object = (T) (Integer) keyValue.toint();
object = (T) (Boolean) keyValue.toboolean(); } else if (keyValue.isnumber()) {
} else { object = (T) (Float) keyValue.tofloat(); // terrible...
object = (T) keyValue; } else if (keyValue.isstring()) {
} object = (T) keyValue.tojstring();
} else if (keyValue.isboolean()) {
if (object != null) { object = (T) (Boolean) keyValue.toboolean();
map.put(String.valueOf(k), object); } else {
} object = (T) keyValue;
} catch (Exception ex) { }
} if (object != null) {
} map.put(String.valueOf(k), object);
} catch (Exception e) { }
e.printStackTrace(); } catch (Exception ex) {
}
}
return map; }
} } catch (Exception e) {
e.printStackTrace();
public <T> List<T> serializeList(Class<T> type, LuaTable table) { }
List<T> list = new ArrayList<>();
return map;
if (table == null) { }
return list;
} public <T> List<T> serializeList(Class<T> type, LuaTable table) {
List<T> list = new ArrayList<>();
try {
LuaValue[] keys = table.keys(); if (table == null) {
for (LuaValue k : keys) { return list;
try { }
LuaValue keyValue = table.get(k);
try {
T object = null; LuaValue[] keys = table.keys();
for (LuaValue k : keys) {
if (keyValue.istable()) { try {
object = serialize(type, keyValue.checktable()); LuaValue keyValue = table.get(k);
} else if (keyValue.isint()) {
object = (T) (Integer) keyValue.toint(); T object = null;
} else if (keyValue.isnumber()) {
object = (T) (Float) keyValue.tofloat(); // terrible... if (keyValue.istable()) {
} else if (keyValue.isstring()) { object = serialize(type, null, keyValue.checktable());
object = (T) keyValue.tojstring(); } else if (keyValue.isint()) {
} else if (keyValue.isboolean()) { object = (T) (Integer) keyValue.toint();
object = (T) (Boolean) keyValue.toboolean(); } else if (keyValue.isnumber()) {
} else { object = (T) (Float) keyValue.tofloat(); // terrible...
object = (T) keyValue; } else if (keyValue.isstring()) {
} object = (T) keyValue.tojstring();
} else if (keyValue.isboolean()) {
if (object != null) { object = (T) (Boolean) keyValue.toboolean();
list.add(object); } else {
} object = (T) keyValue;
} catch (Exception ex) { }
} if (object != null) {
} list.add(object);
} catch (Exception e) { }
e.printStackTrace(); } catch (Exception ex) {
}
}
return list; }
} } catch (Exception e) {
e.printStackTrace();
public <T> T serialize(Class<T> type, LuaTable table) { }
T object = null;
return list;
if (type == List.class) { }
try {
Class<T> listType = (Class<T>) type.getTypeParameters()[0].getClass(); private Class<?> getListType(Class<?> type, @Nullable Field field) {
return (T) serializeList(listType, table); if (field == null) {
} catch (Exception e) { return type.getTypeParameters()[0].getClass();
e.printStackTrace(); }
return null; Type fieldType = field.getGenericType();
} if (fieldType instanceof ParameterizedType) {
} return (Class<?>) ((ParameterizedType) fieldType).getActualTypeArguments()[0];
}
try {
if (!methodAccessCache.containsKey(type)) { return null;
cacheType(type); }
}
var methodAccess = methodAccessCache.get(type); public <T> T serialize(Class<T> type, @Nullable Field field, LuaTable table) {
var fieldMetaMap = fieldMetaCache.get(type); T object = null;
object = (T) constructorCache.get(type).newInstance(); if (type == List.class) {
try {
if (table == null) { Class<?> listType = getListType(type, field);
return object; return (T) serializeList(listType, table);
} } catch (Exception e) {
e.printStackTrace();
LuaValue[] keys = table.keys(); return null;
for (LuaValue k : keys) { }
try { }
var keyName = k.checkjstring();
if (!fieldMetaMap.containsKey(keyName)) { try {
continue; if (!methodAccessCache.containsKey(type)) {
} cacheType(type);
var fieldMeta = fieldMetaMap.get(keyName); }
LuaValue keyValue = table.get(k); var methodAccess = methodAccessCache.get(type);
var fieldMetaMap = fieldMetaCache.get(type);
if (keyValue.istable()) {
methodAccess.invoke( object = (T) constructorCache.get(type).newInstance();
object, fieldMeta.index, serialize(fieldMeta.getType(), keyValue.checktable()));
} else if (fieldMeta.getType().equals(float.class)) { if (table == null) {
methodAccess.invoke(object, fieldMeta.index, keyValue.tofloat()); return object;
} else if (fieldMeta.getType().equals(int.class)) { }
methodAccess.invoke(object, fieldMeta.index, keyValue.toint());
} else if (fieldMeta.getType().equals(String.class)) { LuaValue[] keys = table.keys();
methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring()); for (LuaValue k : keys) {
} else if (fieldMeta.getType().equals(boolean.class)) { try {
methodAccess.invoke(object, fieldMeta.index, keyValue.toboolean()); var keyName = k.checkjstring();
} else { if (!fieldMetaMap.containsKey(keyName)) {
methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring()); continue;
} }
} catch (Exception ex) { var fieldMeta = fieldMetaMap.get(keyName);
// ex.printStackTrace(); LuaValue keyValue = table.get(k);
continue;
} if (keyValue.istable()) {
} methodAccess.invoke(
} catch (Exception e) { object,
Grasscutter.getLogger().debug(ScriptUtils.toMap(table).toString()); fieldMeta.index,
e.printStackTrace(); serialize(fieldMeta.getType(), fieldMeta.getField(), keyValue.checktable()));
} } else if (fieldMeta.getType().equals(float.class)) {
methodAccess.invoke(object, fieldMeta.index, keyValue.tofloat());
return object; } else if (fieldMeta.getType().equals(int.class)) {
} methodAccess.invoke(object, fieldMeta.index, keyValue.toint());
} else if (fieldMeta.getType().equals(String.class)) {
public <T> Map<String, FieldMeta> cacheType(Class<T> type) { methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring());
if (fieldMetaCache.containsKey(type)) { } else if (fieldMeta.getType().equals(boolean.class)) {
return fieldMetaCache.get(type); methodAccess.invoke(object, fieldMeta.index, keyValue.toboolean());
} } else {
if (!constructorCache.containsKey(type)) { methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring());
constructorCache.putIfAbsent(type, ConstructorAccess.get(type)); }
} } catch (Exception ex) {
var methodAccess = // ex.printStackTrace();
Optional.ofNullable(methodAccessCache.get(type)).orElse(MethodAccess.get(type)); continue;
methodAccessCache.putIfAbsent(type, methodAccess); }
}
var fieldMetaMap = new HashMap<String, FieldMeta>(); } catch (Exception e) {
var methodNameSet = new HashSet<>(Arrays.stream(methodAccess.getMethodNames()).toList()); Grasscutter.getLogger().debug(ScriptUtils.toMap(table).toString());
e.printStackTrace();
Arrays.stream(type.getDeclaredFields()) }
.filter(field -> methodNameSet.contains(getSetterName(field.getName())))
.forEach( return object;
field -> { }
var setter = getSetterName(field.getName());
var index = methodAccess.getIndex(setter); public <T> Map<String, FieldMeta> cacheType(Class<T> type) {
fieldMetaMap.put( if (fieldMetaCache.containsKey(type)) {
field.getName(), new FieldMeta(field.getName(), setter, index, field.getType())); return fieldMetaCache.get(type);
}); }
if (!constructorCache.containsKey(type)) {
Arrays.stream(type.getFields()) constructorCache.putIfAbsent(type, ConstructorAccess.get(type));
.filter(field -> !fieldMetaMap.containsKey(field.getName())) }
.filter(field -> methodNameSet.contains(getSetterName(field.getName()))) var methodAccess =
.forEach( Optional.ofNullable(methodAccessCache.get(type)).orElse(MethodAccess.get(type));
field -> { methodAccessCache.putIfAbsent(type, methodAccess);
var setter = getSetterName(field.getName());
var index = methodAccess.getIndex(setter); var fieldMetaMap = new HashMap<String, FieldMeta>();
fieldMetaMap.put( var methodNameSet = new HashSet<>(Arrays.stream(methodAccess.getMethodNames()).toList());
field.getName(), new FieldMeta(field.getName(), setter, index, field.getType()));
}); Arrays.stream(type.getDeclaredFields())
.filter(field -> methodNameSet.contains(getSetterName(field.getName())))
fieldMetaCache.put(type, fieldMetaMap); .forEach(
return fieldMetaMap; field -> {
} var setter = getSetterName(field.getName());
var index = methodAccess.getIndex(setter);
public String getSetterName(String fieldName) { fieldMetaMap.put(
if (fieldName == null || fieldName.length() == 0) { field.getName(),
return null; new FieldMeta(field.getName(), setter, index, field.getType(), field));
} });
if (fieldName.length() == 1) {
return "set" + fieldName.toUpperCase(); Arrays.stream(type.getFields())
} .filter(field -> !fieldMetaMap.containsKey(field.getName()))
return "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); .filter(field -> methodNameSet.contains(getSetterName(field.getName())))
} .forEach(
field -> {
@Data var setter = getSetterName(field.getName());
@AllArgsConstructor var index = methodAccess.getIndex(setter);
@FieldDefaults(level = AccessLevel.PRIVATE) fieldMetaMap.put(
static class FieldMeta { field.getName(),
String name; new FieldMeta(field.getName(), setter, index, field.getType(), field));
String setter; });
int index;
Class<?> type; 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;
}
}

View File

@ -1,279 +1,281 @@
package emu.grasscutter.server.game; package emu.grasscutter.server.game;
import static emu.grasscutter.config.Configuration.GAME_INFO; import static emu.grasscutter.config.Configuration.GAME_INFO;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account; import emu.grasscutter.game.Account;
import emu.grasscutter.game.battlepass.BattlePassSystem; import emu.grasscutter.game.battlepass.BattlePassSystem;
import emu.grasscutter.game.chat.ChatSystem; import emu.grasscutter.game.chat.ChatSystem;
import emu.grasscutter.game.chat.ChatSystemHandler; import emu.grasscutter.game.chat.ChatSystemHandler;
import emu.grasscutter.game.combine.CombineManger; import emu.grasscutter.game.combine.CombineManger;
import emu.grasscutter.game.drop.DropSystem; import emu.grasscutter.game.drop.DropSystem;
import emu.grasscutter.game.dungeons.DungeonSystem; import emu.grasscutter.game.dungeons.DungeonSystem;
import emu.grasscutter.game.expedition.ExpeditionSystem; import emu.grasscutter.game.expedition.ExpeditionSystem;
import emu.grasscutter.game.gacha.GachaSystem; import emu.grasscutter.game.gacha.GachaSystem;
import emu.grasscutter.game.managers.cooking.CookingCompoundManager; import emu.grasscutter.game.managers.cooking.CookingCompoundManager;
import emu.grasscutter.game.managers.cooking.CookingManager; import emu.grasscutter.game.managers.cooking.CookingManager;
import emu.grasscutter.game.managers.energy.EnergyManager; import emu.grasscutter.game.managers.energy.EnergyManager;
import emu.grasscutter.game.managers.stamina.StaminaManager; import emu.grasscutter.game.managers.stamina.StaminaManager;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.QuestSystem; import emu.grasscutter.game.quest.QuestSystem;
import emu.grasscutter.game.shop.ShopSystem; import emu.grasscutter.game.shop.ShopSystem;
import emu.grasscutter.game.systems.AnnouncementSystem; import emu.grasscutter.game.systems.AnnouncementSystem;
import emu.grasscutter.game.systems.InventorySystem; import emu.grasscutter.game.systems.InventorySystem;
import emu.grasscutter.game.systems.MultiplayerSystem; import emu.grasscutter.game.systems.MultiplayerSystem;
import emu.grasscutter.game.tower.TowerSystem; import emu.grasscutter.game.tower.TowerSystem;
import emu.grasscutter.game.world.World; import emu.grasscutter.game.world.World;
import emu.grasscutter.game.world.WorldDataSystem; import emu.grasscutter.game.world.WorldDataSystem;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
import emu.grasscutter.server.event.game.ServerTickEvent; import emu.grasscutter.server.event.game.ServerTickEvent;
import emu.grasscutter.server.event.internal.ServerStartEvent; import emu.grasscutter.server.event.internal.ServerStartEvent;
import emu.grasscutter.server.event.internal.ServerStopEvent; import emu.grasscutter.server.event.internal.ServerStopEvent;
import emu.grasscutter.server.event.types.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
import emu.grasscutter.server.scheduler.ServerTaskScheduler; import emu.grasscutter.server.scheduler.ServerTaskScheduler;
import emu.grasscutter.task.TaskMap; import emu.grasscutter.task.TaskMap;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.time.Instant; import java.time.Instant;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import kcp.highway.ChannelConfig; import kcp.highway.ChannelConfig;
import kcp.highway.KcpServer; import kcp.highway.KcpServer;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
public final class GameServer extends KcpServer { public final class GameServer extends KcpServer {
// Game server base // Game server base
private final InetSocketAddress address; private final InetSocketAddress address;
private final GameServerPacketHandler packetHandler; private final GameServerPacketHandler packetHandler;
private final Map<Integer, Player> players; private final Map<Integer, Player> players;
private final Set<World> worlds; private final Set<World> worlds;
// Server systems // Server systems
private final InventorySystem inventorySystem; private final InventorySystem inventorySystem;
private final GachaSystem gachaSystem; private final GachaSystem gachaSystem;
private final ShopSystem shopSystem; private final ShopSystem shopSystem;
private final MultiplayerSystem multiplayerSystem; private final MultiplayerSystem multiplayerSystem;
private final DungeonSystem dungeonSystem; private final DungeonSystem dungeonSystem;
private final ExpeditionSystem expeditionSystem; private final ExpeditionSystem expeditionSystem;
private final DropSystem dropSystem; private final DropSystem dropSystem;
private final WorldDataSystem worldDataSystem; private final WorldDataSystem worldDataSystem;
private final BattlePassSystem battlePassSystem; private final BattlePassSystem battlePassSystem;
private final CombineManger combineSystem; private final CombineManger combineSystem;
private final TowerSystem towerSystem; private final TowerSystem towerSystem;
private final AnnouncementSystem announcementSystem; private final AnnouncementSystem announcementSystem;
private final QuestSystem questSystem; private final QuestSystem questSystem;
// Extra // Extra
private final ServerTaskScheduler scheduler; private final ServerTaskScheduler scheduler;
private final TaskMap taskMap; private final TaskMap taskMap;
private ChatSystemHandler chatManager; private ChatSystemHandler chatManager;
public GameServer() { public GameServer() {
this(getAdapterInetSocketAddress()); this(getAdapterInetSocketAddress());
} }
public GameServer(InetSocketAddress address) { public GameServer(InetSocketAddress address) {
ChannelConfig channelConfig = new ChannelConfig(); ChannelConfig channelConfig = new ChannelConfig();
channelConfig.nodelay(true, GAME_INFO.kcpInterval, 2, true); channelConfig.nodelay(true, GAME_INFO.kcpInterval, 2, true);
channelConfig.setMtu(1400); channelConfig.setMtu(1400);
channelConfig.setSndwnd(256); channelConfig.setSndwnd(256);
channelConfig.setRcvwnd(256); channelConfig.setRcvwnd(256);
channelConfig.setTimeoutMillis(30 * 1000); // 30s channelConfig.setTimeoutMillis(30 * 1000); // 30s
channelConfig.setUseConvChannel(true); channelConfig.setUseConvChannel(true);
channelConfig.setAckNoDelay(false); channelConfig.setAckNoDelay(false);
this.init(GameSessionManager.getListener(), channelConfig, address); this.init(GameSessionManager.getListener(), channelConfig, address);
EnergyManager.initialize(); EnergyManager.initialize();
StaminaManager.initialize(); StaminaManager.initialize();
CookingManager.initialize(); CookingManager.initialize();
CookingCompoundManager.initialize(); CookingCompoundManager.initialize();
CombineManger.initialize(); CombineManger.initialize();
// Game Server base // Game Server base
this.address = address; this.address = address;
this.packetHandler = new GameServerPacketHandler(PacketHandler.class); this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
this.players = new ConcurrentHashMap<>(); this.players = new ConcurrentHashMap<>();
this.worlds = Collections.synchronizedSet(new HashSet<>()); this.worlds = Collections.synchronizedSet(new HashSet<>());
// Extra // Extra
this.scheduler = new ServerTaskScheduler(); this.scheduler = new ServerTaskScheduler();
this.taskMap = new TaskMap(true); this.taskMap = new TaskMap(true);
// Create game systems // Create game systems
this.inventorySystem = new InventorySystem(this); this.inventorySystem = new InventorySystem(this);
this.gachaSystem = new GachaSystem(this); this.gachaSystem = new GachaSystem(this);
this.shopSystem = new ShopSystem(this); this.shopSystem = new ShopSystem(this);
this.multiplayerSystem = new MultiplayerSystem(this); this.multiplayerSystem = new MultiplayerSystem(this);
this.dungeonSystem = new DungeonSystem(this); this.dungeonSystem = new DungeonSystem(this);
this.dropSystem = new DropSystem(this); this.dropSystem = new DropSystem(this);
this.expeditionSystem = new ExpeditionSystem(this); this.expeditionSystem = new ExpeditionSystem(this);
this.combineSystem = new CombineManger(this); this.combineSystem = new CombineManger(this);
this.towerSystem = new TowerSystem(this); this.towerSystem = new TowerSystem(this);
this.worldDataSystem = new WorldDataSystem(this); this.worldDataSystem = new WorldDataSystem(this);
this.battlePassSystem = new BattlePassSystem(this); this.battlePassSystem = new BattlePassSystem(this);
this.announcementSystem = new AnnouncementSystem(this); this.announcementSystem = new AnnouncementSystem(this);
this.questSystem = new QuestSystem(this); this.questSystem = new QuestSystem(this);
// Chata manager // Chata manager
this.chatManager = new ChatSystem(this); this.chatManager = new ChatSystem(this);
// Hook into shutdown event. // Hook into shutdown event.
Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown));
} }
private static InetSocketAddress getAdapterInetSocketAddress() { private static InetSocketAddress getAdapterInetSocketAddress() {
InetSocketAddress inetSocketAddress; InetSocketAddress inetSocketAddress;
if (GAME_INFO.bindAddress.equals("")) { if (GAME_INFO.bindAddress.equals("")) {
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindPort); inetSocketAddress = new InetSocketAddress(GAME_INFO.bindPort);
} else { } else {
inetSocketAddress = new InetSocketAddress(GAME_INFO.bindAddress, GAME_INFO.bindPort); inetSocketAddress = new InetSocketAddress(GAME_INFO.bindAddress, GAME_INFO.bindPort);
} }
return inetSocketAddress; return inetSocketAddress;
} }
@Deprecated @Deprecated
public ChatSystemHandler getChatManager() { public ChatSystemHandler getChatManager() {
return chatManager; return chatManager;
} }
@Deprecated @Deprecated
public void setChatManager(ChatSystemHandler chatManager) { public void setChatManager(ChatSystemHandler chatManager) {
this.chatManager = chatManager; this.chatManager = chatManager;
} }
public ChatSystemHandler getChatSystem() { public ChatSystemHandler getChatSystem() {
return chatManager; return chatManager;
} }
public void setChatSystem(ChatSystemHandler chatManager) { public void setChatSystem(ChatSystemHandler chatManager) {
this.chatManager = chatManager; this.chatManager = chatManager;
} }
public void registerPlayer(Player player) { public void registerPlayer(Player player) {
getPlayers().put(player.getUid(), player); getPlayers().put(player.getUid(), player);
} }
public Player getPlayerByUid(int id) { public Player getPlayerByUid(int id) {
return this.getPlayerByUid(id, false); return this.getPlayerByUid(id, false);
} }
public Player getPlayerByUid(int id, boolean allowOfflinePlayers) { public Player getPlayerByUid(int id, boolean allowOfflinePlayers) {
// Console check // Console check
if (id == GameConstants.SERVER_CONSOLE_UID) { if (id == GameConstants.SERVER_CONSOLE_UID) {
return null; return null;
} }
// Get from online players // Get from online players
Player player = this.getPlayers().get(id); Player player = this.getPlayers().get(id);
if (!allowOfflinePlayers) { if (!allowOfflinePlayers) {
return player; return player;
} }
// Check database if character isnt here // Check database if character isnt here
if (player == null) { if (player == null) {
player = DatabaseHelper.getPlayerByUid(id); player = DatabaseHelper.getPlayerByUid(id);
} }
return player; return player;
} }
public Player getPlayerByAccountId(String accountId) { public Player getPlayerByAccountId(String accountId) {
Optional<Player> playerOpt = Optional<Player> playerOpt =
getPlayers().values().stream() getPlayers().values().stream()
.filter(player -> player.getAccount().getId().equals(accountId)) .filter(player -> player.getAccount().getId().equals(accountId))
.findFirst(); .findFirst();
return playerOpt.orElse(null); return playerOpt.orElse(null);
} }
public SocialDetail.Builder getSocialDetailByUid(int id) { public SocialDetail.Builder getSocialDetailByUid(int id) {
// Get from online players // Get from online players
Player player = this.getPlayerByUid(id, true); Player player = this.getPlayerByUid(id, true);
if (player == null) { if (player == null) {
return null; return null;
} }
return player.getSocialDetail(); return player.getSocialDetail();
} }
public Account getAccountByName(String username) { public Account getAccountByName(String username) {
Optional<Player> playerOpt = Optional<Player> playerOpt =
getPlayers().values().stream() getPlayers().values().stream()
.filter(player -> player.getAccount().getUsername().equals(username)) .filter(player -> player.getAccount().getUsername().equals(username))
.findFirst(); .findFirst();
if (playerOpt.isPresent()) { if (playerOpt.isPresent()) {
return playerOpt.get().getAccount(); return playerOpt.get().getAccount();
} }
return DatabaseHelper.getAccountByName(username); return DatabaseHelper.getAccountByName(username);
} }
public synchronized void onTick() { public synchronized void onTick() {
var tickStart = Instant.now(); var tickStart = Instant.now();
// Tick worlds. // Tick worlds.
this.worlds.removeIf(World::onTick); this.worlds.removeIf(World::onTick);
// Tick players. // Tick players.
this.players.values().forEach(Player::onTick); this.players.values().forEach(Player::onTick);
// Tick scheduler. // Tick scheduler.
this.getScheduler().runTasks(); this.getScheduler().runTasks();
// Call server tick event. // Call server tick event.
ServerTickEvent event = new ServerTickEvent(tickStart, Instant.now()); ServerTickEvent event = new ServerTickEvent(tickStart, Instant.now());
event.call(); event.call();
} }
public void registerWorld(World world) { public void registerWorld(World world) {
this.getWorlds().add(world); this.getWorlds().add(world);
} }
public void deregisterWorld(World world) { public void deregisterWorld(World world) {
// TODO Auto-generated method stub // TODO Auto-generated method stub
world.save(); //Save the player's world
} }
public void start() { public void start() {
// Schedule game loop. // Schedule game loop.
Timer gameLoop = new Timer(); Timer gameLoop = new Timer();
gameLoop.scheduleAtFixedRate( gameLoop.scheduleAtFixedRate(
new TimerTask() { new TimerTask() {
@Override @Override
public void run() { public void run() {
try { try {
onTick(); onTick();
} catch (Exception e) { } catch (Exception e) {
Grasscutter.getLogger().error(translate("messages.game.game_update_error"), e); Grasscutter.getLogger().error(translate("messages.game.game_update_error"), e);
} }
} }
}, },
new Date(), new Date(),
1000L); 1000L);
Grasscutter.getLogger().info(translate("messages.status.free_software")); Grasscutter.getLogger().info(translate("messages.status.free_software"));
Grasscutter.getLogger() Grasscutter.getLogger()
.info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort())); .info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort()));
ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now());
event.call(); event.call();
} }
public void onServerShutdown() { public void onServerShutdown() {
ServerStopEvent event = new ServerStopEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); ServerStopEvent event = new ServerStopEvent(ServerEvent.Type.GAME, OffsetDateTime.now());
event.call(); event.call();
// Kick and save all players // Kick and save all players
List<Player> list = new ArrayList<>(this.getPlayers().size()); List<Player> list = new ArrayList<>(this.getPlayers().size());
list.addAll(this.getPlayers().values()); list.addAll(this.getPlayers().values());
for (Player player : list) { for (Player player : list) {
player.getSession().close(); player.getSession().close();
} }
}
} getWorlds().forEach(World::save);
}
}

View File

@ -1,45 +1,61 @@
package emu.grasscutter.server.packet.recv; package emu.grasscutter.server.packet.recv;
import emu.grasscutter.game.quest.GameMainQuest; import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.QuestUpdateQuestVarReqOuterClass; import emu.grasscutter.net.proto.QuestUpdateQuestVarReqOuterClass.QuestUpdateQuestVarReq;
import emu.grasscutter.net.proto.QuestVarOpOuterClass; import emu.grasscutter.net.proto.QuestVarOpOuterClass.QuestVarOp;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.server.packet.send.PacketQuestUpdateQuestVarRsp; import emu.grasscutter.server.game.GameSession;
import java.util.List; import emu.grasscutter.server.packet.send.PacketQuestUpdateQuestVarRsp;
import java.util.List;
@Opcodes(PacketOpcodes.QuestUpdateQuestVarReq)
public class HandlerQuestUpdateQuestVarReq extends PacketHandler { @Opcodes(PacketOpcodes.QuestUpdateQuestVarReq)
public class HandlerQuestUpdateQuestVarReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { @Override
// Client sends packets. One with the value, and one with the index and the new value to public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
// set/inc/dec // Client sends packets. One with the value, and one with the index and the new value to
var req = QuestUpdateQuestVarReqOuterClass.QuestUpdateQuestVarReq.parseFrom(payload); // set/inc/dec
GameMainQuest mainQuest = var req = QuestUpdateQuestVarReq.parseFrom(payload);
session.getPlayer().getQuestManager().getMainQuestById(req.getQuestId() / 100); var questManager = session.getPlayer().getQuestManager();
List<QuestVarOpOuterClass.QuestVarOp> questVars = req.getQuestVarOpListList(); var subQuest = questManager.getQuestById(req.getQuestId());
if (mainQuest.getQuestVarsUpdate().size() == 0) { var mainQuest = questManager.getMainQuestById(req.getParentQuestId());
for (QuestVarOpOuterClass.QuestVarOp questVar : questVars) { if (mainQuest == null && subQuest != null) {
mainQuest.getQuestVarsUpdate().add(questVar.getValue()); mainQuest = subQuest.getMainQuest();
} }
} else {
for (QuestVarOpOuterClass.QuestVarOp questVar : questVars) { if (mainQuest == null) {
if (questVar.getIsAdd()) { session.send(new PacketQuestUpdateQuestVarRsp(req, Retcode.RET_QUEST_NOT_EXIST));
if (questVar.getValue() >= 0) { Grasscutter.getLogger()
mainQuest.incQuestVar(questVar.getIndex(), questVar.getValue()); .debug(
} else { "trying to update QuestVar for non existing quest s{} m{}",
mainQuest.decQuestVar(questVar.getIndex(), questVar.getValue()); req.getQuestId(),
} req.getParentQuestId());
} else { return;
mainQuest.setQuestVar(questVar.getIndex(), mainQuest.getQuestVarsUpdate().get(0)); }
} List<QuestVarOp> questVars = req.getQuestVarOpListList();
// remove the first element from the update list var questVarUpdate = mainQuest.getQuestVarsUpdate();
mainQuest.getQuestVarsUpdate().remove(0); if (questVarUpdate.size() == 0) {
} for (var questVar : questVars) {
} questVarUpdate.add(questVar.getValue());
session.send(new PacketQuestUpdateQuestVarRsp(req.getQuestId())); }
} } 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));
}
}

View File

@ -1,17 +1,25 @@
package emu.grasscutter.server.packet.send; package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.PlayerEnterDungeonRspOuterClass.PlayerEnterDungeonRsp; import emu.grasscutter.net.proto.PlayerEnterDungeonRspOuterClass.PlayerEnterDungeonRsp;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
public class PacketPlayerEnterDungeonRsp extends BasePacket {
public class PacketPlayerEnterDungeonRsp extends BasePacket {
public PacketPlayerEnterDungeonRsp(int pointId, int dungeonId) {
super(PacketOpcodes.PlayerEnterDungeonRsp); public PacketPlayerEnterDungeonRsp(int pointId, int dungeonId, boolean success) {
super(PacketOpcodes.PlayerEnterDungeonRsp);
PlayerEnterDungeonRsp proto =
PlayerEnterDungeonRsp.newBuilder().setPointId(pointId).setDungeonId(dungeonId).build(); PlayerEnterDungeonRsp proto =
PlayerEnterDungeonRsp.newBuilder()
this.setData(proto); .setPointId(pointId)
} .setDungeonId(dungeonId)
} .setRetcode(
success
? Retcode.RET_SUCC_VALUE
: Retcode.RET_FAIL_VALUE)
.build();
this.setData(proto);
}
}

View File

@ -1,32 +1,33 @@
package emu.grasscutter.server.packet.send; package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.PlayerHomeCompInfoNotifyOuterClass; import emu.grasscutter.net.proto.PlayerHomeCompInfoNotifyOuterClass;
import emu.grasscutter.net.proto.PlayerHomeCompInfoOuterClass; import emu.grasscutter.net.proto.PlayerHomeCompInfoOuterClass;
public class PacketPlayerHomeCompInfoNotify extends BasePacket { import java.util.List;
public PacketPlayerHomeCompInfoNotify(Player player) { public class PacketPlayerHomeCompInfoNotify extends BasePacket {
super(PacketOpcodes.PlayerHomeCompInfoNotify);
public PacketPlayerHomeCompInfoNotify(Player player) {
if (player.getRealmList() == null) { super(PacketOpcodes.PlayerHomeCompInfoNotify);
// Do not send
return; if (player.getRealmList() == null) {
} // Do not send
return;
PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify proto = }
PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify.newBuilder()
.setCompInfo( PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify proto =
PlayerHomeCompInfoOuterClass.PlayerHomeCompInfo.newBuilder() PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify.newBuilder()
.addAllUnlockedModuleIdList(player.getRealmList()) .setCompInfo(
.addAllSeenModuleIdList(player.getSeenRealmList()) PlayerHomeCompInfoOuterClass.PlayerHomeCompInfo.newBuilder()
.addAllLevelupRewardGotLevelList(player.getHomeRewardedLevels()) .addAllUnlockedModuleIdList(player.getRealmList())
.setFriendEnterHomeOptionValue(player.getHome().getEnterHomeOption()) .addAllLevelupRewardGotLevelList(List.of(1)) // Hardcoded
.build()) .setFriendEnterHomeOptionValue(player.getHome().getEnterHomeOption())
.build(); .build())
.build();
this.setData(proto);
} this.setData(proto);
} }
}

View File

@ -1,13 +1,16 @@
package emu.grasscutter.server.packet.send; package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.PlayerSetPauseRspOuterClass.PlayerSetPauseRsp;
public class PacketPlayerSetPauseRsp extends BasePacket { import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
public PacketPlayerSetPauseRsp(int clientSequence) { public class PacketPlayerSetPauseRsp extends BasePacket {
super(PacketOpcodes.PlayerSetPauseRsp);
public PacketPlayerSetPauseRsp() {
this.buildHeader(clientSequence); super(PacketOpcodes.PlayerSetPauseRsp);
}
} this.setData(PlayerSetPauseRsp.newBuilder()
.setRetcode(Retcode.RET_SUCC_VALUE));
}
}

View File

@ -1,19 +1,27 @@
package emu.grasscutter.server.packet.send; package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.QuestUpdateQuestVarRspOuterClass; import emu.grasscutter.net.proto.QuestUpdateQuestVarReqOuterClass.QuestUpdateQuestVarReq;
import emu.grasscutter.net.proto.QuestUpdateQuestVarRspOuterClass;
@Opcodes(PacketOpcodes.QuestUpdateQuestVarReq) import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
public class PacketQuestUpdateQuestVarRsp extends BasePacket {
@Opcodes(PacketOpcodes.QuestUpdateQuestVarReq)
public PacketQuestUpdateQuestVarRsp(int questId) { public class PacketQuestUpdateQuestVarRsp extends BasePacket {
super(PacketOpcodes.QuestUpdateQuestVarRsp);
var rsp = public PacketQuestUpdateQuestVarRsp(QuestUpdateQuestVarReq req) {
QuestUpdateQuestVarRspOuterClass.QuestUpdateQuestVarRsp.newBuilder() this(req, Retcode.RET_SUCC);
.setQuestId(questId) }
.build();
this.setData(rsp); 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);
}
}

View File

@ -1,31 +1,29 @@
package emu.grasscutter.server.packet.send; package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.ScenePointUnlockNotifyOuterClass.ScenePointUnlockNotify; import emu.grasscutter.net.proto.ScenePointUnlockNotifyOuterClass.ScenePointUnlockNotify;
public class PacketScenePointUnlockNotify extends BasePacket { public class PacketScenePointUnlockNotify extends BasePacket {
public PacketScenePointUnlockNotify(int sceneId, int pointId) { public PacketScenePointUnlockNotify(int sceneId, int pointId) {
super(PacketOpcodes.ScenePointUnlockNotify); super(PacketOpcodes.ScenePointUnlockNotify);
ScenePointUnlockNotify.Builder p = ScenePointUnlockNotify.Builder p =
ScenePointUnlockNotify.newBuilder() ScenePointUnlockNotify.newBuilder()
.setSceneId(sceneId) .setSceneId(sceneId)
.addPointList(pointId) .addPointList(pointId);
.addUnhidePointList(pointId);
this.setData(p);
this.setData(p); }
}
public PacketScenePointUnlockNotify(int sceneId, Iterable<Integer> pointIds) {
public PacketScenePointUnlockNotify(int sceneId, Iterable<Integer> pointIds) { super(PacketOpcodes.ScenePointUnlockNotify);
super(PacketOpcodes.ScenePointUnlockNotify);
ScenePointUnlockNotify.Builder p =
ScenePointUnlockNotify.Builder p = ScenePointUnlockNotify.newBuilder()
ScenePointUnlockNotify.newBuilder() .setSceneId(sceneId)
.setSceneId(sceneId) .addAllPointList(pointIds);
.addAllPointList(pointIds)
.addAllUnhidePointList(pointIds); this.setData(p);
}
this.setData(p); }
}
}

View File

@ -1,36 +1,27 @@
package emu.grasscutter.server.packet.send; package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.SceneTimeNotifyOuterClass.SceneTimeNotify; import emu.grasscutter.net.proto.SceneTimeNotifyOuterClass.SceneTimeNotify;
public class PacketSceneTimeNotify extends BasePacket { public class PacketSceneTimeNotify extends BasePacket {
public PacketSceneTimeNotify(Player player) { public PacketSceneTimeNotify(Player player) {
super(PacketOpcodes.SceneTimeNotify); this(player.getScene());
}
var proto =
SceneTimeNotify.newBuilder() public PacketSceneTimeNotify(Scene scene) {
.setIsPaused(player.isPaused()) super(PacketOpcodes.SceneTimeNotify);
.setSceneId(player.getSceneId())
.setSceneTime(player.getScene().getSceneTime()) var proto =
.build(); SceneTimeNotify.newBuilder()
.setSceneId(scene.getId())
this.setData(proto); .setSceneTime(scene.getSceneTime())
} .setIsPaused(scene.isPaused())
.build();
public PacketSceneTimeNotify(Scene scene) {
super(PacketOpcodes.SceneTimeNotify); this.setData(proto);
}
var proto = }
SceneTimeNotify.newBuilder()
.setSceneId(scene.getId())
.setSceneTime(scene.getSceneTime())
.setIsPaused(scene.isPaused())
.build();
this.setData(proto);
}
}

View File

@ -1,18 +1,29 @@
package emu.grasscutter.server.packet.send; package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.UnlockPersonalLineRspOuterClass; import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.net.proto.UnlockPersonalLineRspOuterClass;
public class PacketUnlockPersonalLineRsp extends BasePacket {
public class PacketUnlockPersonalLineRsp extends BasePacket {
public PacketUnlockPersonalLineRsp(int id, int level, int chapterId) {
super(PacketOpcodes.UnlockPersonalLineRsp); public PacketUnlockPersonalLineRsp(int id, int level, int chapterId) {
super(PacketOpcodes.UnlockPersonalLineRsp);
var proto = UnlockPersonalLineRspOuterClass.UnlockPersonalLineRsp.newBuilder();
var proto = UnlockPersonalLineRspOuterClass.UnlockPersonalLineRsp.newBuilder();
proto.setPersonalLineId(id).setLevel(level).setChapterId(chapterId);
proto.setPersonalLineId(id).setLevel(level).setChapterId(chapterId);
this.setData(proto);
} 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);
}
}

View File

@ -1,67 +1,73 @@
package emu.grasscutter.server.packet.send; package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.entity.EntityVehicle; import emu.grasscutter.game.entity.EntityVehicle;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.game.quest.enums.QuestContent;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.proto.VehicleInteractRspOuterClass.VehicleInteractRsp; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.VehicleInteractTypeOuterClass.VehicleInteractType; import emu.grasscutter.net.proto.VehicleInteractRspOuterClass.VehicleInteractRsp;
import emu.grasscutter.net.proto.VehicleMemberOuterClass.VehicleMember; import emu.grasscutter.net.proto.VehicleInteractTypeOuterClass.VehicleInteractType;
import emu.grasscutter.net.proto.VehicleMemberOuterClass.VehicleMember;
public class PacketVehicleInteractRsp extends BasePacket {
public class PacketVehicleInteractRsp extends BasePacket {
public PacketVehicleInteractRsp(Player player, int entityId, VehicleInteractType interactType) {
super(PacketOpcodes.VehicleInteractRsp); public PacketVehicleInteractRsp(Player player, int entityId, VehicleInteractType interactType) {
VehicleInteractRsp.Builder proto = VehicleInteractRsp.newBuilder(); super(PacketOpcodes.VehicleInteractRsp);
VehicleInteractRsp.Builder proto = VehicleInteractRsp.newBuilder();
GameEntity vehicle = player.getScene().getEntityById(entityId);
GameEntity vehicle = player.getScene().getEntityById(entityId);
if (vehicle instanceof EntityVehicle) {
proto.setEntityId(vehicle.getId()); if (vehicle instanceof EntityVehicle) {
proto.setEntityId(vehicle.getId());
VehicleMember vehicleMember =
VehicleMember.newBuilder() VehicleMember vehicleMember =
.setUid(player.getUid()) VehicleMember.newBuilder()
.setAvatarGuid(player.getTeamManager().getCurrentCharacterGuid()) .setUid(player.getUid())
.build(); .setAvatarGuid(player.getTeamManager().getCurrentCharacterGuid())
.build();
proto.setInteractType(interactType);
proto.setMember(vehicleMember); proto.setInteractType(interactType);
proto.setMember(vehicleMember);
switch (interactType) {
case VEHICLE_INTERACT_TYPE_IN -> { switch (interactType) {
((EntityVehicle) vehicle).getVehicleMembers().add(vehicleMember); case VEHICLE_INTERACT_TYPE_IN -> {
} ((EntityVehicle) vehicle).getVehicleMembers().add(vehicleMember);
case VEHICLE_INTERACT_TYPE_OUT -> { player
((EntityVehicle) vehicle).getVehicleMembers().remove(vehicleMember); .getQuestManager()
} .queueEvent(
default -> {} QuestContent.QUEST_CONTENT_ENTER_VEHICLE,
} ((EntityVehicle) vehicle).getGadgetId());
} }
this.setData(proto.build()); case VEHICLE_INTERACT_TYPE_OUT -> {
} ((EntityVehicle) vehicle).getVehicleMembers().remove(vehicleMember);
}
public PacketVehicleInteractRsp( default -> {}
EntityVehicle vehicle, VehicleMember vehicleMember, VehicleInteractType interactType) { }
super(PacketOpcodes.VehicleInteractRsp); }
VehicleInteractRsp.Builder proto = VehicleInteractRsp.newBuilder(); this.setData(proto.build());
}
if (vehicle != null) {
proto.setEntityId(vehicle.getId()); public PacketVehicleInteractRsp(
proto.setInteractType(interactType); EntityVehicle vehicle, VehicleMember vehicleMember, VehicleInteractType interactType) {
proto.setMember(vehicleMember); super(PacketOpcodes.VehicleInteractRsp);
VehicleInteractRsp.Builder proto = VehicleInteractRsp.newBuilder();
switch (interactType) {
case VEHICLE_INTERACT_TYPE_IN -> { if (vehicle != null) {
vehicle.getVehicleMembers().add(vehicleMember); proto.setEntityId(vehicle.getId());
} proto.setInteractType(interactType);
case VEHICLE_INTERACT_TYPE_OUT -> { proto.setMember(vehicleMember);
vehicle.getVehicleMembers().remove(vehicleMember);
} switch (interactType) {
default -> {} case VEHICLE_INTERACT_TYPE_IN -> {
} vehicle.getVehicleMembers().add(vehicleMember);
} }
this.setData(proto.build()); case VEHICLE_INTERACT_TYPE_OUT -> {
} vehicle.getVehicleMembers().remove(vehicleMember);
} }
default -> {}
}
}
this.setData(proto.build());
}
}

View File

@ -1,62 +1,66 @@
package emu.grasscutter.server.scheduler; package emu.grasscutter.server.scheduler;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import lombok.Getter; import lombok.Getter;
/** This class works the same as a runnable, except with more information. */ /** This class works the same as a runnable, except with more information. */
public final class ServerTask implements Runnable { public final class ServerTask implements Runnable {
/* The runnable to run. */ /* The runnable to run. */
private final Runnable runnable; private final Runnable runnable;
/* This ID is assigned by the scheduler. */ /* This ID is assigned by the scheduler. */
@Getter private final int taskId; @Getter private final int taskId;
/* The period at which the task should be run. */ /* The period at which the task should be run. */
/* The delay between the first execute. */ /* The delay between the first execute. */
private final int period, delay; private final int period, delay;
/* The amount of times the task has been run. */ /* The amount of times the task has been run. */
@Getter private int ticks = 0; @Getter private int ticks = 0;
/* Should the check consider delay? */ /* Should the check consider delay? */
private boolean considerDelay = true; private boolean considerDelay = true;
public ServerTask(Runnable runnable, int taskId, int period, int delay) { public ServerTask(Runnable runnable, int taskId, int period, int delay) {
this.runnable = runnable; this.runnable = runnable;
this.taskId = taskId; this.taskId = taskId;
this.period = period; this.period = period;
this.delay = delay; this.delay = delay;
} }
/** Cancels the task from running the next time. */ /** Cancels the task from running the next time. */
public void cancel() { public void cancel() {
Grasscutter.getGameServer().getScheduler().cancelTask(this.taskId); Grasscutter.getGameServer().getScheduler().cancelTask(this.taskId);
} }
/** /**
* Checks if the task should run at the current tick. * Checks if the task should run at the current tick.
* *
* @return True if the task should run, false otherwise. * @return True if the task should run, false otherwise.
*/ */
public boolean shouldRun() { public boolean shouldRun() {
if (this.delay != -1 && this.considerDelay) { // Increase tick count.
this.considerDelay = false; var ticks = this.ticks++;
return this.ticks == this.delay; if (this.delay != -1 && this.considerDelay) {
} else if (this.period != -1) return this.ticks % this.period == 0; this.considerDelay = false;
else return true; 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. * Checks if the task should be canceled.
*/ *
public boolean shouldCancel() { * @return True if the task should be canceled, false otherwise.
return this.period == -1; */
} public boolean shouldCancel() {
return this.period == -1 && ticks >= delay;
/** Runs the task. */ }
@Override
public void run() { /** Runs the task. */
// Run the runnable. @Override
this.runnable.run(); public void run() {
// Increase tick count. // Run the runnable.
this.ticks++; try {
} this.runnable.run();
} } catch (Exception ex) {
Grasscutter.getLogger().error("Exception during task: ", ex);
}
}
}

View File

@ -1,16 +1,14 @@
package emu.grasscutter.task; package emu.grasscutter.task;
import org.quartz.Job; import org.quartz.*;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution; @PersistJobDataAfterExecution
public abstract class TaskHandler implements Job {
@PersistJobDataAfterExecution public void restartExecute() throws JobExecutionException {
public abstract class TaskHandler implements Job { execute(null);
public void restartExecute() throws JobExecutionException { }
execute(null);
} public abstract void onEnable();
public abstract void onEnable(); public abstract void onDisable();
}
public abstract void onDisable();
}

View File

@ -1,391 +1,394 @@
package emu.grasscutter.tools; package emu.grasscutter.tools;
import static emu.grasscutter.utils.FileUtils.getResourcePath; import static emu.grasscutter.utils.FileUtils.getResourcePath;
import static emu.grasscutter.utils.Language.getTextMapKey; import static emu.grasscutter.utils.Language.getTextMapKey;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.CommandHandler; import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.CommandMap;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.data.excels.ItemData; import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.data.excels.achievement.AchievementData; import emu.grasscutter.data.excels.achievement.AchievementData;
import emu.grasscutter.data.excels.avatar.AvatarData; import emu.grasscutter.data.excels.avatar.AvatarData;
import emu.grasscutter.game.inventory.MaterialType; import emu.grasscutter.game.inventory.MaterialType;
import emu.grasscutter.utils.Language; import emu.grasscutter.utils.Language;
import emu.grasscutter.utils.Language.TextStrings; import emu.grasscutter.utils.Language.TextStrings;
import it.unimi.dsi.fastutil.ints.Int2IntRBTreeMap; import it.unimi.dsi.fastutil.ints.Int2IntRBTreeMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap; import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.stream.LongStream; import java.util.stream.LongStream;
import lombok.val; import lombok.val;
public final class Tools { public final class Tools {
/** /**
* This generates the GM handbooks with a message by default. * This generates the GM handbooks with a message by default.
* *
* @throws Exception If an error occurs while generating the handbooks. * @throws Exception If an error occurs while generating the handbooks.
*/ */
public static void createGmHandbooks() throws Exception { public static void createGmHandbooks() throws Exception {
Tools.createGmHandbooks(true); Tools.createGmHandbooks(true);
} }
/** /**
* Generates a GM handbook for each language. * Generates a GM handbook for each language.
* *
* @param message Should a message be printed to the console? * @param message Should a message be printed to the console?
* @throws Exception If an error occurs while generating the handbooks. * @throws Exception If an error occurs while generating the handbooks.
*/ */
public static void createGmHandbooks(boolean message) throws Exception { public static void createGmHandbooks(boolean message) throws Exception {
val languages = Language.TextStrings.getLanguages(); val languages = Language.TextStrings.getLanguages();
ResourceLoader.loadAll(); ResourceLoader.loadAll();
val mainQuestTitles = val mainQuestTitles =
new Int2IntRBTreeMap( new Int2IntRBTreeMap(
GameData.getMainQuestDataMap().int2ObjectEntrySet().stream() GameData.getMainQuestDataMap().int2ObjectEntrySet().stream()
.collect( .collect(
Collectors.toMap( Collectors.toMap(
e -> e.getIntKey(), e -> (int) e.getValue().getTitleTextMapHash()))); e -> e.getIntKey(), e -> (int) e.getValue().getTitleTextMapHash())));
// val questDescs = new // val questDescs = new
// Int2IntRBTreeMap(GameData.getQuestDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getDescTextMapHash()))); // Int2IntRBTreeMap(GameData.getQuestDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getDescTextMapHash())));
val avatarDataMap = new Int2ObjectRBTreeMap<>(GameData.getAvatarDataMap()); val avatarDataMap = new Int2ObjectRBTreeMap<>(GameData.getAvatarDataMap());
val itemDataMap = new Int2ObjectRBTreeMap<>(GameData.getItemDataMap()); val itemDataMap = new Int2ObjectRBTreeMap<>(GameData.getItemDataMap());
val monsterDataMap = new Int2ObjectRBTreeMap<>(GameData.getMonsterDataMap()); val monsterDataMap = new Int2ObjectRBTreeMap<>(GameData.getMonsterDataMap());
val sceneDataMap = new Int2ObjectRBTreeMap<>(GameData.getSceneDataMap()); val sceneDataMap = new Int2ObjectRBTreeMap<>(GameData.getSceneDataMap());
val questDataMap = new Int2ObjectRBTreeMap<>(GameData.getQuestDataMap()); val questDataMap = new Int2ObjectRBTreeMap<>(GameData.getQuestDataMap());
val achievementDataMap = new Int2ObjectRBTreeMap<>(GameData.getAchievementDataMap()); val achievementDataMap = new Int2ObjectRBTreeMap<>(GameData.getAchievementDataMap());
Function<SortedMap<?, ?>, String> getPad = m -> "%" + m.lastKey().toString().length() + "s : "; Function<SortedMap<?, ?>, String> getPad = m -> "%" + m.lastKey().toString().length() + "s : ";
// Create builders and helper functions // Create builders and helper functions
val handbookBuilders = val handbookBuilders =
IntStream.range(0, TextStrings.NUM_LANGUAGES).mapToObj(i -> new StringBuilder()).toList(); IntStream.range(0, TextStrings.NUM_LANGUAGES).mapToObj(i -> new StringBuilder()).toList();
var h = var h =
new Object() { new Object() {
void newLine(String line) { void newLine(String line) {
handbookBuilders.forEach(b -> b.append(line + "\n")); handbookBuilders.forEach(b -> b.append(line + "\n"));
} }
void newSection(String title) { void newSection(String title) {
newLine("\n\n// " + title); newLine("\n\n// " + title);
} }
void newTranslatedLine(String template, TextStrings... textstrings) { void newTranslatedLine(String template, TextStrings... textstrings) {
for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) {
String s = template; String s = template;
for (int j = 0; j < textstrings.length; j++) for (int j = 0; j < textstrings.length; j++)
s = s.replace("{" + j + "}", textstrings[j].strings[i]); s = s.replace("{" + j + "}", textstrings[j].strings[i]);
handbookBuilders.get(i).append(s + "\n"); handbookBuilders.get(i).append(s + "\n");
} }
} }
void newTranslatedLine(String template, long... hashes) { void newTranslatedLine(String template, long... hashes) {
newTranslatedLine( newTranslatedLine(
template, template,
LongStream.of(hashes) LongStream.of(hashes)
.mapToObj(hash -> getTextMapKey(hash)) .mapToObj(hash -> getTextMapKey(hash))
.toArray(TextStrings[]::new)); .toArray(TextStrings[]::new));
} }
}; };
// Preamble // Preamble
h.newLine("// Grasscutter " + GameConstants.VERSION + " GM Handbook"); h.newLine("// Grasscutter " + GameConstants.VERSION + " GM Handbook");
h.newLine( h.newLine(
"// Created " "// Created "
+ DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now())); + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now()));
// Commands // Commands
h.newSection("Commands"); h.newSection("Commands");
final List<CommandHandler> cmdList = CommandMap.getInstance().getHandlersAsList(); final List<CommandHandler> cmdList = CommandMap.getInstance().getHandlersAsList();
final String padCmdLabel = final String padCmdLabel =
"%" "%"
+ cmdList.stream() + cmdList.stream()
.map(CommandHandler::getLabel) .map(CommandHandler::getLabel)
.map(String::length) .map(String::length)
.max(Integer::compare) .max(Integer::compare)
.get() .get()
+ "s : "; + "s : ";
for (CommandHandler cmd : cmdList) { for (CommandHandler cmd : cmdList) {
final String label = padCmdLabel.formatted(cmd.getLabel()); final String label = padCmdLabel.formatted(cmd.getLabel());
final String descKey = cmd.getDescriptionKey(); final String descKey = cmd.getDescriptionKey();
for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) {
String desc = String desc =
languages.get(i).get(descKey).replace("\n", "\n\t\t\t\t").replace("\t", " "); languages.get(i).get(descKey).replace("\n", "\n\t\t\t\t").replace("\t", " ");
handbookBuilders.get(i).append(label + desc + "\n"); handbookBuilders.get(i).append(label + desc + "\n");
} }
} }
// Avatars // Avatars
h.newSection("Avatars"); h.newSection("Avatars");
val avatarPre = getPad.apply(avatarDataMap); val avatarPre = getPad.apply(avatarDataMap);
avatarDataMap.forEach( avatarDataMap.forEach(
(id, data) -> (id, data) ->
h.newTranslatedLine(avatarPre.formatted(id) + "{0}", data.getNameTextMapHash())); h.newTranslatedLine(avatarPre.formatted(id) + "{0}", data.getNameTextMapHash()));
// Items // Items
h.newSection("Items"); h.newSection("Items");
val itemPre = getPad.apply(itemDataMap); val itemPre = getPad.apply(itemDataMap);
itemDataMap.forEach( itemDataMap.forEach(
(id, data) -> { (id, data) -> {
val name = getTextMapKey(data.getNameTextMapHash()); val name = getTextMapKey(data.getNameTextMapHash());
if (Objects.requireNonNull(data.getMaterialType()) == MaterialType.MATERIAL_BGM) { switch (data.getMaterialType()) {
val bgmName = case MATERIAL_BGM:
Optional.ofNullable(data.getItemUse()) val bgmName =
.map(u -> u.get(0)) Optional.ofNullable(data.getItemUse())
.map(u -> u.getUseParam()) .map(u -> u.get(0))
.filter(u -> u.length > 0) .map(u -> u.getUseParam())
.map(u -> Integer.parseInt(u[0])) .filter(u -> u.length > 0)
.map(bgmId -> GameData.getHomeWorldBgmDataMap().get(bgmId)) .map(u -> Integer.parseInt(u[0]))
.map(bgm -> bgm.getBgmNameTextMapHash()) .map(bgmId -> GameData.getHomeWorldBgmDataMap().get(bgmId))
.map(hash -> getTextMapKey(hash)); .map(bgm -> bgm.getBgmNameTextMapHash())
if (bgmName.isPresent()) { .map(hash -> getTextMapKey(hash));
h.newTranslatedLine(itemPre.formatted(id) + "{0} - {1}", name, bgmName.get()); if (bgmName.isPresent()) {
return; h.newTranslatedLine(itemPre.formatted(id) + "{0} - {1}", name, bgmName.get());
} // Fall-through return;
} } // Fall-through
h.newTranslatedLine(itemPre.formatted(id) + "{0}", name); default:
}); h.newTranslatedLine(itemPre.formatted(id) + "{0}", name);
// Monsters return;
h.newSection("Monsters"); }
val monsterPre = getPad.apply(monsterDataMap); });
monsterDataMap.forEach( // Monsters
(id, data) -> h.newSection("Monsters");
h.newTranslatedLine( val monsterPre = getPad.apply(monsterDataMap);
monsterPre.formatted(id) + data.getMonsterName() + " - {0}", monsterDataMap.forEach(
data.getNameTextMapHash())); (id, data) ->
// Scenes - no translations h.newTranslatedLine(
h.newSection("Scenes"); monsterPre.formatted(id) + data.getMonsterName() + " - {0}",
val padSceneId = getPad.apply(sceneDataMap); data.getNameTextMapHash()));
sceneDataMap.forEach((id, data) -> h.newLine(padSceneId.formatted(id) + data.getScriptData())); // Scenes - no translations
// Quests h.newSection("Scenes");
h.newSection("Quests"); val padSceneId = getPad.apply(sceneDataMap);
val padQuestId = getPad.apply(questDataMap); sceneDataMap.forEach((id, data) -> h.newLine(padSceneId.formatted(id) + data.getScriptData()));
questDataMap.forEach( // Quests
(id, data) -> h.newSection("Quests");
h.newTranslatedLine( val padQuestId = getPad.apply(questDataMap);
padQuestId.formatted(id) + "{0} - {1}", questDataMap.forEach(
mainQuestTitles.get(data.getMainId()), (id, data) ->
data.getDescTextMapHash())); h.newTranslatedLine(
// Achievements padQuestId.formatted(id) + "{0} - {1}",
h.newSection("Achievements"); mainQuestTitles.get(data.getMainId()),
val padAchievementId = getPad.apply(achievementDataMap); data.getDescTextMapHash()));
achievementDataMap.values().stream() // Achievements
.filter(AchievementData::isUsed) h.newSection("Achievements");
.forEach( val padAchievementId = getPad.apply(achievementDataMap);
data -> { achievementDataMap.values().stream()
h.newTranslatedLine( .filter(AchievementData::isUsed)
padAchievementId.formatted(data.getId()) + "{0} - {1}", .forEach(
data.getTitleTextMapHash(), data -> {
data.getDescTextMapHash()); h.newTranslatedLine(
}); padAchievementId.formatted(data.getId()) + "{0} - {1}",
data.getTitleTextMapHash(),
// Write txt files data.getDescTextMapHash());
for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { });
File GMHandbookOutputpath = new File("./GM Handbook");
GMHandbookOutputpath.mkdir(); // Write txt files
final String fileName = for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) {
"./GM Handbook/GM Handbook - %s.txt".formatted(TextStrings.ARR_LANGUAGES[i]); File GMHandbookOutputpath = new File("./GM Handbook");
try (PrintWriter writer = GMHandbookOutputpath.mkdir();
new PrintWriter( final String fileName =
new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8), "./GM Handbook/GM Handbook - %s.txt".formatted(TextStrings.ARR_LANGUAGES[i]);
false)) { try (PrintWriter writer =
writer.write(handbookBuilders.get(i).toString()); 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<String> createGachaMappingJsons() { if (message) Grasscutter.getLogger().info("GM Handbooks generated!");
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 public static List<String> createGachaMappingJsons() {
final Language.TextStrings STANDARD_WISH = final int NUM_LANGUAGES = Language.TextStrings.NUM_LANGUAGES;
Language.getTextMapKey(332935371L); // "Standard Wish" in EN final Language.TextStrings CHARACTER = Language.getTextMapKey(4233146695L); // "Character" in EN
final Language.TextStrings CHARACTER_EVENT_WISH = final Language.TextStrings WEAPON = Language.getTextMapKey(4231343903L); // "Weapon" in EN
Language.getTextMapKey(2272170627L); // "Character Event Wish" in EN final Language.TextStrings STANDARD_WISH =
final Language.TextStrings CHARACTER_EVENT_WISH_2 = Language.getTextMapKey(332935371L); // "Standard Wish" in EN
Language.getTextMapKey(3352513147L); // "Character Event Wish-2" in EN final Language.TextStrings CHARACTER_EVENT_WISH =
final Language.TextStrings WEAPON_EVENT_WISH = Language.getTextMapKey(2272170627L); // "Character Event Wish" in EN
Language.getTextMapKey(2864268523L); // "Weapon Event Wish" in EN final Language.TextStrings CHARACTER_EVENT_WISH_2 =
final List<StringBuilder> sbs = new ArrayList<>(NUM_LANGUAGES); Language.getTextMapKey(3352513147L); // "Character Event Wish-2" in EN
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) final Language.TextStrings WEAPON_EVENT_WISH =
sbs.add(new StringBuilder("{\n")); // Web requests should never need Windows line endings Language.getTextMapKey(2864268523L); // "Weapon Event Wish" in EN
final List<StringBuilder> sbs = new ArrayList<>(NUM_LANGUAGES);
// Avatars for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
GameData.getAvatarDataMap() sbs.add(new StringBuilder("{\n")); // Web requests should never need Windows line endings
.keySet()
.intStream() // Avatars
.sorted() GameData.getAvatarDataMap()
.forEach( .keySet()
id -> { .intStream()
AvatarData data = GameData.getAvatarDataMap().get(id); .sorted()
int avatarID = data.getId(); .forEach(
if (avatarID >= 11000000) { // skip test avatar id -> {
return; AvatarData data = GameData.getAvatarDataMap().get(id);
} int avatarID = data.getId();
String color = if (avatarID >= 11000000) { // skip test avatar
switch (data.getQualityType()) { return;
case "QUALITY_PURPLE" -> "purple"; }
case "QUALITY_ORANGE" -> "yellow"; String color =
case "QUALITY_BLUE" -> "blue"; switch (data.getQualityType()) {
default -> ""; case "QUALITY_PURPLE" -> "purple";
}; case "QUALITY_ORANGE" -> "yellow";
Language.TextStrings avatarName = Language.getTextMapKey(data.getNameTextMapHash()); case "QUALITY_BLUE" -> "blue";
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) { default -> "";
sbs.get(langIdx) };
.append("\t\"") Language.TextStrings avatarName = Language.getTextMapKey(data.getNameTextMapHash());
.append(avatarID % 1000 + 1000) for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
.append("\": [\"") sbs.get(langIdx)
.append(avatarName.get(langIdx)) .append("\t\"")
.append(" (") .append(avatarID % 1000 + 1000)
.append(CHARACTER.get(langIdx)) .append("\": [\"")
.append(")\", \"") .append(avatarName.get(langIdx))
.append(color) .append(" (")
.append("\"],\n"); .append(CHARACTER.get(langIdx))
} .append(")\", \"")
}); .append(color)
.append("\"],\n");
// Weapons }
GameData.getItemDataMap() });
.keySet()
.intStream() // Weapons
.sorted() GameData.getItemDataMap()
.forEach( .keySet()
id -> { .intStream()
ItemData data = GameData.getItemDataMap().get(id); .sorted()
if (data.getId() <= 11101 || data.getId() >= 20000) { .forEach(
return; // skip non weapon items id -> {
} ItemData data = GameData.getItemDataMap().get(id);
String color = if (data.getId() <= 11101 || data.getId() >= 20000) {
switch (data.getRankLevel()) { return; // skip non weapon items
case 3 -> "blue"; }
case 4 -> "purple"; String color =
case 5 -> "yellow"; switch (data.getRankLevel()) {
default -> null; case 3 -> "blue";
}; case 4 -> "purple";
if (color == null) return; // skip unnecessary entries case 5 -> "yellow";
Language.TextStrings weaponName = Language.getTextMapKey(data.getNameTextMapHash()); default -> null;
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) { };
sbs.get(langIdx) if (color == null) return; // skip unnecessary entries
.append("\t\"") Language.TextStrings weaponName = Language.getTextMapKey(data.getNameTextMapHash());
.append(data.getId()) for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
.append("\": [\"") sbs.get(langIdx)
.append(weaponName.get(langIdx).replaceAll("\"", "\\\\\"")) .append("\t\"")
.append(" (") .append(data.getId())
.append(WEAPON.get(langIdx)) .append("\": [\"")
.append(")\", \"") .append(weaponName.get(langIdx).replaceAll("\"", "\\\\\""))
.append(color) .append(" (")
.append("\"],\n"); .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)) for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
.append("\",\n\t\"301\": \"") sbs.get(langIdx)
.append(CHARACTER_EVENT_WISH.get(langIdx)) .append("\t\"200\": \"")
.append("\",\n\t\"400\": \"") .append(STANDARD_WISH.get(langIdx))
.append(CHARACTER_EVENT_WISH_2.get(langIdx)) .append("\",\n\t\"301\": \"")
.append("\",\n\t\"302\": \"") .append(CHARACTER_EVENT_WISH.get(langIdx))
.append(WEAPON_EVENT_WISH.get(langIdx)) .append("\",\n\t\"400\": \"")
.append("\"\n}"); .append(CHARACTER_EVENT_WISH_2.get(langIdx))
} .append("\",\n\t\"302\": \"")
return sbs.stream().map(StringBuilder::toString).toList(); .append(WEAPON_EVENT_WISH.get(langIdx))
} .append("\"\n}");
}
public static void createGachaMappings(Path location) throws IOException { return sbs.stream().map(StringBuilder::toString).toList();
ResourceLoader.loadResources(); }
List<String> jsons = createGachaMappingJsons();
var usedLocales = new HashSet<String>(); public static void createGachaMappings(Path location) throws IOException {
StringBuilder sb = new StringBuilder("mappings = {\n"); ResourceLoader.loadResources();
for (int i = 0; i < Language.TextStrings.NUM_LANGUAGES; i++) { List<String> jsons = createGachaMappingJsons();
String locale = var usedLocales = new HashSet<String>();
Language.TextStrings.ARR_GC_LANGUAGES[i] StringBuilder sb = new StringBuilder("mappings = {\n");
.toLowerCase(); // TODO: change the templates to not use lowercased locale codes for (int i = 0; i < Language.TextStrings.NUM_LANGUAGES; i++) {
if (usedLocales.add( String locale =
locale)) { // Some locales fallback to en-us, we don't want to redefine en-us with Language.TextStrings.ARR_GC_LANGUAGES[i]
// vietnamese strings .toLowerCase(); // TODO: change the templates to not use lowercased locale codes
sb.append("\t\"%s\": ".formatted(locale)); if (usedLocales.add(
sb.append(jsons.get(i).replace("\n", "\n\t") + ",\n"); 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.setLength(sb.length() - 2); // Delete trailing ",\n" sb.append(jsons.get(i).replace("\n", "\n\t") + ",\n");
sb.append("\n}"); }
}
Files.createDirectories(location.getParent()); sb.setLength(sb.length() - 2); // Delete trailing ",\n"
Files.writeString(location, sb); sb.append("\n}");
Grasscutter.getLogger().debug("Mappings generated to " + location);
} Files.createDirectories(location.getParent());
Files.writeString(location, sb);
public static List<String> getAvailableLanguage() { Grasscutter.getLogger().debug("Mappings generated to " + location);
List<String> availableLangList = new ArrayList<>(); }
try {
Files.newDirectoryStream(getResourcePath("TextMap"), "TextMap*.json") public static List<String> getAvailableLanguage() {
.forEach( List<String> availableLangList = new ArrayList<>();
path -> { try {
availableLangList.add( Files.newDirectoryStream(getResourcePath("TextMap"), "TextMap*.json")
path.getFileName() .forEach(
.toString() path -> {
.replace("TextMap", "") availableLangList.add(
.replace(".json", "") path.getFileName()
.toLowerCase()); .toString()
}); .replace("TextMap", "")
} catch (IOException e) { .replace(".json", "")
Grasscutter.getLogger().error("Failed to get available languages:", e); .toLowerCase());
} });
return availableLangList; } catch (IOException e) {
} Grasscutter.getLogger().error("Failed to get available languages:", e);
}
@Deprecated(forRemoval = true, since = "1.2.3") return availableLangList;
public static String getLanguageOption() { }
List<String> availableLangList = getAvailableLanguage();
@Deprecated(forRemoval = true, since = "1.2.3")
// Use system out for better format public static String getLanguageOption() {
if (availableLangList.size() == 1) { List<String> availableLangList = getAvailableLanguage();
return availableLangList.get(0).toUpperCase();
} // Use system out for better format
StringBuilder stagedMessage = new StringBuilder(); if (availableLangList.size() == 1) {
stagedMessage.append( return availableLangList.get(0).toUpperCase();
"The following languages mappings are available, please select one: [default: EN] \n"); }
StringBuilder stagedMessage = new StringBuilder();
StringBuilder groupedLangList = new StringBuilder(">\t"); stagedMessage.append(
String input; "The following languages mappings are available, please select one: [default: EN] \n");
int groupedLangCount = 0;
StringBuilder groupedLangList = new StringBuilder(">\t");
for (String availableLanguage : availableLangList) { String input;
groupedLangCount++; int groupedLangCount = 0;
groupedLangList.append(availableLanguage).append("\t");
for (String availableLanguage : availableLangList) {
if (groupedLangCount == 6) { groupedLangCount++;
stagedMessage.append(groupedLangList).append("\n"); groupedLangList.append(availableLanguage).append("\t");
groupedLangCount = 0;
groupedLangList = new StringBuilder(">\t"); if (groupedLangCount == 6) {
} stagedMessage.append(groupedLangList).append("\n");
} groupedLangCount = 0;
groupedLangList = new StringBuilder(">\t");
if (groupedLangCount > 0) { }
stagedMessage.append(groupedLangList).append("\n"); }
}
if (groupedLangCount > 0) {
stagedMessage.append("\nYour choice: [EN] "); stagedMessage.append(groupedLangList).append("\n");
}
input = Grasscutter.getConsole().readLine(stagedMessage.toString());
if (availableLangList.contains(input.toLowerCase())) { stagedMessage.append("\nYour choice: [EN] ");
return input.toUpperCase();
} input = Grasscutter.getConsole().readLine(stagedMessage.toString());
if (availableLangList.contains(input.toLowerCase())) {
Grasscutter.getLogger().info("Invalid option. Will use EN (English) as fallback."); return input.toUpperCase();
return "EN"; }
}
} Grasscutter.getLogger().info("Invalid option. Will use EN (English) as fallback.");
return "EN";
}
}