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

@ -25,6 +25,8 @@ public class CommandHelpers {
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 final Pattern suiteRegex = Pattern.compile("suite(\\d+)");
public static int matchIntOrNeg(Pattern pattern, String arg) { public static int matchIntOrNeg(Pattern pattern, String arg) {
Matcher match = pattern.matcher(arg); Matcher match = pattern.matcher(arg);

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));
}
}

View File

@ -20,6 +20,7 @@ import emu.grasscutter.data.excels.world.*;
import emu.grasscutter.data.server.*; import emu.grasscutter.data.server.*;
import emu.grasscutter.game.dungeons.*; import emu.grasscutter.game.dungeons.*;
import emu.grasscutter.game.quest.*; import emu.grasscutter.game.quest.*;
import emu.grasscutter.game.quest.enums.QuestCond;
import emu.grasscutter.game.world.*; import emu.grasscutter.game.world.*;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
@ -611,6 +612,11 @@ public final class GameData {
return datamap.get(dataId); return datamap.get(dataId);
} }
@Nullable public static List<QuestData> getQuestDataByConditions(
QuestCond questCond, int param0, String questStr) {
return beginCondQuestMap.get(QuestData.questConditionKey(questCond, param0, questStr));
}
public static Int2ObjectMap<AchievementData> getAchievementDataMap() { public static Int2ObjectMap<AchievementData> getAchievementDataMap() {
AchievementData.divideIntoGroups(); AchievementData.divideIntoGroups();
return achievementDataMap; return achievementDataMap;

View File

@ -5,8 +5,8 @@ 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,12 +1,13 @@
package emu.grasscutter.game.props; package emu.grasscutter.game.props;
import emu.grasscutter.scripts.constants.IntValueEnum;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
public enum EntityType { public enum EntityType implements IntValueEnum {
None(0), None(0),
Avatar(1), Avatar(1),
Monster(2), Monster(2),

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

@ -122,6 +122,7 @@ public class GameQuest {
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) { public String getTriggerNameById(int id) {
@ -226,6 +227,9 @@ public class GameQuest {
if (getQuestData().getSubId() == 35402) { if (getQuestData().getSubId() == 35402) {
getOwner().getInventory().addItem(1021, 1, ActionReason.QuestItem); // amber item id getOwner().getInventory().addItem(1021, 1, ActionReason.QuestItem); // amber item id
} }
this.save();
Grasscutter.getLogger().debug("Quest {} is finished", subQuestId); Grasscutter.getLogger().debug("Quest {} is finished", subQuestId);
} }

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

@ -11,6 +11,9 @@ 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

@ -11,7 +11,7 @@ import emu.grasscutter.game.quest.handlers.QuestExecHandler;
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

@ -15,6 +15,8 @@ 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 org.luaj.vm2.LuaError;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -73,9 +75,14 @@ public class WorldDataSystem extends BaseGameSystem {
private SceneGroup getInvestigationGroup(int sceneId, int groupId) { private SceneGroup getInvestigationGroup(int sceneId, int groupId) {
var key = sceneId + "_" + groupId; var key = sceneId + "_" + groupId;
if (!sceneInvestigationGroupMap.containsKey(key)) { if (!sceneInvestigationGroupMap.containsKey(key)) {
try {
var group = SceneGroup.of(groupId).load(sceneId); var group = SceneGroup.of(groupId).load(sceneId);
sceneInvestigationGroupMap.putIfAbsent(key, group); sceneInvestigationGroupMap.putIfAbsent(key, group);
return group; return group;
} catch (LuaError luaError) {
Grasscutter.getLogger()
.error("failed to get investigationGroup {} in scene{}:", groupId, sceneId, luaError);
}
} }
return sceneInvestigationGroupMap.get(key); return sceneInvestigationGroupMap.get(key);
} }

View File

@ -256,7 +256,7 @@ public class SceneScriptManager {
return suiteIndex; return suiteIndex;
} }
public boolean refreshGroupSuite(int groupId, int suiteId, GameQuest quest) { public boolean refreshGroupSuite(int groupId, int suiteId) {
var targetGroupInstance = getGroupInstanceById(groupId); var targetGroupInstance = getGroupInstanceById(groupId);
if (targetGroupInstance == null) { if (targetGroupInstance == null) {
getGroupById( getGroupById(
@ -276,9 +276,14 @@ public class SceneScriptManager {
targetGroupInstance, targetGroupInstance,
suiteId, suiteId,
false); // If suiteId is zero, the value of suiteId changes false); // If suiteId is zero, the value of suiteId changes
quest.getOwner().sendPacket(new PacketGroupSuiteNotify(groupId, suiteId)); scene.broadcastPacket(new PacketGroupSuiteNotify(groupId, suiteId));
} }
return true;
}
public boolean refreshGroupSuite(int groupId, int suiteId, GameQuest quest) {
var result = refreshGroupSuite(groupId, suiteId);
if (suiteId != 0 && quest != null) { if (suiteId != 0 && quest != null) {
quest quest
.getMainQuest() .getMainQuest()
@ -287,7 +292,7 @@ public class SceneScriptManager {
QuestGroupSuite.of().scene(getScene().getId()).group(groupId).suite(suiteId).build()); QuestGroupSuite.of().scene(getScene().getId()).group(groupId).suite(suiteId).build());
} }
return true; return result;
} }
public boolean refreshGroupMonster(int groupId) { public boolean refreshGroupMonster(int groupId) {
@ -449,6 +454,8 @@ public class SceneScriptManager {
// Add all entitites here // Add all entitites here
Set<Integer> vision_levels = new HashSet<>(); Set<Integer> vision_levels = new HashSet<>();
if (group.monsters != null) {
group group
.monsters .monsters
.values() .values()
@ -461,13 +468,19 @@ public class SceneScriptManager {
m.pos); m.pos);
vision_levels.add(m.vision_level); vision_levels.add(m.vision_level);
}); });
} else {
Grasscutter.getLogger()
.error("group.monsters null for group {}", group.id);
}
if (group.gadgets != null) {
group group
.gadgets .gadgets
.values() .values()
.forEach( .forEach(
g -> { g -> {
int vision_level = int vision_level =
Math.max(getGadgetVisionLevel(g.gadget_id), g.vision_level); Math.max(
getGadgetVisionLevel(g.gadget_id), g.vision_level);
addGridPositionToMap( addGridPositionToMap(
groupPositions.get(vision_level), groupPositions.get(vision_level),
group.id, group.id,
@ -475,6 +488,12 @@ public class SceneScriptManager {
g.pos); g.pos);
vision_levels.add(vision_level); vision_levels.add(vision_level);
}); });
} else {
Grasscutter.getLogger()
.error("group.gadgets null for group {}", group.id);
}
if (group.npcs != null) {
group group
.npcs .npcs
.values() .values()
@ -485,6 +504,11 @@ public class SceneScriptManager {
group.id, group.id,
n.vision_level, n.vision_level,
n.pos)); n.pos));
} else {
Grasscutter.getLogger().error("group.npcs null for group {}", group.id);
}
if (group.regions != null) {
group group
.regions .regions
.values() .values()
@ -492,6 +516,11 @@ public class SceneScriptManager {
r -> r ->
addGridPositionToMap( addGridPositionToMap(
groupPositions.get(0), group.id, 0, r.pos)); groupPositions.get(0), group.id, 0, r.pos));
} else {
Grasscutter.getLogger()
.error("group.regions null for group {}", group.id);
}
if (group.garbages != null && group.garbages.gadgets != null) if (group.garbages != null && group.garbages.gadgets != null)
group.garbages.gadgets.forEach( group.garbages.gadgets.forEach(
g -> g ->
@ -762,20 +791,21 @@ public class SceneScriptManager {
int eventType = params.type; int eventType = params.type;
Set<SceneTrigger> relevantTriggers = new HashSet<>(); Set<SceneTrigger> relevantTriggers = new HashSet<>();
if (eventType == EventType.EVENT_ENTER_REGION || eventType == EventType.EVENT_LEAVE_REGION) { if (eventType == EventType.EVENT_ENTER_REGION || eventType == EventType.EVENT_LEAVE_REGION) {
List<SceneTrigger> relevantTriggersList = relevantTriggers =
this.getTriggersByEvent(eventType).stream() this.getTriggersByEvent(eventType).stream()
.filter( .filter(
p -> t ->
p.getCondition().contains(String.valueOf(params.param1)) t.getCondition().contains(String.valueOf(params.param1))
&& (p.getSource().isEmpty() && (t.getSource().isEmpty()
|| p.getSource().equals(params.getEventSource()))) || t.getSource().equals(params.getEventSource())))
.toList(); .collect(Collectors.toSet());
relevantTriggers = new HashSet<>(relevantTriggersList);
} else { } else {
relevantTriggers = relevantTriggers =
this.getTriggersByEvent(eventType).stream() this.getTriggersByEvent(eventType).stream()
.filter( .filter(
t -> params.getGroupId() == 0 || t.getCurrentGroup().id == params.getGroupId()) t -> params.getGroupId() == 0 || t.getCurrentGroup().id == params.getGroupId())
.filter(
t -> (t.getSource().isEmpty() || t.getSource().equals(params.getEventSource())))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
for (SceneTrigger trigger : relevantTriggers) { for (SceneTrigger trigger : relevantTriggers) {
@ -1085,6 +1115,7 @@ public class SceneScriptManager {
source, source,
groupID, groupID,
trigger.getName()); trigger.getName());
this.cancelGroupTimerEvent(groupID, source);
var taskIdentifier = var taskIdentifier =
Grasscutter.getGameServer() Grasscutter.getGameServer()
.getScheduler() .getScheduler()
@ -1104,13 +1135,15 @@ public class SceneScriptManager {
public int cancelGroupTimerEvent(int groupID, String source) { public int cancelGroupTimerEvent(int groupID, String source) {
// TODO test // TODO test
var groupTimers = activeGroupTimers.get(groupID); var groupTimers = activeGroupTimers.get(groupID);
if (groupTimers != null && !groupTimers.isEmpty()) if (groupTimers != null && !groupTimers.isEmpty()) {
for (var timer : groupTimers) { for (var timer : new HashSet<>(groupTimers)) {
if (timer.component1().equals(source)) { if (timer.component1().equals(source)) {
Grasscutter.getGameServer().getScheduler().cancelTask(timer.component2()); Grasscutter.getGameServer().getScheduler().cancelTask(timer.component2());
groupTimers.remove(timer);
return 0; return 0;
} }
} }
}
Grasscutter.getLogger() Grasscutter.getLogger()
.warn("trying to cancel a timer that's not active {} {}", groupID, source); .warn("trying to cancel a timer that's not active {} {}", groupID, source);

View File

@ -1,11 +1,12 @@
package emu.grasscutter.scripts; package emu.grasscutter.scripts;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.dungeons.challenge.enums.ChallengeEventMarkType;
import emu.grasscutter.game.dungeons.challenge.enums.FatherChallengeProperty;
import emu.grasscutter.game.props.ElementType;
import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.EntityType;
import emu.grasscutter.game.quest.enums.QuestState; import emu.grasscutter.game.quest.enums.QuestState;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.constants.*;
import emu.grasscutter.scripts.constants.ScriptGadgetState;
import emu.grasscutter.scripts.constants.ScriptRegionShape;
import emu.grasscutter.scripts.data.SceneMeta; import emu.grasscutter.scripts.data.SceneMeta;
import emu.grasscutter.scripts.serializer.LuaSerializer; import emu.grasscutter.scripts.serializer.LuaSerializer;
import emu.grasscutter.scripts.serializer.Serializer; import emu.grasscutter.scripts.serializer.Serializer;
@ -65,15 +66,14 @@ public class ScriptLoader {
} }
}); });
LuaTable table = new LuaTable(); addEnumByIntValue(ctx, EntityType.values(), "EntityType");
Arrays.stream(EntityType.values()) addEnumByIntValue(ctx, QuestState.values(), "QuestState");
.forEach(e -> table.set(e.name().toUpperCase(), e.getValue())); addEnumByIntValue(ctx, ElementType.values(), "ElementType");
ctx.globals.set("EntityType", table);
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",
@ -88,6 +88,30 @@ public class ScriptLoader {
ctx.globals.set("ScriptLib", scriptLibLua); ctx.globals.set("ScriptLib", scriptLibLua);
} }
private static <T extends Enum<T>> void addEnumByOrdinal(
LuajContext ctx, T[] enumArray, String name) {
LuaTable table = new LuaTable();
Arrays.stream(enumArray)
.forEach(
e -> {
table.set(e.name(), e.ordinal());
table.set(e.name().toUpperCase(), e.ordinal());
});
ctx.globals.set(name, table);
}
private static <T extends Enum<T> & IntValueEnum> void addEnumByIntValue(
LuajContext ctx, T[] enumArray, String name) {
LuaTable table = new LuaTable();
Arrays.stream(enumArray)
.forEach(
e -> {
table.set(e.name(), e.getValue());
table.set(e.name().toUpperCase(), e.getValue());
});
ctx.globals.set(name, table);
}
public static <T> Optional<T> tryGet(SoftReference<T> softReference) { public static <T> Optional<T> tryGet(SoftReference<T> softReference) {
try { try {
return Optional.ofNullable(softReference.get()); return Optional.ofNullable(softReference.get());
@ -103,7 +127,7 @@ public class ScriptLoader {
return sc.get(); return sc.get();
} }
Grasscutter.getLogger().debug("Loading script " + path); // Grasscutter.getLogger().debug("Loading script " + path);
File file = new File(path); File file = new File(path);
@ -125,7 +149,7 @@ public class ScriptLoader {
return sc.get(); return sc.get();
} }
Grasscutter.getLogger().debug("Loading script " + path); // Grasscutter.getLogger().debug("Loading script " + path);
final Path scriptPath = FileUtils.getScriptPath(path); final Path scriptPath = FileUtils.getScriptPath(path);
if (!Files.exists(scriptPath)) return null; if (!Files.exists(scriptPath)) return null;

View File

@ -37,10 +37,11 @@ public class SceneBlock {
} }
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) { public SceneBlock load(int sceneId, Bindings bindings) {

View File

@ -4,12 +4,17 @@ 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.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import org.jetbrains.annotations.Nullable;
import org.luaj.vm2.LuaTable; import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue; import org.luaj.vm2.LuaValue;
@ -28,7 +33,7 @@ public class LuaSerializer implements Serializer {
@Override @Override
public <T> T toObject(Class<T> type, Object obj) { public <T> T toObject(Class<T> type, Object obj) {
return serialize(type, (LuaTable) obj); return serialize(type, null, (LuaTable) obj);
} }
@Override @Override
@ -52,7 +57,7 @@ public class LuaSerializer implements Serializer {
T object = null; T object = null;
if (keyValue.istable()) { if (keyValue.istable()) {
object = serialize(type, keyValue.checktable()); object = serialize(type, null, keyValue.checktable());
} else if (keyValue.isint()) { } else if (keyValue.isint()) {
object = (T) (Integer) keyValue.toint(); object = (T) (Integer) keyValue.toint();
} else if (keyValue.isnumber()) { } else if (keyValue.isnumber()) {
@ -95,7 +100,7 @@ public class LuaSerializer implements Serializer {
T object = null; T object = null;
if (keyValue.istable()) { if (keyValue.istable()) {
object = serialize(type, keyValue.checktable()); object = serialize(type, null, keyValue.checktable());
} else if (keyValue.isint()) { } else if (keyValue.isint()) {
object = (T) (Integer) keyValue.toint(); object = (T) (Integer) keyValue.toint();
} else if (keyValue.isnumber()) { } else if (keyValue.isnumber()) {
@ -122,12 +127,24 @@ public class LuaSerializer implements Serializer {
return list; return list;
} }
public <T> T serialize(Class<T> type, LuaTable table) { private Class<?> getListType(Class<?> type, @Nullable Field field) {
if (field == null) {
return type.getTypeParameters()[0].getClass();
}
Type fieldType = field.getGenericType();
if (fieldType instanceof ParameterizedType) {
return (Class<?>) ((ParameterizedType) fieldType).getActualTypeArguments()[0];
}
return null;
}
public <T> T serialize(Class<T> type, @Nullable Field field, LuaTable table) {
T object = null; T object = null;
if (type == List.class) { if (type == List.class) {
try { try {
Class<T> listType = (Class<T>) type.getTypeParameters()[0].getClass(); Class<?> listType = getListType(type, field);
return (T) serializeList(listType, table); return (T) serializeList(listType, table);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@ -160,7 +177,9 @@ public class LuaSerializer implements Serializer {
if (keyValue.istable()) { if (keyValue.istable()) {
methodAccess.invoke( methodAccess.invoke(
object, fieldMeta.index, serialize(fieldMeta.getType(), keyValue.checktable())); object,
fieldMeta.index,
serialize(fieldMeta.getType(), fieldMeta.getField(), keyValue.checktable()));
} else if (fieldMeta.getType().equals(float.class)) { } else if (fieldMeta.getType().equals(float.class)) {
methodAccess.invoke(object, fieldMeta.index, keyValue.tofloat()); methodAccess.invoke(object, fieldMeta.index, keyValue.tofloat());
} else if (fieldMeta.getType().equals(int.class)) { } else if (fieldMeta.getType().equals(int.class)) {
@ -206,7 +225,8 @@ public class LuaSerializer implements Serializer {
var setter = getSetterName(field.getName()); var setter = getSetterName(field.getName());
var index = methodAccess.getIndex(setter); var index = methodAccess.getIndex(setter);
fieldMetaMap.put( fieldMetaMap.put(
field.getName(), new FieldMeta(field.getName(), setter, index, field.getType())); field.getName(),
new FieldMeta(field.getName(), setter, index, field.getType(), field));
}); });
Arrays.stream(type.getFields()) Arrays.stream(type.getFields())
@ -217,7 +237,8 @@ public class LuaSerializer implements Serializer {
var setter = getSetterName(field.getName()); var setter = getSetterName(field.getName());
var index = methodAccess.getIndex(setter); var index = methodAccess.getIndex(setter);
fieldMetaMap.put( fieldMetaMap.put(
field.getName(), new FieldMeta(field.getName(), setter, index, field.getType())); field.getName(),
new FieldMeta(field.getName(), setter, index, field.getType(), field));
}); });
fieldMetaCache.put(type, fieldMetaMap); fieldMetaCache.put(type, fieldMetaMap);
@ -242,5 +263,6 @@ public class LuaSerializer implements Serializer {
String setter; String setter;
int index; int index;
Class<?> type; Class<?> type;
@Nullable Field field;
} }
} }

View File

@ -238,7 +238,7 @@ public final class GameServer extends KcpServer {
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() {
@ -275,5 +275,7 @@ public final class GameServer extends KcpServer {
for (Player player : list) { for (Player player : list) {
player.getSession().close(); player.getSession().close();
} }
getWorlds().forEach(World::save);
} }
} }

View File

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

View File

@ -3,14 +3,22 @@ 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) { public PacketPlayerEnterDungeonRsp(int pointId, int dungeonId, boolean success) {
super(PacketOpcodes.PlayerEnterDungeonRsp); super(PacketOpcodes.PlayerEnterDungeonRsp);
PlayerEnterDungeonRsp proto = PlayerEnterDungeonRsp proto =
PlayerEnterDungeonRsp.newBuilder().setPointId(pointId).setDungeonId(dungeonId).build(); PlayerEnterDungeonRsp.newBuilder()
.setPointId(pointId)
.setDungeonId(dungeonId)
.setRetcode(
success
? Retcode.RET_SUCC_VALUE
: Retcode.RET_FAIL_VALUE)
.build();
this.setData(proto); this.setData(proto);
} }

View File

@ -6,6 +6,8 @@ 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;
import java.util.List;
public class PacketPlayerHomeCompInfoNotify extends BasePacket { public class PacketPlayerHomeCompInfoNotify extends BasePacket {
public PacketPlayerHomeCompInfoNotify(Player player) { public PacketPlayerHomeCompInfoNotify(Player player) {
@ -21,8 +23,7 @@ public class PacketPlayerHomeCompInfoNotify extends BasePacket {
.setCompInfo( .setCompInfo(
PlayerHomeCompInfoOuterClass.PlayerHomeCompInfo.newBuilder() PlayerHomeCompInfoOuterClass.PlayerHomeCompInfo.newBuilder()
.addAllUnlockedModuleIdList(player.getRealmList()) .addAllUnlockedModuleIdList(player.getRealmList())
.addAllSeenModuleIdList(player.getSeenRealmList()) .addAllLevelupRewardGotLevelList(List.of(1)) // Hardcoded
.addAllLevelupRewardGotLevelList(player.getHomeRewardedLevels())
.setFriendEnterHomeOptionValue(player.getHome().getEnterHomeOption()) .setFriendEnterHomeOptionValue(player.getHome().getEnterHomeOption())
.build()) .build())
.build(); .build();

View File

@ -2,12 +2,15 @@ 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;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
public class PacketPlayerSetPauseRsp extends BasePacket { public class PacketPlayerSetPauseRsp extends BasePacket {
public PacketPlayerSetPauseRsp(int clientSequence) { public PacketPlayerSetPauseRsp() {
super(PacketOpcodes.PlayerSetPauseRsp); super(PacketOpcodes.PlayerSetPauseRsp);
this.buildHeader(clientSequence); this.setData(PlayerSetPauseRsp.newBuilder()
.setRetcode(Retcode.RET_SUCC_VALUE));
} }
} }

View File

@ -3,16 +3,24 @@ 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.QuestUpdateQuestVarReqOuterClass.QuestUpdateQuestVarReq;
import emu.grasscutter.net.proto.QuestUpdateQuestVarRspOuterClass; import emu.grasscutter.net.proto.QuestUpdateQuestVarRspOuterClass;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
@Opcodes(PacketOpcodes.QuestUpdateQuestVarReq) @Opcodes(PacketOpcodes.QuestUpdateQuestVarReq)
public class PacketQuestUpdateQuestVarRsp extends BasePacket { public class PacketQuestUpdateQuestVarRsp extends BasePacket {
public PacketQuestUpdateQuestVarRsp(int questId) { public PacketQuestUpdateQuestVarRsp(QuestUpdateQuestVarReq req) {
this(req, Retcode.RET_SUCC);
}
public PacketQuestUpdateQuestVarRsp(QuestUpdateQuestVarReq req, Retcode retcode) {
super(PacketOpcodes.QuestUpdateQuestVarRsp); super(PacketOpcodes.QuestUpdateQuestVarRsp);
var rsp = var rsp =
QuestUpdateQuestVarRspOuterClass.QuestUpdateQuestVarRsp.newBuilder() QuestUpdateQuestVarRspOuterClass.QuestUpdateQuestVarRsp.newBuilder()
.setQuestId(questId) .setQuestId(req.getQuestId())
.setParentQuestId(req.getParentQuestId())
.setRetcode(retcode.getNumber())
.build(); .build();
this.setData(rsp); this.setData(rsp);
} }

View File

@ -11,8 +11,7 @@ public class PacketScenePointUnlockNotify extends BasePacket {
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);
} }
@ -23,8 +22,7 @@ public class PacketScenePointUnlockNotify extends BasePacket {
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

@ -9,16 +9,7 @@ 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()
.setIsPaused(player.isPaused())
.setSceneId(player.getSceneId())
.setSceneTime(player.getScene().getSceneTime())
.build();
this.setData(proto);
} }
public PacketSceneTimeNotify(Scene scene) { public PacketSceneTimeNotify(Scene scene) {

View File

@ -2,6 +2,7 @@ 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.RetcodeOuterClass.Retcode;
import emu.grasscutter.net.proto.UnlockPersonalLineRspOuterClass; import emu.grasscutter.net.proto.UnlockPersonalLineRspOuterClass;
public class PacketUnlockPersonalLineRsp extends BasePacket { public class PacketUnlockPersonalLineRsp extends BasePacket {
@ -15,4 +16,14 @@ public class PacketUnlockPersonalLineRsp extends BasePacket {
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

@ -3,6 +3,7 @@ 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.game.quest.enums.QuestContent;
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.VehicleInteractRspOuterClass.VehicleInteractRsp; import emu.grasscutter.net.proto.VehicleInteractRspOuterClass.VehicleInteractRsp;
@ -32,6 +33,11 @@ public class PacketVehicleInteractRsp extends BasePacket {
switch (interactType) { switch (interactType) {
case VEHICLE_INTERACT_TYPE_IN -> { case VEHICLE_INTERACT_TYPE_IN -> {
((EntityVehicle) vehicle).getVehicleMembers().add(vehicleMember); ((EntityVehicle) vehicle).getVehicleMembers().add(vehicleMember);
player
.getQuestManager()
.queueEvent(
QuestContent.QUEST_CONTENT_ENTER_VEHICLE,
((EntityVehicle) vehicle).getGadgetId());
} }
case VEHICLE_INTERACT_TYPE_OUT -> { case VEHICLE_INTERACT_TYPE_OUT -> {
((EntityVehicle) vehicle).getVehicleMembers().remove(vehicleMember); ((EntityVehicle) vehicle).getVehicleMembers().remove(vehicleMember);

View File

@ -35,10 +35,12 @@ public final class ServerTask implements Runnable {
* @return True if the task should run, false otherwise. * @return True if the task should run, false otherwise.
*/ */
public boolean shouldRun() { public boolean shouldRun() {
// Increase tick count.
var ticks = this.ticks++;
if (this.delay != -1 && this.considerDelay) { if (this.delay != -1 && this.considerDelay) {
this.considerDelay = false; this.considerDelay = false;
return this.ticks == this.delay; return ticks == this.delay;
} else if (this.period != -1) return this.ticks % this.period == 0; } else if (this.period != -1) return ticks % this.period == 0;
else return true; else return true;
} }
@ -48,15 +50,17 @@ public final class ServerTask implements Runnable {
* @return True if the task should be canceled, false otherwise. * @return True if the task should be canceled, false otherwise.
*/ */
public boolean shouldCancel() { public boolean shouldCancel() {
return this.period == -1; return this.period == -1 && ticks >= delay;
} }
/** Runs the task. */ /** Runs the task. */
@Override @Override
public void run() { public void run() {
// Run the runnable. // Run the runnable.
try {
this.runnable.run(); this.runnable.run();
// Increase tick count. } catch (Exception ex) {
this.ticks++; Grasscutter.getLogger().error("Exception during task: ", ex);
}
} }
} }

View File

@ -1,8 +1,6 @@
package emu.grasscutter.task; package emu.grasscutter.task;
import org.quartz.Job; import org.quartz.*;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
@PersistJobDataAfterExecution @PersistJobDataAfterExecution
public abstract class TaskHandler implements Job { public abstract class TaskHandler implements Job {

View File

@ -137,7 +137,8 @@ public final class Tools {
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()) {
case MATERIAL_BGM:
val bgmName = val bgmName =
Optional.ofNullable(data.getItemUse()) Optional.ofNullable(data.getItemUse())
.map(u -> u.get(0)) .map(u -> u.get(0))
@ -151,8 +152,10 @@ public final class Tools {
h.newTranslatedLine(itemPre.formatted(id) + "{0} - {1}", name, bgmName.get()); h.newTranslatedLine(itemPre.formatted(id) + "{0} - {1}", name, bgmName.get());
return; return;
} // Fall-through } // Fall-through
} default:
h.newTranslatedLine(itemPre.formatted(id) + "{0}", name); h.newTranslatedLine(itemPre.formatted(id) + "{0}", name);
return;
}
}); });
// Monsters // Monsters
h.newSection("Monsters"); h.newSection("Monsters");