fix(all): advance skin reward animations and deduplicate owned skins

This commit is contained in:
2026-10-05 19:45:54 +08:00
parent fe659999ba
commit e15ee4645e
5 changed files with 100 additions and 1 deletions
+16
View File
@@ -254,10 +254,26 @@ func (s *Inventory) GrantOnce(identity string, rewards []gamedata.BattleReward)
}
next := cloneOwnedSnapshot(s.owned)
newItems := make([]Item, 0, len(rewards))
// Skin rewards represent ownership; returning an existing skin would also
// replay the client's acquisition UI. Include the immutable starter seed.
ownedSkins := make(map[uint64]bool)
for _, items := range [][]Item{s.starter.Items, next.Items} {
for _, item := range items {
if item.Type == 45 && item.Count > 0 {
ownedSkins[item.ID] = true
}
}
}
for _, r := range rewards {
if r.ID == 0 || r.Type == 0 || r.Count == 0 {
return nil, errors.New("player: invalid battle reward")
}
if r.Type == 45 {
if ownedSkins[r.ID] {
continue
}
ownedSkins[r.ID] = true
}
item := Item{InvenIndex: next.NextIndex, ID: r.ID, Type: r.Type, Count: r.Count, TimeValue: uint64(time.Now().UnixMilli())}
next.NextIndex++
next.Items = append(next.Items, item)
+37
View File
@@ -43,6 +43,43 @@ func TestBattleRewardPersistsWithoutDuplicate(t *testing.T) {
}
}
func TestSkinGrantSkipsOwnedAndBatchDuplicatesAcrossRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "owned.json")
starter := &Starter{Version: "2.35.10", Items: []Item{{InvenIndex: 1, ID: 4001, Type: 45, Count: 1}}}
inv, err := OpenInventory(testStore(path), starter)
if err != nil {
t.Fatal(err)
}
if items, err := inv.GrantOnce("first", []gamedata.BattleReward{{Type: 45, ID: 4091, Count: 1}}); err != nil || len(items) != 1 {
t.Fatalf("first grant=%+v err=%v", items, err)
}
rewards := []gamedata.BattleReward{{Type: 45, ID: 4001, Count: 1}, {Type: 45, ID: 4091, Count: 1}, {Type: 45, ID: 4092, Count: 1}, {Type: 45, ID: 4092, Count: 1}, {Type: 8, ID: 8, Count: 3}}
items, err := inv.GrantOnce("mixed", rewards)
if err != nil || len(items) != 2 || items[0].ID != 4092 || items[1].Type != 8 {
t.Fatalf("mixed grant=%+v err=%v", items, err)
}
inv, err = OpenInventory(testStore(path), starter)
if err != nil {
t.Fatal(err)
}
if items, err := inv.GrantOnce("mixed", rewards); err != nil || len(items) != 0 {
t.Fatalf("replay grant=%+v err=%v", items, err)
}
if prior := inv.GrantedItems("mixed"); len(prior) != 2 || prior[0].ID != 4092 {
t.Fatalf("replay items=%+v", prior)
}
if items, err := inv.GrantOnce("owned-only", rewards[:2]); err != nil || len(items) != 0 {
t.Fatalf("owned-only grant=%+v err=%v", items, err)
}
inv, err = OpenInventory(testStore(path), starter)
if err != nil {
t.Fatal(err)
}
if !inv.WasGranted("owned-only") || len(inv.GrantedItems("owned-only")) != 0 || len(inv.owned.Items) != 3 {
t.Fatalf("owned-only identity or inventory changed: %+v", inv.owned)
}
}
func TestUseRandomBoxPersistsExactStackAndRewardFromInstalledGameData(t *testing.T) {
root := os.Getenv("BD2_TEST_GAMEDATA_ROOT")
if root == "" {
+1
View File
@@ -88,6 +88,7 @@ public sealed class Plugin : BaseUnityPlugin
PatchGeneratedReceiver("<SendEventRewardRequest>b__0", nameof(EventRewards));
PassRewardPresentation.Install(harmony);
RewardBuffRefresh.Install(harmony);
PrestigeSkinRewardPresentation.Install(harmony, Log);
MethodInfo attendance = typeof(EventPacket).GetGameMethod("RecvAttendanceResponse", All, null,
new[] { typeof(IMessage), typeof(int), typeof(int) }, null);
if (attendance == null) throw new MissingMethodException("EventPacket.RecvAttendanceResponse(IMessage,int,int)");
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using BD2.GameNames;
using BepInEx.Logging;
using HarmonyLib;
namespace Bd2CashShop;
// Native reward recursion leaves the previous skin window registered. AddUI
// then rejects the next window with the same name without invoking its callback.
internal static class PrestigeSkinRewardPresentation
{
private static ManualLogSource log;
internal static void Install(Harmony harmony, ManualLogSource logger)
{
log = logger;
var target = typeof(PrestigeSkinGetUI).GetGameMethod("RecursiveShowPrestigeSkin",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null,
new[] { typeof(Queue<ItemBaseInfo>), typeof(Action) }, null);
if (target == null) throw new MissingMethodException("PrestigeSkinGetUI.RecursiveShowPrestigeSkin");
harmony.Patch(target, prefix: new HarmonyMethod(typeof(PrestigeSkinRewardPresentation), nameof(ClosePreviousWindows)));
}
private static void ClosePreviousWindows(Queue<ItemBaseInfo> __0)
{
// Snapshot both aliases before removing either window. CloseProcess
// releases assets and clears the end-step callback; it does not invoke it.
var prestige = UIManager.GetUI<PrestigeSkinGetUI>();
var special = UIManager.GetUI("SpecialSkinGetUI") as PrestigeSkinGetUI;
int closed = 0;
if (prestige != null)
{
prestige.CloseForceWithoutAnimation();
closed++;
}
if (special != null && special != prestige)
{
special.CloseForceWithoutAnimation();
closed++;
}
log?.LogInfo("Prestige skin reward queue: remaining=" + (__0?.Count ?? 0) + " closed_windows=" + closed);
}
}
+1 -1
View File
@@ -9,6 +9,6 @@
"local_identity": "0.6.1",
"capture_environment": "0.2.0",
"login_ui": "0.2.0",
"cash_shop": "0.1.1"
"cash_shop": "0.1.2"
}
}