From f2656903e5de6f86eb464837b01fc799a310e53b Mon Sep 17 00:00:00 2001 From: Melledy <121644117+Melledy@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:32:47 -0800 Subject: [PATCH] Implement gacha history --- .../nebula/game/gacha/GachaBannerInfo.java | 26 ++-- .../nebula/game/gacha/GachaHistoryLog.java | 40 ++++++ .../emu/nebula/game/gacha/GachaManager.java | 119 ++++++++++++------ .../emu/nebula/game/gacha/GachaModule.java | 16 ++- .../java/emu/nebula/game/player/Player.java | 4 +- .../emu/nebula/game/player/PlayerModule.java | 4 +- .../handlers/HandlerGachaHistoriesReq.java | 34 +++++ 7 files changed, 178 insertions(+), 65 deletions(-) create mode 100644 src/main/java/emu/nebula/game/gacha/GachaHistoryLog.java create mode 100644 src/main/java/emu/nebula/server/handlers/HandlerGachaHistoriesReq.java diff --git a/src/main/java/emu/nebula/game/gacha/GachaBannerInfo.java b/src/main/java/emu/nebula/game/gacha/GachaBannerInfo.java index 94fa8fc..19081c2 100644 --- a/src/main/java/emu/nebula/game/gacha/GachaBannerInfo.java +++ b/src/main/java/emu/nebula/game/gacha/GachaBannerInfo.java @@ -1,28 +1,19 @@ package emu.nebula.game.gacha; -import org.bson.types.ObjectId; - import dev.morphia.annotations.Entity; -import dev.morphia.annotations.Id; -import dev.morphia.annotations.Indexed; + import emu.nebula.data.resources.GachaDef; import emu.nebula.data.resources.GachaDef.GachaPackage; import emu.nebula.data.resources.GachaPkgDef; -import emu.nebula.database.GameDatabaseObject; -import emu.nebula.game.player.Player; import emu.nebula.proto.GachaInformation.GachaInfo; import emu.nebula.util.Utils; + import lombok.Getter; @Getter -@Entity(value = "banner_info", useDiscriminator = false) -public class GachaBannerInfo implements GameDatabaseObject { - @Id - private ObjectId id; - - @Indexed - private int playerUid; - private int bannerId; +@Entity(useDiscriminator = false) +public class GachaBannerInfo { + private int id; private int total; private int missTimesA; @@ -35,9 +26,8 @@ public class GachaBannerInfo implements GameDatabaseObject { } - public GachaBannerInfo(Player player, GachaDef data) { - this.playerUid = player.getUid(); - this.bannerId = data.getId(); + public GachaBannerInfo(GachaDef data) { + this.id = data.getId(); } public void setUsedGuarantee(boolean value) { @@ -108,7 +98,7 @@ public class GachaBannerInfo implements GameDatabaseObject { public GachaInfo toProto() { var proto = GachaInfo.newInstance() - .setId(this.getBannerId()) + .setId(this.getId()) .setGachaTotalTimes(this.getTotal()) .setTotalTimes(this.getTotal()) .setAupMissTimes(this.getMissTimesA()) diff --git a/src/main/java/emu/nebula/game/gacha/GachaHistoryLog.java b/src/main/java/emu/nebula/game/gacha/GachaHistoryLog.java new file mode 100644 index 0000000..11eb706 --- /dev/null +++ b/src/main/java/emu/nebula/game/gacha/GachaHistoryLog.java @@ -0,0 +1,40 @@ +package emu.nebula.game.gacha; + +import dev.morphia.annotations.Entity; +import emu.nebula.Nebula; +import emu.nebula.proto.GachaHistoriesOuterClass.GachaHistory; +import it.unimi.dsi.fastutil.ints.IntList; +import lombok.Getter; + +@Getter +@Entity(value = "banner_info", useDiscriminator = false) +public class GachaHistoryLog { + private int type; + private long time; + private IntList ids; + + @Deprecated // Morphia only + public GachaHistoryLog() { + + } + + public GachaHistoryLog(int type, IntList results) { + this.type = type; + this.time = Nebula.getCurrentTime(); + this.ids = results; + } + + // Proto + + public GachaHistory toProto() { + var proto = GachaHistory.newInstance() + .setGid(this.getType()) + .setTime(this.getTime()); + + for (int id : this.getIds()) { + proto.addIds(id); + } + + return proto; + } +} diff --git a/src/main/java/emu/nebula/game/gacha/GachaManager.java b/src/main/java/emu/nebula/game/gacha/GachaManager.java index 196c309..e3cdc81 100644 --- a/src/main/java/emu/nebula/game/gacha/GachaManager.java +++ b/src/main/java/emu/nebula/game/gacha/GachaManager.java @@ -1,57 +1,72 @@ package emu.nebula.game.gacha; +import java.util.ArrayList; import java.util.Collection; +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.Nebula; import emu.nebula.data.GameData; import emu.nebula.data.resources.GachaDef; +import emu.nebula.database.GameDatabaseObject; import emu.nebula.game.player.Player; import emu.nebula.game.player.PlayerChangeInfo; import emu.nebula.game.player.PlayerManager; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; -public class GachaManager extends PlayerManager { - private final Int2ObjectMap bannerInfos; - private boolean loaded; +@Getter +@Entity(value = "gacha", useDiscriminator = false) +public class GachaManager extends PlayerManager implements GameDatabaseObject { + @Id + private int uid; - public GachaManager(Player player) { - super(player); - this.bannerInfos = new Int2ObjectOpenHashMap<>(); + private Map banners; + private Map> histories; + + @Deprecated // Morphia only + public GachaManager() { + } - public synchronized GachaBannerInfo getBannerInfoById(int id) { - if (!this.loaded) { - this.loadFromDatabase(); - } + public GachaManager(Player player) { + this(); + this.setPlayer(player); + this.uid = player.getUid(); - return this.bannerInfos.get(id); + this.banners = new HashMap<>(); + this.histories = new HashMap<>(); + + this.save(); } public synchronized Collection getBannerInfos() { - if (!this.loaded) { - this.loadFromDatabase(); - } - - return this.bannerInfos.values(); + return this.banners.values(); } public synchronized GachaBannerInfo getBannerInfo(GachaDef gachaData) { - if (!this.loaded) { - this.loadFromDatabase(); - } - - return this.bannerInfos.computeIfAbsent( + return this.banners.computeIfAbsent( gachaData.getId(), - i -> new GachaBannerInfo(this.getPlayer(), gachaData) + i -> new GachaBannerInfo(gachaData) + ); + } + + public void saveBanner(GachaBannerInfo info) { + Nebula.getGameDatabase().update( + this, + this.getPlayerUid(), + "banners." + info.getId(), + info ); } public PlayerChangeInfo recvGuarantee(int id) { // Get banner info - var banner = this.getBannerInfoById(id); - if (banner == null) { + var info = this.banners.get(id); + if (info == null) { return null; } @@ -62,32 +77,62 @@ public class GachaManager extends PlayerManager { } // Check if we have enough pulls for a guarantee - if (!data.canGuarantee() || banner.getTotal() < data.getGuaranteeTimes()) { + if (!data.canGuarantee() || info.getTotal() < data.getGuaranteeTimes()) { return null; } // Make sure we havent used our guarantee yet - if (banner.isUsedGuarantee()) { + if (info.isUsedGuarantee()) { return null; } // Set guarantee - banner.setUsedGuarantee(true); - banner.save(); + info.setUsedGuarantee(true); + + // Update to database + this.saveBanner(info); // Give player the guaranteed item return getPlayer().getInventory().addItem(data.getGuaranteeTid(), data.getGuaranteeQty()); } - // Database + // Histories - private void loadFromDatabase() { - var db = Nebula.getGameDatabase(); + public void addGachaHistory(GachaHistoryLog log) { + // Get history + var list = this.histories.computeIfAbsent( + log.getType(), + i -> new ArrayList<>() + ); - db.getObjects(GachaBannerInfo.class, "playerUid", getPlayerUid()).forEach(bannerInfo -> { - this.bannerInfos.put(bannerInfo.getBannerId(), bannerInfo); - }); + // Add to history + list.add(log); - this.loaded = true; + // Limit history + boolean resize = false; + + while (list.size() > 50) { + list.remove(0); + resize = true; + } + + // Update to database + if (resize) { + // Replace history logs + Nebula.getGameDatabase().update( + this, + this.getPlayerUid(), + "histories." + log.getType(), + list + ); + } else { + // Add to history list + Nebula.getGameDatabase().addToSet( + this, + this.getPlayerUid(), + "histories." + log.getType(), + log + ); + } } } diff --git a/src/main/java/emu/nebula/game/gacha/GachaModule.java b/src/main/java/emu/nebula/game/gacha/GachaModule.java index b0e8b4a..36fc156 100644 --- a/src/main/java/emu/nebula/game/gacha/GachaModule.java +++ b/src/main/java/emu/nebula/game/gacha/GachaModule.java @@ -22,12 +22,12 @@ public class GachaModule extends GameContextModule { int amount = mode == 2 ? 10 : 1; // Get banner data - var banner = GameData.getGachaDataTable().get(bannerId); - if (banner == null) { + var data = GameData.getGachaDataTable().get(bannerId); + if (data == null) { return null; } - var bannerStorage = banner.getStorageData(); + var bannerStorage = data.getStorageData(); if (bannerStorage == null) { return null; } @@ -57,13 +57,13 @@ public class GachaModule extends GameContextModule { player.getInventory().removeItem(bannerStorage.getDefaultId(), Math.min(costReq, costQty), change); // Get gacha banner info - var info = player.getGachaManager().getBannerInfo(banner); + var info = player.getGachaManager().getBannerInfo(data); // Do gacha var results = new IntArrayList(); for (int i = 0; i < amount; i++) { - int id = info.doPull(banner); + int id = info.doPull(data); if (id <= 0) continue; results.add(id); @@ -158,7 +158,11 @@ public class GachaModule extends GameContextModule { change.add(transform); // Save banner info to database - info.save(); + player.getGachaManager().saveBanner(info); + + // Add history + var log = new GachaHistoryLog(data.getGachaType(), results); + player.getGachaManager().addGachaHistory(log); // Complete return new GachaResult(info, change, results); diff --git a/src/main/java/emu/nebula/game/player/Player.java b/src/main/java/emu/nebula/game/player/Player.java index cd9cb8d..ff4cfca 100644 --- a/src/main/java/emu/nebula/game/player/Player.java +++ b/src/main/java/emu/nebula/game/player/Player.java @@ -81,7 +81,6 @@ public class Player implements GameDatabaseObject { // Managers private final transient CharacterStorage characters; private final transient FriendList friendList; - private final transient GachaManager gachaManager; private final transient BattlePassManager battlePassManager; private final transient StarTowerManager starTowerManager; private final transient InstanceManager instanceManager; @@ -93,6 +92,7 @@ public class Player implements GameDatabaseObject { private transient Inventory inventory; private transient FormationManager formations; private transient Mailbox mailbox; + private transient GachaManager gachaManager; private transient PlayerProgress progress; private transient StoryManager storyManager; private transient QuestManager questManager; @@ -107,7 +107,6 @@ public class Player implements GameDatabaseObject { // Init player managers this.characters = new CharacterStorage(this); this.friendList = new FriendList(this); - this.gachaManager = new GachaManager(this); this.battlePassManager = new BattlePassManager(this); this.starTowerManager = new StarTowerManager(this); this.instanceManager = new InstanceManager(this); @@ -597,6 +596,7 @@ public class Player implements GameDatabaseObject { this.formations = this.loadManagerFromDatabase(FormationManager.class); this.mailbox = this.loadManagerFromDatabase(Mailbox.class); this.progress = this.loadManagerFromDatabase(PlayerProgress.class); + this.gachaManager = this.loadManagerFromDatabase(GachaManager.class); this.storyManager = this.loadManagerFromDatabase(StoryManager.class); this.questManager = this.loadManagerFromDatabase(QuestManager.class); this.agentManager = this.loadManagerFromDatabase(AgentManager.class); diff --git a/src/main/java/emu/nebula/game/player/PlayerModule.java b/src/main/java/emu/nebula/game/player/PlayerModule.java index ae40b91..6a6087c 100644 --- a/src/main/java/emu/nebula/game/player/PlayerModule.java +++ b/src/main/java/emu/nebula/game/player/PlayerModule.java @@ -16,7 +16,7 @@ import emu.nebula.game.character.Character; import emu.nebula.game.character.GameDisc; import emu.nebula.game.formation.FormationManager; import emu.nebula.game.friends.Friendship; -import emu.nebula.game.gacha.GachaBannerInfo; +import emu.nebula.game.gacha.GachaManager; import emu.nebula.game.inventory.GameItem; import emu.nebula.game.inventory.GameResource; import emu.nebula.game.inventory.Inventory; @@ -172,12 +172,12 @@ public class PlayerModule extends GameContextModule { datastore.getCollection(GameItem.class).deleteMany(multiFilter); datastore.getCollection(GameResource.class).deleteMany(multiFilter); datastore.getCollection(StarTowerBuild.class).deleteMany(multiFilter); - datastore.getCollection(GachaBannerInfo.class).deleteMany(multiFilter); datastore.getCollection(Inventory.class).deleteOne(idFilter); datastore.getCollection(FormationManager.class).deleteOne(idFilter); datastore.getCollection(Mailbox.class).deleteOne(idFilter); datastore.getCollection(PlayerProgress.class).deleteOne(idFilter); + datastore.getCollection(GachaManager.class).deleteOne(idFilter); datastore.getCollection(StoryManager.class).deleteOne(idFilter); datastore.getCollection(QuestManager.class).deleteOne(idFilter); datastore.getCollection(AgentManager.class).deleteOne(idFilter); diff --git a/src/main/java/emu/nebula/server/handlers/HandlerGachaHistoriesReq.java b/src/main/java/emu/nebula/server/handlers/HandlerGachaHistoriesReq.java new file mode 100644 index 0000000..5e280ee --- /dev/null +++ b/src/main/java/emu/nebula/server/handlers/HandlerGachaHistoriesReq.java @@ -0,0 +1,34 @@ +package emu.nebula.server.handlers; + +import emu.nebula.net.NetHandler; +import emu.nebula.net.NetMsgId; +import emu.nebula.proto.GachaHistoriesOuterClass.GachaHistories; +import emu.nebula.proto.Public.UI32; +import emu.nebula.net.HandlerId; +import emu.nebula.net.GameSession; + +@HandlerId(NetMsgId.gacha_histories_req) +public class HandlerGachaHistoriesReq extends NetHandler { + + @Override + public byte[] handle(GameSession session, byte[] message) throws Exception { + // Parse request + var req = UI32.parseFrom(message); + + // Get history log + var list = session.getPlayer().getGachaManager().getHistories().get(req.getValue()); + + // Build response + var rsp = GachaHistories.newInstance(); + + if (list != null) { + for (var log : list) { + rsp.addList(log.toProto()); + } + } + + // Encode and send + return session.encodeMsg(NetMsgId.gacha_histories_succeed_ack, rsp); + } + +}