Fix cant claim beginner banner results and simplify newbie gacha flow

This commit is contained in:
Yostarcc
2026-05-10 21:38:47 -07:00
committed by Melledy
parent 276c8b2ebd
commit 580a7a92e7
6 changed files with 70 additions and 139 deletions
@@ -45,20 +45,20 @@ public class GachaManager extends PlayerManager implements GameDatabaseObject {
this.save();
}
public synchronized NewbieObtainLock lockNewbieObtain(int newbieId) {
public synchronized boolean tryLockNewbieObtain(int newbieId) {
if (this.lockedNewbieObtainIds.contains(newbieId)) {
return null;
return false;
}
this.lockedNewbieObtainIds.add(newbieId);
return new NewbieObtainLockHandle(newbieId);
return true;
}
public synchronized boolean isNewbieObtainLocked(int newbieId) {
return this.lockedNewbieObtainIds.contains(newbieId);
}
private synchronized void unlockNewbieObtainInternal(int newbieId) {
public synchronized void unlockNewbieObtain(int newbieId) {
this.lockedNewbieObtainIds.remove(newbieId);
}
@@ -145,30 +145,4 @@ public class GachaManager extends PlayerManager implements GameDatabaseObject {
);
}
private final class NewbieObtainLockHandle implements NewbieObtainLock {
private final int newbieId;
private boolean closed;
private NewbieObtainLockHandle(int newbieId) {
this.newbieId = newbieId;
}
@Override
public void close() {
synchronized (GachaManager.this) {
if (this.closed) {
return;
}
this.closed = true;
unlockNewbieObtainInternal(this.newbieId);
}
}
}
public interface NewbieObtainLock extends AutoCloseable {
@Override
void close();
}
}
@@ -18,10 +18,12 @@ import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.util.List;
public class GachaModule extends GameContextModule {
private final NewbieGachaModule newbieGachaModule = new NewbieGachaModule();
private final NewbieGachaModule newbieGachaModule;
public GachaModule(GameContext context) {
super(context);
this.newbieGachaModule = new NewbieGachaModule();
}
public GachaResult spin(Player player, int bannerId, int amount) {
@@ -14,9 +14,6 @@ import java.util.List;
public final class NewbieGachaModule {
private record NewbieRequest(GachaManager manager, GachaNewbieDef newbieDef) {
}
public List<GachaNewbieInfo> listInfos(Player player) {
var newbieDefs = GameData.getGachaNewbieDataTable().values();
var infos = new ArrayList<GachaNewbieInfo>(newbieDefs.size());
@@ -54,21 +51,20 @@ public final class NewbieGachaModule {
}
public int[] spin(Player player, int newbieId) {
var request = resolveRequest(player, newbieId);
if (request == null) {
GachaNewbieDef newbieDef = GameData.getGachaNewbieDataTable().get(newbieId);
if (newbieDef == null) {
return null;
}
int newbieStateId = request.newbieDef().getId();
int newbieStateId = newbieDef.getId();
var bannerDef = GameData.getGachaDataTable().get(newbieStateId);
if (bannerDef == null) {
return null;
}
var manager = request.manager();
synchronized (manager) {
var state = loadStateForSpin(manager, request.newbieDef());
if (state == null) {
synchronized (player.getGachaManager()) {
NewbieGachaState state = player.getGachaManager().getOrCreateNewbieState(newbieDef);
if (player.getGachaManager().isNewbieObtainLocked(newbieStateId) || !state.canSpin()) {
return null;
}
@@ -81,56 +77,52 @@ public final class NewbieGachaModule {
return null;
}
manager.saveNewbieState(state);
player.getGachaManager().saveNewbieState(state);
return cards;
}
}
public boolean save(Player player, int newbieId, Integer index) {
int resolvedIndex = index == null ? -1 : index;
var request = resolveRequest(player, newbieId);
if (request == null) {
var newbieDef = GameData.getGachaNewbieDataTable().get(newbieId);
if (newbieDef == null) {
return false;
}
var manager = request.manager();
synchronized (manager) {
var state = loadStateForSave(manager, request.newbieDef());
if (state == null) {
synchronized (player.getGachaManager()) {
if (player.getGachaManager().isNewbieObtainLocked(newbieId)) {
return false;
}
var state = player.getGachaManager().getOrCreateNewbieState(newbieDef);
if (state == null || !state.canSavePendingResult()) {
return false;
}
if (!state.savePendingResult(resolvedIndex)) {
if (!state.savePendingResult(index)) {
return false;
}
manager.saveNewbieState(state);
player.getGachaManager().saveNewbieState(state);
return true;
}
}
public PlayerChangeInfo obtain(Player player, int newbieId, int index) {
if (index < 0) {
GachaNewbieDef gachaNewbieDef = GameData.getGachaNewbieDataTable().get(newbieId);
if (gachaNewbieDef == null) {
return null;
}
var request = resolveRequest(player, newbieId);
if (request == null) {
int newbieStateId = gachaNewbieDef.getId();
if (!player.getGachaManager().tryLockNewbieObtain(newbieStateId)) {
return null;
}
int newbieStateId = request.newbieDef().getId();
var obtainLock = request.manager().lockNewbieObtain(newbieStateId);
if (obtainLock == null) {
return null;
}
try (var ignored = obtainLock) {
try {
NewbieGachaState state;
int[] cards;
synchronized (request.manager()) {
state = loadStateForObtain(request.manager(), request.newbieDef(), index);
if (state == null) {
synchronized (player.getGachaManager()) {
state = player.getGachaManager().findNewbieState(newbieStateId);
if (state == null || !state.canObtain(index)) {
return null;
}
@@ -147,66 +139,19 @@ public final class NewbieGachaModule {
return null;
}
synchronized (request.manager()) {
synchronized (player.getGachaManager()) {
if (!state.markReceived(index)) {
return null;
}
request.manager().saveNewbieState(state);
player.getGachaManager().saveNewbieState(state);
}
GachaRewardResolver.apply(player, rewardPlan, change);
return change;
} finally {
player.getGachaManager().unlockNewbieObtain(newbieStateId);
}
}
private NewbieRequest resolveRequest(Player player, int newbieId) {
var newbieDef = GameData.getGachaNewbieDataTable().get(newbieId);
if (newbieDef == null) {
return null;
}
return new NewbieRequest(player.getGachaManager(), newbieDef);
}
private NewbieGachaState loadStateForSpin(GachaManager manager, GachaNewbieDef newbieDef) {
int newbieStateId = newbieDef.getId();
if (manager.isNewbieObtainLocked(newbieStateId)) {
return null;
}
var state = manager.getOrCreateNewbieState(newbieDef);
if (!state.canSpin(false)) {
return null;
}
return state;
}
private NewbieGachaState loadStateForSave(GachaManager manager,
GachaNewbieDef newbieDef) {
int newbieStateId = newbieDef.getId();
if (manager.isNewbieObtainLocked(newbieStateId)) {
return null;
}
var state = manager.getOrCreateNewbieState(newbieDef);
if (!state.canSavePendingResult(false)) {
return null;
}
return state;
}
private NewbieGachaState loadStateForObtain(GachaManager manager, GachaNewbieDef newbieDef, int index) {
int newbieStateId = newbieDef.getId();
var state = manager.findNewbieState(newbieStateId);
if (state == null) {
return null;
}
if (!state.canObtain(index)) {
return null;
}
return state;
}
}
@@ -30,25 +30,23 @@ public class NewbieGachaState {
}
// Checks if the player can perform a spin.
public boolean canSpin(boolean obtainLocked) {
return !received && !obtainLocked && remainingSpinCount > 0;
public boolean canSpin() {
return !received && remainingSpinCount > 0;
}
// Checks if the current pending result can be moved to saved results
public boolean canSavePendingResult(boolean obtainLocked) {
return !received && !obtainLocked && hasPendingResult();
public boolean canSavePendingResult() {
return !received && hasPendingResult();
}
// Updates the maximum allowed saved results.
public boolean applyConfig(int saveCount) {
int oldSaveCount = this.saveCount;
public void applyConfig(int saveCount) {
this.saveCount = Math.max(1, saveCount);
return this.saveCount != oldSaveCount;
}
// Applies a new spin result to the pending slot and consumes a spin attempt
public boolean applySpinResult(int[] cards) {
if (!canSpin(false) || cards == null || cards.length == 0) {
if (!canSpin() || cards == null || cards.length == 0) {
return false;
}
@@ -58,13 +56,15 @@ public class NewbieGachaState {
}
// Saves the pending result into the saved results list at the specified index or adds it
public boolean savePendingResult(int index) {
if (!hasPendingResult() || received) return false;
public boolean savePendingResult(Integer index) {
if (!hasPendingResult() || this.received) {
return false;
}
if (index >= 0 && index < savedResults.size()) {
if (index != null && index < savedResults.size()) {
// Replace existing slot
savedResults.set(index, pendingResult);
} else if (savedResults.size() < saveCount) {
} else if (index == null && savedResults.size() < saveCount) {
// Add new slot if capacity allows
savedResults.add(pendingResult);
} else {
@@ -75,19 +75,26 @@ public class NewbieGachaState {
return true;
}
// Returns a clone of the saved result at the given index
// Returns a clone of the result referenced by obtain request index.
// Idx=0 -> pendingResult, Idx=1-saveCount -> savedResults[idx-1]
public int[] copySavedResult(int index) {
return (index >= 0 && index < savedResults.size()) ? savedResults.get(index).clone() : null;
if (index == 0) {
return hasPendingResult() ? pendingResult.clone() : null;
}
// Check for claiming a specific result
int savedResultIndex = index - 1;
return (savedResultIndex >= 0 && savedResultIndex < savedResults.size()) ? savedResults.get(savedResultIndex).clone() : null;
}
// Checks whether the obtain request index points to a valid result.
// Idx=0 -> pendingResult, Idx=1-saveCount -> savedResults[idx-1]
public boolean canObtain(int index) {
return !received && index >= 0 && index < savedResults.size();
return !received && copySavedResult(index) != null;
}
// Marks a specific saved result as claimed and closes the gacha session
// Marks the selected obtain request index as claimed and closes the gacha session.
public boolean markReceived(int index) {
if (received || index < 0 || index >= savedResults.size()) {
if (received || copySavedResult(index) == null) {
return false;
}
@@ -13,15 +13,15 @@ public class HandlerGachaNewbieObtainReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = GachaNewbieObtainReq.parseFrom(message);
if (!req.hasId() || req.getId() < 0 || (req.hasIdx() && req.getIdx() < 0)) {
return session.encodeMsg(NetMsgId.gacha_newbie_obtain_failed_ack);
}
// req.getIdx() is optional, it always has a value
var change = Nebula.getGameContext().getGachaModule().obtainNewbie(session.getPlayer(), req.getId(), req.getIdx());
if (change == null) {
return session.encodeMsg(NetMsgId.gacha_newbie_obtain_failed_ack);
}
if (!change.isEmpty()) {
session.getPlayer().addNextPackage(NetMsgId.items_change_notify, change.toProto());
}
return session.encodeMsg(NetMsgId.gacha_newbie_obtain_succeed_ack, change.toProto());
}
@@ -14,8 +14,11 @@ public class HandlerGachaNewbieSaveReq extends NetHandler {
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = GachaNewbieSaveReq.parseFrom(message);
Integer index = req.hasIdx() ? req.getIdx() : null;
boolean succeeded = Nebula.getGameContext().getGachaModule().saveNewbie(session.getPlayer(), req.getId(), index);
if (!succeeded) {
if (!req.hasId() || req.getId() < 0 || (index != null && index < 0)) {
return session.encodeMsg(NetMsgId.gacha_newbie_save_failed_ack);
}
boolean newbieSaveResult = Nebula.getGameContext().getGachaModule().saveNewbie(session.getPlayer(), req.getId(), index);
if (!newbieSaveResult) {
return session.encodeMsg(NetMsgId.gacha_newbie_save_failed_ack);
}