Skip to content

Commit 7ed7cf6

Browse files
authored
Improving Game Center Error Clarity (#19)
# Refactor GameCenter Error Handling [Apple Docs](https://developer.apple.com/documentation/gamekit/gkerror) Replaced generic error strings with a structured `GKError` object across all GameCenter modules to provide robust, code-based error handling in Godot. ## Changes - **New `GKError ` Class**: Wraps `GKError` with a `code` (Int), `message` (String), and `domain` (String). - **New `Code` Enum**: Exposes `GKError` codes as UPPER_CASE constants (0-41) matching Godot conventions (e.g., `GAME_UNRECOGNIZED`, `NOT_AUTHENTICATED`). - **Refactored Modules**: Updated `GKLocalPlayer`, `GKSavedGame`, `GKAchievement`, `GKLeaderboard`, and `GKMatchMakerViewController` to use the new error mapping. - **Documentation**: Added `GKError.xml` and updated existing docs to reference the new error type.
1 parent 571347b commit 7ed7cf6

13 files changed

+428
-128
lines changed

Sources/GodotApplePlugins/GameCenter/GKAchievement.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
// Created by Miguel de Icaza on 11/15/25.
66
//
77

8+
import GameKit
89
@preconcurrency import SwiftGodotRuntime
910
import SwiftUI
11+
1012
#if canImport(UIKit)
11-
import UIKit
13+
import UIKit
1214
#else
13-
import AppKit
15+
import AppKit
1416
#endif
1517

16-
import GameKit
17-
1818
@Godot
1919
class GKAchievement: RefCounted, @unchecked Sendable {
2020
var achievement: GameKit.GKAchievement = GameKit.GKAchievement()
@@ -63,15 +63,15 @@ class GKAchievement: RefCounted, @unchecked Sendable {
6363
}
6464
}
6565
GameKit.GKAchievement.report(array) { error in
66-
_ = callback.call(mapError(error))
66+
_ = callback.call(GKError.from(error))
6767
}
6868
}
6969

7070
/// The callback is invoked with nil on success, or a string with a description of the error
7171
@Callable
7272
static func reset_achievements(callback: Callable) {
7373
GameKit.GKAchievement.resetAchievements { error in
74-
_ = callback.call(mapError(error))
74+
_ = callback.call(GKError.from(error))
7575
}
7676
}
7777

@@ -88,14 +88,15 @@ class GKAchievement: RefCounted, @unchecked Sendable {
8888
res.append(ad)
8989
}
9090
}
91-
_ = callback.call(Variant(res), mapError(error))
91+
_ = callback.call(Variant(res), GKError.from(error))
9292
}
9393
}
9494
}
9595

