docs & feat: add license & attribution notice (Apache-2.0 / GPL-3.0) and core system enhancements
This commit is contained in:
1 parent
507836d367
commit
ce6aabfc55
47 files changed
+5674
-724
No files matched your search
@@ -2,87 +2,114 @@ package gameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"math"
|
||||
)
|
||||
|
||||
const archiveClientVersion uint32 = 4006
|
||||
|
||||
type cwArchiveStore struct {
|
||||
mu sync.RWMutex
|
||||
nextID uint32
|
||||
entries []*pb.FAIHLAKPABN
|
||||
func cwArchiveSnapshot(c *common.CurrencyWarsCatalog) (*common.CurrencyWarsSnapshot, error) {
|
||||
cfg, err := common.LoadRuntimeConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return common.ReadCurrencyWarsSnapshot(cfg.PlayerUID, c.ClientVersion, c.Provenance.GameAssemblySHA256)
|
||||
}
|
||||
|
||||
var globalCWArchive = &cwArchiveStore{nextID: 1}
|
||||
|
||||
func cwRecordArchive(record *pb.OEDHMAHDECC, seasonID, subSeasonID uint32) *pb.FAIHLAKPABN {
|
||||
if record == nil {
|
||||
return nil
|
||||
func (s *Session) saveCurrencyWarsTerminal(data *pb.GridFightGetDataScRsp, c *common.CurrencyWarsCatalog, record *pb.OEDHMAHDECC) error {
|
||||
if record == nil || s.currencyWarsRevision >= math.MaxUint32 {
|
||||
return fmt.Errorf("CW archive record/identifier invalid")
|
||||
}
|
||||
globalCWArchive.mu.Lock()
|
||||
defer globalCWArchive.mu.Unlock()
|
||||
|
||||
id := globalCWArchive.nextID
|
||||
globalCWArchive.nextID++
|
||||
|
||||
entry := &pb.FAIHLAKPABN{
|
||||
BAAGDILLLME: id,
|
||||
OBNEAIPKGKL: time.Now().Unix(),
|
||||
LJNGPCKPJLF: seasonID,
|
||||
GEGMHOMNBDG: subSeasonID,
|
||||
PMBNKEFCICH: archiveClientVersion,
|
||||
IECKKCHJMLP: false,
|
||||
BFLLJJBHLDF: proto.Clone(record).(*pb.OEDHMAHDECC),
|
||||
now, err := common.GameTime()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalCWArchive.entries = append(globalCWArchive.entries, entry)
|
||||
if len(globalCWArchive.entries) > 50 {
|
||||
globalCWArchive.entries = globalCWArchive.entries[len(globalCWArchive.entries)-50:]
|
||||
entry := &pb.FAIHLAKPABN{BAAGDILLLME: uint32(s.currencyWarsRevision + 1), OBNEAIPKGKL: now.Unix(), LJNGPCKPJLF: c.ActiveSeasonID, GEGMHOMNBDG: c.ActiveSubSeasonID, BFLLJJBHLDF: proto.Clone(record).(*pb.OEDHMAHDECC)}
|
||||
// Client-version encoding is unmapped; do not invent it from a patch number.
|
||||
wire, err := proto.Marshal(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return entry
|
||||
return s.saveCurrencyWarsSnapshot(data, c, nil, wire)
|
||||
}
|
||||
|
||||
func cwReadArchives(snapshot *common.CurrencyWarsSnapshot) ([]*pb.FAIHLAKPABN, error) {
|
||||
if snapshot == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var entries []*pb.FAIHLAKPABN
|
||||
for _, wire := range snapshot.Archives {
|
||||
entry := new(pb.FAIHLAKPABN)
|
||||
if err := proto.Unmarshal(wire, entry); err != nil {
|
||||
return nil, fmt.Errorf("decode CW archive: %w", err)
|
||||
}
|
||||
if entry.BAAGDILLLME == 0 || entry.BFLLJJBHLDF == nil {
|
||||
return nil, fmt.Errorf("CW archive entry incomplete")
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
cwHandle("GridFightGetArchive", func(s *Session, req *pb.GridFightGetArchiveCsReq, rsp *pb.GridFightGetArchiveScRsp) error {
|
||||
rsp.Retcode = 0
|
||||
if req != nil {
|
||||
rsp.Type = req.Type
|
||||
cwHandle("GridFightGetArchive", func(_ *Session, req *pb.GridFightGetArchiveCsReq, rsp *pb.GridFightGetArchiveScRsp) error {
|
||||
c, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalCWArchive.mu.RLock()
|
||||
defer globalCWArchive.mu.RUnlock()
|
||||
|
||||
favourOnly := req != nil && req.Type == pb.MLJIILDNDPL_MLJIILDNDPL_FAGECPGMPEH
|
||||
for i := len(globalCWArchive.entries) - 1; i >= 0; i-- {
|
||||
e := globalCWArchive.entries[i]
|
||||
if favourOnly && !e.IECKKCHJMLP {
|
||||
snapshot, err := cwArchiveSnapshot(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := cwReadArchives(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rsp.Type = req.Type
|
||||
for i := len(entries) - 1; i >= 0; i-- {
|
||||
if req.Type == pb.MLJIILDNDPL_MLJIILDNDPL_FAGECPGMPEH && !entries[i].IECKKCHJMLP {
|
||||
continue
|
||||
}
|
||||
rsp.MCGGPONOCKC = append(rsp.MCGGPONOCKC, proto.Clone(e).(*pb.FAIHLAKPABN))
|
||||
rsp.MCGGPONOCKC = append(rsp.MCGGPONOCKC, entries[i])
|
||||
}
|
||||
PacketLog(fmt.Sprintf("CW_GET_ARCHIVE type=%v entries=%d", rsp.Type, len(rsp.MCGGPONOCKC)))
|
||||
return nil
|
||||
})
|
||||
|
||||
cwHandle("FavourArchive", func(s *Session, req *pb.FavourArchiveCsReq, rsp *pb.FavourArchiveScRsp) error {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH)
|
||||
if req == nil {
|
||||
c, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshot, err := cwArchiveSnapshot(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if snapshot == nil || snapshot.Revision != s.currencyWarsRevision {
|
||||
return nil
|
||||
}
|
||||
globalCWArchive.mu.Lock()
|
||||
defer globalCWArchive.mu.Unlock()
|
||||
|
||||
for _, e := range globalCWArchive.entries {
|
||||
if e.BAAGDILLLME == req.BAAGDILLLME {
|
||||
e.IECKKCHJMLP = req.IECKKCHJMLP
|
||||
rsp.BAAGDILLLME = req.BAAGDILLLME
|
||||
rsp.IECKKCHJMLP = req.IECKKCHJMLP
|
||||
rsp.Retcode = 0
|
||||
PacketLog(fmt.Sprintf("CW_FAVOUR_ARCHIVE id=%d favour=%t", req.BAAGDILLLME, req.IECKKCHJMLP))
|
||||
return nil
|
||||
entries, err := cwReadArchives(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, entry := range entries {
|
||||
if entry.BAAGDILLLME != req.BAAGDILLLME {
|
||||
continue
|
||||
}
|
||||
entry.IECKKCHJMLP = req.IECKKCHJMLP
|
||||
wire, err := proto.Marshal(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshot.Archives[i] = wire
|
||||
revision, err := common.CommitCurrencyWarsSnapshot(s.currencyWarsRevision, *snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.currencyWarsRevision = revision
|
||||
rsp.BAAGDILLLME = req.BAAGDILLLME
|
||||
rsp.IECKKCHJMLP = req.IECKKCHJMLP
|
||||
rsp.Retcode = 0
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -111,7 +111,7 @@ func buildCurrencyWarsBattle(s *Session, catalog *common.CurrencyWarsCatalog, ru
|
||||
var ownerIndex uint32
|
||||
hasOwner := false
|
||||
for _, r := range team.GridGameRoleList {
|
||||
if r != nil && r.Pos > 0 && r.Pos < benchFirstPos {
|
||||
if cwRoleOnBoard(catalog, r) {
|
||||
if variant, err := catalog.CapturedRoleVariant(r.Id, r.RoleStar); err == nil {
|
||||
for _, tid := range variant.Config.TraitList {
|
||||
if tid == cwTrailblazeTrait {
|
||||
|
||||
@@ -275,217 +275,199 @@ func cwNextRound(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) (*pb.
|
||||
}
|
||||
|
||||
func init() {
|
||||
cwHandle("GridFightUpdatePos", func(s *Session, req *pb.GridFightUpdatePosCsReq, rsp *pb.GridFightUpdatePosScRsp) error {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH)
|
||||
if _, active := CURRENT_BATTLE.Load(s.Token); active {
|
||||
PacketLog("CW_UPDATE_POS rejected reason=battle_active")
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_NOT_IN_GAMEPLAY)
|
||||
return nil
|
||||
}
|
||||
if revision != s.currencyWarsRevision {
|
||||
return nil
|
||||
}
|
||||
run := data.DHFOFLFCLAH
|
||||
if run.FMIGOKIDKJL != nil || len(req.AAKDHMHCMLP) == 0 {
|
||||
PacketLog("CW_UPDATE_POS rejected reason=not_preparing_or_empty")
|
||||
return nil
|
||||
}
|
||||
player, team, _ := cwComponents(run)
|
||||
if player == nil || team == nil {
|
||||
return nil
|
||||
}
|
||||
front, err := catalog.IntegerRule("GridFight_Front_AvatarMaxNum")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
back, err := catalog.IntegerRule("GridFight_Back_AvatarMaxNum")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activeEnd := uint64(front + back)
|
||||
overflow, err := catalog.IntegerRule("GridFight_Bench_OverFlow_AvatarNum")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
benchEnd := activeEnd + uint64(player.MICEOJBAKGN) + uint64(overflow)
|
||||
changes := map[uint32]uint32{}
|
||||
for _, v := range req.AAKDHMHCMLP {
|
||||
if v == nil || v.UniqueId == 0 {
|
||||
cwHandle("GridFightUpdatePos", cwUpdatePositions)
|
||||
|
||||
// CmdID 8543 (GEBBMDCOLCH) -> ScRsp 8630 (ALNFEBKHCFC)
|
||||
// Protocol revision: 4.6.53 (upstream_cmdids.go).
|
||||
// Assumption: Native Client sends GEBBMDCOLCH during preparation / round transition to sync
|
||||
// the batch formation role positions (CONJCEMAHGN: MDNAGANGNKK = role UID, JFCJNHNBMCB = pos).
|
||||
// Reuse the validated formation transaction; acknowledge only after commit.
|
||||
cwHandleNamed("GEBBMDCOLCH", "ALNFEBKHCFC", func(s *Session, req *pb.GEBBMDCOLCH, rsp *pb.ALNFEBKHCFC) error {
|
||||
mapped := &pb.GridFightUpdatePosCsReq{}
|
||||
for _, entry := range req.CONJCEMAHGN {
|
||||
if entry == nil {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH)
|
||||
return nil
|
||||
}
|
||||
if _, ok := changes[v.UniqueId]; ok {
|
||||
return nil
|
||||
}
|
||||
if uint64(v.Pos) > benchEnd {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_NOT_VALID)
|
||||
return nil
|
||||
}
|
||||
changes[v.UniqueId] = v.Pos
|
||||
mapped.AAKDHMHCMLP = append(mapped.AAKDHMHCMLP, &pb.MHEGMHKHAAD{UniqueId: entry.MDNAGANGNKK, Pos: entry.JFCJNHNBMCB})
|
||||
}
|
||||
// Validate final positions together, so swaps need no intermediate empty slot.
|
||||
occupied := map[uint32]bool{}
|
||||
owned := map[uint32]bool{}
|
||||
active := uint32(0)
|
||||
activeAvatars := map[uint32]bool{}
|
||||
for _, role := range team.GridGameRoleList {
|
||||
owned[role.UniqueId] = true
|
||||
pos := role.Pos
|
||||
if v, ok := changes[role.UniqueId]; ok {
|
||||
pos = v
|
||||
}
|
||||
if pos != 0 {
|
||||
if occupied[pos] {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_EXIST_ROLE)
|
||||
return nil
|
||||
}
|
||||
occupied[pos] = true
|
||||
}
|
||||
if pos != 0 && uint64(pos) <= activeEnd {
|
||||
config, err := catalog.CapturedRoleVariant(role.Id, role.RoleStar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if activeAvatars[config.Config.AvatarID] {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_SAME_ROLE_IN_BATTLE)
|
||||
return nil
|
||||
}
|
||||
activeAvatars[config.Config.AvatarID] = true
|
||||
active++
|
||||
}
|
||||
result := &pb.GridFightUpdatePosScRsp{}
|
||||
err := cwUpdatePositions(s, mapped, result)
|
||||
rsp.Retcode = result.Retcode
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func cwUpdatePositions(s *Session, req *pb.GridFightUpdatePosCsReq, rsp *pb.GridFightUpdatePosScRsp) error {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH)
|
||||
if _, active := CURRENT_BATTLE.Load(s.Token); active {
|
||||
PacketLog("CW_UPDATE_POS rejected reason=battle_active")
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_NOT_IN_GAMEPLAY)
|
||||
return nil
|
||||
}
|
||||
if revision != s.currencyWarsRevision {
|
||||
return nil
|
||||
}
|
||||
run := data.DHFOFLFCLAH
|
||||
if run.FMIGOKIDKJL != nil || len(req.AAKDHMHCMLP) == 0 {
|
||||
PacketLog("CW_UPDATE_POS rejected reason=not_preparing_or_empty")
|
||||
return nil
|
||||
}
|
||||
player, team, _ := cwComponents(run)
|
||||
if player == nil || team == nil {
|
||||
return nil
|
||||
}
|
||||
front, err := catalog.IntegerRule("GridFight_Front_AvatarMaxNum")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
back, err := catalog.IntegerRule("GridFight_Back_AvatarMaxNum")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activeEnd := uint64(front + back)
|
||||
overflow, err := catalog.IntegerRule("GridFight_Bench_OverFlow_AvatarNum")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
benchEnd := activeEnd + uint64(player.MICEOJBAKGN) + uint64(overflow)
|
||||
changes := map[uint32]uint32{}
|
||||
for _, v := range req.AAKDHMHCMLP {
|
||||
if v == nil || v.UniqueId == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, forge := range team.KCOMPKMJCMC {
|
||||
pos := forge.Pos
|
||||
if _, changed := changes[forge.UniqueId]; changed {
|
||||
return nil
|
||||
} // Projection movement needs its separately verified sync arm.
|
||||
if pos != 0 {
|
||||
if occupied[pos] {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_EXIST_ROLE)
|
||||
return nil
|
||||
}
|
||||
occupied[pos] = true
|
||||
}
|
||||
if _, ok := changes[v.UniqueId]; ok {
|
||||
return nil
|
||||
}
|
||||
for _, forge := range team.PMEOPPDAMLM {
|
||||
owned[forge.UniqueId] = true
|
||||
pos := forge.Pos
|
||||
if v, ok := changes[forge.UniqueId]; ok {
|
||||
pos = v
|
||||
}
|
||||
if pos == 0 || uint64(pos) <= activeEnd || uint64(pos) > benchEnd {
|
||||
return nil
|
||||
}
|
||||
if uint64(v.Pos) > benchEnd {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_NOT_VALID)
|
||||
return nil
|
||||
}
|
||||
changes[v.UniqueId] = v.Pos
|
||||
}
|
||||
// Validate final positions together, so swaps need no intermediate empty slot.
|
||||
occupied := map[uint32]bool{}
|
||||
owned := map[uint32]bool{}
|
||||
active := uint32(0)
|
||||
activeAvatars := map[uint32]bool{}
|
||||
for _, role := range team.GridGameRoleList {
|
||||
owned[role.UniqueId] = true
|
||||
pos := role.Pos
|
||||
if v, ok := changes[role.UniqueId]; ok {
|
||||
pos = v
|
||||
}
|
||||
if pos != 0 {
|
||||
if occupied[pos] {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_EXIST_ROLE)
|
||||
return nil
|
||||
}
|
||||
occupied[pos] = true
|
||||
}
|
||||
for id := range changes {
|
||||
if !owned[id] {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_ROLE_NOT_EXIST)
|
||||
if pos != 0 && uint64(pos) <= activeEnd {
|
||||
config, err := catalog.CapturedRoleVariant(role.Id, role.RoleStar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if activeAvatars[config.Config.AvatarID] {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_SAME_ROLE_IN_BATTLE)
|
||||
return nil
|
||||
}
|
||||
activeAvatars[config.Config.AvatarID] = true
|
||||
active++
|
||||
}
|
||||
if active > player.AFFCFLFIFPH {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_REGION_FULL)
|
||||
}
|
||||
for _, forge := range team.KCOMPKMJCMC {
|
||||
pos := forge.Pos
|
||||
if _, changed := changes[forge.UniqueId]; changed {
|
||||
return nil
|
||||
} // Projection movement needs its separately verified sync arm.
|
||||
if pos != 0 {
|
||||
if occupied[pos] {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_EXIST_ROLE)
|
||||
return nil
|
||||
}
|
||||
occupied[pos] = true
|
||||
}
|
||||
}
|
||||
for _, forge := range team.PMEOPPDAMLM {
|
||||
owned[forge.UniqueId] = true
|
||||
pos := forge.Pos
|
||||
if v, ok := changes[forge.UniqueId]; ok {
|
||||
pos = v
|
||||
}
|
||||
if pos == 0 || uint64(pos) <= activeEnd || uint64(pos) > benchEnd {
|
||||
return nil
|
||||
}
|
||||
var updates []*pb.KFNCAIHIHCM
|
||||
for _, role := range team.GridGameRoleList {
|
||||
if pos, ok := changes[role.UniqueId]; ok {
|
||||
role.Pos = pos
|
||||
updates = append(updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_CFOCDALNCBP{CFOCDALNCBP: role}})
|
||||
}
|
||||
if occupied[pos] {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_EXIST_ROLE)
|
||||
return nil
|
||||
}
|
||||
for _, forge := range team.PMEOPPDAMLM {
|
||||
if pos, ok := changes[forge.UniqueId]; ok {
|
||||
forge.Pos = pos
|
||||
updates = append(updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FEKCPKAPHHO{FEKCPKAPHHO: forge}})
|
||||
}
|
||||
occupied[pos] = true
|
||||
}
|
||||
for id := range changes {
|
||||
if !owned[id] {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_ROLE_NOT_EXIST)
|
||||
return nil
|
||||
}
|
||||
runtime, _ := cwBonfireRuntime(catalog, run)
|
||||
cwSyncTraitComponent(catalog, run, runtime)
|
||||
if tbCat, err := common.LoadCurrencyWarsTrailblaze(); err == nil && (cwGetTrailblazeLayer(catalog, run) > 0 || (runtime != nil && runtime.Rules["TbOpened"] != 0)) {
|
||||
updates = append(updates,
|
||||
&pb.KFNCAIHIHCM{
|
||||
MNLCHEJKDHO: &pb.KFNCAIHIHCM_HKHJJCMKKJE{
|
||||
HKHJJCMKKJE: &pb.JLKCBOIFJKC{
|
||||
TraitId: cwTrailblazeTrait,
|
||||
ENPLACDLJGA: cwTrailblazeTraitEffectProto(tbCat, runtime),
|
||||
},
|
||||
}
|
||||
if active > player.AFFCFLFIFPH {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_REGION_FULL)
|
||||
return nil
|
||||
}
|
||||
var updates []*pb.KFNCAIHIHCM
|
||||
for _, role := range team.GridGameRoleList {
|
||||
if pos, ok := changes[role.UniqueId]; ok {
|
||||
role.Pos = pos
|
||||
updates = append(updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_CFOCDALNCBP{CFOCDALNCBP: role}})
|
||||
}
|
||||
}
|
||||
for _, forge := range team.PMEOPPDAMLM {
|
||||
if pos, ok := changes[forge.UniqueId]; ok {
|
||||
forge.Pos = pos
|
||||
updates = append(updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FEKCPKAPHHO{FEKCPKAPHHO: forge}})
|
||||
}
|
||||
}
|
||||
runtime, err := cwBonfireRuntime(catalog, run)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cwSyncTraitComponent(catalog, run, runtime)
|
||||
if tbCat, err := common.LoadCurrencyWarsTrailblaze(); err == nil && (cwGetTrailblazeLayer(catalog, run) > 0 || (runtime != nil && runtime.Rules["TbOpened"] != 0)) {
|
||||
updates = append(updates,
|
||||
&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),
|
||||
},
|
||||
},
|
||||
&pb.KFNCAIHIHCM{
|
||||
MNLCHEJKDHO: &pb.KFNCAIHIHCM_GOOCHGKBIPN{
|
||||
GOOCHGKBIPN: cwTrailblazeModifierProto(tbCat, runtime),
|
||||
},
|
||||
)
|
||||
}
|
||||
if err = s.saveCurrencyWarsDataWithRuntime(data, catalog, runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cwSend(s, cwNotify(updates...)); err != nil {
|
||||
return err
|
||||
}
|
||||
rsp.AAKDHMHCMLP = req.AAKDHMHCMLP
|
||||
rsp.Retcode = 0
|
||||
PacketLog(fmt.Sprintf("CW_UPDATE_POS saved count=%d active=%d revision=%d", len(changes), active, s.currencyWarsRevision))
|
||||
return nil
|
||||
})
|
||||
|
||||
// CmdID 8543 (GEBBMDCOLCH) -> ScRsp 8630 (ALNFEBKHCFC)
|
||||
// Protocol revision: 4.6.53 (upstream_cmdids.go).
|
||||
// Assumption: Native Client sends GEBBMDCOLCH during preparation / round transition to sync
|
||||
// the batch formation role positions (CONJCEMAHGN: MDNAGANGNKK = role UID, JFCJNHNBMCB = pos).
|
||||
// Acknowledging with retcode 0 unblocks the Client UI input controller.
|
||||
cwHandleNamed("GEBBMDCOLCH", "ALNFEBKHCFC", func(s *Session, req *pb.GEBBMDCOLCH, rsp *pb.ALNFEBKHCFC) error {
|
||||
catalog, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
rsp.Retcode = 0
|
||||
return nil
|
||||
}
|
||||
data, _, err := readCurrencyWarsData(catalog)
|
||||
if err != nil || data == nil || data.DHFOFLFCLAH == nil {
|
||||
rsp.Retcode = 0
|
||||
return nil
|
||||
}
|
||||
run := data.DHFOFLFCLAH
|
||||
_, team, _ := cwComponents(run)
|
||||
if team != nil && len(req.CONJCEMAHGN) > 0 {
|
||||
posMap := make(map[uint32]uint32, len(req.CONJCEMAHGN))
|
||||
for _, entry := range req.CONJCEMAHGN {
|
||||
if entry != nil && entry.MDNAGANGNKK != 0 {
|
||||
posMap[entry.MDNAGANGNKK] = entry.JFCJNHNBMCB
|
||||
}
|
||||
}
|
||||
changed := false
|
||||
for _, role := range team.GridGameRoleList {
|
||||
if newPos, ok := posMap[role.UniqueId]; ok && role.Pos != newPos {
|
||||
role.Pos = newPos
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
runtime, _ := cwBonfireRuntime(catalog, run)
|
||||
cwSyncTraitComponent(catalog, run, runtime)
|
||||
_ = s.saveCurrencyWarsDataWithRuntime(data, catalog, runtime)
|
||||
}
|
||||
}
|
||||
rsp.Retcode = 0
|
||||
PacketLog(fmt.Sprintf("CW_BATCH_UPDATE_POS count=%d revision=%d", len(req.CONJCEMAHGN), s.currencyWarsRevision))
|
||||
return nil
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
if err = s.saveCurrencyWarsDataWithRuntime(data, catalog, runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cwSend(s, cwNotify(updates...)); err != nil {
|
||||
return err
|
||||
}
|
||||
rsp.AAKDHMHCMLP = req.AAKDHMHCMLP
|
||||
rsp.Retcode = 0
|
||||
PacketLog(fmt.Sprintf("CW_UPDATE_POS saved count=%d active=%d revision=%d", len(changes), active, s.currencyWarsRevision))
|
||||
return nil
|
||||
}
|
||||
@@ -203,8 +203,6 @@ func currencyWarsTerminalSettlement(catalog *common.CurrencyWarsCatalog, data *p
|
||||
seasonLevel = maxLevel
|
||||
}
|
||||
|
||||
cwRecordArchive(finish, catalog.ActiveSeasonID, catalog.ActiveSubSeasonID)
|
||||
|
||||
if data != nil && data.LFLCDNGALME != nil && data.LFLCDNGALME.HFPEJIDDDJG != nil {
|
||||
if data.LFLCDNGALME.HFPEJIDDDJG.Exp == nil {
|
||||
data.LFLCDNGALME.HFPEJIDDDJG.Exp = &pb.GLIFOLCEMIN{}
|
||||
@@ -275,7 +273,7 @@ func init() {
|
||||
if !active {
|
||||
return nil
|
||||
}
|
||||
if err = s.saveCurrencyWarsData(data, catalog); err != nil {
|
||||
if err = s.saveCurrencyWarsTerminal(data, catalog, settlement.BFLLJJBHLDF); err != nil {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
||||
PacketLog(fmt.Sprintf("CW_QUIT_SETTLE rejected run=%d reason=save_error error=%v", runID, err))
|
||||
return nil
|
||||
|
||||
@@ -84,6 +84,10 @@ func (s *Session) saveCurrencyWarsData(data *goproto.GridFightGetDataScRsp, cata
|
||||
}
|
||||
|
||||
func (s *Session) saveCurrencyWarsDataWithRuntime(data *goproto.GridFightGetDataScRsp, catalog *common.CurrencyWarsCatalog, runtime *common.CurrencyWarsRuntimeState, commandID ...string) error {
|
||||
return s.saveCurrencyWarsSnapshot(data, catalog, runtime, nil, commandID...)
|
||||
}
|
||||
|
||||
func (s *Session) saveCurrencyWarsSnapshot(data *goproto.GridFightGetDataScRsp, catalog *common.CurrencyWarsCatalog, runtime *common.CurrencyWarsRuntimeState, archive []byte, commandID ...string) error {
|
||||
cwProjectRoleIDs(data, catalog)
|
||||
if err := validateCurrencyWarsSavedData(data, catalog); err != nil {
|
||||
return err
|
||||
@@ -97,6 +101,7 @@ func (s *Session) saveCurrencyWarsDataWithRuntime(data *goproto.GridFightGetData
|
||||
return err
|
||||
}
|
||||
next := common.NewCurrencyWarsSnapshot(cfg.PlayerUID, catalog.ClientVersion, catalog.Provenance.GameAssemblySHA256, wire)
|
||||
next.PendingArchive = archive
|
||||
if run := data.DHFOFLFCLAH; run != nil {
|
||||
if runtime != nil && runtime.RunID != run.UniqueId {
|
||||
return fmt.Errorf("CW runtime rules belong to a different run")
|
||||
|
||||
@@ -620,7 +620,7 @@ func finishCurrencyWarsBattle(s *Session, req *pb.PVEBattleResultCsReq, current
|
||||
return termErr
|
||||
}
|
||||
data.DHFOFLFCLAH = nil
|
||||
if termErr = s.saveCurrencyWarsData(data, catalog); termErr != nil {
|
||||
if termErr = s.saveCurrencyWarsTerminal(data, catalog, settle.BFLLJJBHLDF); termErr != nil {
|
||||
return termErr
|
||||
}
|
||||
CURRENT_BATTLE.Delete(s.Token)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -10,151 +11,97 @@ import (
|
||||
)
|
||||
|
||||
func TestCurrencyWarsTalentsHandlers(t *testing.T) {
|
||||
origPersistent, _ := os.ReadFile("persistent")
|
||||
if origPersistent != nil {
|
||||
t.Cleanup(func() {
|
||||
_ = os.WriteFile("persistent", origPersistent, 0644)
|
||||
})
|
||||
}
|
||||
|
||||
session := &Session{Token: 12345}
|
||||
catalog, err := common.LoadCurrencyWarsCatalog()
|
||||
before, err := os.ReadFile("persistent")
|
||||
if err != nil {
|
||||
t.Fatalf("load catalog: %v", err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, rev, err := readCurrencyWarsData(catalog)
|
||||
if err != nil || data == nil {
|
||||
data, err = newCurrencyWarsData(catalog)
|
||||
s := &Session{Token: 12345}
|
||||
requests := map[string]proto.Message{
|
||||
"GridFightPermanentTalentEnableCsReq": &pb.GridFightPermanentTalentEnableCsReq{CKHCLKOALKE: ^uint32(0)},
|
||||
"GridFightSeasonTalentEnableCsReq": &pb.GridFightSeasonTalentEnableCsReq{CKHCLKOALKE: ^uint32(0)},
|
||||
"GridFightPermanentTalentResetCsReq": &pb.GridFightPermanentTalentResetCsReq{},
|
||||
"GridFightSeasonTalentResetCsReq": &pb.GridFightSeasonTalentResetCsReq{},
|
||||
}
|
||||
for name, req := range requests {
|
||||
raw, _ := proto.Marshal(req)
|
||||
if err := typedHandlers[uint16(pb.CmdIDByName[name])](s, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, err := os.ReadFile("persistent")
|
||||
if err != nil {
|
||||
t.Fatalf("new data: %v", err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
rev = 0
|
||||
}
|
||||
session.currencyWarsRevision = rev
|
||||
if err := session.saveCurrencyWarsData(data, catalog); err != nil {
|
||||
t.Fatalf("save initial data: %v", err)
|
||||
}
|
||||
|
||||
// 1. Enable Permanent Talent
|
||||
permReq := &pb.GridFightPermanentTalentEnableCsReq{CKHCLKOALKE: 101}
|
||||
permCmdID, ok := pb.CmdIDByName["GridFightPermanentTalentEnableCsReq"]
|
||||
if !ok || typedHandlers[uint16(permCmdID)] == nil {
|
||||
t.Fatal("handler GridFightPermanentTalentEnable not registered")
|
||||
}
|
||||
rawPerm, err := proto.Marshal(permReq)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal permReq: %v", err)
|
||||
}
|
||||
if err := typedHandlers[uint16(permCmdID)](session, rawPerm); err != nil {
|
||||
t.Fatalf("call GridFightPermanentTalentEnable: %v", err)
|
||||
}
|
||||
|
||||
saved, _, err := readCurrencyWarsData(catalog)
|
||||
if err != nil || saved == nil {
|
||||
t.Fatalf("read saved data: %v", err)
|
||||
}
|
||||
if saved.LFLCDNGALME == nil || saved.LFLCDNGALME.PKELAIPIHMF == nil || saved.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC == nil {
|
||||
t.Fatal("permanent talent container missing")
|
||||
}
|
||||
found := false
|
||||
for _, id := range saved.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC.BDGMNPIJDDL {
|
||||
if id == 101 {
|
||||
found = true
|
||||
break
|
||||
if !bytes.Equal(before, after) {
|
||||
t.Fatalf("unsupported talent request changed persistent: %s", name)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("permanent talent 101 was not persisted: %v", saved.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC.BDGMNPIJDDL)
|
||||
}
|
||||
|
||||
// 2. Enable Season Talent
|
||||
seasonReq := &pb.GridFightSeasonTalentEnableCsReq{CKHCLKOALKE: 202}
|
||||
seasonCmdID, ok := pb.CmdIDByName["GridFightSeasonTalentEnableCsReq"]
|
||||
if !ok || typedHandlers[uint16(seasonCmdID)] == nil {
|
||||
t.Fatal("handler GridFightSeasonTalentEnable not registered")
|
||||
}
|
||||
rawSeason, err := proto.Marshal(seasonReq)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal seasonReq: %v", err)
|
||||
}
|
||||
if err := typedHandlers[uint16(seasonCmdID)](session, rawSeason); err != nil {
|
||||
t.Fatalf("call GridFightSeasonTalentEnable: %v", err)
|
||||
}
|
||||
|
||||
saved, _, _ = readCurrencyWarsData(catalog)
|
||||
if saved.LFLCDNGALME == nil || saved.LFLCDNGALME.HFPEJIDDDJG == nil || saved.LFLCDNGALME.HFPEJIDDDJG.MHFHLBHFIBC == nil {
|
||||
t.Fatal("season talent container missing")
|
||||
}
|
||||
foundSeason := false
|
||||
for _, id := range saved.LFLCDNGALME.HFPEJIDDDJG.MHFHLBHFIBC.BDGMNPIJDDL {
|
||||
if id == 202 {
|
||||
foundSeason = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundSeason {
|
||||
t.Errorf("season talent 202 was not persisted: %v", saved.LFLCDNGALME.HFPEJIDDDJG.MHFHLBHFIBC.BDGMNPIJDDL)
|
||||
}
|
||||
|
||||
// 3. Reset Talents
|
||||
resetCmdID, ok := pb.CmdIDByName["GridFightPermanentTalentResetCsReq"]
|
||||
if ok && typedHandlers[uint16(resetCmdID)] != nil {
|
||||
rawReset, _ := proto.Marshal(&pb.GridFightPermanentTalentResetCsReq{})
|
||||
_ = typedHandlers[uint16(resetCmdID)](session, rawReset)
|
||||
}
|
||||
saved, _, _ = readCurrencyWarsData(catalog)
|
||||
if len(saved.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC.BDGMNPIJDDL) != 0 {
|
||||
t.Errorf("expected permanent talents to be empty after reset, got %v", saved.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC.BDGMNPIJDDL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrencyWarsArchiveHandlers(t *testing.T) {
|
||||
record := &pb.OEDHMAHDECC{
|
||||
Reason: pb.PEIJNEFOCHI_PEIJNEFOCHI_JOJPGBCECLN,
|
||||
IsOverlock: false,
|
||||
}
|
||||
entry := cwRecordArchive(record, 2, 1)
|
||||
if entry == nil || entry.BAAGDILLLME == 0 {
|
||||
t.Fatal("archive record failed")
|
||||
}
|
||||
|
||||
session := &Session{Token: 54321}
|
||||
|
||||
// Toggle Favour
|
||||
favourReq := &pb.FavourArchiveCsReq{BAAGDILLLME: entry.BAAGDILLLME, IECKKCHJMLP: true}
|
||||
favourCmdID, ok := pb.CmdIDByName["FavourArchiveCsReq"]
|
||||
if !ok || typedHandlers[uint16(favourCmdID)] == nil {
|
||||
t.Fatal("FavourArchive handler missing")
|
||||
}
|
||||
rawFavour, err := proto.Marshal(favourReq)
|
||||
c, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
t.Fatalf("marshal favourReq: %v", err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := typedHandlers[uint16(favourCmdID)](session, rawFavour); err != nil {
|
||||
t.Fatalf("call FavourArchive: %v", err)
|
||||
data, revision, err := readCurrencyWarsData(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
foundFavour := false
|
||||
globalCWArchive.mu.RLock()
|
||||
for _, e := range globalCWArchive.entries {
|
||||
if e.BAAGDILLLME == entry.BAAGDILLLME && e.IECKKCHJMLP {
|
||||
foundFavour = true
|
||||
break
|
||||
if data == nil {
|
||||
data, err = newCurrencyWarsData(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
globalCWArchive.mu.RUnlock()
|
||||
if !foundFavour {
|
||||
t.Errorf("expected archive %d to be favoured", entry.BAAGDILLLME)
|
||||
session := &Session{Token: 54321, currencyWarsRevision: revision}
|
||||
record := &pb.OEDHMAHDECC{Reason: pb.PEIJNEFOCHI_PEIJNEFOCHI_JOJPGBCECLN}
|
||||
if err := session.saveCurrencyWarsTerminal(data, c, record); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get Archive
|
||||
getArchiveCmdID, ok := pb.CmdIDByName["GridFightGetArchiveCsReq"]
|
||||
if !ok || typedHandlers[uint16(getArchiveCmdID)] == nil {
|
||||
t.Fatal("GridFightGetArchive handler missing")
|
||||
snapshot, err := cwArchiveSnapshot(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rawGetArchive, _ := proto.Marshal(&pb.GridFightGetArchiveCsReq{Type: pb.MLJIILDNDPL_MLJIILDNDPL_FAGECPGMPEH})
|
||||
if err := typedHandlers[uint16(getArchiveCmdID)](session, rawGetArchive); err != nil {
|
||||
t.Fatalf("call GridFightGetArchive: %v", err)
|
||||
entries, err := cwReadArchives(snapshot)
|
||||
if err != nil || len(entries) == 0 {
|
||||
t.Fatalf("read archive: %v", err)
|
||||
}
|
||||
entry := entries[len(entries)-1]
|
||||
raw, _ := proto.Marshal(&pb.FavourArchiveCsReq{BAAGDILLLME: entry.BAAGDILLLME, IECKKCHJMLP: true})
|
||||
if err := typedHandlers[uint16(pb.CmdIDByName["FavourArchiveCsReq"])](session, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A new read/session has no in-memory archive state to rely on.
|
||||
snapshot, err = cwArchiveSnapshot(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries, err = cwReadArchives(snapshot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !entries[len(entries)-1].IECKKCHJMLP {
|
||||
t.Fatal("favourite was not persisted")
|
||||
}
|
||||
stale := &Session{currencyWarsRevision: revision}
|
||||
if err := stale.saveCurrencyWarsTerminal(data, c, record); err == nil {
|
||||
t.Fatal("stale settlement appended archive")
|
||||
}
|
||||
again, err := cwArchiveSnapshot(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(again.Archives) != len(snapshot.Archives) {
|
||||
t.Fatal("failed transaction changed archives")
|
||||
}
|
||||
if err := session.saveCurrencyWarsData(data, c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again, err = cwArchiveSnapshot(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(again.Archives) != len(snapshot.Archives) {
|
||||
t.Fatal("ordinary save lost archives")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,141 +1,25 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
import pb "gac-hkrpg-go/protocol/proto"
|
||||
|
||||
// The reference talent service is a stub. No revision-bound talent costs,
|
||||
// prerequisites, refund rules or match effects are available in our catalog.
|
||||
// Keep the typed callbacks but reject mutation until those contracts exist.
|
||||
func init() {
|
||||
cwHandle("GridFightPermanentTalentEnable", func(s *Session, req *pb.GridFightPermanentTalentEnableCsReq, rsp *pb.GridFightPermanentTalentEnableScRsp) error {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH)
|
||||
if req == nil || req.CKHCLKOALKE == 0 {
|
||||
return nil
|
||||
}
|
||||
catalog, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, revision, err := readCurrencyWarsData(catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if data == nil {
|
||||
data, err = newCurrencyWarsData(catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.currencyWarsRevision = revision
|
||||
if data.LFLCDNGALME == nil {
|
||||
data.LFLCDNGALME = &pb.CLEDCMIOIPF{}
|
||||
}
|
||||
if data.LFLCDNGALME.PKELAIPIHMF == nil {
|
||||
data.LFLCDNGALME.PKELAIPIHMF = &pb.CKPOBLIFMGN{}
|
||||
}
|
||||
if data.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC == nil {
|
||||
data.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC = &pb.FMNKKAMOHEE{}
|
||||
}
|
||||
list := &data.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC.BDGMNPIJDDL
|
||||
if !slices.Contains(*list, req.CKHCLKOALKE) {
|
||||
*list = append(*list, req.CKHCLKOALKE)
|
||||
}
|
||||
if err = s.saveCurrencyWarsData(data, catalog); err != nil {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
||||
return nil
|
||||
}
|
||||
rsp.CKHCLKOALKE = req.CKHCLKOALKE
|
||||
rsp.Retcode = 0
|
||||
PacketLog(fmt.Sprintf("CW_PERMANENT_TALENT_ENABLE node=%d total=%d", req.CKHCLKOALKE, len(*list)))
|
||||
cwHandle("GridFightPermanentTalentEnable", func(_ *Session, _ *pb.GridFightPermanentTalentEnableCsReq, rsp *pb.GridFightPermanentTalentEnableScRsp) error {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
||||
return nil
|
||||
})
|
||||
|
||||
cwHandle("GridFightSeasonTalentEnable", func(s *Session, req *pb.GridFightSeasonTalentEnableCsReq, rsp *pb.GridFightSeasonTalentEnableScRsp) error {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH)
|
||||
if req == nil || req.CKHCLKOALKE == 0 {
|
||||
return nil
|
||||
}
|
||||
catalog, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, revision, err := readCurrencyWarsData(catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if data == nil {
|
||||
data, err = newCurrencyWarsData(catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.currencyWarsRevision = revision
|
||||
if data.LFLCDNGALME == nil {
|
||||
data.LFLCDNGALME = &pb.CLEDCMIOIPF{}
|
||||
}
|
||||
if data.LFLCDNGALME.HFPEJIDDDJG == nil {
|
||||
data.LFLCDNGALME.HFPEJIDDDJG = &pb.AMEEHIIPMFP{}
|
||||
}
|
||||
if data.LFLCDNGALME.HFPEJIDDDJG.MHFHLBHFIBC == nil {
|
||||
data.LFLCDNGALME.HFPEJIDDDJG.MHFHLBHFIBC = &pb.FMNKKAMOHEE{}
|
||||
}
|
||||
list := &data.LFLCDNGALME.HFPEJIDDDJG.MHFHLBHFIBC.BDGMNPIJDDL
|
||||
if !slices.Contains(*list, req.CKHCLKOALKE) {
|
||||
*list = append(*list, req.CKHCLKOALKE)
|
||||
}
|
||||
if err = s.saveCurrencyWarsData(data, catalog); err != nil {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
||||
return nil
|
||||
}
|
||||
rsp.CKHCLKOALKE = req.CKHCLKOALKE
|
||||
rsp.Retcode = 0
|
||||
PacketLog(fmt.Sprintf("CW_SEASON_TALENT_ENABLE node=%d total=%d", req.CKHCLKOALKE, len(*list)))
|
||||
cwHandle("GridFightSeasonTalentEnable", func(_ *Session, _ *pb.GridFightSeasonTalentEnableCsReq, rsp *pb.GridFightSeasonTalentEnableScRsp) error {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
||||
return nil
|
||||
})
|
||||
|
||||
cwHandle("GridFightPermanentTalentReset", func(s *Session, _ *pb.GridFightPermanentTalentResetCsReq, rsp *pb.GridFightPermanentTalentResetScRsp) error {
|
||||
catalog, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, revision, err := readCurrencyWarsData(catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.currencyWarsRevision = revision
|
||||
if data != nil && data.LFLCDNGALME != nil && data.LFLCDNGALME.PKELAIPIHMF != nil && data.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC != nil {
|
||||
data.LFLCDNGALME.PKELAIPIHMF.MHFHLBHFIBC.BDGMNPIJDDL = nil
|
||||
if err = s.saveCurrencyWarsData(data, catalog); err != nil {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
rsp.Retcode = 0
|
||||
PacketLog("CW_PERMANENT_TALENT_RESET completed")
|
||||
cwHandle("GridFightPermanentTalentReset", func(_ *Session, _ *pb.GridFightPermanentTalentResetCsReq, rsp *pb.GridFightPermanentTalentResetScRsp) error {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
||||
return nil
|
||||
})
|
||||
|
||||
cwHandle("GridFightSeasonTalentReset", func(s *Session, _ *pb.GridFightSeasonTalentResetCsReq, rsp *pb.GridFightSeasonTalentResetScRsp) error {
|
||||
catalog, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, revision, err := readCurrencyWarsData(catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.currencyWarsRevision = revision
|
||||
if data != nil && data.LFLCDNGALME != nil && data.LFLCDNGALME.HFPEJIDDDJG != nil && data.LFLCDNGALME.HFPEJIDDDJG.MHFHLBHFIBC != nil {
|
||||
data.LFLCDNGALME.HFPEJIDDDJG.MHFHLBHFIBC.BDGMNPIJDDL = nil
|
||||
if err = s.saveCurrencyWarsData(data, catalog); err != nil {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
rsp.Retcode = 0
|
||||
PacketLog("CW_SEASON_TALENT_RESET completed")
|
||||
cwHandle("GridFightSeasonTalentReset", func(_ *Session, _ *pb.GridFightSeasonTalentResetCsReq, rsp *pb.GridFightSeasonTalentResetScRsp) error {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -9,7 +9,17 @@ import (
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
const benchFirstPos uint32 = 14
|
||||
func cwRoleOnBoard(c *common.CurrencyWarsCatalog, role *pb.CDEAABODKCP) bool {
|
||||
if role == nil || role.Pos == 0 {
|
||||
return false
|
||||
}
|
||||
front, err := c.IntegerRule("GridFight_Front_AvatarMaxNum")
|
||||
if err != nil || front < 0 {
|
||||
return false
|
||||
}
|
||||
back, err := c.IntegerRule("GridFight_Back_AvatarMaxNum")
|
||||
return err == nil && back >= 0 && uint64(role.Pos) <= uint64(front)+uint64(back)
|
||||
}
|
||||
|
||||
func cwCalculateBoardTraits(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) []*pb.FBNGHMHHHLL {
|
||||
if catalog == nil || run == nil {
|
||||
@@ -27,7 +37,7 @@ func cwCalculateBoardTraits(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEM
|
||||
tallies := make(map[uint32]uint32)
|
||||
members := make(map[uint32][]uint32)
|
||||
for _, role := range team.GridGameRoleList {
|
||||
if role == nil || role.Pos >= benchFirstPos {
|
||||
if !cwRoleOnBoard(catalog, role) {
|
||||
continue
|
||||
}
|
||||
variant, err := catalog.CapturedRoleVariant(role.Id, role.RoleStar)
|
||||
@@ -46,7 +56,7 @@ func cwCalculateBoardTraits(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEM
|
||||
equipMap, _ := common.LoadCurrencyWarsEquipment(catalog)
|
||||
if equipMap != nil {
|
||||
for _, role := range team.GridGameRoleList {
|
||||
if role == nil || role.Pos >= benchFirstPos {
|
||||
if !cwRoleOnBoard(catalog, role) {
|
||||
continue
|
||||
}
|
||||
for _, uid := range role.PNJIPHJJONG {
|
||||
@@ -180,7 +190,7 @@ func cwCalculateBattleTraits(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAE
|
||||
tallies := make(map[uint32]uint32)
|
||||
members := make(map[uint32][]*pb.MHFNFINKJKP)
|
||||
for _, role := range team.GridGameRoleList {
|
||||
if role == nil || role.Pos >= benchFirstPos {
|
||||
if !cwRoleOnBoard(catalog, role) {
|
||||
continue
|
||||
}
|
||||
variant, err := catalog.CapturedRoleVariant(role.Id, role.RoleStar)
|
||||
@@ -199,7 +209,7 @@ func cwCalculateBattleTraits(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAE
|
||||
equipMap, _ := common.LoadCurrencyWarsEquipment(catalog)
|
||||
if equipMap != nil {
|
||||
for _, role := range team.GridGameRoleList {
|
||||
if role == nil || role.Pos >= benchFirstPos {
|
||||
if !cwRoleOnBoard(catalog, role) {
|
||||
continue
|
||||
}
|
||||
for _, uid := range role.PNJIPHJJONG {
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"gac-hkrpg-go/internal/common"
|
||||
"gac-hkrpg-go/internal/testutil"
|
||||
kcplib "gac-hkrpg-go/pkg/kcp"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func isolatedCWTest(t *testing.T) *common.CurrencyWarsCatalog {
|
||||
t.Helper()
|
||||
temporary := t.TempDir()
|
||||
if err := testutil.CopyRuntime(testWorkspaceRoot, temporary); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Chdir(temporary)
|
||||
t.Setenv("GAC_CONFIG", "server.json")
|
||||
if err := os.WriteFile("persistent", []byte(`{}`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Use the real typed dispatcher and transport framing, so assertions examine
|
||||
// the Client response rather than merely checking handler registration.
|
||||
func replySession(t *testing.T) (*Session, func(proto.Message)) {
|
||||
t.Helper()
|
||||
client := kcplib.New(1, 1, false, func([]byte) error { return nil })
|
||||
s := NewSession(func(data []byte) error { return client.Input(data) }, 1, 1)
|
||||
s.kcp.SetNodelay(true, 10, 2, true)
|
||||
if err := s.kcp.Update(s.kcp.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s, func(response proto.Message) {
|
||||
t.Helper()
|
||||
wanted := pb.CmdIDByName[string(response.ProtoReflect().Descriptor().Name())]
|
||||
for {
|
||||
buf := make([]byte, 65536)
|
||||
n, err := client.Recv(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
packet, err := DecodeNetPacket(buf[:n])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if packet.CmdType != wanted {
|
||||
continue
|
||||
}
|
||||
if err := proto.Unmarshal(packet.Body, response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func callReviewRequest(t *testing.T, s *Session, request proto.Message) {
|
||||
t.Helper()
|
||||
raw, err := proto.Marshal(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
handler := typedHandlers[uint16(pb.CmdIDByName[string(request.ProtoReflect().Descriptor().Name())])]
|
||||
if handler == nil {
|
||||
t.Fatal("handler missing")
|
||||
}
|
||||
if err := handler(s, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsupportedCWMutationsReturnFailureAndPreserveSave(t *testing.T) {
|
||||
isolatedCWTest(t)
|
||||
before, _ := os.ReadFile("persistent")
|
||||
cases := []struct{ req, rsp proto.Message }{
|
||||
{&pb.GridFightPermanentTalentEnableCsReq{CKHCLKOALKE: ^uint32(0)}, &pb.GridFightPermanentTalentEnableScRsp{}},
|
||||
{&pb.GridFightSeasonTalentEnableCsReq{CKHCLKOALKE: 1}, &pb.GridFightSeasonTalentEnableScRsp{}},
|
||||
{&pb.GridFightPermanentTalentResetCsReq{}, &pb.GridFightPermanentTalentResetScRsp{}},
|
||||
{&pb.GridFightSeasonTalentResetCsReq{}, &pb.GridFightSeasonTalentResetScRsp{}},
|
||||
{&pb.GridFightTakeWeeklyRewardCsReq{JKCEBHFIENI: []uint32{1, ^uint32(0)}}, &pb.GridFightTakeWeeklyRewardScRsp{}},
|
||||
{&pb.CycleScoreRewardTakeRewardCsReq{JKCEBHFIENI: []uint32{1}}, &pb.CycleScoreRewardTakeRewardScRsp{}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
s, receive := replySession(t)
|
||||
callReviewRequest(t, s, tc.req)
|
||||
receive(tc.rsp)
|
||||
view := tc.rsp.ProtoReflect()
|
||||
fd := view.Descriptor().Fields().ByName("retcode")
|
||||
if view.Get(fd).Uint() == 0 {
|
||||
t.Fatalf("unsupported operation succeeded: %s", view.Descriptor().Name())
|
||||
}
|
||||
after, _ := os.ReadFile("persistent")
|
||||
if !bytes.Equal(before, after) {
|
||||
t.Fatal("unsupported mutation changed save")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrencyWarsPositionValidationBothHandlers(t *testing.T) {
|
||||
c := isolatedCWTest(t)
|
||||
data, err := newCurrencyWarsData(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var roles []*pb.CDEAABODKCP
|
||||
seen := map[uint32]bool{}
|
||||
for _, v := range c.CapturedVariants {
|
||||
if seen[v.Config.AvatarID] {
|
||||
continue
|
||||
}
|
||||
seen[v.Config.AvatarID] = true
|
||||
roles = append(roles, &pb.CDEAABODKCP{Id: v.Config.ID, RoleStar: v.Star, UniqueId: uint32(len(roles) + 1), Pos: uint32(len(roles) + 1)})
|
||||
if len(roles) == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(roles) != 2 {
|
||||
t.Fatal("fixture needs two avatars")
|
||||
}
|
||||
data.DHFOFLFCLAH = &pb.MPIMAEMCEFH{UniqueId: 1, DivisionId: c.CapturedStages[0].ID, LJNGPCKPJLF: c.ActiveSeasonID, NAGDJGMMCBE: []*pb.GridFightGameInfo{
|
||||
{LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{EJIPINIKNCK: &pb.HKFECLCHBFJ{GridGameRoleList: roles}}},
|
||||
{LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{FAOCFBOFCJP: &pb.NIHNGPKHEPC{AFFCFLFIFPH: 2, MICEOJBAKGN: 1}}},
|
||||
}}
|
||||
writer := &Session{}
|
||||
if err := writer.saveCurrencyWarsData(data, c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, batch := range []bool{false, true} {
|
||||
for _, kind := range []string{"duplicate", "outside", "unknown", "battle", "stale", "swap"} {
|
||||
s, receive := replySession(t)
|
||||
s.currencyWarsRevision = writer.currencyWarsRevision
|
||||
before, _ := os.ReadFile("persistent")
|
||||
positions := []*pb.MHEGMHKHAAD{{UniqueId: 1, Pos: 2}, {UniqueId: 2, Pos: 1}}
|
||||
switch kind {
|
||||
case "duplicate":
|
||||
positions[1].Pos = 2
|
||||
case "outside":
|
||||
positions[0].Pos = ^uint32(0)
|
||||
case "unknown":
|
||||
positions[0].UniqueId = ^uint32(0)
|
||||
case "battle":
|
||||
CURRENT_BATTLE.Store(s.Token, &pb.SceneBattleInfo{})
|
||||
case "stale":
|
||||
s.currencyWarsRevision = 0
|
||||
}
|
||||
var code uint32
|
||||
if batch {
|
||||
req := &pb.GEBBMDCOLCH{}
|
||||
for _, p := range positions {
|
||||
req.CONJCEMAHGN = append(req.CONJCEMAHGN, &pb.EPKFPIDHMOC{MDNAGANGNKK: p.UniqueId, JFCJNHNBMCB: p.Pos})
|
||||
}
|
||||
callReviewRequest(t, s, req)
|
||||
rsp := &pb.ALNFEBKHCFC{}
|
||||
receive(rsp)
|
||||
code = rsp.Retcode
|
||||
} else {
|
||||
callReviewRequest(t, s, &pb.GridFightUpdatePosCsReq{AAKDHMHCMLP: positions})
|
||||
rsp := &pb.GridFightUpdatePosScRsp{}
|
||||
receive(rsp)
|
||||
code = rsp.Retcode
|
||||
}
|
||||
CURRENT_BATTLE.Delete(s.Token)
|
||||
after, _ := os.ReadFile("persistent")
|
||||
if kind == "swap" {
|
||||
if code != 0 {
|
||||
t.Fatalf("valid swap rejected batch=%t code=%d", batch, code)
|
||||
}
|
||||
writer.currencyWarsRevision = s.currencyWarsRevision
|
||||
} else if code == 0 || !bytes.Equal(before, after) {
|
||||
t.Fatalf("invalid move changed save or succeeded: batch=%t case=%s code=%d", batch, kind, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrencyWarsUnplacedAndBenchRolesGiveNoTraits(t *testing.T) {
|
||||
c := isolatedCWTest(t)
|
||||
var sample common.CurrencyWarsRoleVariant
|
||||
for _, v := range c.CapturedVariants {
|
||||
if len(v.Config.TraitList) > 0 {
|
||||
sample = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if sample.Config.ID == 0 {
|
||||
t.Fatal("trait role fixture missing")
|
||||
}
|
||||
front, _ := c.IntegerRule("GridFight_Front_AvatarMaxNum")
|
||||
back, _ := c.IntegerRule("GridFight_Back_AvatarMaxNum")
|
||||
role := &pb.CDEAABODKCP{Id: sample.Config.ID, RoleStar: sample.Star, UniqueId: 1}
|
||||
run := &pb.MPIMAEMCEFH{NAGDJGMMCBE: []*pb.GridFightGameInfo{{LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{EJIPINIKNCK: &pb.HKFECLCHBFJ{GridGameRoleList: []*pb.CDEAABODKCP{role}}}}}}
|
||||
for _, pos := range []uint32{0, uint32(front + back + 1)} {
|
||||
role.Pos = pos
|
||||
if len(cwCalculateBoardTraits(c, run)) != 0 || len(cwCalculateBattleTraits(c, run, nil)) != 0 {
|
||||
t.Fatalf("off-board role contributes traits: pos=%d", pos)
|
||||
}
|
||||
}
|
||||
role.Pos = 1
|
||||
if len(cwCalculateBoardTraits(c, run)) == 0 || len(cwCalculateBattleTraits(c, run, nil)) == 0 {
|
||||
t.Fatal("active role traits missing")
|
||||
}
|
||||
}
|
||||
@@ -194,54 +194,16 @@ func init() {
|
||||
})
|
||||
|
||||
handle("CycleScoreRewardTakeReward", func(s *Session, req *goproto.CycleScoreRewardTakeRewardCsReq, rsp *goproto.CycleScoreRewardTakeRewardScRsp) error {
|
||||
cfg, err := common.LoadRuntimeConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rsp.Reward = &goproto.ItemList{}
|
||||
if req != nil {
|
||||
rsp.NGKOFMBBGNL = append(rsp.NGKOFMBBGNL, req.JKCEBHFIENI...)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
rsp.Retcode = 0
|
||||
PacketLog(fmt.Sprintf("CYCLE_SCORE_REWARD_TAKE claims=%v", rsp.NGKOFMBBGNL))
|
||||
// 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 {
|
||||
cfg, err := common.LoadRuntimeConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rsp.Reward = &goproto.ItemList{}
|
||||
if req != nil {
|
||||
rsp.NGKOFMBBGNL = append(rsp.NGKOFMBBGNL, req.JKCEBHFIENI...)
|
||||
}
|
||||
rsp.MGBNEAHCDMA = &goproto.NDLNIKCCALK{
|
||||
EndTime: *cfg.ActivitySchedule.EndTime,
|
||||
}
|
||||
if cfg.ActivitySchedule.BeginTime != nil {
|
||||
rsp.MGBNEAHCDMA.IFEPJHGNCOI = *cfg.ActivitySchedule.BeginTime
|
||||
}
|
||||
if scoreCatalog, err := common.LoadCurrencyWarsScore(nil); err == nil {
|
||||
rsp.MGBNEAHCDMA.EDHKHJAPMKA = scoreCatalog.WeeklyCap
|
||||
for _, r := range scoreCatalog.WeeklyRanks {
|
||||
rsp.MGBNEAHCDMA.OPLNKCOLCKJ = append(rsp.MGBNEAHCDMA.OPLNKCOLCKJ, r.Rank)
|
||||
}
|
||||
}
|
||||
rsp.Retcode = 0
|
||||
PacketLog(fmt.Sprintf("CW_TAKE_WEEKLY_REWARD claims=%v", rsp.NGKOFMBBGNL))
|
||||
// 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
|
||||
})
|
||||
|
||||
|
||||
@@ -11,15 +11,30 @@ import (
|
||||
|
||||
func init() {
|
||||
handle("GetGachaInfo", func(s *Session, req *pb.GetGachaInfoCsReq, rsp *pb.GetGachaInfoScRsp) error {
|
||||
banners, _, err := common.LoadBrowsableGachaBanners()
|
||||
player := s.JSON()
|
||||
mode := 0
|
||||
if player != nil {
|
||||
mode = player.WarpUnlockMode
|
||||
}
|
||||
|
||||
// Read API changes on reopening Warp without requiring a full login.
|
||||
savedMode, err := common.ReadWarpUnlockMode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if savedMode != nil {
|
||||
mode = *savedMode
|
||||
}
|
||||
banners, err := common.LoadVisibleGachaBanners(mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(banners) == 0 {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NOT_SUPPORT)
|
||||
PacketLog("GACHA entry rejected: no verified installed banner assets")
|
||||
PacketLog("GACHA entry rejected: no visible banners configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
config, err := common.LoadRuntimeConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -32,73 +47,42 @@ func init() {
|
||||
}
|
||||
rsp.GachaInfoList = append(rsp.GachaInfoList, info)
|
||||
}
|
||||
// Synthetic IDs are not native Client config rows. The presentation
|
||||
// adapter registers its own pools; sending these IDs in GetGachaInfo has
|
||||
// caused Client response handlers to throw. Keep the preview opt-in.
|
||||
mapped, _, err := common.LoadLimitedGachaBanners()
|
||||
|
||||
randVal, err := rand.Int(rand.Reader, big.NewInt(1000))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, policy, err := common.LoadSandboxDrawPool()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count := policy.MappedBannerPreviewCount
|
||||
if count > uint32(len(mapped)) {
|
||||
count = uint32(len(mapped))
|
||||
}
|
||||
for i := uint32(0); i < count; i++ {
|
||||
banner := mapped[i]
|
||||
info := &pb.GachaInfo{GachaId: banner.GachaID, BeginTime: *config.ActivitySchedule.BeginTime, EndTime: *config.ActivitySchedule.EndTime, GachaCeiling: &pb.GachaCeiling{}}
|
||||
info.PrizeItemList = []uint32{banner.AvatarID}
|
||||
info.ItemDetailList = append([]uint32{banner.AvatarID}, banner.FourStarIDs...)
|
||||
rsp.GachaInfoList = append(rsp.GachaInfoList, info)
|
||||
}
|
||||
PacketLog(fmt.Sprintf("GACHA_INFO banners=%d mapped_preview=%d", len(banners), count))
|
||||
rsp.GachaRandom = 1000 + uint32(randVal.Int64())
|
||||
rsp.Retcode = 0
|
||||
PacketLog(fmt.Sprintf("GACHA_INFO count=%d mode=%d", len(rsp.GachaInfoList), mode))
|
||||
return nil
|
||||
})
|
||||
|
||||
handle("DoGacha", func(s *Session, req *pb.DoGachaCsReq, rsp *pb.DoGachaScRsp) error {
|
||||
rsp.GachaId = req.GachaId
|
||||
rsp.GachaNum = req.GachaNum
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_ID_NOT_EXIST)
|
||||
banners, _, err := common.LoadFeaturedGachaBanners()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var avatarID uint32
|
||||
for _, row := range banners {
|
||||
if row.ID == req.GachaId {
|
||||
avatarID = row.AvatarID
|
||||
break
|
||||
}
|
||||
}
|
||||
if avatarID == 0 {
|
||||
mapped, _, err := common.LoadLimitedGachaBanners()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range mapped {
|
||||
if row.GachaID == req.GachaId {
|
||||
return drawLimitedAvatar(s, req, rsp, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
if avatarID != 0 {
|
||||
return drawFeaturedAvatar(s, req, rsp, avatarID)
|
||||
}
|
||||
// Featured-only path found nothing. Verified browsable banners may
|
||||
// still draw from the policy-declared sandbox pool: every pick is a
|
||||
// duplicate grant on an avatar from the live imported roster, so no
|
||||
// invented IDs or first-acquisition synthesis are involved.
|
||||
sandboxDrawn, err := drawSandboxPool(s, req, rsp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sandboxDrawn {
|
||||
|
||||
if _, active := CURRENT_BATTLE.Load(s.Token); active {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NOT_SUPPORT)
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.GachaNum != 1 && req.GachaNum != 10 {
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NUM_INVALID)
|
||||
return nil
|
||||
}
|
||||
|
||||
player := s.JSON()
|
||||
if player == nil {
|
||||
return nil
|
||||
}
|
||||
// The imported banner lists target another Client. Reject draws until
|
||||
// their pools, grants and protocol fields are verified for this Client.
|
||||
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NOT_SUPPORT)
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// drawFeaturedAvatar grants req.GachaNum copies of one featured avatar using
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestGetGachaInfoScRspWireTags(t *testing.T) {
|
||||
if err := common.LoadBannersConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rsp := &pb.GetGachaInfoScRsp{
|
||||
Retcode: 0,
|
||||
GachaRandom: 1234,
|
||||
}
|
||||
banners := common.GetVisibleBanners(1)
|
||||
if len(banners) == 0 {
|
||||
t.Fatal("expected visible banners")
|
||||
}
|
||||
for _, b := range banners {
|
||||
rsp.GachaInfoList = append(rsp.GachaInfoList, b.ToProtoInfo())
|
||||
}
|
||||
|
||||
bin, err := proto.Marshal(rsp)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal GetGachaInfoScRsp: %v", err)
|
||||
}
|
||||
|
||||
// Verify that tag 15 (gacha_info_list) is present:
|
||||
// Field number 15, wire type 2 (length delimited) -> (15 << 3) | 2 = 122 = 0x7a
|
||||
tag15Found := false
|
||||
for _, b := range bin {
|
||||
if b == 0x7a {
|
||||
tag15Found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !tag15Found {
|
||||
t.Errorf("Tag 15 (0x7a) for gacha_info_list was NOT found in serialized payload!")
|
||||
}
|
||||
|
||||
// Unmarshal back to ensure clean round-trip
|
||||
var decoded pb.GetGachaInfoScRsp
|
||||
if err := proto.Unmarshal(bin, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
if len(decoded.GachaInfoList) != len(banners) {
|
||||
t.Fatalf("expected %d banners, got %d", len(banners), len(decoded.GachaInfoList))
|
||||
}
|
||||
|
||||
// Verify banner 1001
|
||||
var found1001 *pb.GachaInfo
|
||||
for _, info := range decoded.GachaInfoList {
|
||||
if info.GachaId == 1001 {
|
||||
found1001 = info
|
||||
break
|
||||
}
|
||||
}
|
||||
if found1001 == nil {
|
||||
t.Fatal("banner 1001 not found in decoded response")
|
||||
}
|
||||
if found1001.EndTime == 0 {
|
||||
t.Errorf("banner 1001 EndTime should not be 0")
|
||||
}
|
||||
if found1001.GachaCeiling == nil {
|
||||
t.Errorf("banner 1001 GachaCeiling should not be nil")
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,8 @@ const heartbeatLua = `local function beta_text()
|
||||
end
|
||||
end
|
||||
beta_text()
|
||||
|
||||
|
||||
`
|
||||
|
||||
func luaQuotedString(value string) string {
|
||||
|
||||
@@ -74,7 +74,9 @@ func sceneOfEntryForTeleport(res *common.JsonConfig, entryID, floorID, teleportI
|
||||
want := fmt.Sprintf("_F%d", floorID)
|
||||
for _, name := range names {
|
||||
cfg := configs[name]
|
||||
if len(name) > len(want) && name[len(name)-len(want):] == want && sceneHasTeleport(cfg, teleportID) {
|
||||
// The requested floor is authoritative even when its teleport is
|
||||
// present only in teleports.json. IDs may repeat on other floors.
|
||||
if len(name) > len(want) && name[len(name)-len(want):] == want {
|
||||
return name, cfg, true
|
||||
}
|
||||
}
|
||||
@@ -87,14 +89,6 @@ func sceneOfEntryForTeleport(res *common.JsonConfig, entryID, floorID, teleportI
|
||||
}
|
||||
}
|
||||
}
|
||||
if floorID != 0 {
|
||||
want := fmt.Sprintf("_F%d", floorID)
|
||||
for _, name := range names {
|
||||
if len(name) > len(want) && name[len(name)-len(want):] == want {
|
||||
return name, configs[name], true
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstSceneOfEntry(res, entryID)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"gac-hkrpg-go/internal/common"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSceneOfEntryForTeleportKeepsRequestedFloorWhenPointIsExternal(t *testing.T) {
|
||||
res := &common.JsonConfig{LevelOutputConfigs: map[uint32]map[string]*common.LevelOutputConfig{
|
||||
1: {
|
||||
"P1_F10": {Scenes: map[uint32]common.SceneData{1: {Teleports: map[uint32]common.TeleportInfo{7: {}}}}},
|
||||
"P1_F20": {Scenes: map[uint32]common.SceneData{}},
|
||||
},
|
||||
}}
|
||||
name, _, ok := sceneOfEntryForTeleport(res, 1, 20, 7)
|
||||
if !ok || name != "P1_F20" {
|
||||
t.Fatalf("requested floor lost: %q, %t", name, ok)
|
||||
}
|
||||
name, _, ok = sceneOfEntryForTeleport(res, 1, 0, 7)
|
||||
if !ok || name != "P1_F10" {
|
||||
t.Fatalf("floor-less lookup failed: %q, %t", name, ok)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@ package gameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gac-hkrpg-go/internal/testutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testWorkspaceRoot string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
_, source, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
@@ -15,11 +18,22 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(2)
|
||||
}
|
||||
root := filepath.Clean(filepath.Join(filepath.Dir(source), "..", ".."))
|
||||
if err := os.Chdir(root); err != nil {
|
||||
temporary, err := os.MkdirTemp("", "gac-gameserver-tests-")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := testutil.CopyRuntime(root, temporary); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
testWorkspaceRoot = temporary
|
||||
if err := os.Chdir(temporary); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot change to repository root %s: %v\n", root, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
code := m.Run()
|
||||
_ = os.Chdir(root)
|
||||
_ = os.RemoveAll(temporary)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func chdirRepositoryRoot(t *testing.T) {
|
||||
@@ -32,5 +46,5 @@ func chdirRepositoryRoot(t *testing.T) {
|
||||
if _, err := os.Stat(filepath.Join(root, "go.mod")); err != nil {
|
||||
t.Fatalf("cannot locate repository root from %s: %v", source, err)
|
||||
}
|
||||
t.Chdir(root)
|
||||
t.Chdir(testWorkspaceRoot)
|
||||
}
|
||||
Reference in new issue
Block a user