292 lines
12 KiB
Go
292 lines
12 KiB
Go
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
|
|
}
|