From 4c66a494f2b54a07e0834080b08c186ce5efbe22 Mon Sep 17 00:00:00 2001 From: Melledy <121644117+Melledy@users.noreply.github.com> Date: Thu, 13 Nov 2025 07:52:38 -0800 Subject: [PATCH] Implement boss blitz server leaderboard --- .../emu/nebula/database/DatabaseManager.java | 11 ++ .../java/emu/nebula/game/GameContext.java | 3 + .../game/scoreboss/ScoreBossManager.java | 48 ++++- .../game/scoreboss/ScoreBossModule.java | 52 ++++++ .../game/scoreboss/ScoreBossRankEntry.java | 171 ++++++++++++++++++ .../handlers/HandlerScoreBossRankReq.java | 23 +++ .../handlers/HandlerScoreBossSettleReq.java | 6 +- 7 files changed, 311 insertions(+), 3 deletions(-) create mode 100644 src/main/java/emu/nebula/game/scoreboss/ScoreBossModule.java create mode 100644 src/main/java/emu/nebula/game/scoreboss/ScoreBossRankEntry.java diff --git a/src/main/java/emu/nebula/database/DatabaseManager.java b/src/main/java/emu/nebula/database/DatabaseManager.java index 1d3fad2..8599650 100644 --- a/src/main/java/emu/nebula/database/DatabaseManager.java +++ b/src/main/java/emu/nebula/database/DatabaseManager.java @@ -1,5 +1,6 @@ package emu.nebula.database; +import java.util.List; import java.util.stream.Stream; import emu.nebula.Config.DatabaseInfo; @@ -27,6 +28,8 @@ import dev.morphia.*; import dev.morphia.annotations.Entity; import dev.morphia.mapping.Mapper; import dev.morphia.mapping.MapperOptions; +import dev.morphia.query.FindOptions; +import dev.morphia.query.Sort; import dev.morphia.query.filters.Filters; import dev.morphia.query.updates.UpdateOperators; import lombok.Getter; @@ -159,6 +162,14 @@ public final class DatabaseManager { public Stream getObjects(Class cls) { return getDatastore().find(cls).stream(); } + + public List getSortedObjects(Class cls, String filter, int limit) { + var options = new FindOptions() + .sort(Sort.descending(filter)) + .limit(limit); + + return getDatastore().find(cls).iterator(options).toList(); + } public void save(T obj) { getDatastore().save(obj, INSERT_OPTIONS); diff --git a/src/main/java/emu/nebula/game/GameContext.java b/src/main/java/emu/nebula/game/GameContext.java index 86c1a2e..03f917e 100644 --- a/src/main/java/emu/nebula/game/GameContext.java +++ b/src/main/java/emu/nebula/game/GameContext.java @@ -10,6 +10,7 @@ import emu.nebula.GameConstants; import emu.nebula.Nebula; import emu.nebula.game.gacha.GachaModule; import emu.nebula.game.player.PlayerModule; +import emu.nebula.game.scoreboss.ScoreBossModule; import emu.nebula.net.GameSession; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; @@ -24,6 +25,7 @@ public class GameContext implements Runnable { // Modules private final PlayerModule playerModule; private final GachaModule gachaModule; + private final ScoreBossModule scoreBossModule; // Game loop private final ScheduledExecutorService scheduler; @@ -37,6 +39,7 @@ public class GameContext implements Runnable { // Setup game modules this.playerModule = new PlayerModule(this); this.gachaModule = new GachaModule(this); + this.scoreBossModule = new ScoreBossModule(this); // Run game loop this.scheduler = Executors.newScheduledThreadPool(1); diff --git a/src/main/java/emu/nebula/game/scoreboss/ScoreBossManager.java b/src/main/java/emu/nebula/game/scoreboss/ScoreBossManager.java index b7828bc..f9b178f 100644 --- a/src/main/java/emu/nebula/game/scoreboss/ScoreBossManager.java +++ b/src/main/java/emu/nebula/game/scoreboss/ScoreBossManager.java @@ -1,5 +1,6 @@ package emu.nebula.game.scoreboss; +import emu.nebula.Nebula; import emu.nebula.data.GameData; import emu.nebula.data.resources.ScoreBossControlDef; import emu.nebula.game.player.Player; @@ -11,6 +12,9 @@ public class ScoreBossManager extends PlayerManager { private int levelId; private long buildId; + private boolean checkedDatabase; + private ScoreBossRankEntry ranking; + public ScoreBossManager(Player player) { super(player); } @@ -23,6 +27,15 @@ public class ScoreBossManager extends PlayerManager { return GameData.getScoreBossControlDataTable().get(this.getControlId()); } + public ScoreBossRankEntry getRanking() { + if (this.ranking == null && !this.checkedDatabase) { + this.ranking = Nebula.getGameDatabase().getObjectByUid(ScoreBossRankEntry.class, this.getPlayerUid()); + this.checkedDatabase = true; + } + + return this.ranking; + } + public boolean apply(int levelId, long buildId) { // Get level var control = getControlData(); @@ -44,7 +57,38 @@ public class ScoreBossManager extends PlayerManager { return true; } - public void settle(int star, int score) { - // TODO + public boolean settle(int stars, int score) { + // Get level + var control = getControlData(); + if (control == null || !control.getLevelGroup().contains(this.getLevelId())) { + return false; + } + + // Get build + var build = getPlayer().getStarTowerManager().getBuildById(this.getBuildId()); + if (build == null) { + return false; + } + + // Get ranking from database + this.getRanking(); + + // Create ranking if its not in the database + if (this.ranking == null) { + this.ranking = new ScoreBossRankEntry(this.getPlayer(), this.getControlId()); + } + + // Settle + this.ranking.settle(this.getPlayer(), build, this.getLevelId(), stars, score); + + // Save ranking + this.ranking.save(); + + // Clear + this.levelId = 0; + this.buildId = 0; + + // Success + return true; } } diff --git a/src/main/java/emu/nebula/game/scoreboss/ScoreBossModule.java b/src/main/java/emu/nebula/game/scoreboss/ScoreBossModule.java new file mode 100644 index 0000000..3547853 --- /dev/null +++ b/src/main/java/emu/nebula/game/scoreboss/ScoreBossModule.java @@ -0,0 +1,52 @@ +package emu.nebula.game.scoreboss; + +import java.util.ArrayList; +import java.util.List; + +import emu.nebula.Nebula; +import emu.nebula.game.GameContext; +import emu.nebula.game.GameContextModule; +import emu.nebula.proto.ScoreBossRank.ScoreBossRankData; +import lombok.Getter; + +@Getter +public class ScoreBossModule extends GameContextModule { + private long lastUpdate; + private long nextUpdate; + private List ranking; + + public ScoreBossModule(GameContext context) { + super(context); + this.nextUpdate = -1; + this.ranking = new ArrayList<>(); + } + + public synchronized List getRanking() { + if (System.currentTimeMillis() > this.nextUpdate) { + this.updateRanking(); + } + + return this.ranking; + } + + // Cache ranking so we dont query the database too much + private void updateRanking() { + // Clear + this.ranking.clear(); + + // Get from database + var list = Nebula.getGameDatabase().getSortedObjects(ScoreBossRankEntry.class, "score", 50); + + for (int i = 0; i < list.size(); i++) { + // Get rank entry and set proto + var entry = list.get(i); + entry.setRank(i + 1); + + // Add to ranking + this.ranking.add(entry.toProto()); + } + + this.nextUpdate = System.currentTimeMillis() + 1000; + this.lastUpdate = Nebula.getCurrentTime(); + } +} diff --git a/src/main/java/emu/nebula/game/scoreboss/ScoreBossRankEntry.java b/src/main/java/emu/nebula/game/scoreboss/ScoreBossRankEntry.java new file mode 100644 index 0000000..a071165 --- /dev/null +++ b/src/main/java/emu/nebula/game/scoreboss/ScoreBossRankEntry.java @@ -0,0 +1,171 @@ +package emu.nebula.game.scoreboss; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Id; + +import emu.nebula.database.GameDatabaseObject; +import emu.nebula.game.player.Player; +import emu.nebula.game.tower.StarTowerBuild; +import emu.nebula.game.character.Character; +import emu.nebula.proto.Public.HonorInfo; +import emu.nebula.proto.ScoreBossRank.ScoreBossRankChar; +import emu.nebula.proto.ScoreBossRank.ScoreBossRankData; +import emu.nebula.proto.ScoreBossRank.ScoreBossRankTeam; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Entity(value = "score_boss_rank", useDiscriminator = false) +public class ScoreBossRankEntry implements GameDatabaseObject { + @Id + private int playerUid; + + private String name; + private int level; + private int headIcon; + private int titlePrefix; + private int titleSuffix; + private int[] honor; + private int score; + + private int controlId; + private Map teams; + + @Setter + private transient int rank; + + @Deprecated // Morphia only + public ScoreBossRankEntry() { + this.rank = 999; + } + + public ScoreBossRankEntry(Player player, int controlId) { + this.playerUid = player.getUid(); + this.controlId = controlId; + this.teams = new HashMap<>(); + } + + public void update(Player player) { + this.name = player.getName(); + this.level = player.getLevel(); + this.headIcon = player.getHeadIcon(); + this.titlePrefix = player.getTitlePrefix(); + this.titleSuffix = player.getTitleSuffix(); + this.honor = player.getHonor(); + } + + public void settle(Player player, StarTowerBuild build, int level, int stars, int score) { + // Update player data + this.update(player); + + // Set team entry + var team = new ScoreBossTeamEntry(player, build, stars, score); + this.getTeams().put(level, team); + + // Calculate score + this.score = 0; + + for (var t : this.getTeams().values()) { + this.score += t.getLevelScore(); + } + } + + // Proto + + public ScoreBossRankData toProto() { + var proto = ScoreBossRankData.newInstance() + .setId(this.getPlayerUid()) + .setNickName(this.getName()) + .setWorldClass(this.getLevel()) + .setHeadIcon(this.getHeadIcon()) + .setScore(this.getScore()) + .setTitlePrefix(this.getTitlePrefix()) + .setTitleSuffix(this.getTitleSuffix()) + .setRank(this.getRank()); + + for (int id : this.getHonor()) { + proto.addHonors(HonorInfo.newInstance().setId(id)); + } + + for (var team : this.getTeams().values()) { + proto.addTeams(team.toProto()); + } + + return proto; + } + + // Extra classes + + @Getter + @Entity(useDiscriminator = false) + public static class ScoreBossTeamEntry { + private int buildId; + private int buildScore; + private int stars; + private int levelScore; + private List characters; + + @Deprecated // Morphia only + public ScoreBossTeamEntry() { + + } + + public ScoreBossTeamEntry(Player player, StarTowerBuild build, int stars, int score) { + this.buildId = build.getUid(); + this.buildScore = build.getScore(); + this.stars = stars; + this.levelScore = score; + this.characters = new ArrayList<>(); + + for (var charId : build.getCharIds()) { + var character = player.getCharacters().getCharacterById(charId); + if (character != null) { + this.getCharacters().add(new ScoreBossCharEntry(character)); + } + } + } + + public ScoreBossRankTeam toProto() { + var proto = ScoreBossRankTeam.newInstance() + .setBuildScore(this.getBuildScore()) + .setLevelScore(this.getLevelScore()); + + for (var c : this.getCharacters()) { + proto.addChars(c.toProto()); + } + + return proto; + } + } + + @Getter + @Entity(useDiscriminator = false) + public static class ScoreBossCharEntry { + private int id; + private int level; + + @Deprecated // Morphia only + public ScoreBossCharEntry() { + + } + + public ScoreBossCharEntry(Character character) { + this.id = character.getCharId(); + this.level = character.getLevel(); + } + + public ScoreBossRankChar toProto() { + var proto = ScoreBossRankChar.newInstance() + .setId(this.getId()) + .setLevel(this.getLevel()); + + return proto; + } + } +} diff --git a/src/main/java/emu/nebula/server/handlers/HandlerScoreBossRankReq.java b/src/main/java/emu/nebula/server/handlers/HandlerScoreBossRankReq.java index 8f58f0b..d490279 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerScoreBossRankReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerScoreBossRankReq.java @@ -16,6 +16,29 @@ public class HandlerScoreBossRankReq extends NetHandler { var rsp = ScoreBossRankInfo.newInstance() .setLastRefreshTime(Nebula.getCurrentTime()); + // Get self + var self = session.getPlayer().getScoreBossManager().getRanking(); + + if (self != null) { + rsp.setSelf(self.toProto()); + } + + // Get ranking + var ranking = Nebula.getGameContext().getScoreBossModule().getRanking(); + + for (var entry : ranking) { + // Check self + if (self != null && self.getPlayerUid() == entry.getId()) { + rsp.getMutableSelf().setRank(entry.getRank()); + } + + // Add to ranking + rsp.addRank(entry); + } + + // Set total + rsp.setTotal(ranking.size()); + // Encode and send return session.encodeMsg(NetMsgId.score_boss_rank_succeed_ack, rsp); } diff --git a/src/main/java/emu/nebula/server/handlers/HandlerScoreBossSettleReq.java b/src/main/java/emu/nebula/server/handlers/HandlerScoreBossSettleReq.java index 0303b5e..f6c2979 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerScoreBossSettleReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerScoreBossSettleReq.java @@ -16,7 +16,11 @@ public class HandlerScoreBossSettleReq extends NetHandler { var req = ScoreBossSettleReq.parseFrom(message); // Settle - session.getPlayer().getScoreBossManager().settle(req.getStar(), req.getScore()); + boolean success = session.getPlayer().getScoreBossManager().settle(req.getStar(), req.getScore()); + + if (success == false) { + return session.encodeMsg(NetMsgId.score_boss_settle_failed_ack); + } // Build response var rsp = ScoreBossSettleResp.newInstance();