package gameserver import ( "fmt" "gac-hkrpg-go/internal/common" pb "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" "math" ) // cwAdvanceRouteNode follows gfPendingAdvanceNode -> gfFxHEnterNode in the // reference workflow. The caller owns the detached snapshot and its single CAS // commit; this helper grants no rewards and never reads/writes a player save. // Saved route displays retain run-specific Tea Break/spotlight substitutions. func cwAdvanceRouteNode(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, runtime *common.CurrencyWarsRuntimeState) (*pb.GridFightSyncUpdateResultScNotify, error) { if run == nil || run.FMIGOKIDKJL == nil { return nil, fmt.Errorf("CW advance requires current pending queue") } queue := run.FMIGOKIDKJL.QueuePosition if queue == math.MaxUint32 { return nil, fmt.Errorf("CW pending queue exhausted") } stage, _ := cwStage(run) if stage == nil { return nil, fmt.Errorf("CW advance stage missing") } route, err := catalog.RouteNodes(stage.EMOHLJHPOPC) if err != nil { return nil, err } index := -1 for i, row := range route { if row.ChapterID == stage.DCAHJELNJNP && row.SectionID == stage.SectionId { index = i break } } if index < 0 || index+1 >= len(route) { return nil, fmt.Errorf("CW route has no next node") } next := route[index+1] var display *pb.OHMDHKGAEMM for _, node := range stage.OOMMIIKPHBC { if node != nil && node.DCAHJELNJNP == next.ChapterID && node.SectionId == next.SectionID { if display != nil { return nil, fmt.Errorf("CW duplicate next route display") } display = node.DisplayValue } } if display == nil { return nil, fmt.Errorf("CW next route display missing") } refs, err := common.LoadCurrencyWarsReferenceNodes(catalog) if err != nil { return nil, err } template, err := refs.Template(next.NodeTemplateID) if err != nil { return nil, err } if template.StageID != display.StageId { return nil, fmt.Errorf("CW next template/display stage differs") } // Build against a temporary run at the destination: the enemy constructor // selects that chapter's saved boss/camp, leaving the source untouched if a // recipe fails validation. Dynamic non-enemy nodes need their own factory. shadow := proto.Clone(run).(*pb.MPIMAEMCEFH) shadowStage, _ := cwStage(shadow) shadowStage.DCAHJELNJNP = next.ChapterID shadowStage.SectionId = next.SectionID shadowStage.IMPNAHBCADN = nil var layer *pb.NCKOKDHNJDC var difficulty *pb.KFNCAIHIHCM if template.NodeType == "Supply" { layer, err = cwCapturedSupplyLayer(catalog) } else if template.NodeType == "EliteBranch" { if runtime == nil || runtime.RunID != run.UniqueId { return nil, fmt.Errorf("CW generated Encounter requires matching runtime transaction") } layer, err = cwGenerateXaodevEncounter(catalog, shadow, display) } else { layer, err = cwGenerateReferenceEnemyLayer(catalog, shadow, display) } if err != nil { return nil, err } if layer == nil { return nil, fmt.Errorf("CW generated next node layer missing") } hasBranchDiff := stage.ANJBLBEOJND != nil && stage.ANJBLBEOJND.PIFNOKAMBLL != nil && stage.ANJBLBEOJND.PIFNOKAMBLL[cwDifficultySourceBranch] != 0 if template.NodeType == "EliteBranch" || cwXaodevGenerated(run, runtime) || hasBranchDiff { difficulty, err = cwXaodevDifficulty(catalog, shadow, 0) if err != nil { return nil, err } } pending := &pb.CHCHAJIANIM{QueuePosition: queue + 1, FGLIODAJFML: &pb.CHCHAJIANIM_NNDGEJFLGGF{NNDGEJFLGGF: &pb.DPHPAKFLNOD{}}} stage.DCAHJELNJNP = next.ChapterID stage.SectionId = next.SectionID stage.IMPNAHBCADN = layer stage.ANJBLBEOJND = shadowStage.ANJBLBEOJND run.FMIGOKIDKJL = pending if template.NodeType == "EliteBranch" { if runtime.Rules == nil { runtime.Rules = map[string]uint32{} } runtime.Rules[cwXaodevNodeKey(stage)] = 1 } cwTrailblazeNodeEntered(catalog, run, runtime) cwSyncTraitComponent(catalog, run, runtime) notify := cwNotify(cwFinish(queue), &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_OOICHLAMNIG{OOICHLAMNIG: &pb.AINFNFIDGFF{DCAHJELNJNP: stage.DCAHJELNJNP, SectionId: stage.SectionId, IMPNAHBCADN: layer}}}) if difficulty != nil { notify.GCPNIHHBGGD[0].JKPJFKCBIEM = append(notify.GCPNIHHBGGD[0].JKPJFKCBIEM, difficulty) } if tbCat, err := common.LoadCurrencyWarsTrailblaze(); err == nil && (cwGetTrailblazeLayer(catalog, run) > 0 || (runtime != nil && runtime.Rules["TbOpened"] != 0)) { notify.GCPNIHHBGGD[0].JKPJFKCBIEM = append(notify.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), }, }, ) } notify.GCPNIHHBGGD[0].JKPJFKCBIEM = append(notify.GCPNIHHBGGD[0].JKPJFKCBIEM, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: pending}}) return notify, nil } // cwGeneratedRoundPreparation handles the same RoundBegin -> shop refresh -> // ReturnPreparation transition as the captured initial rounds, without replaying // an unrelated node's opaque wire or its fixed queue. Restrict this fallback to // constructed enemy layers; other node families keep their typed handlers. func cwGeneratedRoundPreparation(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) (*pb.CHCHAJIANIM, error) { if run == nil || run.GetFMIGOKIDKJL().GetNNDGEJFLGGF() == nil { return nil, fmt.Errorf("CW generated round is not RoundBegin") } stage, display := cwStage(run) if stage == nil || display == nil || stage.GetIMPNAHBCADN().GetMPOBBIPMNNJ() == nil { return nil, fmt.Errorf("CW generated RoundBegin enemy layer missing") } queue := run.FMIGOKIDKJL.QueuePosition if queue == math.MaxUint32 { return nil, fmt.Errorf("CW pending queue exhausted") } name, err := catalog.NodeTypeName(display.CJCCMAACJDB) if err != nil { return nil, err } if name == "Supply" { offer, err := cwGenerateSupplyOffer(catalog, run) if err != nil { return nil, err } return &pb.CHCHAJIANIM{QueuePosition: queue + 1, FGLIODAJFML: &pb.CHCHAJIANIM_FIKAGBAEOAP{FIKAGBAEOAP: offer}}, nil } if display.HPNNLMBHOPF { offer, err := cwSpotlightChoices(catalog, run) if err != nil { return nil, err } return &pb.CHCHAJIANIM{QueuePosition: queue + 1, FGLIODAJFML: &pb.CHCHAJIANIM_EEKMIAMGHFL{EEKMIAMGHFL: offer}}, nil } if name == "EliteBranch" && stage.IMPNAHBCADN.GetMPOBBIPMNNJ().MOADBALHMFD == 0 { return &pb.CHCHAJIANIM{QueuePosition: queue + 1, FGLIODAJFML: &pb.CHCHAJIANIM_BAKAKODKECM{BAKAKODKECM: &pb.HNPHLAHJPOB{}}}, nil } return &pb.CHCHAJIANIM{QueuePosition: queue + 1, FGLIODAJFML: &pb.CHCHAJIANIM_LGNPHHBGKFH{LGNPHHBGKFH: &pb.HJPDALHDJHC{}}}, nil }