Promote subsystem namespaces to the top level of the module

System, Display, Graphics, Sprite, Sound, File, JSON, Lua, Scoreboards,
Network, Rect, SystemEvent, and AccessReply now live at module scope
instead of being nested in the Playdate enum; consumers write
System.log(...) instead of Playdate.System.log(...), and can qualify with
the module name (PlayDate.System) on collision. The Playdate enum remains
only as the raw C API bootstrap (initialize(with:), api, apiPointer), and
Playdate.Error is renamed PlaydateError so no top-level type shadows
Swift.Error.

Breaking change for any existing consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 11:10:25 +02:00
co-authored by Claude Fable 5
parent 3ee071e098
commit 6485997edc
22 changed files with 801 additions and 822 deletions
+70 -67
View File
@@ -15,16 +15,16 @@ All ten C subsystems are covered:
| Namespace | Wraps | Highlights |
|---|---|---|
| `Playdate.System` | `playdate->system` | input, time, menu items, logging |
| `Playdate.Display` | `playdate->display` | refresh rate, scale, mosaic, flip |
| `Playdate.Graphics` | `playdate->graphics` | drawing, `Bitmap`, `Font`, `TileMap`, video |
| `Playdate.Sprite` | `playdate->sprite` | display list, collisions, custom draw |
| `Playdate.Sound` | `playdate->sound` | players, synths, sequences, effects |
| `Playdate.File` | `playdate->file` | `Handle`, directory operations |
| `Playdate.JSON` | `playdate->json` | `Value` tree decode/encode |
| `Playdate.Lua` | `playdate->lua` | C functions, classes, stack access |
| `Playdate.Scoreboards` | `playdate->scoreboards` | online leaderboards |
| `Playdate.Network` | `playdate->network` | wifi, `HTTPConnection`, `TCPConnection` |
| `System` | `playdate->system` | input, time, menu items, logging |
| `Display` | `playdate->display` | refresh rate, scale, mosaic, flip |
| `Graphics` | `playdate->graphics` | drawing, `Bitmap`, `Font`, `TileMap`, video |
| `Sprite` | `playdate->sprite` | display list, collisions, custom draw |
| `Sound` | `playdate->sound` | players, synths, sequences, effects |
| `File` | `playdate->file` | `Handle`, directory operations |
| `JSON` | `playdate->json` | `Value` tree decode/encode |
| `Lua` | `playdate->lua` | C functions, classes, stack access |
| `Scoreboards` | `playdate->scoreboards` | online leaderboards |
| `Network` | `playdate->network` | wifi, `HTTPConnection`, `TCPConnection` |
## Requirements
@@ -82,7 +82,7 @@ import PlayDate
func eventHandler(pointer: UnsafeMutableRawPointer,
event: PDSystemEvent,
argument: UInt32) -> Int32 {
switch Playdate.SystemEvent(event: event, argument: argument) {
switch SystemEvent(event: event, argument: argument) {
case .initialize:
Playdate.initialize(with: pointer) // must happen before anything else
Game.shared.start()
@@ -97,25 +97,25 @@ func eventHandler(pointer: UnsafeMutableRawPointer,
final class Game {
nonisolated(unsafe) static let shared = Game()
var player = Playdate.Sprite()
var player = Sprite()
func start() {
Playdate.Display.setRefreshRate(50)
Display.setRefreshRate(50)
Playdate.System.setUpdateCallback {
System.setUpdateCallback {
self.update()
return true // true = redraw the display this frame
}
}
func update() {
let (_, pushed, _) = Playdate.System.buttonState
let (_, pushed, _) = System.buttonState
if pushed.contains(.a) {
Playdate.System.log("A pressed at \(Playdate.System.currentTimeMilliseconds)ms")
System.log("A pressed at \(System.currentTimeMilliseconds)ms")
}
Playdate.Sprite.updateAndDrawAll()
Playdate.System.drawFPS()
Sprite.updateAndDrawAll()
System.drawFPS()
}
}
```
@@ -130,68 +130,68 @@ a programmer error and will crash.
```swift
// Buttons are an OptionSet: current (held), pushed and released this frame.
let (current, pushed, released) = Playdate.System.buttonState
let (current, pushed, released) = System.buttonState
if current.contains([.b, .down]) { /* charge shot */ }
// Crank.
if !Playdate.System.isCrankDocked {
aim(degrees: Playdate.System.crankAngle)
spin(by: Playdate.System.crankChange)
if !System.isCrankDocked {
aim(degrees: System.crankAngle)
spin(by: System.crankChange)
}
// Accelerometer is a peripheral you enable first.
Playdate.System.setPeripheralsEnabled(.accelerometer)
let (x, y, z) = Playdate.System.accelerometer
System.setPeripheralsEnabled(.accelerometer)
let (x, y, z) = System.accelerometer
// System menu items take closures; the binding keeps them alive until removed.
Playdate.System.addCheckmarkMenuItem(title: "music", isChecked: true) { item in
System.addCheckmarkMenuItem(title: "music", isChecked: true) { item in
Audio.musicEnabled = item.isChecked
}
Playdate.System.addOptionsMenuItem(title: "mode", options: ["easy", "hard"]) { item in
System.addOptionsMenuItem(title: "mode", options: ["easy", "hard"]) { item in
Game.shared.difficulty = item.value
}
// Logging goes to the simulator console or device serial.
Playdate.System.log("spawned \(count) enemies")
Playdate.System.error("unrecoverable") // stops execution
System.log("spawned \(count) enemies")
System.error("unrecoverable") // stops execution
```
### Graphics: drawing, bitmaps, fonts
Fallible loads (`Bitmap(path:)`, `Font(path:)`, …) throw `Playdate.Error`,
Fallible loads (`Bitmap(path:)`, `Font(path:)`, …) throw `PlaydateError`,
which carries the message produced by the OS:
```swift
let font = try Playdate.Graphics.Font(path: "fonts/Asheville-Sans-14-Bold.pft")
Playdate.Graphics.setFont(font)
let font = try Graphics.Font(path: "fonts/Asheville-Sans-14-Bold.pft")
Graphics.setFont(font)
Playdate.Graphics.clear(color: .white)
Playdate.Graphics.fillRect(x: 0, y: 0, width: 400, height: 32, color: .black)
Playdate.Graphics.drawText("Hëllo, Playdate", x: 8, y: 8)
Graphics.clear(color: .white)
Graphics.fillRect(x: 0, y: 0, width: 400, height: 32, color: .black)
Graphics.drawText("Hëllo, Playdate", x: 8, y: 8)
// Colors are solid or 8×8 patterns.
let checker = Playdate.Graphics.Pattern(rows: (0xAA, 0x55, 0xAA, 0x55,
0xAA, 0x55, 0xAA, 0x55))
Playdate.Graphics.fillEllipse(x: 100, y: 100, width: 64, height: 64,
color: .pattern(checker))
let checker = Graphics.Pattern(rows: (0xAA, 0x55, 0xAA, 0x55,
0xAA, 0x55, 0xAA, 0x55))
Graphics.fillEllipse(x: 100, y: 100, width: 64, height: 64,
color: .pattern(checker))
// Bitmaps draw themselves; draw into one by pushing it as the context.
let logo = try Playdate.Graphics.Bitmap(path: "images/logo")
let logo = try Graphics.Bitmap(path: "images/logo")
logo.draw(x: 168, y: 88)
let canvas = Playdate.Graphics.Bitmap(width: 64, height: 64)
Playdate.Graphics.pushContext(canvas)
Playdate.Graphics.drawLine(x1: 0, y1: 0, x2: 63, y2: 63, width: 2, color: .black)
Playdate.Graphics.popContext()
let canvas = Graphics.Bitmap(width: 64, height: 64)
Graphics.pushContext(canvas)
Graphics.drawLine(x1: 0, y1: 0, x2: 63, y2: 63, width: 2, color: .black)
Graphics.popContext()
```
### Sprites and collisions
```swift
let ball = Playdate.Sprite()
ball.setImage(try Playdate.Graphics.Bitmap(path: "images/ball"))
let ball = Sprite()
ball.setImage(try Graphics.Bitmap(path: "images/ball"))
ball.moveTo(x: 200, y: 120)
ball.collideRect = Playdate.Rect(x: 0, y: 0, width: 16, height: 16)
ball.collideRect = Rect(x: 0, y: 0, width: 16, height: 16)
ball.setCollisionResponseFunction { _, _ in .bounce }
ball.add() // adds to the display list; the binding keeps it alive while added
@@ -211,29 +211,29 @@ sprite userdata slot is reserved by the binding for that recovery — use the
```swift
// Stream music from disk.
let music = try Playdate.Sound.FilePlayer(path: "audio/theme")
let music = try Sound.FilePlayer(path: "audio/theme")
music.play(repeat: 0) // 0 = loop forever
// Play short effects from memory.
let blip = try Playdate.Sound.SamplePlayer(path: "audio/blip")
let blip = try Sound.SamplePlayer(path: "audio/blip")
blip.play()
// Synthesis.
let synth = Playdate.Sound.Synth(waveform: .square)
let synth = Sound.Synth(waveform: .square)
synth.setAttackTime(0.01)
synth.setReleaseTime(0.2)
synth.playMIDINote(Playdate.Sound.noteC4, velocity: 0.8, length: 0.5)
synth.playMIDINote(Sound.noteC4, velocity: 0.8, length: 0.5)
// Channels mix sources and effects.
let channel = Playdate.Sound.Channel()
let channel = Sound.Channel()
channel.add()
channel.addSource(synth)
let filter = Playdate.Sound.TwoPoleFilter(kind: .lowPass)
let filter = Sound.TwoPoleFilter(kind: .lowPass)
filter.setFrequency(800)
channel.addEffect(filter)
// Anything that takes a modulator accepts any SignalValue (LFO, Envelope, ).
let wobble = Playdate.Sound.LFO(shape: .sine)
let wobble = Sound.LFO(shape: .sine)
wobble.setRate(2)
synth.frequencyModulator = wobble
```
@@ -242,20 +242,20 @@ synth.frequencyModulator = wobble
```swift
// Paths resolve against the game's Data directory and pdx per the open mode.
let save = try Playdate.File.Handle(path: "save.json", mode: .write)
try save.write(Playdate.JSON.encode(.table([
let save = try File.Handle(path: "save.json", mode: .write)
try save.write(JSON.encode(.table([
"level": .int(3),
"name": .string("Röck"),
])))
try save.close()
let loaded = try Playdate.JSON.decodeFile(at: "save.json")
let loaded = try JSON.decodeFile(at: "save.json")
if case .table(let entries) = loaded, case .int(let level)? = entries["level"] {
Game.shared.level = level
}
try Playdate.File.listFiles(at: "replays") { name in
Playdate.System.log("found \(name)")
try File.listFiles(at: "replays") { name in
System.log("found \(name)")
}
```
@@ -264,14 +264,14 @@ try Playdate.File.listFiles(at: "replays") { name in
Network access requires user permission per server:
```swift
let reply = Playdate.Network.HTTPConnection.requestAccess(
let reply = Network.HTTPConnection.requestAccess(
server: "example.com", purpose: "Fetching daily puzzles") { allowed in
guard allowed else { return }
Puzzles.fetch()
}
func fetch() {
guard let connection = Playdate.Network.HTTPConnection(server: "example.com") else { return }
guard let connection = Network.HTTPConnection(server: "example.com") else { return }
connection.setRequestCompleteCallback { connection in
let body = try? connection.read(length: connection.bytesAvailable)
// keep `connection` referenced somewhere until this fires
@@ -286,19 +286,22 @@ Lua callbacks are C function pointers with no context, so they must be
`@convention(c)` functions rather than capturing closures:
```swift
let double: Playdate.Lua.CFunction = { _ in
Playdate.Lua.push(Playdate.Lua.intArgument(at: 1) * 2)
let double: Lua.CFunction = { _ in
Lua.push(Lua.intArgument(at: 1) * 2)
return 1 // number of return values pushed
}
try Playdate.Lua.addFunction(double, name: "mylib.double")
try Lua.addFunction(double, name: "mylib.double")
```
## Conventions
- **Namespaces.** Everything lives under `Playdate`. Games that find that
verbose can alias: `typealias Graphics = Playdate.Graphics`.
- **Errors.** Fallible operations use typed throws — `throws(Playdate.Error)`
generally, `throws(Playdate.Network.NetError)` for network I/O — so `catch`
- **Namespaces.** The subsystem namespaces (`System`, `Graphics`, `Sound`,
…) live at the top level of the module; only the raw C API bootstrap
stays under `Playdate` (`Playdate.initialize(with:)`, `Playdate.api`).
On a name collision with another module, qualify with the module name:
`PlayDate.System`.
- **Errors.** Fallible operations use typed throws — `throws(PlaydateError)`
generally, `throws(Network.NetError)` for network I/O — so `catch`
gives you a concrete type, and no `any Error` existentials are needed.
- **Ownership.** A wrapper that *creates* a C object frees it on `deinit`;
keep the wrapper referenced for as long as you use it. Wrappers vending
+2 -2
View File
@@ -37,8 +37,8 @@ import CPlaydate
import PlayDate
// Touch a type from each module to prove both import and link.
let event = Playdate.SystemEvent(event: kEventInit, argument: 0)
let buttons: Playdate.System.Buttons = [.a, .up]
let event = SystemEvent(event: kEventInit, argument: 0)
let buttons: System.Buttons = [.a, .up]
print(event != nil && buttons.contains(.a) ? "ok" : "broken")
EOF
+3 -5
View File
@@ -5,12 +5,10 @@
internal import CPlaydate
extension Playdate {
/// The display API: resolution, refresh rate, scaling, and effects.
public enum Display {}
}
/// The display API: resolution, refresh rate, scaling, and effects.
public enum Display {}
extension Playdate.Display {
extension Display {
private static var api: playdate_display { Playdate.api.display.pointee }
/// The display width in pixels, taking the current scale into account.
+22 -24
View File
@@ -11,16 +11,14 @@ internal import CPlaydate
private var fileAPI: playdate_file { Playdate.api.file.pointee }
/// The most recent file system error as a thrown error.
private func lastFileError() -> Playdate.Error {
Playdate.Error(cString: fileAPI.geterr.unsafelyUnwrapped())
private func lastFileError() -> PlaydateError {
PlaydateError(cString: fileAPI.geterr.unsafelyUnwrapped())
}
extension Playdate {
/// The file API: access to the game's Data directory and pdx contents.
public enum File {}
}
/// The file API: access to the game's Data directory and pdx contents.
public enum File {}
extension Playdate.File {
extension File {
// MARK: - Types
/// How to open a file.
@@ -44,7 +42,7 @@ extension Playdate.File {
public struct Stat: Sendable {
public let isDirectory: Bool
public let size: UInt32
public let modified: Playdate.System.DateTime
public let modified: System.DateTime
}
/// The origin used by `Handle.seek(to:from:)`.
@@ -59,7 +57,7 @@ extension Playdate.File {
/// Calls `each` with the name of every file in `path`. Subdirectory names
/// end in a slash. Throws if the directory does not exist.
public static func listFiles(at path: String, showHidden: Bool = false,
_ each: (String) -> Void) throws(Playdate.Error) {
_ each: (String) -> Void) throws(PlaydateError) {
let result = withoutActuallyEscaping(each) { each in
var callback = each
return path.withPlaydateCString { cPath in
@@ -76,27 +74,27 @@ extension Playdate.File {
}
/// Information about the file or directory at `path`.
public static func stat(_ path: String) throws(Playdate.Error) -> Stat {
public static func stat(_ path: String) throws(PlaydateError) -> Stat {
var stat = FileStat()
let result = path.withPlaydateCString { fileAPI.stat.unsafelyUnwrapped($0, &stat) }
if result != 0 { throw lastFileError() }
return Stat(
isDirectory: stat.isdir != 0,
size: stat.size,
modified: Playdate.System.DateTime(
modified: System.DateTime(
year: UInt16(stat.m_year), month: UInt8(stat.m_month), day: UInt8(stat.m_day),
hour: UInt8(stat.m_hour), minute: UInt8(stat.m_minute), second: UInt8(stat.m_second)))
}
/// Creates a directory (and intermediate directories) in the Data directory.
public static func mkdir(_ path: String) throws(Playdate.Error) {
public static func mkdir(_ path: String) throws(PlaydateError) {
let result = path.withPlaydateCString { fileAPI.mkdir.unsafelyUnwrapped($0) }
if result != 0 { throw lastFileError() }
}
/// Deletes the file or directory at `path`. Directories require
/// `recursive` to be deleted with their contents.
public static func unlink(_ path: String, recursive: Bool = false) throws(Playdate.Error) {
public static func unlink(_ path: String, recursive: Bool = false) throws(PlaydateError) {
let result = path.withPlaydateCString {
fileAPI.unlink.unsafelyUnwrapped($0, recursive ? 1 : 0)
}
@@ -105,7 +103,7 @@ extension Playdate.File {
/// Renames (moves) a file in the Data directory, overwriting any existing
/// file at the destination.
public static func rename(from: String, to: String) throws(Playdate.Error) {
public static func rename(from: String, to: String) throws(PlaydateError) {
let result = from.withPlaydateCString { cFrom in
to.withPlaydateCString { cTo in
fileAPI.rename.unsafelyUnwrapped(cFrom, cTo)
@@ -123,7 +121,7 @@ extension Playdate.File {
private var isClosed = false
/// Opens the file at `path`.
public init(path: String, mode: Options) throws(Playdate.Error) {
public init(path: String, mode: Options) throws(PlaydateError) {
let pointer = path.withPlaydateCString {
fileAPI.open.unsafelyUnwrapped($0, mode.cValue)
}
@@ -138,7 +136,7 @@ extension Playdate.File {
}
/// Closes the file. Further operations are invalid.
public func close() throws(Playdate.Error) {
public func close() throws(PlaydateError) {
guard !isClosed else { return }
isClosed = true
if fileAPI.close.unsafelyUnwrapped(pointer) != 0 { throw lastFileError() }
@@ -146,7 +144,7 @@ extension Playdate.File {
/// Reads up to `buffer.count` bytes into `buffer`. Returns the number
/// of bytes read; 0 indicates end of file.
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(Playdate.Error) -> Int {
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(PlaydateError) -> Int {
let result = fileAPI.read.unsafelyUnwrapped(
pointer, buffer.baseAddress, UInt32(buffer.count))
if result < 0 { throw lastFileError() }
@@ -154,7 +152,7 @@ extension Playdate.File {
}
/// Reads up to `length` bytes and returns them.
public func read(length: Int) throws(Playdate.Error) -> [UInt8] {
public func read(length: Int) throws(PlaydateError) -> [UInt8] {
var bytes = [UInt8](repeating: 0, count: length)
let result = bytes.withUnsafeMutableBytes { buffer in
fileAPI.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
@@ -166,7 +164,7 @@ extension Playdate.File {
/// Writes the buffer to the file. Returns the number of bytes written.
@discardableResult
public func write(_ buffer: UnsafeRawBufferPointer) throws(Playdate.Error) -> Int {
public func write(_ buffer: UnsafeRawBufferPointer) throws(PlaydateError) -> Int {
let result = fileAPI.write.unsafelyUnwrapped(
pointer, buffer.baseAddress, UInt32(buffer.count))
if result < 0 { throw lastFileError() }
@@ -175,7 +173,7 @@ extension Playdate.File {
/// Writes the bytes to the file. Returns the number of bytes written.
@discardableResult
public func write(_ bytes: [UInt8]) throws(Playdate.Error) -> Int {
public func write(_ bytes: [UInt8]) throws(PlaydateError) -> Int {
let result = bytes.withUnsafeBytes { buffer in
fileAPI.write.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
}
@@ -185,27 +183,27 @@ extension Playdate.File {
/// Writes the string's UTF-8 to the file. Returns the bytes written.
@discardableResult
public func write(_ string: String) throws(Playdate.Error) -> Int {
public func write(_ string: String) throws(PlaydateError) -> Int {
try write(Array(string.utf8))
}
/// Flushes buffered writes to disk. Returns the bytes written.
@discardableResult
public func flush() throws(Playdate.Error) -> Int {
public func flush() throws(PlaydateError) -> Int {
let result = fileAPI.flush.unsafelyUnwrapped(pointer)
if result < 0 { throw lastFileError() }
return Int(result)
}
/// The current read/write offset.
public func tell() throws(Playdate.Error) -> Int {
public func tell() throws(PlaydateError) -> Int {
let result = fileAPI.tell.unsafelyUnwrapped(pointer)
if result < 0 { throw lastFileError() }
return Int(result)
}
/// Moves the read/write offset to `offset` relative to `origin`.
public func seek(to offset: Int, from origin: SeekOrigin = .start) throws(Playdate.Error) {
public func seek(to offset: Int, from origin: SeekOrigin = .start) throws(PlaydateError) {
if fileAPI.seek.unsafelyUnwrapped(pointer, Int32(offset), origin.rawValue) != 0 {
throw lastFileError()
}
+3 -5
View File
@@ -7,14 +7,12 @@
internal import CPlaydate
extension Playdate {
/// The graphics API: drawing, bitmaps, fonts, tilemaps, and video.
public enum Graphics {}
}
/// The graphics API: drawing, bitmaps, fonts, tilemaps, and video.
public enum Graphics {}
var gfx: playdate_graphics { Playdate.api.graphics.pointee }
extension Playdate.Graphics {
extension Graphics {
// MARK: - Screen constants
/// The width of the screen in pixels (`LCD_COLUMNS`).
+9 -9
View File
@@ -5,7 +5,7 @@
internal import CPlaydate
extension Playdate.Graphics {
extension Graphics {
/// An image that can be drawn to the screen or used as a drawing target.
/// Wraps `LCDBitmap`.
public final class Bitmap {
@@ -29,10 +29,10 @@ extension Playdate.Graphics {
}
/// Loads a bitmap from a file in the game's pdx or Data directory.
public convenience init(path: String) throws(Playdate.Error) {
public convenience init(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let pointer = path.withPlaydateCString { gfx.loadBitmap.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw Playdate.Error(cString: error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer, isOwned: true)
}
@@ -74,10 +74,10 @@ extension Playdate.Graphics {
// MARK: Operations
/// Replaces the bitmap's contents with the image at `path`.
public func load(path: String) throws(Playdate.Error) {
public func load(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
path.withPlaydateCString { gfx.loadIntoBitmap.unsafelyUnwrapped($0, pointer, &error) }
if let error { throw Playdate.Error(cString: error) }
if let error { throw PlaydateError(cString: error) }
}
/// Fills the bitmap with `color`.
@@ -166,10 +166,10 @@ extension Playdate.Graphics {
}
/// Loads an image table from a file.
public convenience init(path: String) throws(Playdate.Error) {
public convenience init(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let pointer = path.withPlaydateCString { gfx.loadBitmapTable.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw Playdate.Error(cString: error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer)
}
@@ -178,10 +178,10 @@ extension Playdate.Graphics {
}
/// Replaces the table's contents with the image table at `path`.
public func load(path: String) throws(Playdate.Error) {
public func load(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
path.withPlaydateCString { gfx.loadIntoBitmapTable.unsafelyUnwrapped($0, pointer, &error) }
if let error { throw Playdate.Error(cString: error) }
if let error { throw PlaydateError(cString: error) }
}
/// The bitmap at `index`, or `nil` if out of range. The bitmap
+4 -4
View File
@@ -5,7 +5,7 @@
internal import CPlaydate
extension Playdate.Graphics {
extension Graphics {
/// A font loaded from a .pft file. Wraps `LCDFont`.
public final class Font {
let pointer: OpaquePointer
@@ -19,10 +19,10 @@ extension Playdate.Graphics {
}
/// Loads a font from a file.
public convenience init(path: String) throws(Playdate.Error) {
public convenience init(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let pointer = path.withPlaydateCString { gfx.loadFont.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw Playdate.Error(cString: error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer)
}
@@ -42,7 +42,7 @@ extension Playdate.Graphics {
deinit {
// Per the C API docs, fonts are freed with the system allocator.
Playdate.System.systemFree(UnsafeMutableRawPointer(pointer))
System.systemFree(UnsafeMutableRawPointer(pointer))
retainedData?.deallocate()
}
+1 -1
View File
@@ -7,7 +7,7 @@ internal import CPlaydate
private var tilemapAPI: playdate_tilemap { gfx.tilemap.pointee }
extension Playdate.Graphics {
extension Graphics {
/// A grid of tiles drawn from a bitmap table. Wraps `LCDTileMap`.
public final class TileMap {
let pointer: OpaquePointer
+12 -12
View File
@@ -9,7 +9,7 @@ internal import CPlaydate
private var videoAPI: playdate_video { gfx.video.pointee }
private var streamAPI: playdate_videostream { gfx.videostream.pointee }
extension Playdate.Graphics {
extension Graphics {
/// Plays .pdv video files. Wraps `LCDVideoPlayer`.
public final class VideoPlayer {
let pointer: OpaquePointer
@@ -23,10 +23,10 @@ extension Playdate.Graphics {
}
/// Opens the .pdv file at `path`.
public convenience init(path: String) throws(Playdate.Error) {
public convenience init(path: String) throws(PlaydateError) {
let pointer = path.withPlaydateCString { videoAPI.loadVideo.unsafelyUnwrapped($0) }
guard let pointer else {
throw Playdate.Error(message: "unable to load video: \(path)")
throw PlaydateError(message: "unable to load video: \(path)")
}
self.init(pointer: pointer, isOwned: true)
}
@@ -38,9 +38,9 @@ extension Playdate.Graphics {
}
/// Sets the bitmap the video renders into.
public func setContext(_ context: Bitmap) throws(Playdate.Error) {
public func setContext(_ context: Bitmap) throws(PlaydateError) {
guard videoAPI.setContext.unsafelyUnwrapped(pointer, context.pointer) != 0 else {
throw Playdate.Error(message: error ?? "unable to set video context")
throw PlaydateError(message: error ?? "unable to set video context")
}
retainedContext = context
}
@@ -58,9 +58,9 @@ extension Playdate.Graphics {
}
/// Renders frame `frame` into the current context.
public func renderFrame(_ frame: Int) throws(Playdate.Error) {
public func renderFrame(_ frame: Int) throws(PlaydateError) {
guard videoAPI.renderFrame.unsafelyUnwrapped(pointer, Int32(frame)) != 0 else {
throw Playdate.Error(message: error ?? "unable to render frame \(frame)")
throw PlaydateError(message: error ?? "unable to render frame \(frame)")
}
}
@@ -100,27 +100,27 @@ extension Playdate.Graphics {
}
/// Streams from an open file.
public func setFile(_ file: Playdate.File.Handle) {
public func setFile(_ file: File.Handle) {
retainedSource = file
streamAPI.setFile.unsafelyUnwrapped(pointer, file.pointer)
}
/// Streams from an HTTP connection.
public func setHTTPConnection(_ connection: Playdate.Network.HTTPConnection) {
public func setHTTPConnection(_ connection: Network.HTTPConnection) {
retainedSource = connection
streamAPI.setHTTPConnection.unsafelyUnwrapped(pointer, connection.pointer)
}
/// Streams from a TCP connection.
public func setTCPConnection(_ connection: Playdate.Network.TCPConnection) {
public func setTCPConnection(_ connection: Network.TCPConnection) {
retainedSource = connection
streamAPI.setTCPConnection.unsafelyUnwrapped(pointer, connection.pointer)
}
/// The player used for the stream's audio track. Owned by the stream.
public var filePlayer: Playdate.Sound.FilePlayer? {
public var filePlayer: Sound.FilePlayer? {
guard let player = streamAPI.getFilePlayer.unsafelyUnwrapped(pointer) else { return nil }
return Playdate.Sound.FilePlayer(pointer: player, isOwned: false)
return Sound.FilePlayer(pointer: player, isOwned: false)
}
/// The player used for the stream's video track. Owned by the stream.
+12 -14
View File
@@ -11,12 +11,10 @@ internal import CPlaydate
private var jsonAPI: playdate_json { Playdate.api.json.pointee }
extension Playdate {
/// The JSON API: decoding to and encoding from a `Value` tree.
public enum JSON {}
}
/// The JSON API: decoding to and encoding from a `Value` tree.
public enum JSON {}
extension Playdate.JSON {
extension JSON {
/// A decoded JSON value.
public indirect enum Value {
case null
@@ -88,12 +86,12 @@ extension Playdate.JSON {
decoder.didDecodeTableValue = { decoder, key, value in
guard let userdata = decoder?.pointee.userdata else { return }
let context = Unmanaged<DecodeContext>.fromOpaque(userdata).takeUnretainedValue()
context.append(Playdate.JSON.convert(value), key: String(playdateCString: key))
context.append(JSON.convert(value), key: String(playdateCString: key))
}
decoder.didDecodeArrayValue = { decoder, _, value in
guard let userdata = decoder?.pointee.userdata else { return }
let context = Unmanaged<DecodeContext>.fromOpaque(userdata).takeUnretainedValue()
context.append(Playdate.JSON.convert(value), key: nil)
context.append(JSON.convert(value), key: nil)
}
decoder.didDecodeSublist = { decoder, _, _ in
guard let userdata = decoder?.pointee.userdata else { return nil }
@@ -107,7 +105,7 @@ extension Playdate.JSON {
}
/// Decodes a JSON string into a `Value` tree.
public static func decode(_ jsonString: String) throws(Playdate.Error) -> Value {
public static func decode(_ jsonString: String) throws(PlaydateError) -> Value {
let context = DecodeContext()
let unmanaged = Unmanaged.passUnretained(context)
var decoder = makeDecoder(context: unmanaged)
@@ -124,14 +122,14 @@ extension Playdate.JSON {
}
/// Decodes JSON read from an open file into a `Value` tree.
public static func decode(file: Playdate.File.Handle) throws(Playdate.Error) -> Value {
public static func decode(file: File.Handle) throws(PlaydateError) -> Value {
let context = DecodeContext()
var decoder = makeDecoder(context: Unmanaged.passUnretained(context))
var reader = json_reader()
reader.userdata = Unmanaged.passUnretained(file).toOpaque()
reader.read = { userdata, buffer, size in
guard let userdata, let buffer else { return -1 }
let file = Unmanaged<Playdate.File.Handle>.fromOpaque(userdata).takeUnretainedValue()
let file = Unmanaged<File.Handle>.fromOpaque(userdata).takeUnretainedValue()
let destination = UnsafeMutableRawBufferPointer(start: buffer, count: Int(size))
do {
let count = try file.read(into: destination)
@@ -153,13 +151,13 @@ extension Playdate.JSON {
}
/// Opens and decodes the JSON file at `path`.
public static func decodeFile(at path: String) throws(Playdate.Error) -> Value {
let file = try Playdate.File.Handle(path: path, mode: [.read, .readData])
public static func decodeFile(at path: String) throws(PlaydateError) -> Value {
let file = try File.Handle(path: path, mode: [.read, .readData])
return try decode(file: file)
}
private static func decodeError(_ context: DecodeContext) -> Playdate.Error {
Playdate.Error(message: "\(context.errorMessage ?? "JSON decode failed") (line \(context.errorLine))")
private static func decodeError(_ context: DecodeContext) -> PlaydateError {
PlaydateError(message: "\(context.errorMessage ?? "JSON decode failed") (line \(context.errorLine))")
}
// MARK: - Encoding
+16 -18
View File
@@ -11,13 +11,11 @@ public import CPlaydate
private var luaAPI: playdate_lua { Playdate.api.lua.pointee }
extension Playdate {
/// The Lua bridge: registering C functions and classes, and exchanging
/// values with Lua code.
public enum Lua {}
}
/// The Lua bridge: registering C functions and classes, and exchanging
/// values with Lua code.
public enum Lua {}
extension Playdate.Lua {
extension Lua {
/// A function callable from Lua. Returns the number of values it pushed
/// onto the stack.
public typealias CFunction = lua_CFunction
@@ -58,12 +56,12 @@ extension Playdate.Lua {
/// Makes `function` callable from Lua as `name` (which may contain dots
/// for namespacing, e.g. "mylib.myfunc").
public static func addFunction(_ function: CFunction, name: String) throws(Playdate.Error) {
public static func addFunction(_ function: CFunction, name: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let ok = name.withPlaydateCString {
luaAPI.addFunction.unsafelyUnwrapped(function, $0, &error) != 0
}
if !ok { throw Playdate.Error(cString: error) }
if !ok { throw PlaydateError(cString: error) }
}
/// Registers a Lua class named `name` with the given methods and
@@ -72,7 +70,7 @@ extension Playdate.Lua {
public static func registerClass(name: String,
functions: [(name: String, function: CFunction)],
values: [ClassValue] = [],
isStatic: Bool = false) throws(Playdate.Error) {
isStatic: Bool = false) throws(PlaydateError) {
// The registration tables are kept alive permanently: the OS
// documents no copying guarantees for them.
var registrations: [lua_reg] = functions.map { entry in
@@ -107,7 +105,7 @@ extension Playdate.Lua {
values.isEmpty ? nil : constantsBuffer,
isStatic ? 1 : 0, &error) != 0
}
if !ok { throw Playdate.Error(cString: error) }
if !ok { throw PlaydateError(cString: error) }
}
/// Pushes a function onto the stack, e.g. for `setUserValue`.
@@ -189,15 +187,15 @@ extension Playdate.Lua {
/// The argument as a bitmap. References an object owned by Lua; retain
/// the Lua value while using it.
public static func bitmapArgument(at position: Int) -> Playdate.Graphics.Bitmap? {
public static func bitmapArgument(at position: Int) -> Graphics.Bitmap? {
guard let bitmap = luaAPI.getBitmap.unsafelyUnwrapped(Int32(position)) else { return nil }
return Playdate.Graphics.Bitmap(pointer: bitmap, isOwned: false)
return Graphics.Bitmap(pointer: bitmap, isOwned: false)
}
/// The argument as a sprite.
public static func spriteArgument(at position: Int) -> Playdate.Sprite? {
public static func spriteArgument(at position: Int) -> Sprite? {
guard let sprite = luaAPI.getSprite.unsafelyUnwrapped(Int32(position)) else { return nil }
return Playdate.Sprite.wrapper(for: sprite)
return Sprite.wrapper(for: sprite)
}
// MARK: - Return values
@@ -229,11 +227,11 @@ extension Playdate.Lua {
}
}
public static func push(_ bitmap: Playdate.Graphics.Bitmap) {
public static func push(_ bitmap: Graphics.Bitmap) {
luaAPI.pushBitmap.unsafelyUnwrapped(bitmap.pointer)
}
public static func push(_ sprite: Playdate.Sprite) {
public static func push(_ sprite: Sprite) {
luaAPI.pushSprite.unsafelyUnwrapped(sprite.pointer)
}
@@ -283,11 +281,11 @@ extension Playdate.Lua {
/// Calls the Lua function `name`. Push the arguments onto the stack
/// first. Calling Lua from Swift has overhead; use sparingly.
public static func callFunction(_ name: String, argumentCount: Int = 0) throws(Playdate.Error) {
public static func callFunction(_ name: String, argumentCount: Int = 0) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let ok = name.withPlaydateCString {
luaAPI.callFunction.unsafelyUnwrapped($0, Int32(argumentCount), &error) != 0
}
if !ok { throw Playdate.Error(cString: error) }
if !ok { throw PlaydateError(cString: error) }
}
}
+17 -21
View File
@@ -13,14 +13,10 @@ private var networkAPI: playdate_network { Playdate.api.network.pointee }
private var httpAPI: playdate_http { networkAPI.http.pointee }
private var tcpAPI: playdate_tcp { networkAPI.tcp.pointee }
extension Playdate {
/// The network API: wifi status, HTTP, and TCP.
public enum Network {}
}
extension Playdate.Network {
public typealias AccessReply = Playdate.AccessReply
/// The network API: wifi status, HTTP, and TCP.
public enum Network {}
extension Network {
/// A network error code (`PDNetErr`).
public enum NetError: Int32, Swift.Error, Sendable {
case noDevice = -1
@@ -79,9 +75,9 @@ extension Playdate.Network {
setEnabledCompletion = completion
if completion != nil {
networkAPI.setEnabled.unsafelyUnwrapped(enabled, { error in
let completion = Playdate.Network.setEnabledCompletion
Playdate.Network.setEnabledCompletion = nil
completion?(Playdate.Network.optionalError(error))
let completion = Network.setEnabledCompletion
Network.setEnabledCompletion = nil
completion?(Network.optionalError(error))
})
} else {
networkAPI.setEnabled.unsafelyUnwrapped(enabled, nil)
@@ -140,7 +136,7 @@ extension Playdate.Network {
public static func requestAccess(server: String, port: Int = 443, useSSL: Bool = true,
purpose: String? = nil,
completion: @escaping (Bool) -> Void) -> AccessReply {
Playdate.Network.requestAccess(
Network.requestAccess(
rawRequest: { httpAPI.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
server: server, port: port, useSSL: useSSL, purpose: purpose,
completion: completion)
@@ -205,7 +201,7 @@ extension Playdate.Network {
httpAPI.get.unsafelyUnwrapped(pointer, cPath, cHeaders, headers.utf8.count)
}
}
try Playdate.Network.check(error)
try Network.check(error)
}
/// Sends a POST request for `path` with the given body.
@@ -220,7 +216,7 @@ extension Playdate.Network {
}
}
}
try Playdate.Network.check(error)
try Network.check(error)
}
/// Sends a request with an arbitrary HTTP method.
@@ -238,14 +234,14 @@ extension Playdate.Network {
}
}
}
try Playdate.Network.check(error)
try Network.check(error)
}
// MARK: Response
/// The last error on the connection, if any.
public var error: NetError? {
Playdate.Network.optionalError(httpAPI.getError.unsafelyUnwrapped(pointer))
Network.optionalError(httpAPI.getError.unsafelyUnwrapped(pointer))
}
/// The number of bytes read of the current response, and the total
@@ -380,7 +376,7 @@ extension Playdate.Network {
public static func requestAccess(server: String, port: Int, useSSL: Bool = true,
purpose: String? = nil,
completion: @escaping (Bool) -> Void) -> AccessReply {
Playdate.Network.requestAccess(
Network.requestAccess(
rawRequest: { tcpAPI.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
server: server, port: port, useSSL: useSSL, purpose: purpose,
completion: completion)
@@ -410,7 +406,7 @@ extension Playdate.Network {
/// The last error on the connection, if any.
public var error: NetError? {
Playdate.Network.optionalError(tcpAPI.getError.unsafelyUnwrapped(pointer))
Network.optionalError(tcpAPI.getError.unsafelyUnwrapped(pointer))
}
/// The time to wait for the connection to open, in milliseconds.
@@ -425,14 +421,14 @@ extension Playdate.Network {
guard let wrapper = TCPConnection.wrapper(for: connection) else { return }
let completion = wrapper.openCompletion
wrapper.openCompletion = nil
completion?(wrapper, Playdate.Network.optionalError(error))
completion?(wrapper, Network.optionalError(error))
}, nil)
try Playdate.Network.check(error)
try Network.check(error)
}
/// Closes the connection.
public func close() throws(NetError) {
try Playdate.Network.check(tcpAPI.close.unsafelyUnwrapped(pointer))
try Network.check(tcpAPI.close.unsafelyUnwrapped(pointer))
}
/// Called when the connection closes, with the reason if it closed
@@ -442,7 +438,7 @@ extension Playdate.Network {
if callback != nil {
tcpAPI.setConnectionClosedCallback.unsafelyUnwrapped(pointer, { connection, error in
guard let wrapper = TCPConnection.wrapper(for: connection) else { return }
wrapper.connectionClosedCallback?(wrapper, Playdate.Network.optionalError(error))
wrapper.connectionClosedCallback?(wrapper, Network.optionalError(error))
})
} else {
tcpAPI.setConnectionClosedCallback.unsafelyUnwrapped(pointer, nil)
+49 -46
View File
@@ -10,6 +10,9 @@
public import CPlaydate
/// The raw C API bootstrap. Everything else in this module (System,
/// Graphics, Sprite, Sound, ...) lives at the top level of the `PlayDate`
/// module and requires `initialize(with:)` to have been called first.
public enum Playdate {
/// The raw C API. Populated by `initialize(with:)`.
///
@@ -29,59 +32,59 @@ public enum Playdate {
apiPointer = pointer.assumingMemoryBound(to: PlaydateAPI.self)
api = apiPointer.pointee
}
}
/// An error reported by the Playdate OS.
public struct Error: Swift.Error, Sendable {
public let message: String
/// An error reported by the Playdate OS.
public struct PlaydateError: Swift.Error, Sendable {
public let message: String
init(message: String) {
self.message = message
}
init(cString: UnsafePointer<CChar>?) {
self.init(message: String(playdateCString: cString) ?? "unknown error")
}
init(message: String) {
self.message = message
}
/// The user's answer to a permission request (microphone, network).
public enum AccessReply: UInt32, Sendable {
case ask = 0
case deny = 1
case allow = 2
init(cString: UnsafePointer<CChar>?) {
self.init(message: String(playdateCString: cString) ?? "unknown error")
}
}
/// A Swift view of `PDSystemEvent` with the key code folded into the
/// key events.
public enum SystemEvent {
case initialize
case initializeLua
case lock
case unlock
case pause
case resume
case terminate
case keyPressed(keyCode: UInt32)
case keyReleased(keyCode: UInt32)
case lowPower
case mirrorStarted
case mirrorEnded
/// The user's answer to a permission request (microphone, network).
public enum AccessReply: UInt32, Sendable {
case ask = 0
case deny = 1
case allow = 2
}
public init?(event: PDSystemEvent, argument: UInt32) {
switch event {
case kEventInit: self = .initialize
case kEventInitLua: self = .initializeLua
case kEventLock: self = .lock
case kEventUnlock: self = .unlock
case kEventPause: self = .pause
case kEventResume: self = .resume
case kEventTerminate: self = .terminate
case kEventKeyPressed: self = .keyPressed(keyCode: argument)
case kEventKeyReleased: self = .keyReleased(keyCode: argument)
case kEventLowPower: self = .lowPower
case kEventMirrorStarted: self = .mirrorStarted
case kEventMirrorEnded: self = .mirrorEnded
default: return nil
}
/// A Swift view of `PDSystemEvent` with the key code folded into the
/// key events.
public enum SystemEvent {
case initialize
case initializeLua
case lock
case unlock
case pause
case resume
case terminate
case keyPressed(keyCode: UInt32)
case keyReleased(keyCode: UInt32)
case lowPower
case mirrorStarted
case mirrorEnded
public init?(event: PDSystemEvent, argument: UInt32) {
switch event {
case kEventInit: self = .initialize
case kEventInitLua: self = .initializeLua
case kEventLock: self = .lock
case kEventUnlock: self = .unlock
case kEventPause: self = .pause
case kEventResume: self = .resume
case kEventTerminate: self = .terminate
case kEventKeyPressed: self = .keyPressed(keyCode: argument)
case kEventKeyReleased: self = .keyReleased(keyCode: argument)
case kEventLowPower: self = .lowPower
case kEventMirrorStarted: self = .mirrorStarted
case kEventMirrorEnded: self = .mirrorEnded
default: return nil
}
}
}
+25 -27
View File
@@ -11,12 +11,10 @@ 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 {}
}
/// The scoreboards API for games with online leaderboards.
public enum Scoreboards {}
extension Playdate.Scoreboards {
extension Scoreboards {
/// A score on a board.
public struct Score {
public let rank: UInt32
@@ -92,22 +90,22 @@ extension Playdate.Scoreboards {
}
}
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)?
nonisolated(unsafe) private static var addScoreCompletion: ((Result<Score, PlaydateError>) -> Void)?
nonisolated(unsafe) private static var personalBestCompletion: ((Result<Score, PlaydateError>) -> Void)?
nonisolated(unsafe) private static var boardsCompletion: ((Result<BoardsList, PlaydateError>) -> Void)?
nonisolated(unsafe) private static var scoresCompletion: ((Result<ScoresList, PlaydateError>) -> 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 {
completion: @escaping (Result<Score, PlaydateError>) -> 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))
let completion = Scoreboards.addScoreCompletion
Scoreboards.addScoreCompletion = nil
completion?(Scoreboards.result(score, errorMessage))
}) != 0
}
}
@@ -115,26 +113,26 @@ extension Playdate.Scoreboards {
/// 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 {
completion: @escaping (Result<Score, PlaydateError>) -> 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))
let completion = Scoreboards.personalBestCompletion
Scoreboards.personalBestCompletion = nil
completion?(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 {
public static func getScoreboards(completion: @escaping (Result<BoardsList, PlaydateError>) -> Void) -> Bool {
boardsCompletion = completion
return scoreboardsAPI.getScoreboards.unsafelyUnwrapped({ boards, errorMessage in
let completion = Playdate.Scoreboards.boardsCompletion
Playdate.Scoreboards.boardsCompletion = nil
let completion = Scoreboards.boardsCompletion
Scoreboards.boardsCompletion = nil
guard let boards else {
completion?(.failure(Playdate.Error(cString: errorMessage)))
completion?(.failure(PlaydateError(cString: errorMessage)))
return
}
let list = BoardsList(boards.pointee)
@@ -146,14 +144,14 @@ extension Playdate.Scoreboards {
/// Fetches the scores on the board.
@discardableResult
public static func getScores(boardID: String,
completion: @escaping (Result<ScoresList, Playdate.Error>) -> Void) -> Bool {
completion: @escaping (Result<ScoresList, PlaydateError>) -> 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
let completion = Scoreboards.scoresCompletion
Scoreboards.scoresCompletion = nil
guard let scores else {
completion?(.failure(Playdate.Error(cString: errorMessage)))
completion?(.failure(PlaydateError(cString: errorMessage)))
return
}
let list = ScoresList(scores.pointee)
@@ -164,9 +162,9 @@ extension Playdate.Scoreboards {
}
private static func result(_ score: UnsafeMutablePointer<PDScore>?,
_ errorMessage: UnsafePointer<CChar>?) -> Result<Score, Playdate.Error> {
_ errorMessage: UnsafePointer<CChar>?) -> Result<Score, PlaydateError> {
guard let score else {
return .failure(Playdate.Error(cString: errorMessage))
return .failure(PlaydateError(cString: errorMessage))
}
let value = Score(score.pointee)
scoreboardsAPI.freeScore.unsafelyUnwrapped(score)
+7 -12
View File
@@ -9,12 +9,10 @@ internal import CPlaydate
var snd: playdate_sound { Playdate.api.sound.pointee }
extension Playdate {
/// The sound API: channels, players, synths, sequences, and effects.
public enum Sound {}
}
/// The sound API: channels, players, synths, sequences, and effects.
public enum Sound {}
extension Playdate.Sound {
extension Sound {
/// A note as a MIDI note number, where 60 is middle C. Fractional values
/// are valid.
public typealias MIDINote = Float
@@ -60,12 +58,9 @@ extension Playdate.Sound {
case headset = 2
}
/// The user's answer to a permission request.
public typealias AccessReply = Playdate.AccessReply
/// The most recent sound error as a thrown error.
static func lastError() -> Playdate.Error {
Playdate.Error(cString: snd.getError.unsafelyUnwrapped())
static func lastError() -> PlaydateError {
PlaydateError(cString: snd.getError.unsafelyUnwrapped())
}
// MARK: - Top-level functions
@@ -96,7 +91,7 @@ extension Playdate.Sound {
if callback != nil {
return snd.setMicCallback.unsafelyUnwrapped({ _, buffer, length in
let samples = UnsafeMutableBufferPointer(start: buffer, count: Int(length))
return Playdate.Sound.micCallback?(samples) == true ? 1 : 0
return Sound.micCallback?(samples) == true ? 1 : 0
}, nil, CPlaydate.MicSource(source.rawValue)) != 0
} else {
return snd.setMicCallback.unsafelyUnwrapped(nil, nil, CPlaydate.MicSource(source.rawValue)) != 0
@@ -147,7 +142,7 @@ extension Playdate.Sound {
headphoneChangeCallback = callback
if callback != nil {
snd.getHeadphoneState.unsafelyUnwrapped(nil, nil, { headphone, mic in
Playdate.Sound.headphoneChangeCallback?(headphone != 0, mic != 0)
Sound.headphoneChangeCallback?(headphone != 0, mic != 0)
})
} else {
snd.getHeadphoneState.unsafelyUnwrapped(nil, nil, nil)
+1 -1
View File
@@ -8,7 +8,7 @@ internal import CPlaydate
private var effectAPI: playdate_sound_effect { snd.effect.pointee }
extension Playdate.Sound {
extension Sound {
/// An effect that processes a channel's audio: the base class of the
/// built-in effects. Wraps `SoundEffect`.
public class Effect {
+1 -1
View File
@@ -6,7 +6,7 @@
internal import CPlaydate
extension Playdate.Sound {
extension Sound {
/// A value that can modulate a parameter. The base class of `Signal`,
/// `LFO`, `Envelope`, and `ControlSignal`. Wraps `PDSynthSignalValue`.
public class SignalValue {
+9 -9
View File
@@ -5,7 +5,7 @@
internal import CPlaydate
extension Playdate.Sound {
extension Sound {
/// A source of audio: the base class of `FilePlayer`, `SamplePlayer`,
/// `Synth`, `DelayLineTap`, and `CallbackSource`. Wraps `SoundSource`.
public class Source {
@@ -116,7 +116,7 @@ extension Playdate.Sound {
}
/// Creates a player and loads the audio file at `path`.
public convenience init(path: String) throws(Playdate.Error) {
public convenience init(path: String) throws(PlaydateError) {
self.init()
try load(path: path)
}
@@ -128,12 +128,12 @@ extension Playdate.Sound {
}
/// Prepares the player to stream the file at `path`.
public func load(path: String) throws(Playdate.Error) {
public func load(path: String) throws(PlaydateError) {
let loaded = path.withPlaydateCString {
FilePlayer.api.loadIntoPlayer.unsafelyUnwrapped(pointer, $0) != 0
}
if !loaded {
throw Playdate.Error(message: "unable to load audio file: \(path)")
throw PlaydateError(message: "unable to load audio file: \(path)")
}
}
@@ -266,10 +266,10 @@ extension Playdate.Sound {
}
/// Loads the wav or aiff file at `path`.
public convenience init(path: String) throws(Playdate.Error) {
public convenience init(path: String) throws(PlaydateError) {
let pointer = path.withPlaydateCString { AudioSample.api.load.unsafelyUnwrapped($0) }
guard let pointer else {
throw Playdate.Error(message: "unable to load sample: \(path)")
throw PlaydateError(message: "unable to load sample: \(path)")
}
self.init(pointer: pointer, isOwned: true)
}
@@ -294,12 +294,12 @@ extension Playdate.Sound {
}
/// Loads the file at `path` into this sample's buffer.
public func load(path: String) throws(Playdate.Error) {
public func load(path: String) throws(PlaydateError) {
let loaded = path.withPlaydateCString {
AudioSample.api.loadIntoSample.unsafelyUnwrapped(pointer, $0) != 0
}
if !loaded {
throw Playdate.Error(message: "unable to load sample: \(path)")
throw PlaydateError(message: "unable to load sample: \(path)")
}
}
@@ -346,7 +346,7 @@ extension Playdate.Sound {
}
/// Creates a player for the sample at `path`.
public convenience init(path: String) throws(Playdate.Error) {
public convenience init(path: String) throws(PlaydateError) {
self.init()
sample = try AudioSample(path: path)
}
+6 -6
View File
@@ -5,7 +5,7 @@
internal import CPlaydate
extension Playdate.Sound {
extension Sound {
/// A synthesizer voice. Wraps `PDSynth`.
public final class Synth: Source {
private static var api: playdate_sound_synth { snd.synth.pointee }
@@ -109,11 +109,11 @@ extension Playdate.Sound {
/// Uses a wavetable for the synth. `log2size` is the base-2 log of
/// each waveform's size (e.g. 8 for 256 samples).
public func setWavetable(_ sample: AudioSample, log2size: Int,
columns: Int, rows: Int) throws(Playdate.Error) {
columns: Int, rows: Int) throws(PlaydateError) {
retainedSample = sample
guard Synth.api.setWavetable.unsafelyUnwrapped(
pointer, sample.pointer, Int32(log2size), Int32(columns), Int32(rows)) != 0 else {
throw Playdate.Error(message: "invalid wavetable dimensions")
throw PlaydateError(message: "invalid wavetable dimensions")
}
}
@@ -497,7 +497,7 @@ extension Playdate.Sound {
}
/// Creates a sequence and loads the MIDI file at `path`.
public convenience init(midiFilePath: String) throws(Playdate.Error) {
public convenience init(midiFilePath: String) throws(PlaydateError) {
self.init()
try loadMIDIFile(path: midiFilePath)
}
@@ -506,12 +506,12 @@ extension Playdate.Sound {
Sequence.api.freeSequence.unsafelyUnwrapped(pointer)
}
public func loadMIDIFile(path: String) throws(Playdate.Error) {
public func loadMIDIFile(path: String) throws(PlaydateError) {
let loaded = path.withPlaydateCString {
Sequence.api.loadMIDIFile.unsafelyUnwrapped(pointer, $0) != 0
}
if !loaded {
throw Playdate.Error(message: "unable to load MIDI file: \(path)")
throw PlaydateError(message: "unable to load MIDI file: \(path)")
}
}
File diff suppressed because it is too large Load Diff
+9 -11
View File
@@ -5,12 +5,10 @@
internal import CPlaydate
extension Playdate {
/// The system API: logging, input, time, menu items, and device state.
public enum System {}
}
/// The system API: logging, input, time, menu items, and device state.
public enum System {}
extension Playdate.System {
extension System {
private static var api: playdate_sys { Playdate.api.system.pointee }
// MARK: - Types
@@ -189,8 +187,8 @@ extension Playdate.System {
public static func getServerTime(_ completion: @escaping (_ time: String?, _ error: String?) -> Void) {
serverTimeCompletion = completion
api.getServerTime.unsafelyUnwrapped { time, error in
let completion = Playdate.System.serverTimeCompletion
Playdate.System.serverTimeCompletion = nil
let completion = System.serverTimeCompletion
System.serverTimeCompletion = nil
completion?(String(playdateCString: time), String(playdateCString: error))
}
}
@@ -203,7 +201,7 @@ extension Playdate.System {
public static func setUpdateCallback(_ callback: @escaping () -> Bool) {
updateCallback = callback
api.setUpdateCallback.unsafelyUnwrapped({ _ in
Playdate.System.updateCallback?() == true ? 1 : 0
System.updateCallback?() == true ? 1 : 0
}, nil)
}
@@ -231,7 +229,7 @@ extension Playdate.System {
buttonCallback = callback
if callback != nil {
api.setButtonCallback.unsafelyUnwrapped({ button, down, when, _ in
Playdate.System.buttonCallback?(Buttons(button), down != 0, when) ?? 0
System.buttonCallback?(Buttons(button), down != 0, when) ?? 0
}, nil, Int32(queueSize))
} else {
api.setButtonCallback.unsafelyUnwrapped(nil, nil, Int32(queueSize))
@@ -280,7 +278,7 @@ extension Playdate.System {
if callback != nil {
api.setSerialMessageCallback.unsafelyUnwrapped { data in
guard let message = String(playdateCString: data) else { return }
Playdate.System.serialMessageCallback?(message)
System.serialMessageCallback?(message)
}
} else {
api.setSerialMessageCallback.unsafelyUnwrapped(nil)
@@ -415,7 +413,7 @@ extension Playdate.System {
/// Sets a custom image for the pause menu, optionally shifted left by
/// `xOffset` (0...200).
public static func setMenuImage(_ bitmap: Playdate.Graphics.Bitmap?, xOffset: Int = 0) {
public static func setMenuImage(_ bitmap: Graphics.Bitmap?, xOffset: Int = 0) {
api.setMenuImage.unsafelyUnwrapped(bitmap?.pointer, Int32(xOffset))
}
+23 -23
View File
@@ -6,20 +6,20 @@ import Testing
// inline C helpers that work without it.
@Test func buttonsOptionSetMatchesCMasks() {
#expect(Playdate.System.Buttons.left.rawValue == 1 << 0)
#expect(Playdate.System.Buttons.right.rawValue == 1 << 1)
#expect(Playdate.System.Buttons.up.rawValue == 1 << 2)
#expect(Playdate.System.Buttons.down.rawValue == 1 << 3)
#expect(Playdate.System.Buttons.b.rawValue == 1 << 4)
#expect(Playdate.System.Buttons.a.rawValue == 1 << 5)
#expect(System.Buttons.left.rawValue == 1 << 0)
#expect(System.Buttons.right.rawValue == 1 << 1)
#expect(System.Buttons.up.rawValue == 1 << 2)
#expect(System.Buttons.down.rawValue == 1 << 3)
#expect(System.Buttons.b.rawValue == 1 << 4)
#expect(System.Buttons.a.rawValue == 1 << 5)
let combined: Playdate.System.Buttons = [.a, .up]
let combined: System.Buttons = [.a, .up]
#expect(combined.contains(.a))
#expect(!combined.contains(.b))
}
@Test func graphicsRectConvertsBetweenOriginSizeAndEdges() {
let rect = Playdate.Graphics.Rect(x: 10, y: 20, width: 30, height: 40)
let rect = Graphics.Rect(x: 10, y: 20, width: 30, height: 40)
#expect(rect.left == 10)
#expect(rect.right == 40)
#expect(rect.top == 20)
@@ -29,13 +29,13 @@ import Testing
#expect(translated.left == 15)
#expect(translated.top == 15)
let roundTripped = Playdate.Graphics.Rect(rect.cValue)
let roundTripped = Graphics.Rect(rect.cValue)
#expect(roundTripped.left == rect.left && roundTripped.bottom == rect.bottom)
}
@Test func spriteRectRoundTripsThroughC() {
let rect = Playdate.Rect(x: 1.5, y: 2.5, width: 3, height: 4)
let roundTripped = Playdate.Rect(rect.cValue)
let rect = Rect(x: 1.5, y: 2.5, width: 3, height: 4)
let roundTripped = Rect(rect.cValue)
#expect(roundTripped.x == 1.5)
#expect(roundTripped.y == 2.5)
#expect(roundTripped.width == 3)
@@ -43,23 +43,23 @@ import Testing
}
@Test func soundFormatPropertiesMatchCMacros() {
#expect(!Playdate.Sound.Format.mono8bit.isStereo)
#expect(Playdate.Sound.Format.stereo16bit.isStereo)
#expect(Playdate.Sound.Format.mono16bit.is16bit)
#expect(!Playdate.Sound.Format.monoADPCM.is16bit)
#expect(!Sound.Format.mono8bit.isStereo)
#expect(Sound.Format.stereo16bit.isStereo)
#expect(Sound.Format.mono16bit.is16bit)
#expect(!Sound.Format.monoADPCM.is16bit)
#expect(Playdate.Sound.Format.mono8bit.bytesPerFrame == 1)
#expect(Playdate.Sound.Format.stereo8bit.bytesPerFrame == 2)
#expect(Playdate.Sound.Format.mono16bit.bytesPerFrame == 2)
#expect(Playdate.Sound.Format.stereo16bit.bytesPerFrame == 4)
#expect(Sound.Format.mono8bit.bytesPerFrame == 1)
#expect(Sound.Format.stereo8bit.bytesPerFrame == 2)
#expect(Sound.Format.mono16bit.bytesPerFrame == 2)
#expect(Sound.Format.stereo16bit.bytesPerFrame == 4)
}
@Test func midiNoteFrequencyConversionRoundTrips() {
// A4 (MIDI 69) is 440 Hz.
let a4 = Playdate.Sound.frequency(forNote: 69)
let a4 = Sound.frequency(forNote: 69)
#expect(abs(a4 - 440) < 0.01)
let note = Playdate.Sound.note(forFrequency: 440)
let note = Sound.note(forFrequency: 440)
#expect(abs(note - 69) < 0.001)
}
@@ -74,9 +74,9 @@ import Testing
}
@Test func dateTimeMirrorsCStruct() {
let dateTime = Playdate.System.DateTime(year: 2026, month: 7, day: 24, weekday: 5,
let dateTime = System.DateTime(year: 2026, month: 7, day: 24, weekday: 5,
hour: 12, minute: 34, second: 56)
let roundTripped = Playdate.System.DateTime(dateTime.cValue)
let roundTripped = System.DateTime(dateTime.cValue)
#expect(roundTripped.year == 2026)
#expect(roundTripped.month == 7)
#expect(roundTripped.day == 24)