Implement gacha history

This commit is contained in:
Melledy
2025-11-16 00:32:47 -08:00
parent c859252ddd
commit f2656903e5
7 changed files with 178 additions and 65 deletions

View File

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

View File

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

View File

@@ -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<GachaBannerInfo> 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<Integer, GachaBannerInfo> banners;
private Map<Integer, List<GachaHistoryLog>> 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<GachaBannerInfo> 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
);
}
}
}

View File

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

View File

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

View File

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

View File

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