From c51268bcb8150dbb9a182fe829b1f6f1933b33f8 Mon Sep 17 00:00:00 2001 From: Melledy <121644117+Melledy@users.noreply.github.com> Date: Fri, 12 Dec 2025 01:31:31 -0800 Subject: [PATCH] Rework how story is handled, fixes choices not saving --- src/main/java/emu/nebula/data/GameData.java | 1 + .../data/resources/StoryEvidenceDef.java | 17 +++ .../java/emu/nebula/game/player/Player.java | 14 +- .../nebula/game/player/PlayerProgress.java | 2 +- .../emu/nebula/game/quest/QuestManager.java | 2 +- .../nebula/game/story/StoryChoiceInfo.java | 32 ++++ .../emu/nebula/game/story/StoryManager.java | 123 ++++++++++++++- .../emu/nebula/game/story/StoryOptionLog.java | 143 ++++++++++++++++++ .../handlers/HandlerStorySettleReq.java | 15 +- src/main/java/emu/nebula/util/Utils.java | 1 - 10 files changed, 320 insertions(+), 30 deletions(-) create mode 100644 src/main/java/emu/nebula/data/resources/StoryEvidenceDef.java create mode 100644 src/main/java/emu/nebula/game/story/StoryChoiceInfo.java create mode 100644 src/main/java/emu/nebula/game/story/StoryOptionLog.java diff --git a/src/main/java/emu/nebula/data/GameData.java b/src/main/java/emu/nebula/data/GameData.java index c69258b..4bd1585 100644 --- a/src/main/java/emu/nebula/data/GameData.java +++ b/src/main/java/emu/nebula/data/GameData.java @@ -93,6 +93,7 @@ public class GameData { // ===== Story ===== @Getter private static DataTable StoryDataTable = new DataTable<>(); @Getter private static DataTable StorySetSectionDataTable = new DataTable<>(); + @Getter private static DataTable StoryEvidenceDataTable = new DataTable<>(); // ===== Daily Quests ===== @Getter private static DataTable DailyQuestDataTable = new DataTable<>(); diff --git a/src/main/java/emu/nebula/data/resources/StoryEvidenceDef.java b/src/main/java/emu/nebula/data/resources/StoryEvidenceDef.java new file mode 100644 index 0000000..4062a61 --- /dev/null +++ b/src/main/java/emu/nebula/data/resources/StoryEvidenceDef.java @@ -0,0 +1,17 @@ +package emu.nebula.data.resources; + +import emu.nebula.data.BaseDef; +import emu.nebula.data.ResourceType; + +import lombok.Getter; + +@Getter +@ResourceType(name = "StoryEvidence.json") +public class StoryEvidenceDef extends BaseDef { + private int Id; + + @Override + public int getId() { + return Id; + } +} diff --git a/src/main/java/emu/nebula/game/player/Player.java b/src/main/java/emu/nebula/game/player/Player.java index e4afae8..de5c35e 100644 --- a/src/main/java/emu/nebula/game/player/Player.java +++ b/src/main/java/emu/nebula/game/player/Player.java @@ -45,7 +45,6 @@ import emu.nebula.proto.Public.Friend; import emu.nebula.proto.Public.HonorInfo; import emu.nebula.proto.Public.NewbieInfo; import emu.nebula.proto.Public.QuestType; -import emu.nebula.proto.Public.Story; import emu.nebula.proto.Public.WorldClass; import emu.nebula.proto.Public.WorldClassRewardState; import emu.nebula.util.Utils; @@ -931,14 +930,7 @@ public class Player implements GameDatabaseObject { acc.addNewbies(NewbieInfo.newInstance().setGroupId(GameConstants.INTRO_GUIDE_ID).setStepId(-1)); // Story - var story = proto.getMutableStory(); - - for (int storyId : this.getStoryManager().getCompletedStories()) { - var storyProto = Story.newInstance() - .setIdx(storyId); - - story.addStories(storyProto); - } + this.getStoryManager().encodePlayerInfo(proto); // Add titles for (int titleId : this.getInventory().getTitles()) { @@ -954,7 +946,7 @@ public class Player implements GameDatabaseObject { } // Quests - this.getQuestManager().encodeProto(proto); + this.getQuestManager().encodePlayerInfo(proto); // Add dictionary tabs for (var dictionaryData : GameData.getDictionaryTabDataTable()) { @@ -973,7 +965,7 @@ public class Player implements GameDatabaseObject { } // Add progress - this.getProgress().encodeProto(proto); + this.getProgress().encodePlayerInfo(proto); // Handbook proto.addHandbook(this.getCharacters().getCharacterHandbook()); diff --git a/src/main/java/emu/nebula/game/player/PlayerProgress.java b/src/main/java/emu/nebula/game/player/PlayerProgress.java index fa21642..1408725 100644 --- a/src/main/java/emu/nebula/game/player/PlayerProgress.java +++ b/src/main/java/emu/nebula/game/player/PlayerProgress.java @@ -186,7 +186,7 @@ public class PlayerProgress extends PlayerManager implements GameDatabaseObject // Proto - public void encodeProto(PlayerInfo proto) { + public void encodePlayerInfo(PlayerInfo proto) { // Check if we want to unlock all instances boolean unlockAll = Nebula.getConfig().getServerOptions().unlockInstances; diff --git a/src/main/java/emu/nebula/game/quest/QuestManager.java b/src/main/java/emu/nebula/game/quest/QuestManager.java index 3e88104..0f75656 100644 --- a/src/main/java/emu/nebula/game/quest/QuestManager.java +++ b/src/main/java/emu/nebula/game/quest/QuestManager.java @@ -312,7 +312,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject { // Serialization - public void encodeProto(PlayerInfo proto) { + public void encodePlayerInfo(PlayerInfo proto) { var quests = proto.getMutableQuests(); for (var quest : this.getQuests().values()) { diff --git a/src/main/java/emu/nebula/game/story/StoryChoiceInfo.java b/src/main/java/emu/nebula/game/story/StoryChoiceInfo.java new file mode 100644 index 0000000..86082ad --- /dev/null +++ b/src/main/java/emu/nebula/game/story/StoryChoiceInfo.java @@ -0,0 +1,32 @@ +package emu.nebula.game.story; + +import dev.morphia.annotations.Entity; +import emu.nebula.proto.Public.StoryChoice; +import lombok.Getter; + +@Getter +@Entity(useDiscriminator = false) +public class StoryChoiceInfo { + private int group; + private int value; + + @Deprecated + public StoryChoiceInfo() { + // Morphia only + } + + public StoryChoiceInfo(int group, int value) { + this.group = group; + this.value = value; + } + + // Proto + + public StoryChoice toProto() { + var proto = StoryChoice.newInstance() + .setGroup(this.getGroup()) + .setValue(this.getValue()); + + return proto; + } +} diff --git a/src/main/java/emu/nebula/game/story/StoryManager.java b/src/main/java/emu/nebula/game/story/StoryManager.java index d7f13db..35a974b 100644 --- a/src/main/java/emu/nebula/game/story/StoryManager.java +++ b/src/main/java/emu/nebula/game/story/StoryManager.java @@ -1,20 +1,28 @@ package emu.nebula.game.story; +import java.util.HashMap; +import java.util.Map; + import dev.morphia.annotations.Entity; import dev.morphia.annotations.Id; +import dev.morphia.annotations.PostLoad; import emu.nebula.Nebula; import emu.nebula.data.GameData; import emu.nebula.database.GameDatabaseObject; import emu.nebula.game.player.Player; import emu.nebula.game.player.PlayerChangeInfo; import emu.nebula.game.player.PlayerManager; +import emu.nebula.proto.PlayerData.PlayerInfo; +import emu.nebula.proto.Public.Story; +import emu.nebula.proto.StorySett.StorySettle; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import lombok.Getter; +import us.hebi.quickbuf.RepeatedInt; +import us.hebi.quickbuf.RepeatedMessage; @Getter @Entity(value = "story", useDiscriminator = false) @@ -24,6 +32,10 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject { private IntSet completedStories; private Int2IntMap completedSets; + private IntSet evidences; + + // Note: Story options are seperate from regular story ids to save database space, since most stories do not have options + private Map options; @Deprecated // Morphia only public StoryManager() { @@ -35,6 +47,8 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject { this.uid = player.getUid(); this.completedStories = new IntOpenHashSet(); this.completedSets = new Int2IntOpenHashMap(); + this.evidences = new IntOpenHashSet(); + this.options = new HashMap<>(); this.save(); } @@ -51,15 +65,22 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject { return false; } - public PlayerChangeInfo settle(IntList list) { + public PlayerChangeInfo settle(RepeatedMessage list, RepeatedInt evidences) { // Player change info - var changes = new PlayerChangeInfo(); + var change = new PlayerChangeInfo(); - for (int id : list) { + // Handle regular story + for (var settle : list) { + // Get id + int id = settle.getIdx(); + // Get story data var data = GameData.getStoryDataTable().get(id); if (data == null) continue; + // Settle options (Must be before the completion check as we need to do the same story multiple times to get all the endings) + this.settleOptions(settle); + // Check if we already completed the story if (this.getCompletedStories().contains(id)) { continue; @@ -69,14 +90,61 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject { this.getCompletedStories().add(id); // Add rewards - this.getPlayer().getInventory().addItems(data.getRewards(), changes); + this.getPlayer().getInventory().addItems(data.getRewards(), change); // Save to db Nebula.getGameDatabase().addToSet(this, this.getPlayerUid(), "completedStories", id); } + // Handle evidences + for (int id : evidences) { + // Verify that evidence id exists + var data = GameData.getStoryEvidenceDataTable().get(id); + if (data == null) continue; + + // Sanity check + if (this.getEvidences().contains(id)) { + continue; + } + + // Save to db + Nebula.getGameDatabase().addToSet(this, this.getPlayerUid(), "evidences", id); + } + // Complete - return changes; + return change; + } + + private void settleOptions(StorySettle settle) { + // Init variables + boolean changed = false; + StoryOptionLog log = null; + + // Update + if (settle.hasMajor()) { + if (log == null) { + log = getOptions().computeIfAbsent(settle.getIdx(), idx -> new StoryOptionLog()); + } + + if (log.settleMajor(settle.getMajor())) { + changed = true; + } + } + + if (settle.hasPersonality()) { + if (log == null) { + log = getOptions().computeIfAbsent(settle.getIdx(), idx -> new StoryOptionLog()); + } + + if (log.settlePersonality(settle.getPersonality())) { + changed = true; + } + } + + // Save to database if we changed anything + if (changed) { + Nebula.getGameDatabase().update(this, this.getPlayerUid(), "options." + settle.getIdx(), log); + } } public PlayerChangeInfo settleSet(int chapterId, int sectionId) { @@ -106,4 +174,47 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject { // Complete return changes; } + + // Proto + + public void encodePlayerInfo(PlayerInfo proto) { + var story = proto.getMutableStory(); + + for (int storyId : this.getCompletedStories()) { + var storyProto = Story.newInstance() + .setIdx(storyId); + + var storyOptions = this.getOptions().get(storyId); + if (storyOptions != null) { + storyOptions.encodeStoryProto(storyProto); + } + + story.addStories(storyProto); + } + + for (int id : this.getEvidences()) { + story.addEvidences(id); + } + } + + // Database fixes + + @PostLoad + public void onLoad() { + boolean save = false; + + if (this.evidences == null) { + this.evidences = new IntOpenHashSet(); + save = true; + } + + if (this.options == null) { + this.options = new HashMap<>(); + save = true; + } + + if (save) { + this.save(); + } + } } diff --git a/src/main/java/emu/nebula/game/story/StoryOptionLog.java b/src/main/java/emu/nebula/game/story/StoryOptionLog.java new file mode 100644 index 0000000..7cc71db --- /dev/null +++ b/src/main/java/emu/nebula/game/story/StoryOptionLog.java @@ -0,0 +1,143 @@ +package emu.nebula.game.story; + +import java.util.ArrayList; +import java.util.List; + +import dev.morphia.annotations.Entity; + +import emu.nebula.proto.Public.Story; +import emu.nebula.proto.StorySett.StoryOptions; + +import lombok.Getter; + +import us.hebi.quickbuf.RepeatedMessage; + +@Getter +@Entity(useDiscriminator = false) +public class StoryOptionLog { + private List major; + private List personality; + + public StoryOptionLog() { + + } + + public int getMajorOptionSize() { + if (this.major == null) { + return 0; + } + + return this.major.size(); + } + + public boolean hasMajorOption(int group, int choice) { + if (this.major == null) { + return false; + } + + return this.major.stream() + .filter(c -> c.getGroup() == group && c.getValue() == choice) + .findFirst() + .isPresent(); + } + + public boolean addMajorOption(int group, int choice) { + if (this.major == null) { + this.major = new ArrayList<>(); + } + + return this.major.add(new StoryChoiceInfo(group, choice)); + } + + public boolean settleMajor(RepeatedMessage options) { + boolean success = false; + + for (var option : options) { + // Sanity check + if (this.getMajorOptionSize() >= 5) { + break; + } + + // Skip if we already have this choice + if (this.hasMajorOption(option.getGroup(), option.getChoice())) { + continue; + } + + // Add + this.addMajorOption(option.getGroup(), option.getChoice()); + + // Set success flag + success = true; + } + + return success; + } + + public int getPersonalityOptionSize() { + if (this.personality == null) { + return 0; + } + + return this.personality.size(); + } + + public boolean hasPersonalityOption(int group, int choice) { + if (this.personality == null) { + return false; + } + + return this.personality.stream() + .filter(c -> c.getGroup() == group && c.getValue() == choice) + .findFirst() + .isPresent(); + } + + public boolean addPersonalityOption(int group, int choice) { + if (this.personality == null) { + this.personality = new ArrayList<>(); + } + + return this.personality.add(new StoryChoiceInfo(group, choice)); + } + + public boolean settlePersonality(RepeatedMessage options) { + boolean success = false; + + for (var option : options) { + // Sanity check + if (this.getPersonalityOptionSize() >= 5) { + break; + } + + // Skip if we already have this choice + if (this.hasPersonalityOption(option.getGroup(), option.getChoice())) { + continue; + } + + // Add + this.addPersonalityOption(option.getGroup(), option.getChoice()); + + // Set success flag + success = true; + } + + return success; + } + + // Proto + + public void encodeStoryProto(Story proto) { + if (this.major != null) { + for (var choice : this.major) { + proto.addMajor(choice.toProto()); + } + } + + if (this.personality != null) { + for (var choice : this.personality) { + proto.addMajor(choice.toProto()); + } + } + } + +} diff --git a/src/main/java/emu/nebula/server/handlers/HandlerStorySettleReq.java b/src/main/java/emu/nebula/server/handlers/HandlerStorySettleReq.java index fd1105b..31ec99d 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerStorySettleReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerStorySettleReq.java @@ -3,7 +3,6 @@ package emu.nebula.server.handlers; import emu.nebula.net.NetHandler; import emu.nebula.net.NetMsgId; import emu.nebula.proto.StorySett.StorySettleReq; -import it.unimi.dsi.fastutil.ints.IntArrayList; import emu.nebula.net.HandlerId; import emu.nebula.net.GameSession; @@ -15,18 +14,14 @@ public class HandlerStorySettleReq extends NetHandler { // Parse request var req = StorySettleReq.parseFrom(message); - // Get list of settled story ids - var list = new IntArrayList(); - - for (var settle : req.getList()) { - list.add(settle.getIdx()); - } - // Settle - var changes = session.getPlayer().getStoryManager().settle(list); + var change = session.getPlayer().getStoryManager().settle(req.getList(), req.getEvidences()); + + // Handle client events for achievements + session.getPlayer().getAchievementManager().handleClientEvents(req.getEvents()); // Send response - return session.encodeMsg(NetMsgId.story_settle_succeed_ack, changes.toProto()); + return session.encodeMsg(NetMsgId.story_settle_succeed_ack, change.toProto()); } } diff --git a/src/main/java/emu/nebula/util/Utils.java b/src/main/java/emu/nebula/util/Utils.java index a4b7097..25a4934 100644 --- a/src/main/java/emu/nebula/util/Utils.java +++ b/src/main/java/emu/nebula/util/Utils.java @@ -7,7 +7,6 @@ import java.net.ServerSocket; import java.time.Instant; import java.time.LocalDate; import java.time.YearMonth; -import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Base64;