From c9b5f8569fdc793ccb6859d330b2f0530f930614 Mon Sep 17 00:00:00 2001 From: xinian Date: Tue, 14 Apr 2026 13:06:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=81=93=E5=85=B7?= =?UTF-8?q?=E6=89=A3=E9=99=A4=E5=92=8C=E5=AE=A0=E7=89=A9=E8=9E=8D=E5=90=88?= =?UTF-8?q?=E4=BA=8B=E5=8A=A1=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- logic/controller/action_大师杯.go | 11 +- logic/controller/item_sale.go | 5 +- logic/controller/item_use.go | 48 +++-- logic/controller/item_use_test.go | 60 ++++++ logic/controller/pet_elo.go | 4 +- logic/controller/pet_fusion.go | 93 +++++--- logic/controller/pet_manage.go | 41 +++- logic/controller/pet_manage_test.go | 75 +++++++ logic/service/player/pet.go | 2 - logic/service/player/player.go | 10 +- modules/player/service/pet_fusion_tx.go | 269 ++++++++++++++++++++++++ 11 files changed, 562 insertions(+), 56 deletions(-) create mode 100644 logic/controller/item_use_test.go create mode 100644 logic/controller/pet_manage_test.go create mode 100644 modules/player/service/pet_fusion_tx.go diff --git a/logic/controller/action_大师杯.go b/logic/controller/action_大师杯.go index 8170f167d..525cc5b09 100644 --- a/logic/controller/action_大师杯.go +++ b/logic/controller/action_大师杯.go @@ -82,7 +82,9 @@ func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (resul return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed) } - consumeMasterCupItems(c, requiredItems) + if err := consumeMasterCupItems(c, requiredItems); err != nil { + return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrInsufficientItems) + } progress.Set(uint(req.ElementType)) taskData.Data = progress.Bytes() if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil { @@ -130,10 +132,13 @@ func hasEnoughMasterCupItems(c *player.Player, requiredItems []ItemS) bool { return true } -func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) { +func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) error { for _, item := range requiredItems { - c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt)) + if err := c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt)); err != nil { + return err + } } + return nil } func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, itemList []data.ItemInfo) { diff --git a/logic/controller/item_sale.go b/logic/controller/item_sale.go index 4e31b149f..e7779e0e3 100644 --- a/logic/controller/item_sale.go +++ b/logic/controller/item_sale.go @@ -18,10 +18,13 @@ func (h Controller) ItemSale(data *C2S_ITEM_SALE, c *player.Player) (result *fig return nil, errorcode.ErrorCodes.ErrSystemError } + if err := c.Service.Item.UPDATE(data.ItemId, -gconv.Int(data.Amount)); err != nil { + return nil, errorcode.ErrorCodes.ErrInsufficientItems + } + itemConfig := xmlres.ItemsMAP[int(data.ItemId)] if itemConfig.SellPrice != 0 { c.Info.Coins += int64(int64(data.Amount) * int64(itemConfig.SellPrice)) } - c.Service.Item.UPDATE(data.ItemId, -gconv.Int(data.Amount)) return result, 0 } diff --git a/logic/controller/item_use.go b/logic/controller/item_use.go index b5ba15b74..2ff86fc58 100644 --- a/logic/controller/item_use.go +++ b/logic/controller/item_use.go @@ -58,7 +58,10 @@ func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c return nil, errcode } refreshPetPaneKeepHP(currentPet, oldHP) - c.Service.Item.UPDATE(itemID, -1) + if err := c.Service.Item.UPDATE(itemID, -1); err != nil { + return nil, errorcode.ErrorCodes.ErrInsufficientItems + } + c.Service.Info.Save(*c.Info) result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{} copier.Copy(&result, currentPet) return result, 0 @@ -90,7 +93,10 @@ func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c return nil, errcode } - c.Service.Item.UPDATE(itemID, -1) + if err := c.Service.Item.UPDATE(itemID, -1); err != nil { + return nil, errorcode.ErrorCodes.ErrInsufficientItems + } + c.Service.Info.Save(*c.Info) result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{} copier.Copy(&result, currentPet) return result, 0 @@ -133,7 +139,9 @@ func (h Controller) handlexuancaiItem(currentPet *model.PetInfo, c *player.Playe return errorcode.ErrorCodes.ErrItemUnusable } - c.Service.Item.UPDATE(itemid, -100) + if err := c.Service.Item.UPDATE(itemid, -100); err != nil { + return errorcode.ErrorCodes.ErrInsufficientItems + } return 0 } @@ -214,7 +222,10 @@ func (h Controller) ResetNature(data *C2S_PET_RESET_NATURE, c *player.Player) (r currentHP := currentPet.Hp currentPet.Nature = data.Nature refreshPetPaneKeepHP(currentPet, currentHP) - c.Service.Item.UPDATE(data.ItemId, -1) + if err := c.Service.Item.UPDATE(data.ItemId, -1); err != nil { + return nil, errorcode.ErrorCodes.ErrInsufficientItems + } + c.Service.Info.Save(*c.Info) return result, 0 } @@ -242,29 +253,38 @@ func (h Controller) UseSpeedupItem(data *C2S_USE_SPEEDUP_ITEM, c *player.Player) if c.Info.TwoTimes != 0 { return nil, errorcode.ErrorCodes.ErrItemInUse } - c.Info.TwoTimes += 50 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数 case 300067: if c.Info.TwoTimes != 0 { return nil, errorcode.ErrorCodes.ErrItemInUse } - c.Info.TwoTimes += 25 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数 case 300051: // 假设1002是三倍经验加速器道具ID if c.Info.ThreeTimes != 0 { return nil, errorcode.ErrorCodes.ErrItemInUse } - c.Info.ThreeTimes += 50 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数 case 300115: if c.Info.ThreeTimes != 0 { return nil, errorcode.ErrorCodes.ErrItemInUse } - c.Info.ThreeTimes += 30 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数 default: return nil, errorcode.ErrorCodes.ErrSystemError // 未知道具ID } // 3. 扣减道具(数量-1) - c.Service.Item.UPDATE(data.ItemID, -1) + if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil { + return nil, errorcode.ErrorCodes.ErrInsufficientItems + } + + switch data.ItemID { + case 300027: // 假设1001是双倍经验加速器道具ID + c.Info.TwoTimes += 50 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数 + case 300067: + c.Info.TwoTimes += 25 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数 + case 300051: // 假设1002是三倍经验加速器道具ID + c.Info.ThreeTimes += 50 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数 + case 300115: + c.Info.ThreeTimes += 30 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数 + } result.ThreeTimes = uint32(c.Info.ThreeTimes) // 返回三倍经验剩余次数 result.TwoTimes = uint32(c.Info.TwoTimes) // 返回双倍经验剩余次数 @@ -295,10 +315,11 @@ func (h Controller) UseEnergyXishou(data *C2S_USE_ENERGY_XISHOU, c *player.Playe } // 2. 核心业务逻辑:更新能量吸收器剩余次数 // (注:可根据道具ID配置不同的次数加成,此处默认+1) + if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil { + return nil, errorcode.ErrorCodes.ErrInsufficientItems + } c.Info.EnergyTime += 40 // 玩家对象新增 EnergyTimes 字段存储能量吸收剩余次数 - // 3. 扣减道具(数量-1) - c.Service.Item.UPDATE(data.ItemID, -1) result = &item.S2C_USE_ENERGY_XISHOU{ EnergyTimes: uint32(c.Info.EnergyTime), } @@ -329,6 +350,9 @@ func (h Controller) UseAutoFightItem(data *C2S_USE_AUTO_FIGHT_ITEM, c *player.Pl if c.Info.AutoFightTime != 0 { return nil, errorcode.ErrorCodes.ErrItemInUse } + if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil { + return nil, errorcode.ErrorCodes.ErrInsufficientItems + } result = &item.S2C_USE_AUTO_FIGHT_ITEM{} // 2. 核心业务逻辑:开启自动战斗 + 更新剩余次数 c.Info.AutoFight = 3 // 按需求设置自动战斗flag为3(需测试) @@ -344,8 +368,6 @@ func (h Controller) UseAutoFightItem(data *C2S_USE_AUTO_FIGHT_ITEM, c *player.Pl c.Info.AutoFightTime += 50 } result.AutoFightTimes = c.Info.AutoFightTime - // 3. 扣减道具(数量-1) - c.Service.Item.UPDATE(data.ItemID, -1) return result, 0 } diff --git a/logic/controller/item_use_test.go b/logic/controller/item_use_test.go new file mode 100644 index 000000000..384074439 --- /dev/null +++ b/logic/controller/item_use_test.go @@ -0,0 +1,60 @@ +package controller + +import ( + "blazing/common/data/xmlres" + "blazing/logic/service/player" + playermodel "blazing/modules/player/model" + blservice "blazing/modules/player/service" + "testing" +) + +func TestUsePetItemOutOfFightAppliesToBackupPetInMemory(t *testing.T) { + petID := firstPetIDForControllerTest(t) + backupPet := playermodel.GenPetInfo(petID, 31, 0, 0, 50, nil, 0) + if backupPet == nil { + t.Fatal("failed to generate backup pet") + } + if backupPet.MaxHp <= 1 { + t.Fatalf("expected generated pet to have max hp > 1, got %d", backupPet.MaxHp) + } + backupPet.Hp = 1 + + testPlayer := player.NewPlayer(nil) + testPlayer.Info = &playermodel.PlayerInfo{ + UserID: 1, + PetList: []playermodel.PetInfo{}, + BackupPetList: []playermodel.PetInfo{*backupPet}, + } + testPlayer.Service = blservice.NewUserService(testPlayer.Info.UserID) + + itemID, recoverHP := firstRecoverHPItemForControllerTest(t) + if recoverHP <= 0 { + t.Fatalf("expected positive recover hp for item %d, got %d", itemID, recoverHP) + } + + _, err := (Controller{}).UsePetItemOutOfFight(&C2S_USE_PET_ITEM_OUT_OF_FIGHT{ + CatchTime: backupPet.CatchTime, + ItemID: int32(itemID), + }, testPlayer) + if err != 0 { + t.Fatalf("expected backup pet item use to succeed in-memory, got err=%d", err) + } + + updatedPet := testPlayer.Info.BackupPetList[0] + if updatedPet.Hp <= 1 { + t.Fatalf("expected backup pet hp to increase in memory, got hp=%d", updatedPet.Hp) + } +} + +func firstRecoverHPItemForControllerTest(t *testing.T) (uint32, int) { + t.Helper() + + for id, cfg := range xmlres.ItemsMAP { + if cfg.HP > 0 { + return uint32(id), cfg.HP + } + } + + t.Fatal("xmlres.ItemsMAP has no HP recovery item") + return 0, 0 +} diff --git a/logic/controller/pet_elo.go b/logic/controller/pet_elo.go index 42b51240e..a685bf817 100644 --- a/logic/controller/pet_elo.go +++ b/logic/controller/pet_elo.go @@ -37,7 +37,9 @@ func (h Controller) PetELV(data *C2S_PET_EVOLVTION, c *player.Player) (result *f return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti } if branch.EvolvItem != 0 { - c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount) + if err := c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount); err != nil { + return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti + } } currentPet.ID = uint32(branch.MonTo) diff --git a/logic/controller/pet_fusion.go b/logic/controller/pet_fusion.go index 395959bf3..f5e67a33f 100644 --- a/logic/controller/pet_fusion.go +++ b/logic/controller/pet_fusion.go @@ -65,16 +65,33 @@ func (h Controller) PetFusion(data *C2S_PetFusion, c *player.Player) (result *pe return result, errorcode.ErrorCodes.ErrSunDouInsufficient10016 } - consumeItems(c, materialCounts) - c.Info.Coins -= petFusionCost - if resultPetID == 0 { - if useOptionalItem(c, data.GoldItem1[:], petFusionFailureItemID) { - result.CostItemFlag = 1 - } else if auxPet.Level > 5 { - auxPet.Downgrade(auxPet.Level - 5) + failedAux := *auxPet + if auxPet.Level > 5 { + failedAux.Downgrade(auxPet.Level - 5) } else { - auxPet.Downgrade(1) + failedAux.Downgrade(1) + } + txResult, errCode := c.Service.PetFusionTx( + *c.Info, + data.Mcatchtime, + data.Auxcatchtime, + materialCounts, + data.GoldItem1[:], + petFusionKeepAuxItemID, + petFusionFailureItemID, + petFusionCost, + nil, + &failedAux, + ) + if errCode != 0 { + return result, errCode + } + c.Info.Coins -= petFusionCost + if txResult.CostItemUsed { + result.CostItemFlag = 1 + } else if txResult.UpdatedAux != nil { + *auxPet = *txResult.UpdatedAux } return &pet.PetFusionInfo{}, 0 } @@ -101,18 +118,37 @@ func (h Controller) PetFusion(data *C2S_PetFusion, c *player.Player) (result *pe newPet.RandomByWeightShiny() } - c.Service.Pet.PetAdd(newPet, 0) - //println(c.Info.UserID, "进行融合", len(c.Info.PetList), masterPet.ID, auxPet.ID, newPet.ID) - - c.PetDel(data.Mcatchtime) - if useOptionalItem(c, data.GoldItem1[:], petFusionKeepAuxItemID) { - result.CostItemFlag = 1 - } else { - c.PetDel(data.Auxcatchtime) + txResult, errCode := c.Service.PetFusionTx( + *c.Info, + data.Mcatchtime, + data.Auxcatchtime, + materialCounts, + data.GoldItem1[:], + petFusionKeepAuxItemID, + petFusionFailureItemID, + petFusionCost, + newPet, + nil, + ) + if errCode != 0 { + return result, errCode } - result.ObtainTime = newPet.CatchTime - result.StarterCpTm = newPet.ID + c.Info.Coins -= petFusionCost + if txResult.CostItemUsed { + result.CostItemFlag = 1 + } else { + removePetFromPlayerInfo(c, data.Auxcatchtime) + } + removePetFromPlayerInfo(c, data.Mcatchtime) + + if txResult.NewPet == nil { + return result, errorcode.ErrorCodes.ErrSystemError + } + c.Info.PetList = append(c.Info.PetList, *txResult.NewPet) + + result.ObtainTime = txResult.NewPet.CatchTime + result.StarterCpTm = txResult.NewPet.ID return result, 0 } @@ -149,21 +185,10 @@ func hasEnoughItems(c *player.Player, itemCounts map[uint32]int) bool { return true } -func consumeItems(c *player.Player, itemCounts map[uint32]int) { - for itemID, count := range itemCounts { - _ = c.Service.Item.UPDATE(itemID, -count) +func removePetFromPlayerInfo(c *player.Player, catchTime uint32) { + index, _, ok := c.FindPet(catchTime) + if !ok { + return } -} - -func useOptionalItem(c *player.Player, itemIDs []uint32, target uint32) bool { - if c.Service.Item.CheakItem(target) <= 0 { - return false - } - for _, itemID := range itemIDs { - if itemID == target { - _ = c.Service.Item.UPDATE(target, -1) - return true - } - } - return false + c.Info.PetList = append(c.Info.PetList[:index], c.Info.PetList[index+1:]...) } diff --git a/logic/controller/pet_manage.go b/logic/controller/pet_manage.go index 69afcf8c6..9fa391682 100644 --- a/logic/controller/pet_manage.go +++ b/logic/controller/pet_manage.go @@ -6,8 +6,42 @@ import ( "blazing/logic/service/fight" "blazing/logic/service/pet" "blazing/logic/service/player" + playermodel "blazing/modules/player/model" ) +func petSetExpLimit(currentPet *playermodel.PetInfo) int64 { + if currentPet == nil || currentPet.Level >= 100 { + return 0 + } + + simulatedPet := *currentPet + var allowedExp int64 + + for simulatedPet.Level < 100 { + needExp := simulatedPet.NextLvExp - simulatedPet.Exp + if needExp <= 0 { + simulatedPet.Level++ + simulatedPet.Exp = 0 + simulatedPet.Update(true) + continue + } + + allowedExp += needExp + simulatedPet.Level++ + simulatedPet.Exp = 0 + simulatedPet.Update(true) + } + + return allowedExp +} + +func minInt64(a, b int64) int64 { + if a < b { + return a + } + return b +} + // PetReleaseToWarehouse 将精灵从仓库包中放生 func (h Controller) PetReleaseToWarehouse( data *PET_ROWEI, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { @@ -70,6 +104,11 @@ func (h Controller) SetPetExp( return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, errorcode.ErrorCodes.ErrSystemError } - player.AddPetExp(currentPet, data.Exp) + allowedExp := petSetExpLimit(currentPet) + if allowedExp <= 0 { + return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, errorcode.ErrorCodes.ErrSystemError + } + + player.AddPetExp(currentPet, minInt64(data.Exp, allowedExp)) return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, 0 } diff --git a/logic/controller/pet_manage_test.go b/logic/controller/pet_manage_test.go new file mode 100644 index 000000000..b810ca2c5 --- /dev/null +++ b/logic/controller/pet_manage_test.go @@ -0,0 +1,75 @@ +package controller + +import ( + "blazing/common/data/xmlres" + "blazing/common/socket/errorcode" + "blazing/logic/service/player" + playermodel "blazing/modules/player/model" + "testing" +) + +func firstPetIDForControllerTest(t *testing.T) int { + t.Helper() + + for id := range xmlres.PetMAP { + return id + } + + t.Fatal("xmlres.PetMAP is empty") + return 0 +} + +func TestSetPetExpCapsLevelAt100(t *testing.T) { + petID := firstPetIDForControllerTest(t) + petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 99, nil, 0) + if petInfo == nil { + t.Fatal("failed to generate test pet") + } + + expPool := petInfo.NextLvExp + 10_000 + testPlayer := player.NewPlayer(nil) + testPlayer.Info = &playermodel.PlayerInfo{ + ExpPool: expPool, + PetList: []playermodel.PetInfo{*petInfo}, + } + + currentPet := &testPlayer.Info.PetList[0] + result, err := (Controller{}).SetPetExp(&PetSetExpInboundInfo{ + CatchTime: currentPet.CatchTime, + Exp: expPool, + }, testPlayer) + if err != 0 { + t.Fatalf("expected SetPetExp to succeed, got err=%d", err) + } + if currentPet.Level != 100 { + t.Fatalf("expected pet level to stop at 100, got %d", currentPet.Level) + } + if result.Exp != 10_000 { + t.Fatalf("expected overflow exp to remain in pool, got %d", result.Exp) + } +} + +func TestSetPetExpRejectsPetAtLevel100(t *testing.T) { + petID := firstPetIDForControllerTest(t) + petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0) + if petInfo == nil { + t.Fatal("failed to generate test pet") + } + + testPlayer := player.NewPlayer(nil) + testPlayer.Info = &playermodel.PlayerInfo{ + ExpPool: 50_000, + PetList: []playermodel.PetInfo{*petInfo}, + } + + result, err := (Controller{}).SetPetExp(&PetSetExpInboundInfo{ + CatchTime: petInfo.CatchTime, + Exp: 12_345, + }, testPlayer) + if err != errorcode.ErrorCodes.ErrSystemError { + t.Fatalf("expected level-100 pet to be rejected, got err=%d", err) + } + if result.Exp != 50_000 { + t.Fatalf("expected exp pool to remain unchanged, got %d", result.Exp) + } +} diff --git a/logic/service/player/pet.go b/logic/service/player/pet.go index de151c105..508086246 100644 --- a/logic/service/player/pet.go +++ b/logic/service/player/pet.go @@ -39,9 +39,7 @@ func (p *Player) AddPetExp(petInfo *model.PetInfo, addExp int64) { p.Info.ExpPool -= addExp //减去已使用的经验 gainedExp := exp //已获得的经验 for exp >= int64(petInfo.NextLvExp) { - petInfo.Level++ - exp -= int64(petInfo.LvExp) petInfo.Update(true) } diff --git a/logic/service/player/player.go b/logic/service/player/player.go index 5aac9130c..628a43fdb 100644 --- a/logic/service/player/player.go +++ b/logic/service/player/player.go @@ -419,7 +419,15 @@ func (p *Player) ItemAdd(ItemId, ItemCnt int64) (result bool) { p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样 return false } - p.Service.Item.UPDATE(uint32(ItemId), gconv.Int(ItemCnt)) + if err := p.Service.Item.UPDATE(uint32(ItemId), gconv.Int(ItemCnt)); err != nil { + cool.Logger.Error(context.TODO(), "item add update failed", p.Info.UserID, ItemId, ItemCnt, err) + + t1 := common.NewTomeeHeader(2601, p.Info.UserID) + t1.Result = uint32(errorcode.ErrorCodes.ErrSystemError200007) + + p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样 + return false + } return true } diff --git a/modules/player/service/pet_fusion_tx.go b/modules/player/service/pet_fusion_tx.go new file mode 100644 index 000000000..5f2c8e60c --- /dev/null +++ b/modules/player/service/pet_fusion_tx.go @@ -0,0 +1,269 @@ +package service + +import ( + "blazing/common/socket/errorcode" + "blazing/cool" + baseservice "blazing/modules/base/service" + "blazing/modules/player/model" + "context" + "errors" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +var ( + errPetFusionInsufficientItems = errors.New("pet fusion insufficient items") + errPetFusionPetNotFound = errors.New("pet fusion pet not found") + errPetFusionPlayerNotFound = errors.New("pet fusion player not found") +) + +type PetFusionTxResult struct { + NewPet *model.PetInfo + CostItemUsed bool + UpdatedAux *model.PetInfo +} + +func (s *UserService) PetFusionTx( + currentInfo model.PlayerInfo, + masterCatchTime uint32, + auxCatchTime uint32, + materialCounts map[uint32]int, + goldItemIDs []uint32, + keepAuxItemID uint32, + failureItemID uint32, + cost int64, + newPet *model.PetInfo, + failedAux *model.PetInfo, +) (*PetFusionTxResult, errorcode.ErrorCode) { + if s == nil || s.Pet == nil { + return nil, errorcode.ErrorCodes.ErrSystemError + } + + userID := s.Pet.userid + nextInfo := currentInfo + if nextInfo.Coins < cost { + return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016 + } + nextInfo.Coins -= cost + + result := &PetFusionTxResult{} + err := g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error { + if err := updatePlayerInfoTx(tx, userID, nextInfo); err != nil { + return err + } + if err := consumeItemCountsTx(tx, userID, materialCounts); err != nil { + return err + } + + if newPet == nil { + used, err := consumeOptionalItemTx(tx, userID, goldItemIDs, failureItemID) + if err != nil { + return err + } + result.CostItemUsed = used + if !used && failedAux != nil { + if err := updatePetDataTx(tx, userID, auxCatchTime, *failedAux); err != nil { + return err + } + auxCopy := *failedAux + result.UpdatedAux = &auxCopy + } + return nil + } + + if err := deletePetTx(tx, userID, masterCatchTime); err != nil { + return err + } + + used, err := consumeOptionalItemTx(tx, userID, goldItemIDs, keepAuxItemID) + if err != nil { + return err + } + result.CostItemUsed = used + if !used { + if err := deletePetTx(tx, userID, auxCatchTime); err != nil { + return err + } + } + + catchTime, err := addPetTx(tx, s.Pet, newPet, 0) + if err != nil { + return err + } + petCopy := *newPet + petCopy.CatchTime = catchTime + result.NewPet = &petCopy + return nil + }) + if err == nil { + return result, 0 + } + + switch { + case errors.Is(err, errPetFusionInsufficientItems): + return nil, errorcode.ErrorCodes.ErrInsufficientItems + case errors.Is(err, errPetFusionPetNotFound): + return nil, errorcode.ErrorCodes.ErrPokemonNotFusionReady2 + case errors.Is(err, errPetFusionPlayerNotFound): + return nil, errorcode.ErrorCodes.ErrSystemError + default: + cool.Logger.Error(context.TODO(), "pet fusion tx failed", userID, err) + return nil, errorcode.ErrorCodes.ErrSystemError + } +} + +func updatePlayerInfoTx(tx gdb.TX, userID uint32, info model.PlayerInfo) error { + res, err := tx.Model(model.NewPlayer()).Where("player_id", userID).Data("data", info).Update() + if err != nil { + return err + } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return errPetFusionPlayerNotFound + } + return nil +} + +func consumeItemCountsTx(tx gdb.TX, userID uint32, itemCounts map[uint32]int) error { + for itemID, count := range itemCounts { + if err := updateItemCountTx(tx, userID, itemID, -count); err != nil { + return err + } + } + return nil +} + +func consumeOptionalItemTx(tx gdb.TX, userID uint32, itemIDs []uint32, target uint32) (bool, error) { + for _, itemID := range itemIDs { + if itemID != target { + continue + } + if err := updateItemCountTx(tx, userID, target, -1); err != nil { + return false, err + } + return true, nil + } + return false, nil +} + +func updateItemCountTx(tx gdb.TX, userID uint32, id uint32, count int) error { + if cool.Config.ServerInfo.IsVip != 0 && count < 0 { + return nil + } + if id == 0 { + return nil + } + + baseModel := tx.Model(model.NewPlayerBag()). + Where("player_id", userID). + Where("is_vip", cool.Config.ServerInfo.IsVip) + + ok, err := baseModel.Where("item_id", id).Exist() + if err != nil { + return err + } + + if ok { + updateModel := tx.Model(model.NewPlayerBag()). + Where("player_id", userID). + Where("is_vip", cool.Config.ServerInfo.IsVip). + Where("item_id", id) + if count < 0 { + updateModel = updateModel.Where("item_cnt + ? > 0", count) + } + + result, err := updateModel.Increment("item_cnt", count) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return errPetFusionInsufficientItems + } + return nil + } + + if count <= 0 { + return errPetFusionInsufficientItems + } + + _, err = tx.Model(model.NewPlayerBag()).Data(g.Map{ + "player_id": userID, + "item_id": id, + "item_cnt": count, + "is_vip": cool.Config.ServerInfo.IsVip, + }).Insert() + return err +} + +func updatePetDataTx(tx gdb.TX, userID uint32, catchTime uint32, pet model.PetInfo) error { + res, err := tx.Model(model.NewPet()). + Where("player_id", userID). + Where("is_vip", cool.Config.ServerInfo.IsVip). + Where("catch_time", catchTime). + Data("data", pet). + Update() + if err != nil { + return err + } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return errPetFusionPetNotFound + } + return nil +} + +func deletePetTx(tx gdb.TX, userID uint32, catchTime uint32) error { + res, err := tx.Model(model.NewPet()). + Where("player_id", userID). + Where("is_vip", cool.Config.ServerInfo.IsVip). + Where("catch_time", catchTime). + Delete() + if err != nil { + return err + } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return errPetFusionPetNotFound + } + return nil +} + +func addPetTx(tx gdb.TX, petService *PetService, petInfo *model.PetInfo, saleCount uint32) (uint32, error) { + if petInfo == nil { + return 0, nil + } + + catchTime, err := petService.nextCatchTime(tx.Model(baseservice.NewBaseSysUserService().Model)) + if err != nil { + return 0, err + } + + petCopy := *petInfo + petCopy.CatchTime = catchTime + playerPet := model.Pet{ + PlayerID: petService.userid, + Data: petCopy, + CatchTime: catchTime, + Free: 0, + SaleCount: saleCount, + } + playerPet.IsVip = cool.Config.ServerInfo.IsVip + if _, err := tx.Model(model.NewPet()).Insert(playerPet); err != nil { + return 0, err + } + return catchTime, nil +}