mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-14 20:42:02 +00:00
fix(multiplayer): validate ranked arena paths durably
This commit is contained in:
@@ -49,6 +49,18 @@ func RankedArenaForProposal(proposalID string) RankedArena {
|
||||
return rankedArenas[rankedArenaOrder[int(digest[0])%len(rankedArenaOrder)]]
|
||||
}
|
||||
|
||||
// IsRankedArenaPath is the durable-store boundary for arena paths. Proposal
|
||||
// and allocation records must not accept a merely non-empty caller supplied
|
||||
// scene path, even when the caller bypasses matcher formation.
|
||||
func IsRankedArenaPath(path string) bool {
|
||||
for _, arena := range rankedArenas {
|
||||
if arena.Path == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ValidateRankedAdmission(participants []RankedParticipant, arena RankedArena) error {
|
||||
if len(participants) != 6 || !validRankedArena(arena) {
|
||||
return fmt.Errorf("ranked admission requirements not met")
|
||||
|
||||
@@ -48,6 +48,19 @@ func TestRankedArenaRegistryExcludesElevatedVariants(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRankedArenaPathOnlyAllowsFloorGoalRegistry(t *testing.T) {
|
||||
for _, path := range []string{"res://scenes/arena_01.tscn", "res://scenes/arena_02.tscn", "res://scenes/arena_03.tscn"} {
|
||||
if !IsRankedArenaPath(path) {
|
||||
t.Fatalf("eligible path %q rejected", path)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{"", "res://scenes/arena_01_elevated.tscn", "res://forged.tscn"} {
|
||||
if IsRankedArenaPath(path) {
|
||||
t.Fatalf("ineligible path %q accepted", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRankedArenaForProposalIsStableAndRotatesEligibleRegistry(t *testing.T) {
|
||||
first := RankedArenaForProposal("proposal-stable")
|
||||
if first != RankedArenaForProposal("proposal-stable") {
|
||||
|
||||
@@ -234,8 +234,8 @@ func ClaimAllocatingMatch(ctx context.Context, db *sql.DB, transport string, now
|
||||
if build == "" {
|
||||
return fmt.Errorf("allocating match has no participants")
|
||||
}
|
||||
if domain.Playlist(playlist) == domain.Ranked && !arenaPath.Valid {
|
||||
return fmt.Errorf("ranked allocating match has no arena")
|
||||
if domain.Playlist(playlist) == domain.Ranked && (!arenaPath.Valid || !domain.IsRankedArenaPath(arenaPath.String)) {
|
||||
return fmt.Errorf("ranked allocating match has invalid arena")
|
||||
}
|
||||
item.Request = domain.AllocationRequest{AllocationID: claimedID, MatchID: matchID, Playlist: domain.Playlist(playlist), Region: region, Build: build, Protocol: protocol, ArenaPath: arenaPath.String, Transport: transport}
|
||||
found = true
|
||||
|
||||
@@ -120,8 +120,8 @@ func CreateMatchFromAcceptedProposal(ctx context.Context, db *sql.DB, plan Accep
|
||||
if !validAcceptedPlaylistCount(domain.Playlist(playlist), len(plan.Players)) {
|
||||
return fmt.Errorf("accepted proposal playlist does not match player count")
|
||||
}
|
||||
if domain.Playlist(playlist) == domain.Ranked && plan.ArenaPath == "" {
|
||||
return fmt.Errorf("ranked accepted match plan has no arena")
|
||||
if domain.Playlist(playlist) == domain.Ranked && !domain.IsRankedArenaPath(plan.ArenaPath) {
|
||||
return fmt.Errorf("ranked accepted match plan has invalid arena")
|
||||
}
|
||||
participants, err := acceptedProposalParticipants(ctx, tx, plan)
|
||||
if err != nil {
|
||||
|
||||
@@ -75,7 +75,7 @@ func validProposalMatchPlan(proposal domain.Proposal) bool {
|
||||
if (proposal.Region != "EU" && proposal.Region != "NA") || proposal.Protocol < 1 {
|
||||
return false
|
||||
}
|
||||
if proposal.Playlist == domain.Ranked && proposal.ArenaPath == "" {
|
||||
if proposal.Playlist == domain.Ranked && !domain.IsRankedArenaPath(proposal.ArenaPath) {
|
||||
return false
|
||||
}
|
||||
seenSlots := make(map[int]struct{}, len(proposal.Participants))
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cosmic-clash/cosmic-clash/server/domain"
|
||||
)
|
||||
|
||||
func TestRankedProposalMatchPlanRequiresRegisteredArenaPath(t *testing.T) {
|
||||
proposal := domain.Proposal{
|
||||
Playlist: domain.Ranked,
|
||||
Region: "EU",
|
||||
Protocol: 1,
|
||||
ArenaPath: "res://scenes/arena_01.tscn",
|
||||
Participants: []domain.ProposalParticipant{
|
||||
{PlayerID: "player-a", Team: 0, Slot: 0},
|
||||
{PlayerID: "player-b", Team: 1, Slot: 3},
|
||||
},
|
||||
}
|
||||
if !validProposalMatchPlan(proposal) {
|
||||
t.Fatal("registered ranked arena rejected")
|
||||
}
|
||||
for _, path := range []string{"", "res://scenes/arena_01_elevated.tscn", "res://forged.tscn"} {
|
||||
proposal.ArenaPath = path
|
||||
if validProposalMatchPlan(proposal) {
|
||||
t.Fatalf("ranked path %q accepted", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user