package gameserver import ( "os" "path/filepath" "strconv" "strings" "testing" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) func TestCurrencyWarsEntranceUsesCapturedRoomConditionWithoutMutatingScene(t *testing.T) { scene := &common.LevelOutputConfig{SavedValues: map[string]int32{ "FSV_370GridFight": 0, "unrelated": 7, }} group := common.CurrencyWarsRoomGroup{GroupID: 1} group.Condition.Name = "FSV_370GridFight" group.Condition.Comparison = "GreaterEqual" group.Condition.Value = 1 entrance := common.CurrencyWarsEntrance{ EntryID: 2, RoomGroups: []common.CurrencyWarsRoomGroup{group}, SavedValues: map[string]int32{"FSV_370GridFight": 2}, } got, err := currencyWarsEntranceScene(scene, entrance) if err != nil { t.Fatalf("apply captured CW entrance profile: %v", err) } if got.SavedValues["FSV_370GridFight"] != 2 || got.SavedValues["unrelated"] != 7 { t.Fatalf("CW entrance profile = %v; want selected and unrelated values preserved", got.SavedValues) } if scene.SavedValues["FSV_370GridFight"] != 0 { t.Fatal("applying CW freeplay profile mutated shared scene data") } got.SavedValues["FSV_370GridFight"] = 99 if scene.SavedValues["FSV_370GridFight"] != 0 { t.Fatal("returned CW scene shares its saved-value map with the original") } } func TestUnimplementedCurrencyWarsRequestsRemainSuppressed(t *testing.T) { for _, name := range []string{ "GridFightEnterBattleStageCsReq", "GridFightRefreshShopCsReq", } { t.Run(name, func(t *testing.T) { id, ok := goproto.CmdIDByName[name] if !ok { t.Fatalf("protocol command table is missing %s", name) } if !isDangerCmd(uint16(id)) { t.Fatalf("unsupported CW request %s is not protected from an empty response", name) } if typedHandlers[uint16(id)] != nil || rawHandlers[uint16(id)] != nil { t.Fatalf("unsupported CW request %s unexpectedly has a handler", name) } if _, ok := dummyResponses[uint16(id)]; ok { t.Fatalf("unsupported CW request %s unexpectedly has a dummy response", name) } if _, ok := goproto.RspIDByReqID[uint32(id)]; !ok { t.Fatalf("test request %s has no paired response to protect", name) } }) } getDataID, ok := goproto.CmdIDByName["GridFightGetDataCsReq"] if !ok || typedHandlers[uint16(getDataID)] == nil { t.Fatal("the verified CW bootstrap request must keep its typed handler") } } func TestCurrencyWarsStartGameplayLogsRequestedModeWithoutReply(t *testing.T) { id := uint16(goproto.CmdIDByName["GridFightStartGamePlayCsReq"]) handler := rawHandlers[id] if handler == nil { t.Fatal("CW start request must have a mode-observation handler") } if typedHandlers[id] != nil { t.Fatal("CW start request must not send an unverified typed response") } if _, ok := dummyResponses[id]; ok { t.Fatal("CW start request must not receive an empty response") } if !isDangerCmd(id) { t.Fatal("CW start request must remain protected from dispatcher auto-replies") } if err := handler(nil, nil); err == nil { t.Fatal("empty start request must not be misreported as Standard Gambit") } oldWD, err := os.Getwd() if err != nil { t.Fatal(err) } if err := os.Chdir(t.TempDir()); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chdir(oldWD) }) for _, tc := range []struct { name string overclock bool wantMode string divisionID uint32 }{ {name: "standard", wantMode: "standard", divisionID: 42}, {name: "overclock", overclock: true, wantMode: "overclock", divisionID: 42}, } { t.Run(tc.name, func(t *testing.T) { req := &goproto.GridFightStartGamePlayCsReq{DivisionId: tc.divisionID, IsOverlock: tc.overclock} payload, err := proto.Marshal(req) if err != nil { t.Fatal(err) } // A nil session makes any accidental attempt to send a response fail. if err := handler(nil, payload); err != nil { t.Fatalf("observe mode: %v", err) } logData, err := os.ReadFile(filepath.Join("gameserver.log")) if err != nil { t.Fatal(err) } log := string(logData) if !strings.Contains(log, "CW_START_GAMEPLAY observed mode="+tc.wantMode+" division="+strconv.FormatUint(uint64(tc.divisionID), 10)) || !strings.Contains(log, "response_suppressed=true reason=run_state_unverified") { t.Fatalf("mode diagnostic missing or malformed: %s", log) } }) } } func TestCurrencyWarsEntranceRejectsUnsupportedOrUnmetConditions(t *testing.T) { tests := []struct { name string value int32 comparison string wantErr string }{ {name: "condition unmet", value: 0, comparison: "GreaterEqual", wantErr: "condition is not satisfied"}, {name: "unsupported comparison", value: 2, comparison: "Equal", wantErr: "unsupported CW room comparison"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { scene := &common.LevelOutputConfig{SavedValues: map[string]int32{"condition": 0}} group := common.CurrencyWarsRoomGroup{GroupID: 1} group.Condition.Name = "condition" group.Condition.Comparison = tt.comparison group.Condition.Value = 1 entrance := common.CurrencyWarsEntrance{ RoomGroups: []common.CurrencyWarsRoomGroup{group}, SavedValues: map[string]int32{"condition": tt.value}, } if _, err := currencyWarsEntranceScene(scene, entrance); err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("entrance error = %v; want %q", err, tt.wantErr) } if scene.SavedValues["condition"] != 0 { t.Fatal("rejected CW profile mutated shared scene data") } }) } }