fix(all): advance skin reward animations and deduplicate owned skins
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user