Rework how story is handled, fixes choices not saving

This commit is contained in:
Melledy
2025-12-12 01:31:31 -08:00
parent 11ea526a35
commit c51268bcb8
10 changed files with 320 additions and 30 deletions

View File

@@ -93,6 +93,7 @@ public class GameData {
// ===== Story =====
@Getter private static DataTable<StoryDef> StoryDataTable = new DataTable<>();
@Getter private static DataTable<StorySetSectionDef> StorySetSectionDataTable = new DataTable<>();
@Getter private static DataTable<StoryEvidenceDef> StoryEvidenceDataTable = new DataTable<>();
// ===== Daily Quests =====
@Getter private static DataTable<DailyQuestDef> DailyQuestDataTable = new DataTable<>();

View File

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

View File

@@ -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());

View File

@@ -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;

View File

@@ -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()) {

View File

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

View File

@@ -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<Integer, StoryOptionLog> 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<StorySettle> 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();
}
}
}

View File

@@ -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<StoryChoiceInfo> major;
private List<StoryChoiceInfo> 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<StoryOptions> 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<StoryOptions> 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());
}
}
}
}

View File

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

View File

@@ -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;