9696
@Godot
9797
class GKAchievementDescription: RefCounted, @unchecked Sendable {
98-
var achievementDescription: GameKit.GKAchievementDescription = GameKit.GKAchievementDescription()
98+
var achievementDescription: GameKit.GKAchievementDescription =
99+
GameKit.GKAchievementDescription()
99100

100101
convenience init(_ ad: GameKit.GKAchievementDescription) {
101102
self.init()
@@ -125,7 +126,7 @@ class GKAchievementDescription: RefCounted, @unchecked Sendable {
125126
func load_image(callback: Callable) {
126127
achievementDescription.loadImage { image, error in
127128
if let error {
128-
_ = callback.call(nil, mapError(error))
129+
_ = callback.call(nil, GKError.from(error))
129130
} else if let image, let godotImage = image.asGodotImage() {
130131
_ = callback.call(godotImage, nil)
131132
} else {
@@ -138,7 +139,8 @@ class GKAchievementDescription: RefCounted, @unchecked Sendable {
138139
/// either one can be nil.
139140
@Callable
140141
static func load_achievement_descriptions(callback: Callable) {
141-
GameKit.GKAchievementDescription.loadAchievementDescriptions { achievementDescriptions, error in
142+
GameKit.GKAchievementDescription.loadAchievementDescriptions {
143+
achievementDescriptions, error in
142144
let res = TypedArray<GKAchievementDescription?>()
143145

144146
if let achievementDescriptions {
@@ -147,7 +149,7 @@ class GKAchievementDescription: RefCounted, @unchecked Sendable {
147149
res.append(ad)
148150
}
149151
}
150-
_ = callback.call(Variant(res), mapError(error))
152+
_ = callback.call(Variant(res), GKError.from(error))
151153
}
152154
}
153155
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
//
2+
// GKError.swift
3+
// GodotApplePlugins
4+
//
5+
//
6+
7+
import GameKit
8+
@preconcurrency import SwiftGodotRuntime
9+
10+
@Godot
11+
public class GKError: RefCounted, @unchecked Sendable {
12+
@Export var code: Int = 0
13+
@Export var message: String = ""
14+
@Export var domain: String = ""
15+
16+
public enum Code: Int, CaseIterable {
17+
// Configuration Errors
18+
case GAME_UNRECOGNIZED
19+
case NOT_SUPPORTED
20+
case APP_UNLISTED
21+
22+
// Communication Errors
23+
case UNKNOWN
24+
case CANCELLED
25+
case COMMUNICATIONS_FAILURE
26+
case INVALID_PLAYER
27+
case INVALID_PARAMETER
28+
case GAME_SESSION_REQUEST_INVALID
29+
case API_NOT_AVAILABLE
30+
case CONNECTION_TIMEOUT
31+
case API_OBSOLETE
32+
33+
// Player-Related Errors
34+
case USER_DENIED
35+
case INVALID_CREDENTIALS
36+
case NOT_AUTHENTICATED
37+
case AUTHENTICATION_IN_PROGRESS
38+
case PARENTAL_CONTROLS_BLOCKED
39+
case PLAYER_STATUS_EXCEEDS_MAXIMUM_LENGTH
40+
case PLAYER_STATUS_INVALID
41+
case UNDERAGE
42+
case PLAYER_PHOTO_FAILURE
43+
case UBIQUITY_CONTAINER_UNAVAILABLE
44+
case NOT_AUTHORIZED
45+
case ICLOUD_UNAVAILABLE
46+
case LOCKDOWN_MODE
47+
48+
// Friend List Errors
49+
case FRIEND_LIST_DESCRIPTION_MISSING
50+
case FRIEND_LIST_RESTRICTED
51+
case FRIEND_LIST_DENIED
52+
case FRIEND_REQUEST_NOT_AVAILABLE
53+
54+
// Matchmaking Errors
55+
case MATCH_REQUEST_INVALID
56+
case UNEXPECTED_CONNECTION
57+
case INVITATIONS_DISABLED
58+
case MATCH_NOT_CONNECTED
59+
case RESTRICTED_TO_AUTOMATCH
60+
61+
// Turn-Based Game Errors
62+
case TURN_BASED_MATCH_DATA_TOO_LARGE
63+
case TURN_BASED_TOO_MANY_SESSIONS
64+
case TURN_BASED_INVALID_PARTICIPANT
65+
case TURN_BASED_INVALID_TURN
66+
case TURN_BASED_INVALID_STATE
67+
68+
// Leaderboard Errors
69+
case SCORE_NOT_SET
70+
71+
// Challenges Errors
72+
case CHALLENGE_INVALID // Deprecated
73+
74+
// Enumeration Cases
75+
case DEBUG_MODE
76+
}
77+
78+
convenience init(error: Error) {
79+
self.init()
80+
self.message = error.localizedDescription
81+
self.domain = (error as NSError).domain
82+
self.code = Self.mapCode(error)
83+
}
84+
85+
static func from(_ error: Error?) -> Variant? {
86+
guard let error else { return nil }
87+
return Variant(GKError(error: error))
88+
}
89+
90+
static func mapCode(_ error: Error) -> Int {
91+
if let gkError = error as? GameKit.GKError {
92+
switch gkError.code {
93+
// Configuration Errors
94+
case .gameUnrecognized: return Code.GAME_UNRECOGNIZED.rawValue
95+
case .notSupported: return Code.NOT_SUPPORTED.rawValue
96+
case .appUnlisted: return Code.APP_UNLISTED.rawValue
97+
98+
// Communication Errors
99+
case .unknown: return Code.UNKNOWN.rawValue
100+
case .cancelled: return Code.CANCELLED.rawValue
101+
case .communicationsFailure: return Code.COMMUNICATIONS_FAILURE.rawValue
102+
case .invalidPlayer: return Code.INVALID_PLAYER.rawValue
103+
case .invalidParameter: return Code.INVALID_PARAMETER.rawValue
104+
case .gameSessionRequestInvalid: return Code.GAME_SESSION_REQUEST_INVALID.rawValue
105+
case .apiNotAvailable: return Code.API_NOT_AVAILABLE.rawValue
106+
case .connectionTimeout: return Code.CONNECTION_TIMEOUT.rawValue
107+
case .apiObsolete: return Code.API_OBSOLETE.rawValue
108+
109+
// Player-Related Errors
110+
case .userDenied: return Code.USER_DENIED.rawValue
111+
case .invalidCredentials: return Code.INVALID_CREDENTIALS.rawValue
112+
case .notAuthenticated: return Code.NOT_AUTHENTICATED.rawValue
113+
case .authenticationInProgress: return Code.AUTHENTICATION_IN_PROGRESS.rawValue
114+
case .parentalControlsBlocked: return Code.PARENTAL_CONTROLS_BLOCKED.rawValue
115+
case .playerStatusExceedsMaximumLength:
116+
return Code.PLAYER_STATUS_EXCEEDS_MAXIMUM_LENGTH.rawValue
117+
case .playerStatusInvalid: return Code.PLAYER_STATUS_INVALID.rawValue
118+
case .underage: return Code.UNDERAGE.rawValue
119+
case .playerPhotoFailure: return Code.PLAYER_PHOTO_FAILURE.rawValue
120+
case .ubiquityContainerUnavailable: return Code.UBIQUITY_CONTAINER_UNAVAILABLE.rawValue
121+
case .notAuthorized: return Code.NOT_AUTHORIZED.rawValue
122+
case .iCloudUnavailable: return Code.ICLOUD_UNAVAILABLE.rawValue
123+
case .lockdownMode: return Code.LOCKDOWN_MODE.rawValue
124+
125+
// Friend List Errors
126+
case .friendListDescriptionMissing: return Code.FRIEND_LIST_DESCRIPTION_MISSING.rawValue
127+
case .friendListRestricted: return Code.FRIEND_LIST_RESTRICTED.rawValue
128+
case .friendListDenied: return Code.FRIEND_LIST_DENIED.rawValue
129+
case .friendRequestNotAvailable: return Code.FRIEND_REQUEST_NOT_AVAILABLE.rawValue
130+
131+
// Matchmaking Errors
132+
case .matchRequestInvalid: return Code.MATCH_REQUEST_INVALID.rawValue
133+
case .unexpectedConnection: return Code.UNEXPECTED_CONNECTION.rawValue
134+
case .invitationsDisabled: return Code.INVITATIONS_DISABLED.rawValue
135+
case .matchNotConnected: return Code.MATCH_NOT_CONNECTED.rawValue
136+
case .restrictedToAutomatch: return Code.RESTRICTED_TO_AUTOMATCH.rawValue
137+
138+
// Turn-Based Game Errors
139+
case .turnBasedMatchDataTooLarge: return Code.TURN_BASED_MATCH_DATA_TOO_LARGE.rawValue
140+
case .turnBasedTooManySessions: return Code.TURN_BASED_TOO_MANY_SESSIONS.rawValue
141+
case .turnBasedInvalidParticipant: return Code.TURN_BASED_INVALID_PARTICIPANT.rawValue
142+
case .turnBasedInvalidTurn: return Code.TURN_BASED_INVALID_TURN.rawValue
143+
case .turnBasedInvalidState: return Code.TURN_BASED_INVALID_STATE.rawValue
144+
145+
// Leaderboard Errors
146+
case .scoreNotSet: return Code.SCORE_NOT_SET.rawValue
147+
148+
// Challenges Errors
149+
case .challengeInvalid: return Code.CHALLENGE_INVALID.rawValue // Deprecated
150+
151+
// Enumeration Cases
152+
case .debugMode: return Code.DEBUG_MODE.rawValue
153+
154+
@unknown default: return Code.UNKNOWN.rawValue
155+
}
156+
}
157+
return Code.UNKNOWN.rawValue
158+
}
159+
}

Sources/GodotApplePlugins/GameCenter/GKLeaderboard.swift

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import GameKit
12
//
23
// AppleLeaderboard.swift
34
// GodotApplePlugins
@@ -6,14 +7,13 @@
67
//
78
@preconcurrency import SwiftGodotRuntime
89
import SwiftUI
10+
911
#if canImport(UIKit)
10-
import UIKit
12+
import UIKit
1113
#else
12-
import AppKit
14+
import AppKit
1315
#endif
1416

15-
import GameKit
16-
1717
@Godot
1818
class GKLeaderboard: RefCounted, @unchecked Sendable {
1919
var board: GameKit.GKLeaderboard = GameKit.GKLeaderboard()
@@ -95,13 +95,7 @@ class GKLeaderboard: RefCounted, @unchecked Sendable {
9595
/// Callback is invoked with nil on success, or a string on error
9696
func submit_score(score: Int, context: Int, player: GKPlayer, callback: Callable) {
9797
board.submitScore(score, context: context, player: player.player) { error in
98-
let result: Variant?
99-
if let error {
100-
result = Variant(error.localizedDescription)
101-
} else {
102-
result = nil
103-
}
104-
_ = callback.call(result)
98+
_ = callback.call(GKError.from(error))
10599
}
106100
}
107101

@@ -114,7 +108,7 @@ class GKLeaderboard: RefCounted, @unchecked Sendable {
114108
if let image, let godotImage = image.asGodotImage() {
115109
_ = callback.call(godotImage, nil)
116110
} else if let error {
117-
_ = callback.call(nil, Variant(error.localizedDescription))
111+
_ = callback.call(nil, GKError.from(error))
118112
} else {
119113
_ = callback.call(nil, Variant("Could not load leaderboard image"))
120114
}
@@ -143,7 +137,7 @@ class GKLeaderboard: RefCounted, @unchecked Sendable {
143137
wrapped.append(wrap)
144138
}
145139
}
146-
_ = callback.call(Variant(wrapped), error != nil ? Variant(String(describing: error)) : nil)
140+
_ = callback.call(Variant(wrapped), GKError.from(error))
147141
}
148142
}
149143

@@ -172,9 +166,9 @@ class GKLeaderboard: RefCounted, @unchecked Sendable {
172166
re = nil
173167
}
174168
if let range {
175-
_ = callback.call(Variant(le), re, Variant(range), mapError(error))
169+
_ = callback.call(Variant(le), re, Variant(range), GKError.from(error))
176170
} else {
177-
_ = callback.call(Variant(le), re, mapError(error))
171+
_ = callback.call(Variant(le), re, GKError.from(error))
178172
}
179173
}
180174

@@ -185,23 +179,33 @@ class GKLeaderboard: RefCounted, @unchecked Sendable {
185179
/// - Scores for the specified players
186180
/// - Error if not nil
187181
@Callable
188-
func load_entries(players: VariantArray, timeScope: GKLeaderboard.TimeScope, callback: Callable) {
182+
func load_entries(players: VariantArray, timeScope: GKLeaderboard.TimeScope, callback: Callable)
183+
{
189184
var gkPlayers: [GameKit.GKPlayer] = []
190185
for p in players {
191186
guard let p, let po = p.asObject(GKPlayer.self) else { continue }
192187
gkPlayers.append(po.player)
193188
}
194189

195-
board.loadEntries(for: gkPlayers, timeScope: timeScope.toGameKit()) { local, requested, error in
196-
self.processEntries(callback: callback, local: local, requested: requested, error: error)
190+
board.loadEntries(for: gkPlayers, timeScope: timeScope.toGameKit()) {
191+
local, requested, error in
192+
self.processEntries(
193+
callback: callback, local: local, requested: requested, error: error)
197194
}
198195
}
199196

200197
@Callable
201-
func load_local_player_entries(playerScope: GKLeaderboard.PlayerScope, timeScope: GKLeaderboard.TimeScope, rangeStart: Int, rangeLenght: Int, callback: Callable) {
202-
let range = NSRange(location: rangeStart, length: rangeLenght)
203-
board.loadEntries(for: playerScope.toGameKit(), timeScope: timeScope.toGameKit(), range: range) { localPlayerEntry, entries, totalPlayerCount, error in
204-
self.processEntries(callback: callback, local: localPlayerEntry, requested: entries, range: totalPlayerCount, error: error)
198+
func load_local_player_entries(
199+
playerScope: GKLeaderboard.PlayerScope, timeScope: GKLeaderboard.TimeScope, rangeStart: Int,
200+
rangeLength: Int, callback: Callable
201+
) {
202+
let range = NSRange(location: rangeStart, length: rangeLength)
203+
board.loadEntries(
204+
for: playerScope.toGameKit(), timeScope: timeScope.toGameKit(), range: range
205+
) { localPlayerEntry, entries, totalPlayerCount, error in
206+
self.processEntries(
207+
callback: callback, local: localPlayerEntry, requested: entries,
208+
range: totalPlayerCount, error: error)
205209
}
206210
}
207211
}

0 commit comments

Comments
 (0)