fix(multiplayer): validate ranked arena paths durably

This commit is contained in:
Josh Creek
2026-09-01 21:07:56 +01:00
parent ff80ab46b7
commit bd617dbf06
8 changed files with 62 additions and 7 deletions
+12
View File
@@ -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")
+13
View File
@@ -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") {
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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))
+29
View File
@@ -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)
}
}
}