package gameserver import ( "crypto/rand" "encoding/json" "fmt" "math" "math/big" "sort" "strconv" "strings" "gac-hkrpg-go/internal/common" pb "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) const cwPercentScale uint32 = 100 // Uploaded progress is an integer percentage. type cwNodeTransition struct { Route uint32 `json:"route_id"` Chapter uint32 `json:"chapter_id"` Section uint32 `json:"section_id"` Stage uint32 `json:"stage_id"` Penalty uint32 `json:"penalty_id"` Division uint32 `json:"division_id"` Overclock bool `json:"is_overclock"` LevelWire []byte `json:"next_level_wire"` PendingWire []byte `json:"next_pending_wire"` RoundWire []byte `json:"round_pending_wire"` } func cwNodeTransitions(c *common.CurrencyWarsCatalog) ([]cwNodeTransition, error) { raw, e := common.ReadResourceFile("resources/CurrencyWarsNodeTransitions.json") if e != nil { return nil, e } var r struct { Format uint32 `json:"format_version"` Client string `json:"target_client_version"` Assembly string `json:"target_assembly_sha256"` Entries []cwNodeTransition `json:"entries"` } if e = json.Unmarshal(raw, &r); e != nil { return nil, e } if r.Format != 1 || r.Client != c.ClientVersion || r.Assembly != c.Provenance.GameAssemblySHA256 { return nil, fmt.Errorf("CW node transition revision differs") } return r.Entries, nil } func cwStage(run *pb.MPIMAEMCEFH) (*pb.BEEFLEJAOCN, *pb.OHMDHKGAEMM) { if run == nil { return nil, nil } for _, c := range run.NAGDJGMMCBE { if c == nil { continue } if s := c.GetKLCPAAHDPJF(); s != nil { for _, n := range s.OOMMIIKPHBC { if n != nil && n.SectionId == s.SectionId && n.DCAHJELNJNP == s.DCAHJELNJNP { return s, n.DisplayValue } } return s, nil } } return nil, nil } func cwRollNodeOrbs(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, routeID, chapter, section, progress uint32) ([]uint32, error) { route, err := catalog.RouteNodes(routeID) if err != nil { return nil, err } refNodes, err := common.LoadCurrencyWarsReferenceNodes(catalog) if err != nil { return nil, err } var template common.CurrencyWarsReferenceNodeTemplate found := false for _, r := range route { if r.ChapterID == chapter && r.SectionID == section { t, err := refNodes.Template(r.NodeTemplateID) if err == nil { template = t found = true } break } } if !found || len(template.ParamList) == 0 { return nil, nil } nodeDropsCat, err := common.LoadCurrencyWarsNodeDrops(catalog) if err != nil { return nil, err } dropKey := strconv.FormatUint(uint64(template.ParamList[0]), 10) drop, ok := nodeDropsCat.NodeDrops[dropKey] if !ok || len(drop.Weights) == 0 { return nil, nil } recipes, err := common.LoadCurrencyWarsOrbRecipes() if err != nil { return nil, err } supported := map[uint32]bool{} for _, recipe := range recipes.Recipes { if recipe.Status == "Implemented" && recipe.Engine == "Orb" { supported[recipe.ID] = true } } type weightEntry struct { id uint32 weight uint32 } var entries []weightEntry var totalWeight uint32 for k, w := range drop.Weights { id64, err := strconv.ParseUint(k, 10, 32) if err != nil { continue } id := uint32(id64) if supported[id] && w > 0 { entries = append(entries, weightEntry{id: id, weight: w}) totalWeight += w } } if totalWeight == 0 { return nil, nil } sort.Slice(entries, func(i, j int) bool { return entries[i].id < entries[j].id }) percent := progress if percent > 100 { percent = 100 } count := (drop.Rolls*percent + 99) / 100 var rolled []uint32 for i := uint32(0); i < count; i++ { rBig, err := rand.Int(rand.Reader, big.NewInt(int64(totalWeight))) if err != nil { return nil, err } roll := uint32(rBig.Int64()) for _, e := range entries { if roll < e.weight { rolled = append(rolled, e.id) break } roll -= e.weight } } return rolled, nil } func finishCurrencyWarsBattle(s *Session, req *pb.PVEBattleResultCsReq, current *pb.SceneBattleInfo) error { if current == nil || req.BattleId != current.BattleId || req.StageId != current.StageId { return fmt.Errorf("CW battle identity differs") } if req.EndStatus != pb.BattleEndStatus_BATTLE_END_WIN { // Quitting returns to the same preparation; it does not award a victory. CURRENT_BATTLE.Delete(s.Token) return s.Send(cwNotify(cwUnlock())) } if req.Stt == nil || req.Stt.APAGMJNECCN == nil { return fmt.Errorf("CW uploaded statistics missing") } progress := req.Stt.APAGMJNECCN.HFLPDCJDIAC if progress > cwPercentScale { return fmt.Errorf("CW progress is outside percentage range") } catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { return err } data, revision, err := readCurrencyWarsData(catalog) if err != nil { return err } if data == nil || data.DHFOFLFCLAH == nil || revision != s.currencyWarsRevision { return fmt.Errorf("CW run/revision differs at settlement") } run := data.DHFOFLFCLAH stage, node := cwStage(run) player, team, shop := cwComponents(run) if stage == nil || node == nil || player == nil || team == nil || shop == nil || node.StageId != req.StageId { return fmt.Errorf("CW settlement components differ") } // Encounter penalties belong to the selected offer, not the route node's // base display rule. Validate against the battle we actually issued before // using that rule for HP/rewards or selecting the next transition. penaltyID := node.IFIIOIIKJHO var selectedBranch *pb.KKBGIPCAGGH if layer := stage.GetIMPNAHBCADN().GetMPOBBIPMNNJ(); layer != nil && len(layer.BPDABHGFBFO) > 0 { selected, e := cwSelectedEnemyBranch(layer) if e != nil { return e } penaltyID = selected.IFIIOIIKJHO selectedBranch = selected if current.GetEIGMKOJOEEI().GetIFIIOIIKJHO() != penaltyID { return fmt.Errorf("CW selected Encounter penalty differs from issued battle") } } transitions, err := cwNodeTransitions(catalog) if err != nil { return err } var transition *cwNodeTransition for i := range transitions { t := &transitions[i] if t.Route == stage.EMOHLJHPOPC && t.Chapter == stage.DCAHJELNJNP && t.Section == stage.SectionId && t.Stage == node.StageId && t.Penalty == penaltyID && t.Division == run.DivisionId && t.Overclock == run.IsOverlock { if transition != nil { return fmt.Errorf("CW transition duplicated") } transition = t } } nextLevel := new(pb.AINFNFIDGFF) nextPending := new(pb.CHCHAJIANIM) if transition != nil { if err = proto.Unmarshal(transition.LevelWire, nextLevel); err != nil { return err } if err = proto.Unmarshal(transition.PendingWire, nextPending); err != nil { return err } if nextLevel.IMPNAHBCADN == nil || nextPending.GetNNDGEJFLGGF() == nil { return fmt.Errorf("CW next node recipe incomplete") } found := false for _, n := range stage.OOMMIIKPHBC { found = found || (n.DCAHJELNJNP == nextLevel.DCAHJELNJNP && n.SectionId == nextLevel.SectionId) } if !found { return fmt.Errorf("CW next node outside route") } } raw, err := common.ReadResourceFile("resources/CurrencyWarsSettlementRules.json") if err != nil { return err } var rules struct { Format uint32 `json:"format_version"` Client string `json:"target_client_version"` Assembly string `json:"target_assembly_sha256"` Penalties []struct{ ID, ThresholdPosition, ThresholdFailPlayerHPPenalty, ThresholdPassBasicPlayerHPPenalty, ProgressPenaltyCoefficient uint32 } `json:"penalty_rules"` Victories []struct{ Low, Up, Bonus uint32 } `json:"victory_rules"` StreakNodes []string `json:"streak_node_types"` } if err = json.Unmarshal(raw, &rules); err != nil { return err } if rules.Format != 1 || rules.Client != catalog.ClientVersion || rules.Assembly != catalog.Provenance.GameAssemblySHA256 { return fmt.Errorf("CW settlement rule revision differs") } penaltyIndex := -1 for i, p := range rules.Penalties { if p.ID == penaltyID { if penaltyIndex >= 0 { return fmt.Errorf("CW penalty duplicated") } penaltyIndex = i } } var penalty common.CurrencyWarsReferencePenalty if penaltyIndex >= 0 { p := rules.Penalties[penaltyIndex] penalty = common.CurrencyWarsReferencePenalty{ID: p.ID, ThresholdPosition: p.ThresholdPosition, ThresholdFailPlayerHPPenalty: p.ThresholdFailPlayerHPPenalty, ThresholdPassBasicPlayerHPPenalty: p.ThresholdPassBasicPlayerHPPenalty, ProgressPenaltyCoefficient: p.ProgressPenaltyCoefficient} } else { // Preserve the proven current-Client captures above. Newly constructed // nodes use the separately labelled, revision-gated reference table. reference, e := common.LoadCurrencyWarsReferencePenalties(catalog) if e != nil { return e } var ok bool penalty, ok = reference[penaltyID] if !ok { return fmt.Errorf("CW target penalty %d has no reference row", penaltyID) } } if player.PPKKHHNEOHB > math.MaxInt32 || player.GridFightLineupHp > player.PPKKHHNEOHB { return fmt.Errorf("CW HP cannot be represented in settlement") } perfect := progress == cwPercentScale previousHP := player.GridFightLineupHp previousStreak := player.GDAKCOICIDA previousLevel, previousExp := player.EHNJDIDABHD, player.GridFightCurLevelExp var hpChanges []*pb.GEDDNPGHNGP loseHP := func(amount uint32, reason pb.DFIJIBKBEJL) { before := player.GridFightLineupHp if amount > before { amount = before } player.GridFightLineupHp -= amount hpChanges = append(hpChanges, &pb.GEDDNPGHNGP{Reason: reason, ODJEBDNBLLC: int32(before), BMMADDOCJOG: int32(player.GridFightLineupHp), IOAOHNDEJNB: amount}) } basicLoss, progressLoss, thresholdLoss := uint32(0), uint32(0), uint32(0) if !perfect { basicLoss = penalty.ThresholdPassBasicPlayerHPPenalty loss := uint64(cwPercentScale-progress) * uint64(penalty.ProgressPenaltyCoefficient) / uint64(cwPercentScale) if loss > math.MaxUint32 { return fmt.Errorf("CW progress penalty overflow") } progressLoss = uint32(loss) if progress < penalty.ThresholdPosition { thresholdLoss = penalty.ThresholdFailPlayerHPPenalty } } loseHP(basicLoss, pb.DFIJIBKBEJL_DFIJIBKBEJL_FDKCPHMMFCD) loseHP(progressLoss, pb.DFIJIBKBEJL_DFIJIBKBEJL_IEGEHAKDEHA) loseHP(thresholdLoss, pb.DFIJIBKBEJL_DFIJIBKBEJL_KEOPICGMGPI) isDefeat := (player.GridFightLineupHp == 0) kind, err := catalog.NodeTypeName(node.CJCCMAACJDB) if err != nil { return err } if isDefeat { player.GDAKCOICIDA = 0 } else { for _, name := range rules.StreakNodes { if name == kind { if perfect { if player.GDAKCOICIDA == math.MaxUint32 { return fmt.Errorf("CW streak overflow") } player.GDAKCOICIDA++ } else { player.GDAKCOICIDA = 0 } break } } } var streakGold uint32 if !isDefeat { bonusFound := false for _, v := range rules.Victories { if v.Low <= player.GDAKCOICIDA && player.GDAKCOICIDA <= v.Up { if bonusFound { return fmt.Errorf("CW victory ranges overlap") } bonusFound = true streakGold = v.Bonus } } if !bonusFound { return fmt.Errorf("CW victory range missing") } } interest, err := catalog.BaseInterest(player.Gold, player.KNFOCCPOJEA) if err != nil { return err } basicGold := node.PICMDONCKCO if isDefeat { basicGold = 0 streakGold = 0 } sum := uint64(player.Gold) + uint64(basicGold) + uint64(interest) + uint64(streakGold) if sum > math.MaxUint32 { sum = math.MaxUint32 } player.Gold = uint32(sum) var gain uint32 if !isDefeat { g, err := catalog.BattleNodeExperience(run.IsOverlock, stage.DCAHJELNJNP, kind) if err != nil { return err } gain = uint32(g) } plan, err := catalog.PlanBaseExperience(player.EHNJDIDABHD, player.GridFightCurLevelExp, gain, player.NDAINFLOLPJ) if err != nil { return err } previousRow, err := catalog.PlayerLevel(previousLevel) if err != nil { return err } row, err := catalog.PlayerLevel(plan.Level) if err != nil { return err } if gain > 0 { if err = cwApplyBaseExperience(catalog, player, shop, gain); err != nil { return err } } var encounterUpdates []*pb.KFNCAIHIHCM runtime, err := cwBonfireRuntime(catalog, run) if err != nil { return err } if kind == "EliteBranch" && !isDefeat { encounterUpdates, err = cwEncounterRewards(catalog, run, selectedBranch, progress, penalty.ThresholdPosition, runtime) if err != nil { return err } } damage := &pb.BHBAILHMCOI{} frontMax, err := catalog.IntegerRule("GridFight_Front_AvatarMaxNum") if err != nil { return err } for _, role := range current.EIGMKOJOEEI.GridGameRoleList { total := float64(0) if req.Stt != nil && req.Stt.APAGMJNECCN != nil { for _, stat := range req.Stt.APAGMJNECCN.DOGMMGLBJIJ { if (stat.RoleId == role.RoleId || stat.RoleId == role.AvatarId || stat.RoleId == role.UniqueId) && !math.IsNaN(stat.Damage) && !math.IsInf(stat.Damage, 0) && stat.Damage > 0 { total += stat.Damage } } } if total == 0 && req.Stt != nil { for _, ba := range req.Stt.BattleAvatarList { if (ba.Id == role.AvatarId || ba.Id == role.RoleId) && !math.IsNaN(ba.TotalDamage) && !math.IsInf(ba.TotalDamage, 0) && ba.TotalDamage > 0 { total += ba.TotalDamage } } } damage.EEGPILJDLBG = append(damage.EEGPILJDLBG, &pb.PLFKMENCDMM{RoleId: role.RoleId, RoleStar: role.RoleStar, TotalDamage: total, DIGNECHKINI: uint64(role.Pos) > uint64(frontMax)}) } oldChapter, oldSection := stage.DCAHJELNJNP, stage.SectionId var nextNotify *pb.GridFightSyncUpdateResultScNotify isFinalNode := false if isDefeat { // Terminal defeat: stage does not advance } else if transition != nil { stage.DCAHJELNJNP = nextLevel.DCAHJELNJNP stage.SectionId = nextLevel.SectionId stage.IMPNAHBCADN = proto.Clone(nextLevel.IMPNAHBCADN).(*pb.NCKOKDHNJDC) run.FMIGOKIDKJL = nextPending nextNotify = cwNotify(&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_OOICHLAMNIG{OOICHLAMNIG: nextLevel}}, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: nextPending}}) if stage.ANJBLBEOJND != nil && stage.ANJBLBEOJND.PIFNOKAMBLL != nil && stage.ANJBLBEOJND.PIFNOKAMBLL[cwDifficultySourceBranch] != 0 { diffUpdate, err := cwXaodevDifficulty(catalog, run, 0) if err == nil && diffUpdate != nil { nextNotify.GCPNIHHBGGD[0].JKPJFKCBIEM = append(nextNotify.GCPNIHHBGGD[0].JKPJFKCBIEM, diffUpdate) } } } else { // A battle has no pending action to allocate from. The persisted CAS // revision is monotonic and also backs Bonfire entry queue allocation. if revision >= math.MaxUint32 { return fmt.Errorf("CW settlement queue exhausted") } run.FMIGOKIDKJL = &pb.CHCHAJIANIM{QueuePosition: uint32(revision)} nextNotify, err = cwAdvanceRouteNode(catalog, run, runtime) if err != nil { if strings.Contains(err.Error(), "no next node") { isFinalNode = true } else { return err } } else { // The allocation seed is not a Client pending action, so it must not // produce a synthetic FinishPendingAction notification. for _, group := range nextNotify.GCPNIHHBGGD { kept := group.JKPJFKCBIEM[:0] for _, update := range group.JKPJFKCBIEM { if update.GetPMFBIGLPJJA() != uint32(revision) { kept = append(kept, update) } } group.JKPJFKCBIEM = kept } } } if !isDefeat && !isFinalNode { runtime, err = cwApplyBonfireEnemyRules(catalog, run, runtime) if err != nil { return err } if runtime.Rules["PetPalStarDown"] != 0 && stage.GetIMPNAHBCADN().GetMPOBBIPMNNJ() != nil { nextNotify.GCPNIHHBGGD = append(nextNotify.GCPNIHHBGGD, cwNotify(&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_OOICHLAMNIG{OOICHLAMNIG: &pb.AINFNFIDGFF{DCAHJELNJNP: stage.DCAHJELNJNP, SectionId: stage.SectionId, IMPNAHBCADN: stage.IMPNAHBCADN}}}).GCPNIHHBGGD...) } } tbLayer := cwGetTrailblazeLayer(catalog, run) if tbLayer > 0 { if tbCat, err := common.LoadCurrencyWarsTrailblaze(); err == nil { income := tbCat.Income(tbLayer) runtime.Rules["TbPower"] += income PacketLog(fmt.Sprintf("CW_TRAILBLAZE_INCOME layer=%d income=%d total_power=%d", tbLayer, income, runtime.Rules["TbPower"])) } } cwSyncTraitComponent(catalog, run, runtime) var droppedOrbs []*pb.IBPDPCOGDEP var orbUpdates []*pb.KFNCAIHIHCM if !isDefeat { orbIDs, dropErr := cwRollNodeOrbs(catalog, run, stage.EMOHLJHPOPC, oldChapter, oldSection, progress) if dropErr != nil { PacketLog(fmt.Sprintf("CW_DROP_ORBS error: %v", dropErr)) } else if len(orbIDs) > 0 { var orbBag *pb.DBONCGFHHDN for _, c := range run.NAGDJGMMCBE { if b := c.GetDBAGKJNPDOL(); b != nil { orbBag = b break } } if orbBag == nil { orbBag = &pb.DBONCGFHHDN{} run.NAGDJGMMCBE = append(run.NAGDJGMMCBE, &pb.GridFightGameInfo{ LEOIAOFBBFA: &pb.GridFightGameInfo_DBAGKJNPDOL{ DBAGKJNPDOL: orbBag, }, }) } for _, orbID := range orbIDs { uid, idErr := cwNextEntityID(run) if idErr != nil { break } orb := &pb.IBPDPCOGDEP{ UniqueId: uid, LDBNOMPCOKK: orbID, } orbBag.MDKGCIKOMLI = append(orbBag.MDKGCIKOMLI, orb) droppedOrbs = append(droppedOrbs, orb) orbUpdates = append(orbUpdates, &pb.KFNCAIHIHCM{ MNLCHEJKDHO: &pb.KFNCAIHIHCM_AFILIPBHMOO{ AFILIPBHMOO: &pb.MMEDLCAENAL{ UniqueId: uid, LDBNOMPCOKK: orbID, }, }, }) } PacketLog(fmt.Sprintf("CW_DROP_ORBS node=%d-%d dropped=%d", oldChapter, oldSection, len(droppedOrbs))) } } player.GAJGBBFELEL = &pb.GMBMNFLMKPI{IBCPAKJAPFF: pb.KMJNPEMPNMC_KMJNPEMPNMC_AEINKINPAFL, LMICDNNABNM: pb.GGNKDLAHAGH_GGNKDLAHAGH_FHOHPFBDNOH} end := &pb.GridFightEndBattleStageNotify{DCAHJELNJNP: oldChapter, SectionId: oldSection, EMOHLJHPOPC: stage.EMOHLJHPOPC, HFLPDCJDIAC: progress, OAHFCPPIMLF: penalty.ThresholdPosition, CBHJKFNCDAD: perfect, JPKHBELGHCG: previousStreak, OCCOHKIMGCN: player.GDAKCOICIDA, CLFNPMGHMFC: basicGold, DMNILJEGICO: interest, OGLOCDMNNIM: streakGold, ODJEBDNBLLC: previousHP, PAGPKOKMDBK: player.GridFightLineupHp, LKCJAPIGEND: player.PPKKHHNEOHB, NKBIDLBLONC: damage, HMKOKACNHJH: hpChanges, AEOCOGHMEBG: &pb.GEEMFDBDMDH{PBMAHOJBAGG: uint32(gain), EDCLIPCEHAO: &pb.NFPLDMFCLHA{Level: previousLevel, Exp: previousExp, KDBMPKJKAKI: previousRow.LevelUpExp}, LHIAODNJIIC: &pb.NFPLDMFCLHA{Level: plan.Level, Exp: plan.Exp, KDBMPKJKAKI: row.LevelUpExp}}} if len(droppedOrbs) > 0 { var dropEntries []*pb.BCCONKJHDJL for _, orb := range droppedOrbs { dropEntries = append(dropEntries, &pb.BCCONKJHDJL{ FKKGLFOHGGA: pb.MJJIFGINJAJ_MJJIFGINJAJ_ANMDMBGNOHL, FNLMKJLKLFH: orb.LDBNOMPCOKK, Num: 1, }) } end.AFFFCONCDMG = map[uint32]*pb.GridFightDropInfo{ 0: {NMGIABHIJJB: dropEntries}, } } update := cwNotify(cwUnlock(), cwGoldSync(player.Gold), cwCapacitySync(player), &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_GridFightLineupHp{GridFightLineupHp: &pb.FMJDDIHBELC{PPKKHHNEOHB: player.PPKKHHNEOHB, GridFightLineupHp: player.GridFightLineupHp}}}, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_PLPHEIBLPDL{PLPHEIBLPDL: &pb.PIHKLPJGEPM{Level: plan.Level, Exp: plan.Exp, HFMKLAPEKFA: player.NDAINFLOLPJ}}}) update.GCPNIHHBGGD[0].JKPJFKCBIEM = append(update.GCPNIHHBGGD[0].JKPJFKCBIEM, encounterUpdates...) update.GCPNIHHBGGD[0].JKPJFKCBIEM = append(update.GCPNIHHBGGD[0].JKPJFKCBIEM, orbUpdates...) if tbCat, err := common.LoadCurrencyWarsTrailblaze(); err == nil && (tbLayer > 0 || runtime.Rules["TbOpened"] != 0) { update.GCPNIHHBGGD[0].JKPJFKCBIEM = append(update.GCPNIHHBGGD[0].JKPJFKCBIEM, &pb.KFNCAIHIHCM{ MNLCHEJKDHO: &pb.KFNCAIHIHCM_HKHJJCMKKJE{ HKHJJCMKKJE: &pb.JLKCBOIFJKC{ TraitId: cwTrailblazeTrait, ENPLACDLJGA: cwTrailblazeTraitEffectProto(tbCat, runtime), }, }, }, &pb.KFNCAIHIHCM{ MNLCHEJKDHO: &pb.KFNCAIHIHCM_GOOCHGKBIPN{ GOOCHGKBIPN: cwTrailblazeModifierProto(tbCat, runtime), }, }, ) } if isDefeat || isFinalNode { reason := pb.PEIJNEFOCHI_PEIJNEFOCHI_JOJPGBCECLN if isDefeat { reason = pb.PEIJNEFOCHI_PEIJNEFOCHI_HIGMFLKHGNJ } keepWin, settle, termErr := currencyWarsTerminalSettlement(catalog, data, run, reason, !isDefeat, damage.EEGPILJDLBG) if termErr != nil { return termErr } data.DHFOFLFCLAH = nil if termErr = s.saveCurrencyWarsData(data, catalog); termErr != nil { return termErr } CURRENT_BATTLE.Delete(s.Token) s.currencyWarsOpen = false if err = s.Send(update); err != nil { return err } if err = s.Send(end); err != nil { return err } if err = cwSend(s, keepWin); err != nil { return err } if err = cwSend(s, settle); err != nil { return err } PacketLog(fmt.Sprintf("CW_SETTLEMENT terminal victory=%v battle=%d progress=%d gold=%d node=%d-%d", !isDefeat, req.BattleId, progress, player.Gold, oldChapter, oldSection)) return nil } if err = s.saveCurrencyWarsDataWithRuntime(data, catalog, runtime); err != nil { return err } CURRENT_BATTLE.Delete(s.Token) // Duplicate uploads cannot award again. if err = s.Send(update); err != nil { return err } if err = s.Send(end); err != nil { return err } nextNotify.GCPNIHHBGGD = append(nextNotify.GCPNIHHBGGD, cwNotify(&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_JOMFJHOCHJP{JOMFJHOCHJP: player.GAJGBBFELEL}}).GCPNIHHBGGD...) if err = s.Send(nextNotify); err != nil { return err } PacketLog(fmt.Sprintf("CW_SETTLEMENT saved battle=%d progress=%d gold=%d basic=%d interest=%d streak=%d exp=%d next=%d/%d revision=%d", req.BattleId, progress, player.Gold, basicGold, interest, streakGold, plan.Exp, stage.DCAHJELNJNP, stage.SectionId, s.currencyWarsRevision)) return nil }