Swift bindings to the Playdate C API

Wraps all ten subsystems of the Playdate SDK 3.1.1 C API (system, display,
graphics, sprites, sound, file, JSON, Lua, scoreboards, network) in
idiomatic Swift: namespaced APIs, wrapper types with ownership semantics,
closures for callbacks, and typed throws. Written within the Embedded Swift
subset so the same code can target the device.

The SDK headers are resolved through a "playdate" pkg-config module
(Scripts/install-pkgconfig.sh), so the package works as a normal SwiftPM
dependency without unsafe build flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 10:33:45 +02:00
co-authored by Claude Fable 5
commit 09760f99b0
29 changed files with 6533 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
//
// Scoreboards.swift
// Wraps `playdate->scoreboards` (pd_api_scoreboards.h).
//
// The C callbacks carry no userdata, so one completion per operation kind
// is tracked at a time; starting a second request of the same kind before
// the first completes replaces the stored completion.
//
internal import CPlaydate
private var scoreboardsAPI: playdate_scoreboards { Playdate.api.scoreboards.pointee }
extension Playdate {
/// The scoreboards API for games with online leaderboards.
public enum Scoreboards {}
}
extension Playdate.Scoreboards {
/// A score on a board.
public struct Score {
public let rank: UInt32
public let value: UInt32
public let player: String
public let boardID: String?
init(_ score: PDScore) {
rank = score.rank
value = score.value
player = String(playdateCString: score.player) ?? ""
boardID = String(playdateCString: score.boardID)
}
init(_ score: PDListScore, boardID: String?) {
rank = score.rank
value = score.value
player = String(playdateCString: score.player) ?? ""
self.boardID = boardID
}
}
/// The scores on a board.
public struct ScoresList {
public let boardID: String
public let lastUpdated: UInt32
public let playerIncluded: Bool
public let limit: UInt32
public let scores: [Score]
init(_ list: PDScoresList) {
boardID = String(playdateCString: list.boardID) ?? ""
lastUpdated = list.lastUpdated
playerIncluded = list.playerIncluded != 0
limit = list.limit
var scores = [Score]()
if let entries = list.scores {
scores.reserveCapacity(Int(list.count))
for index in 0..<Int(list.count) {
scores.append(Score(entries[index], boardID: boardID))
}
}
self.scores = scores
}
}
/// A board belonging to the game.
public struct Board {
public let boardID: String
public let name: String
init(_ board: PDBoard) {
boardID = String(playdateCString: board.boardID) ?? ""
name = String(playdateCString: board.name) ?? ""
}
}
/// The game's boards.
public struct BoardsList {
public let lastUpdated: UInt32
public let boards: [Board]
init(_ list: PDBoardsList) {
lastUpdated = list.lastUpdated
var boards = [Board]()
if let entries = list.boards {
boards.reserveCapacity(Int(list.count))
for index in 0..<Int(list.count) {
boards.append(Board(entries[index]))
}
}
self.boards = boards
}
}
nonisolated(unsafe) private static var addScoreCompletion: ((Result<Score, Playdate.Error>) -> Void)?
nonisolated(unsafe) private static var personalBestCompletion: ((Result<Score, Playdate.Error>) -> Void)?
nonisolated(unsafe) private static var boardsCompletion: ((Result<BoardsList, Playdate.Error>) -> Void)?
nonisolated(unsafe) private static var scoresCompletion: ((Result<ScoresList, Playdate.Error>) -> Void)?
/// Submits a score to the board. Returns `false` if the request could
/// not be started.
@discardableResult
public static func addScore(boardID: String, value: UInt32,
completion: @escaping (Result<Score, Playdate.Error>) -> Void) -> Bool {
addScoreCompletion = completion
return boardID.withPlaydateCString { cBoardID in
scoreboardsAPI.addScore.unsafelyUnwrapped(cBoardID, value, { score, errorMessage in
let completion = Playdate.Scoreboards.addScoreCompletion
Playdate.Scoreboards.addScoreCompletion = nil
completion?(Playdate.Scoreboards.result(score, errorMessage))
}) != 0
}
}
/// Fetches the current player's best score on the board.
@discardableResult
public static func getPersonalBest(boardID: String,
completion: @escaping (Result<Score, Playdate.Error>) -> Void) -> Bool {
personalBestCompletion = completion
return boardID.withPlaydateCString { cBoardID in
scoreboardsAPI.getPersonalBest.unsafelyUnwrapped(cBoardID, { score, errorMessage in
let completion = Playdate.Scoreboards.personalBestCompletion
Playdate.Scoreboards.personalBestCompletion = nil
completion?(Playdate.Scoreboards.result(score, errorMessage))
}) != 0
}
}
/// Fetches the list of the game's boards.
@discardableResult
public static func getScoreboards(completion: @escaping (Result<BoardsList, Playdate.Error>) -> Void) -> Bool {
boardsCompletion = completion
return scoreboardsAPI.getScoreboards.unsafelyUnwrapped({ boards, errorMessage in
let completion = Playdate.Scoreboards.boardsCompletion
Playdate.Scoreboards.boardsCompletion = nil
guard let boards else {
completion?(.failure(Playdate.Error(cString: errorMessage)))
return
}
let list = BoardsList(boards.pointee)
scoreboardsAPI.freeBoardsList.unsafelyUnwrapped(boards)
completion?(.success(list))
}) != 0
}
/// Fetches the scores on the board.
@discardableResult
public static func getScores(boardID: String,
completion: @escaping (Result<ScoresList, Playdate.Error>) -> Void) -> Bool {
scoresCompletion = completion
return boardID.withPlaydateCString { cBoardID in
scoreboardsAPI.getScores.unsafelyUnwrapped(cBoardID, { scores, errorMessage in
let completion = Playdate.Scoreboards.scoresCompletion
Playdate.Scoreboards.scoresCompletion = nil
guard let scores else {
completion?(.failure(Playdate.Error(cString: errorMessage)))
return
}
let list = ScoresList(scores.pointee)
scoreboardsAPI.freeScoresList.unsafelyUnwrapped(scores)
completion?(.success(list))
}) != 0
}
}
private static func result(_ score: UnsafeMutablePointer<PDScore>?,
_ errorMessage: UnsafePointer<CChar>?) -> Result<Score, Playdate.Error> {
guard let score else {
return .failure(Playdate.Error(cString: errorMessage))
}
let value = Score(score.pointee)
scoreboardsAPI.freeScore.unsafelyUnwrapped(score)
return .success(value)
}
}