package gameserver import ( "fmt" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) // Apply the resource-defined freeplay lobby state on this entrance only. // Client-managed groups create their own props once their condition is true. func currencyWarsEntranceScene(scene *common.LevelOutputConfig, entrance common.CurrencyWarsEntrance) (*common.LevelOutputConfig, error) { copyScene := *scene copyScene.SavedValues = make(map[string]int32, len(scene.SavedValues)) for name, value := range scene.SavedValues { copyScene.SavedValues[name] = value } for name, value := range entrance.SavedValues { old, exists := copyScene.SavedValues[name] if !exists { return nil, fmt.Errorf("CW room activation value missing: %s", name) } copyScene.SavedValues[name] = value SceneDebugLog("CW_ROOM_ACTIVATION entry=%d name=%s previous=%d selected=%d", entrance.EntryID, name, old, value) } for _, room := range entrance.RoomGroups { value, exists := copyScene.SavedValues[room.Condition.Name] if !exists { return nil, fmt.Errorf("CW room activation value missing: %s", room.Condition.Name) } if room.Condition.Comparison != "GreaterEqual" { return nil, fmt.Errorf("unsupported CW room comparison") } if value < room.Condition.Value { return nil, fmt.Errorf("CW room activation condition is not satisfied") } } return ©Scene, nil } func init() { // Bind to this revision's command table. Save a complete initial preset // before returning success so the immediately following GetData sees it. cwHandle("GridFightStartGamePlay", func(s *Session, req *goproto.GridFightStartGamePlayCsReq, rsp *goproto.GridFightStartGamePlayScRsp) error { mode := "standard" if req.IsOverlock { mode = "overclock" } rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_CONF_MISS) catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { PacketLog(fmt.Sprintf("CW_START_GAMEPLAY rejected mode=%s retcode=%d reason=catalog_error error=%v", mode, rsp.Retcode, err)) return nil } division, exists := catalog.Division(req.DivisionId) if !exists || division.SeasonID != catalog.ActiveSeasonID { rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) PacketLog(fmt.Sprintf("CW_START_GAMEPLAY rejected mode=%s division=%d param_3=%d season=%d retcode=%d reason=division_mismatch", mode, req.DivisionId, req.MPJPLKKJNGE, req.LJNGPCKPJLF, rsp.Retcode)) return nil } data, revision, err := readCurrencyWarsData(catalog) if err != nil { PacketLog(fmt.Sprintf("CW_START_GAMEPLAY rejected mode=%s retcode=%d reason=snapshot_error error=%v", mode, rsp.Retcode, err)) return nil } if data != nil && data.DHFOFLFCLAH != nil { rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_ALREADY_IN_GAMEPLAY) PacketLog(fmt.Sprintf("CW_START_GAMEPLAY rejected mode=%s retcode=%d reason=active_run_exists", mode, rsp.Retcode)) return nil } if req.LJNGPCKPJLF != catalog.ActiveSeasonID || revision != s.currencyWarsRevision { rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) PacketLog(fmt.Sprintf("CW_START_GAMEPLAY rejected mode=%s retcode=%d reason=season_or_session_revision_mismatch", mode, rsp.Retcode)) return nil } if revision >= uint64(^uint32(0)) { PacketLog("CW_START_GAMEPLAY rejected reason=run_identifier_exhausted") return nil } run, err := buildCurrencyWarsInitialRun(catalog, req, uint32(revision+1)) if err != nil { PacketLog(fmt.Sprintf("CW_START_GAMEPLAY rejected mode=%s division=%d retcode=%d error=%v", mode, req.DivisionId, rsp.Retcode, err)) return nil } if data == nil { data, err = newCurrencyWarsData(catalog) if err != nil { return err } } data.DHFOFLFCLAH = run if err := s.saveCurrencyWarsData(data, catalog); err != nil { PacketLog(fmt.Sprintf("CW_START_GAMEPLAY rejected reason=save_error error=%v", err)) return nil } rsp.DHFOFLFCLAH = proto.Clone(run).(*goproto.MPIMAEMCEFH) rsp.Retcode = 0 PacketLog(fmt.Sprintf("CW_START_GAMEPLAY started mode=%s division=%d season=%d run=%d revision=%d source=captured_initial_preset", mode, req.DivisionId, req.LJNGPCKPJLF, run.UniqueId, s.currencyWarsRevision)) return nil }) cwHandle("GridFightResumeGamePlay", func(s *Session, req *goproto.GridFightResumeGamePlayCsReq, rsp *goproto.GridFightResumeGamePlayScRsp) error { rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_NOT_IN_GAMEPLAY) 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 { return nil } if req.UniqueId != data.DHFOFLFCLAH.UniqueId { rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) return nil } if s.currencyWarsRevision == 0 { s.currencyWarsRevision = revision } else if revision != s.currencyWarsRevision { rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) return nil } // Reconcile triplets left by the former buy handler on an explicit // Resume action. Snapshot readers remain read-only. if data.DHFOFLFCLAH.GetFMIGOKIDKJL().GetBAKAKODKECM() != nil { if _, active := CURRENT_BATTLE.Load(s.Token); !active { changed, e := cwAlignEncounterCamp(catalog, data.DHFOFLFCLAH) if e != nil { PacketLog("CW_RESUME Encounter rejected: " + e.Error()) return nil } if changed { if e = s.saveCurrencyWarsData(data, catalog); e != nil { return nil } } } } if data.DHFOFLFCLAH.FMIGOKIDKJL == nil { if _, active := CURRENT_BATTLE.Load(s.Token); !active { groups, e := cwMergeRoles(catalog, data.DHFOFLFCLAH, 0) if e != nil { PacketLog("CW_RESUME merge rejected: " + e.Error()) return nil } addedSpotlights := cwEnsureSpotlights(data.DHFOFLFCLAH) runtime, _ := cwBonfireRuntime(catalog, data.DHFOFLFCLAH) cwSyncTraitComponent(catalog, data.DHFOFLFCLAH, runtime) recovered, _ := s.cwRecoverMissingPendingAction(data, catalog) if len(groups) > 0 || addedSpotlights || recovered { if e = s.saveCurrencyWarsDataWithRuntime(data, catalog, runtime); e != nil { return nil } // Resume is followed by GetData. Return the reconciled roster in // that snapshot, after the Client constructs its game session. PacketLog(fmt.Sprintf("CW_RESUME merged=%d recovered=%t revision=%d", len(groups), recovered, s.currencyWarsRevision)) } } } rsp.Retcode = 0 return nil }) // Leaving the UI keeps the active run for Resume, matching the reference. cwHandle("GridFightQuitLeaveGamePlay", func(s *Session, _ *goproto.GridFightQuitLeaveGamePlayCsReq, _ *goproto.GridFightQuitLeaveGamePlayScRsp) error { s.currencyWarsOpen = false return nil }) // CW's points panel uses the shared CycleScore reward period. Missing // GetData leaves its MonoRemainTimer at the Unix epoch and closes the page. // This fresh-profile bootstrap uses the explicit operator activity window; // weekly rollover, earned scores and reward claims still need persistence. handle("CycleScoreRewardGetData", func(s *Session, req *goproto.CycleScoreRewardGetDataCsReq, rsp *goproto.CycleScoreRewardGetDataScRsp) error { cfg, err := common.LoadRuntimeConfig() if err != nil { return err } rsp.NFEDANFBIBE = &goproto.GAAMBBFPCHB{ EndTime: *cfg.ActivitySchedule.EndTime, } if cfg.ActivitySchedule.BeginTime != nil { rsp.NFEDANFBIBE.IFEPJHGNCOI = *cfg.ActivitySchedule.BeginTime } if scoreCatalog, err := common.LoadCurrencyWarsScore(nil); err == nil { rsp.NFEDANFBIBE.PMJGAJGIFOA = scoreCatalog.WeeklyCap for _, r := range scoreCatalog.WeeklyRanks { rsp.NFEDANFBIBE.OPLNKCOLCKJ = append(rsp.NFEDANFBIBE.OPLNKCOLCKJ, r.Rank) } } PacketLog(fmt.Sprintf("CYCLE_SCORE_REWARD_DATA end_time=%d weekly_cap=%d ranks=%d policy=score_catalog", *cfg.ActivitySchedule.EndTime, rsp.NFEDANFBIBE.PMJGAJGIFOA, len(rsp.NFEDANFBIBE.OPLNKCOLCKJ))) return nil }) handle("CycleScoreRewardTakeReward", func(s *Session, req *goproto.CycleScoreRewardTakeRewardCsReq, rsp *goproto.CycleScoreRewardTakeRewardScRsp) error { // Reward eligibility, grant mapping and atomic claim persistence are not // verified yet. Never consume a claim or report an empty success. rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_CONF_MISS) return nil }) cwHandle("GridFightTakeWeeklyReward", func(s *Session, req *goproto.GridFightTakeWeeklyRewardCsReq, rsp *goproto.GridFightTakeWeeklyRewardScRsp) error { // Reward eligibility, grant mapping and atomic claim persistence are not // verified yet. Never consume a claim or report an empty success. rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_CONF_MISS) return nil }) // Bootstrap only. Keep the danger-family guard for every unsupported CW // operation; it must never receive a successful empty gameplay response. cwHandle("GridFightGetData", func(s *Session, req *goproto.GridFightGetDataCsReq, rsp *goproto.GridFightGetDataScRsp) error { catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { return err } saved, revision, err := readCurrencyWarsData(catalog) if err != nil { return err } if saved != nil { s.currencyWarsRevision = revision s.currencyWarsOpen = saved.DHFOFLFCLAH != nil if saved.DHFOFLFCLAH != nil { runtime, _ := cwBonfireRuntime(catalog, saved.DHFOFLFCLAH) cwSyncTraitComponent(catalog, saved.DHFOFLFCLAH, runtime) if recovered, recErr := s.cwRecoverMissingPendingAction(saved, catalog); recErr != nil { PacketLog("CW_GET_DATA recovery failed: " + recErr.Error()) } else if recovered { if err := s.saveCurrencyWarsDataWithRuntime(saved, catalog, runtime); err != nil { PacketLog("CW_GET_DATA save recovery failed: " + err.Error()) } } } proto.Merge(rsp, saved) PacketLog(fmt.Sprintf("CW_GET_DATA restored=true revision=%d active_run=%t", s.currencyWarsRevision, saved.DHFOFLFCLAH != nil)) return nil } s.currencyWarsRevision = 0 initial, err := newCurrencyWarsData(catalog) if err != nil { return err } proto.Merge(rsp, initial) PacketLog(fmt.Sprintf("CW_GET_DATA season=%d subseason=%d division=%d active_run=false bootstrap=true tutorial_policy=freeplay_unlocked", catalog.ActiveSeasonID, catalog.ActiveSubSeasonID, initial.LFLCDNGALME.HFPEJIDDDJG.DivisionId)) return nil }) } // cwRecoverMissingPendingAction detects if an active run is in an interactive non-battle stage // (such as Supply) where a pending action was dropped or interrupted, and regenerates the offer. func (s *Session) cwRecoverMissingPendingAction(data *goproto.GridFightGetDataScRsp, catalog *common.CurrencyWarsCatalog) (bool, error) { if data == nil || data.DHFOFLFCLAH == nil { return false, nil } run := data.DHFOFLFCLAH stage, display := cwStage(run) if stage == nil || display == nil { return false, nil } name, err := catalog.NodeTypeName(display.CJCCMAACJDB) if err != nil { return false, err } if name == "Supply" && (run.FMIGOKIDKJL == nil || run.FMIGOKIDKJL.GetFIKAGBAEOAP() == nil) { offer, err := cwGenerateSupplyOffer(catalog, run) if err != nil { return false, err } queue := run.GetFMIGOKIDKJL().GetQueuePosition() if queue == 0 { queue = uint32(s.currencyWarsRevision + 1) } run.FMIGOKIDKJL = &goproto.CHCHAJIANIM{ QueuePosition: queue, FGLIODAJFML: &goproto.CHCHAJIANIM_FIKAGBAEOAP{ FIKAGBAEOAP: offer, }, } player, _, _ := cwComponents(run) if player != nil { player.GAJGBBFELEL = &goproto.GMBMNFLMKPI{ IBCPAKJAPFF: goproto.KMJNPEMPNMC_KMJNPEMPNMC_AEINKINPAFL, LMICDNNABNM: goproto.GGNKDLAHAGH_GGNKDLAHAGH_FHOHPFBDNOH, } } PacketLog(fmt.Sprintf("CW_RECOVER_SUPPLY restored pending offer queue=%d choices=%d", queue, len(offer.MFBAKLCBPCJ))) return true, nil } return false, nil }