From 6485997edcbd3c4c6884d72c5c8a125d734dc32f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 24 Jul 2026 11:10:25 +0200 Subject: [PATCH] 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 --- README.md | 137 ++-- Scripts/consumer-test.sh | 4 +- Sources/PlayDate/Display.swift | 8 +- Sources/PlayDate/File.swift | 46 +- Sources/PlayDate/Graphics.swift | 8 +- Sources/PlayDate/GraphicsBitmap.swift | 18 +- Sources/PlayDate/GraphicsFont.swift | 8 +- Sources/PlayDate/GraphicsTileMap.swift | 2 +- Sources/PlayDate/GraphicsVideo.swift | 24 +- Sources/PlayDate/JSON.swift | 26 +- Sources/PlayDate/Lua.swift | 34 +- Sources/PlayDate/Network.swift | 38 +- Sources/PlayDate/PlayDate.swift | 95 +-- Sources/PlayDate/Scoreboards.swift | 52 +- Sources/PlayDate/Sound.swift | 19 +- Sources/PlayDate/SoundEffect.swift | 2 +- Sources/PlayDate/SoundSignal.swift | 2 +- Sources/PlayDate/SoundSource.swift | 18 +- Sources/PlayDate/SoundSynth.swift | 12 +- Sources/PlayDate/Sprite.swift | 1004 ++++++++++++------------ Sources/PlayDate/System.swift | 20 +- Tests/PlayDate/PlayDateTests.swift | 46 +- 22 files changed, 801 insertions(+), 822 deletions(-) diff --git a/README.md b/README.md index 0636f9f..4fda821 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Scripts/consumer-test.sh b/Scripts/consumer-test.sh index 0d69c80..65d9d9d 100755 --- a/Scripts/consumer-test.sh +++ b/Scripts/consumer-test.sh @@ -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 diff --git a/Sources/PlayDate/Display.swift b/Sources/PlayDate/Display.swift index fce9ce2..8d68dd6 100644 --- a/Sources/PlayDate/Display.swift +++ b/Sources/PlayDate/Display.swift @@ -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. diff --git a/Sources/PlayDate/File.swift b/Sources/PlayDate/File.swift index e412f03..0de36a5 100644 --- a/Sources/PlayDate/File.swift +++ b/Sources/PlayDate/File.swift @@ -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() } diff --git a/Sources/PlayDate/Graphics.swift b/Sources/PlayDate/Graphics.swift index d4471f4..1b191fc 100644 --- a/Sources/PlayDate/Graphics.swift +++ b/Sources/PlayDate/Graphics.swift @@ -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`). diff --git a/Sources/PlayDate/GraphicsBitmap.swift b/Sources/PlayDate/GraphicsBitmap.swift index 7c4ceb1..b0f46de 100644 --- a/Sources/PlayDate/GraphicsBitmap.swift +++ b/Sources/PlayDate/GraphicsBitmap.swift @@ -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? 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? 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? 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? 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 diff --git a/Sources/PlayDate/GraphicsFont.swift b/Sources/PlayDate/GraphicsFont.swift index d288e7f..032d6ff 100644 --- a/Sources/PlayDate/GraphicsFont.swift +++ b/Sources/PlayDate/GraphicsFont.swift @@ -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? 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() } diff --git a/Sources/PlayDate/GraphicsTileMap.swift b/Sources/PlayDate/GraphicsTileMap.swift index 523f2db..61ab691 100644 --- a/Sources/PlayDate/GraphicsTileMap.swift +++ b/Sources/PlayDate/GraphicsTileMap.swift @@ -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 diff --git a/Sources/PlayDate/GraphicsVideo.swift b/Sources/PlayDate/GraphicsVideo.swift index 1fb5c6c..b2a58aa 100644 --- a/Sources/PlayDate/GraphicsVideo.swift +++ b/Sources/PlayDate/GraphicsVideo.swift @@ -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. diff --git a/Sources/PlayDate/JSON.swift b/Sources/PlayDate/JSON.swift index 4ce26c7..e210e45 100644 --- a/Sources/PlayDate/JSON.swift +++ b/Sources/PlayDate/JSON.swift @@ -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.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.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.fromOpaque(userdata).takeUnretainedValue() + let file = Unmanaged.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 diff --git a/Sources/PlayDate/Lua.swift b/Sources/PlayDate/Lua.swift index f5afb94..c3f7bd0 100644 --- a/Sources/PlayDate/Lua.swift +++ b/Sources/PlayDate/Lua.swift @@ -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? 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? 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) } } } diff --git a/Sources/PlayDate/Network.swift b/Sources/PlayDate/Network.swift index 4d20742..9745ea7 100644 --- a/Sources/PlayDate/Network.swift +++ b/Sources/PlayDate/Network.swift @@ -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) diff --git a/Sources/PlayDate/PlayDate.swift b/Sources/PlayDate/PlayDate.swift index 3f7bfb3..673b0c3 100644 --- a/Sources/PlayDate/PlayDate.swift +++ b/Sources/PlayDate/PlayDate.swift @@ -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?) { - 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?) { + 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 } } } diff --git a/Sources/PlayDate/Scoreboards.swift b/Sources/PlayDate/Scoreboards.swift index 5c19a20..2237347 100644 --- a/Sources/PlayDate/Scoreboards.swift +++ b/Sources/PlayDate/Scoreboards.swift @@ -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) -> Void)? - nonisolated(unsafe) private static var personalBestCompletion: ((Result) -> Void)? - nonisolated(unsafe) private static var boardsCompletion: ((Result) -> Void)? - nonisolated(unsafe) private static var scoresCompletion: ((Result) -> Void)? + nonisolated(unsafe) private static var addScoreCompletion: ((Result) -> Void)? + nonisolated(unsafe) private static var personalBestCompletion: ((Result) -> Void)? + nonisolated(unsafe) private static var boardsCompletion: ((Result) -> Void)? + nonisolated(unsafe) private static var scoresCompletion: ((Result) -> 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) -> Void) -> Bool { + completion: @escaping (Result) -> 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) -> Void) -> Bool { + completion: @escaping (Result) -> 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) -> Void) -> Bool { + public static func getScoreboards(completion: @escaping (Result) -> 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) -> Void) -> Bool { + completion: @escaping (Result) -> 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?, - _ errorMessage: UnsafePointer?) -> Result { + _ errorMessage: UnsafePointer?) -> Result { guard let score else { - return .failure(Playdate.Error(cString: errorMessage)) + return .failure(PlaydateError(cString: errorMessage)) } let value = Score(score.pointee) scoreboardsAPI.freeScore.unsafelyUnwrapped(score) diff --git a/Sources/PlayDate/Sound.swift b/Sources/PlayDate/Sound.swift index f2d5954..87dff3c 100644 --- a/Sources/PlayDate/Sound.swift +++ b/Sources/PlayDate/Sound.swift @@ -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) diff --git a/Sources/PlayDate/SoundEffect.swift b/Sources/PlayDate/SoundEffect.swift index 865dcca..722259c 100644 --- a/Sources/PlayDate/SoundEffect.swift +++ b/Sources/PlayDate/SoundEffect.swift @@ -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 { diff --git a/Sources/PlayDate/SoundSignal.swift b/Sources/PlayDate/SoundSignal.swift index d653cae..890f843 100644 --- a/Sources/PlayDate/SoundSignal.swift +++ b/Sources/PlayDate/SoundSignal.swift @@ -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 { diff --git a/Sources/PlayDate/SoundSource.swift b/Sources/PlayDate/SoundSource.swift index 31baf65..ebbda1b 100644 --- a/Sources/PlayDate/SoundSource.swift +++ b/Sources/PlayDate/SoundSource.swift @@ -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) } diff --git a/Sources/PlayDate/SoundSynth.swift b/Sources/PlayDate/SoundSynth.swift index 089af6d..28f9c01 100644 --- a/Sources/PlayDate/SoundSynth.swift +++ b/Sources/PlayDate/SoundSynth.swift @@ -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)") } } diff --git a/Sources/PlayDate/Sprite.swift b/Sources/PlayDate/Sprite.swift index b6ce39d..7a67488 100644 --- a/Sources/PlayDate/Sprite.swift +++ b/Sources/PlayDate/Sprite.swift @@ -12,548 +12,544 @@ internal import CPlaydate private var spriteAPI: playdate_sprite { Playdate.api.sprite.pointee } -extension Playdate { - /// A floating-point rectangle mirroring `PDRect`. - public struct Rect: Sendable { - public var x: Float - public var y: Float - public var width: Float - public var height: Float +/// A floating-point rectangle mirroring `PDRect`. +public struct Rect: Sendable { + public var x: Float + public var y: Float + public var width: Float + public var height: Float - public init(x: Float, y: Float, width: Float, height: Float) { - self.x = x - self.y = y - self.width = width - self.height = height - } - - init(_ rect: PDRect) { - self.init(x: rect.x, y: rect.y, width: rect.width, height: rect.height) - } - - var cValue: PDRect { PDRect(x: x, y: y, width: width, height: height) } + public init(x: Float, y: Float, width: Float, height: Float) { + self.x = x + self.y = y + self.width = width + self.height = height } + + init(_ rect: PDRect) { + self.init(x: rect.x, y: rect.y, width: rect.width, height: rect.height) + } + + var cValue: PDRect { PDRect(x: x, y: y, width: width, height: height) } } -extension Playdate { - /// A sprite: a drawable object with position, z-order, and collision - /// support. Wraps `LCDSprite`. Static members wrap the global sprite - /// system functions. - public final class Sprite { - let pointer: OpaquePointer - let isOwned: Bool +/// A sprite: a drawable object with position, z-order, and collision +/// support. Wraps `LCDSprite`. Static members wrap the global sprite +/// system functions. +public final class Sprite { + let pointer: OpaquePointer + let isOwned: Bool - /// Per-sprite callbacks and retained resources. - var updateFunction: ((Sprite) -> Void)? - var drawFunction: ((Sprite, _ bounds: Rect, _ drawRect: Rect) -> Void)? - var collisionResponseFunction: ((Sprite, _ other: Sprite) -> CollisionResponse)? - private var retainedImage: Graphics.Bitmap? - private var retainedStencil: Graphics.Bitmap? - private var retainedTilemap: Graphics.TileMap? + /// Per-sprite callbacks and retained resources. + var updateFunction: ((Sprite) -> Void)? + var drawFunction: ((Sprite, _ bounds: Rect, _ drawRect: Rect) -> Void)? + var collisionResponseFunction: ((Sprite, _ other: Sprite) -> CollisionResponse)? + private var retainedImage: Graphics.Bitmap? + private var retainedStencil: Graphics.Bitmap? + private var retainedTilemap: Graphics.TileMap? - /// Free-form storage for game use (the C userdata slot is reserved - /// by the binding). - public var userdata: AnyObject? + /// Free-form storage for game use (the C userdata slot is reserved + /// by the binding). + public var userdata: AnyObject? - init(pointer: OpaquePointer, isOwned: Bool) { - self.pointer = pointer - self.isOwned = isOwned - spriteAPI.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque()) + init(pointer: OpaquePointer, isOwned: Bool) { + self.pointer = pointer + self.isOwned = isOwned + spriteAPI.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque()) + } + + /// Allocates a new sprite. + public convenience init() { + self.init(pointer: spriteAPI.newSprite.unsafelyUnwrapped().unsafelyUnwrapped, isOwned: true) + } + + deinit { + if isOwned { + spriteAPI.setUserdata.unsafelyUnwrapped(pointer, nil) + spriteAPI.freeSprite.unsafelyUnwrapped(pointer) } + } - /// Allocates a new sprite. - public convenience init() { - self.init(pointer: spriteAPI.newSprite.unsafelyUnwrapped().unsafelyUnwrapped, isOwned: true) + /// Returns the Swift wrapper stored in the sprite's userdata, or a + /// transient unowned wrapper for sprites created outside the binding. + static func wrapper(for pointer: OpaquePointer) -> Sprite { + if let userdata = spriteAPI.getUserdata.unsafelyUnwrapped(pointer) { + return Unmanaged.fromOpaque(userdata).takeUnretainedValue() } + return Sprite(pointer: pointer, isOwned: false) + } - deinit { - if isOwned { - spriteAPI.setUserdata.unsafelyUnwrapped(pointer, nil) - spriteAPI.freeSprite.unsafelyUnwrapped(pointer) - } + /// Copies the sprite. Callbacks and retained resources are carried + /// over to the copy. + public func copy() -> Sprite { + let copy = Sprite(pointer: spriteAPI.copy.unsafelyUnwrapped(pointer).unsafelyUnwrapped, + isOwned: true) + copy.updateFunction = updateFunction + copy.drawFunction = drawFunction + copy.collisionResponseFunction = collisionResponseFunction + copy.retainedImage = retainedImage + copy.retainedStencil = retainedStencil + copy.retainedTilemap = retainedTilemap + return copy + } + + // MARK: - Types + + /// How a sprite reacts when a collision occurs. + public enum CollisionResponse: UInt32, Sendable { + case slide = 0 + case freeze = 1 + case overlap = 2 + case bounce = 3 + + init(_ response: SpriteCollisionResponseType) { + self = CollisionResponse(rawValue: response.rawValue) ?? .freeze } + var cValue: SpriteCollisionResponseType { SpriteCollisionResponseType(rawValue) } + } - /// Returns the Swift wrapper stored in the sprite's userdata, or a - /// transient unowned wrapper for sprites created outside the binding. - static func wrapper(for pointer: OpaquePointer) -> Sprite { - if let userdata = spriteAPI.getUserdata.unsafelyUnwrapped(pointer) { - return Unmanaged.fromOpaque(userdata).takeUnretainedValue() - } - return Sprite(pointer: pointer, isOwned: false) + /// Information about a single collision, mirroring `SpriteCollisionInfo`. + public struct CollisionInfo { + /// The sprite being moved. + public let sprite: Sprite + /// The sprite it collided with. + public let other: Sprite + /// The collision response used. + public let response: CollisionResponse + /// `true` if the sprites were overlapping when the collision + /// started; `false` if the sprite tunneled through. + public let overlaps: Bool + /// How far along the movement (0...1) the collision occurred. + public let ti: Float + /// The difference between the requested and actual positions. + public let move: (x: Float, y: Float) + /// The collision normal (each component -1, 0, or 1). + public let normal: (x: Int, y: Int) + /// Where the sprite started touching `other`. + public let touch: (x: Float, y: Float) + /// The sprite's rect at the moment of the touch. + public let spriteRect: Rect + /// `other`'s rect at the moment of the touch. + public let otherRect: Rect + + init(_ info: SpriteCollisionInfo) { + sprite = Sprite.wrapper(for: info.sprite) + other = Sprite.wrapper(for: info.other) + response = CollisionResponse(info.responseType) + overlaps = info.overlaps != 0 + ti = info.ti + move = (info.move.x, info.move.y) + normal = (Int(info.normal.x), Int(info.normal.y)) + touch = (info.touch.x, info.touch.y) + spriteRect = Rect(info.spriteRect) + otherRect = Rect(info.otherRect) } + } - /// Copies the sprite. Callbacks and retained resources are carried - /// over to the copy. - public func copy() -> Sprite { - let copy = Sprite(pointer: spriteAPI.copy.unsafelyUnwrapped(pointer).unsafelyUnwrapped, - isOwned: true) - copy.updateFunction = updateFunction - copy.drawFunction = drawFunction - copy.collisionResponseFunction = collisionResponseFunction - copy.retainedImage = retainedImage - copy.retainedStencil = retainedStencil - copy.retainedTilemap = retainedTilemap - return copy + /// Information about a sprite intersected by a line segment, + /// mirroring `SpriteQueryInfo`. + public struct QueryInfo { + public let sprite: Sprite + /// How far along the segment (0...1) the segment enters the sprite. + public let ti1: Float + /// How far along the segment (0...1) the segment exits the sprite. + public let ti2: Float + public let entryPoint: (x: Float, y: Float) + public let exitPoint: (x: Float, y: Float) + + init(_ info: SpriteQueryInfo) { + sprite = Sprite.wrapper(for: info.sprite) + ti1 = info.ti1 + ti2 = info.ti2 + entryPoint = (info.entryPoint.x, info.entryPoint.y) + exitPoint = (info.exitPoint.x, info.exitPoint.y) } + } - // MARK: - Types + // MARK: - Display list - /// How a sprite reacts when a collision occurs. - public enum CollisionResponse: UInt32, Sendable { - case slide = 0 - case freeze = 1 - case overlap = 2 - case bounce = 3 + /// Sprites currently added to the display list, kept alive here. + nonisolated(unsafe) private static var displayList: [Sprite] = [] - init(_ response: SpriteCollisionResponseType) { - self = CollisionResponse(rawValue: response.rawValue) ?? .freeze - } - var cValue: SpriteCollisionResponseType { SpriteCollisionResponseType(rawValue) } + /// When `true`, all sprites redraw every frame instead of only when + /// marked dirty. + public static func setAlwaysRedraw(_ flag: Bool) { + spriteAPI.setAlwaysRedraw.unsafelyUnwrapped(flag ? 1 : 0) + } + + /// Marks the given screen region as needing a redraw. + public static func addDirtyRect(_ rect: Graphics.Rect) { + spriteAPI.addDirtyRect.unsafelyUnwrapped(rect.cValue) + } + + /// Draws every sprite in the display list. + public static func drawAll() { + spriteAPI.drawSprites.unsafelyUnwrapped() + } + + /// Updates and then draws every sprite in the display list. + public static func updateAndDrawAll() { + spriteAPI.updateAndDrawSprites.unsafelyUnwrapped() + } + + /// The number of sprites in the display list. + public static var count: Int { + Int(spriteAPI.getSpriteCount.unsafelyUnwrapped()) + } + + /// Adds the sprite to the display list. + public func add() { + spriteAPI.addSprite.unsafelyUnwrapped(pointer) + if !Sprite.displayList.contains(where: { $0 === self }) { + Sprite.displayList.append(self) } + } - /// Information about a single collision, mirroring `SpriteCollisionInfo`. - public struct CollisionInfo { - /// The sprite being moved. - public let sprite: Sprite - /// The sprite it collided with. - public let other: Sprite - /// The collision response used. - public let response: CollisionResponse - /// `true` if the sprites were overlapping when the collision - /// started; `false` if the sprite tunneled through. - public let overlaps: Bool - /// How far along the movement (0...1) the collision occurred. - public let ti: Float - /// The difference between the requested and actual positions. - public let move: (x: Float, y: Float) - /// The collision normal (each component -1, 0, or 1). - public let normal: (x: Int, y: Int) - /// Where the sprite started touching `other`. - public let touch: (x: Float, y: Float) - /// The sprite's rect at the moment of the touch. - public let spriteRect: Playdate.Rect - /// `other`'s rect at the moment of the touch. - public let otherRect: Playdate.Rect + /// Removes the sprite from the display list. + public func remove() { + spriteAPI.removeSprite.unsafelyUnwrapped(pointer) + Sprite.displayList.removeAll { $0 === self } + } - init(_ info: SpriteCollisionInfo) { - sprite = Sprite.wrapper(for: info.sprite) - other = Sprite.wrapper(for: info.other) - response = CollisionResponse(info.responseType) - overlaps = info.overlaps != 0 - ti = info.ti - move = (info.move.x, info.move.y) - normal = (Int(info.normal.x), Int(info.normal.y)) - touch = (info.touch.x, info.touch.y) - spriteRect = Playdate.Rect(info.spriteRect) - otherRect = Playdate.Rect(info.otherRect) - } - } + /// Removes the given sprites from the display list. + public static func remove(_ sprites: [Sprite]) { + for sprite in sprites { sprite.remove() } + } - /// Information about a sprite intersected by a line segment, - /// mirroring `SpriteQueryInfo`. - public struct QueryInfo { - public let sprite: Sprite - /// How far along the segment (0...1) the segment enters the sprite. - public let ti1: Float - /// How far along the segment (0...1) the segment exits the sprite. - public let ti2: Float - public let entryPoint: (x: Float, y: Float) - public let exitPoint: (x: Float, y: Float) + /// Removes every sprite from the display list. + public static func removeAll() { + spriteAPI.removeAllSprites.unsafelyUnwrapped() + displayList = [] + } - init(_ info: SpriteQueryInfo) { - sprite = Sprite.wrapper(for: info.sprite) - ti1 = info.ti1 - ti2 = info.ti2 - entryPoint = (info.entryPoint.x, info.entryPoint.y) - exitPoint = (info.exitPoint.x, info.exitPoint.y) - } - } + // MARK: - Geometry - // MARK: - Display list + /// The sprite's bounds. Setting this positions and sizes the sprite. + public var bounds: Rect { + get { Rect(spriteAPI.getBounds.unsafelyUnwrapped(pointer)) } + set { spriteAPI.setBounds.unsafelyUnwrapped(pointer, newValue.cValue) } + } - /// Sprites currently added to the display list, kept alive here. - nonisolated(unsafe) private static var displayList: [Sprite] = [] + /// Moves the sprite so its anchor point is at (x, y). + public func moveTo(x: Float, y: Float) { + spriteAPI.moveTo.unsafelyUnwrapped(pointer, x, y) + } - /// When `true`, all sprites redraw every frame instead of only when - /// marked dirty. - public static func setAlwaysRedraw(_ flag: Bool) { - spriteAPI.setAlwaysRedraw.unsafelyUnwrapped(flag ? 1 : 0) - } + /// Moves the sprite by (dx, dy). + public func moveBy(dx: Float, dy: Float) { + spriteAPI.moveBy.unsafelyUnwrapped(pointer, dx, dy) + } - /// Marks the given screen region as needing a redraw. - public static func addDirtyRect(_ rect: Graphics.Rect) { - spriteAPI.addDirtyRect.unsafelyUnwrapped(rect.cValue) - } + /// The sprite's anchor position. + public var position: (x: Float, y: Float) { + var x: Float = 0, y: Float = 0 + spriteAPI.getPosition.unsafelyUnwrapped(pointer, &x, &y) + return (x, y) + } - /// Draws every sprite in the display list. - public static func drawAll() { - spriteAPI.drawSprites.unsafelyUnwrapped() - } + /// Sets the sprite's size without changing its image. + public func setSize(width: Float, height: Float) { + spriteAPI.setSize.unsafelyUnwrapped(pointer, width, height) + } - /// Updates and then draws every sprite in the display list. - public static func updateAndDrawAll() { - spriteAPI.updateAndDrawSprites.unsafelyUnwrapped() - } - - /// The number of sprites in the display list. - public static var count: Int { - Int(spriteAPI.getSpriteCount.unsafelyUnwrapped()) - } - - /// Adds the sprite to the display list. - public func add() { - spriteAPI.addSprite.unsafelyUnwrapped(pointer) - if !Sprite.displayList.contains(where: { $0 === self }) { - Sprite.displayList.append(self) - } - } - - /// Removes the sprite from the display list. - public func remove() { - spriteAPI.removeSprite.unsafelyUnwrapped(pointer) - Sprite.displayList.removeAll { $0 === self } - } - - /// Removes the given sprites from the display list. - public static func remove(_ sprites: [Sprite]) { - for sprite in sprites { sprite.remove() } - } - - /// Removes every sprite from the display list. - public static func removeAll() { - spriteAPI.removeAllSprites.unsafelyUnwrapped() - displayList = [] - } - - // MARK: - Geometry - - /// The sprite's bounds. Setting this positions and sizes the sprite. - public var bounds: Playdate.Rect { - get { Playdate.Rect(spriteAPI.getBounds.unsafelyUnwrapped(pointer)) } - set { spriteAPI.setBounds.unsafelyUnwrapped(pointer, newValue.cValue) } - } - - /// Moves the sprite so its anchor point is at (x, y). - public func moveTo(x: Float, y: Float) { - spriteAPI.moveTo.unsafelyUnwrapped(pointer, x, y) - } - - /// Moves the sprite by (dx, dy). - public func moveBy(dx: Float, dy: Float) { - spriteAPI.moveBy.unsafelyUnwrapped(pointer, dx, dy) - } - - /// The sprite's anchor position. - public var position: (x: Float, y: Float) { + /// The anchor point used for positioning, where (0, 0) is the top + /// left and (1, 1) the bottom right. Defaults to (0.5, 0.5). + public var center: (x: Float, y: Float) { + get { var x: Float = 0, y: Float = 0 - spriteAPI.getPosition.unsafelyUnwrapped(pointer, &x, &y) + spriteAPI.getCenter.unsafelyUnwrapped(pointer, &x, &y) return (x, y) } + set { spriteAPI.setCenter.unsafelyUnwrapped(pointer, newValue.x, newValue.y) } + } - /// Sets the sprite's size without changing its image. - public func setSize(width: Float, height: Float) { - spriteAPI.setSize.unsafelyUnwrapped(pointer, width, height) - } + /// Draw order: higher values draw on top. + public var zIndex: Int16 { + get { spriteAPI.getZIndex.unsafelyUnwrapped(pointer) } + set { spriteAPI.setZIndex.unsafelyUnwrapped(pointer, newValue) } + } - /// The anchor point used for positioning, where (0, 0) is the top - /// left and (1, 1) the bottom right. Defaults to (0.5, 0.5). - public var center: (x: Float, y: Float) { - get { - var x: Float = 0, y: Float = 0 - spriteAPI.getCenter.unsafelyUnwrapped(pointer, &x, &y) - return (x, y) - } - set { spriteAPI.setCenter.unsafelyUnwrapped(pointer, newValue.x, newValue.y) } - } + // MARK: - Appearance - /// Draw order: higher values draw on top. - public var zIndex: Int16 { - get { spriteAPI.getZIndex.unsafelyUnwrapped(pointer) } - set { spriteAPI.setZIndex.unsafelyUnwrapped(pointer, newValue) } - } + /// Sets the sprite's image, resizing its bounds to match. + public func setImage(_ image: Graphics.Bitmap?, flip: Graphics.BitmapFlip = .unflipped) { + retainedImage = image + spriteAPI.setImage.unsafelyUnwrapped(pointer, image?.pointer, flip.cValue) + } - // MARK: - Appearance + /// The sprite's image. + public var image: Graphics.Bitmap? { + if let retainedImage { return retainedImage } + guard let image = spriteAPI.getImage.unsafelyUnwrapped(pointer) else { return nil } + return Graphics.Bitmap(pointer: image, isOwned: false) + } - /// Sets the sprite's image, resizing its bounds to match. - public func setImage(_ image: Graphics.Bitmap?, flip: Graphics.BitmapFlip = .unflipped) { - retainedImage = image - spriteAPI.setImage.unsafelyUnwrapped(pointer, image?.pointer, flip.cValue) - } - - /// The sprite's image. - public var image: Graphics.Bitmap? { - if let retainedImage { return retainedImage } - guard let image = spriteAPI.getImage.unsafelyUnwrapped(pointer) else { return nil } - return Graphics.Bitmap(pointer: image, isOwned: false) - } - - /// Sets the sprite's tilemap, resizing its bounds to match. - public var tilemap: Graphics.TileMap? { - get { retainedTilemap } - set { - retainedTilemap = newValue - spriteAPI.setTilemap.unsafelyUnwrapped(pointer, newValue?.pointer) - } - } - - /// The mode used to draw the sprite's image. - public func setDrawMode(_ mode: Graphics.DrawMode) { - spriteAPI.setDrawMode.unsafelyUnwrapped(pointer, mode.cValue) - } - - /// How the sprite's image is mirrored when drawn. - public var imageFlip: Graphics.BitmapFlip { - get { Graphics.BitmapFlip(spriteAPI.getImageFlip.unsafelyUnwrapped(pointer)) } - set { spriteAPI.setImageFlip.unsafelyUnwrapped(pointer, newValue.cValue) } - } - - /// Sets the stencil applied when drawing the sprite. If `tile` is - /// `true` the image width must be a multiple of 32. - public func setStencil(_ stencil: Graphics.Bitmap?, tile: Bool = false) { - retainedStencil = stencil - spriteAPI.setStencilImage.unsafelyUnwrapped(pointer, stencil?.pointer, tile ? 1 : 0) - } - - /// Sets an 8×8 stencil pattern (8 rows of image data). - public func setStencilPattern(_ rows: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) { - var pattern: [UInt8] = [rows.0, rows.1, rows.2, rows.3, rows.4, rows.5, rows.6, rows.7] - pattern.withUnsafeMutableBufferPointer { buffer in - spriteAPI.setStencilPattern.unsafelyUnwrapped(pointer, buffer.baseAddress) - } - } - - public func clearStencil() { - retainedStencil = nil - spriteAPI.clearStencil.unsafelyUnwrapped(pointer) - } - - /// Clips the sprite's drawing to `rect` (screen coordinates). - public func setClipRect(_ rect: Graphics.Rect) { - spriteAPI.setClipRect.unsafelyUnwrapped(pointer, rect.cValue) - } - - public func clearClipRect() { - spriteAPI.clearClipRect.unsafelyUnwrapped(pointer) - } - - /// Clips all sprites with z-index in `startZ...endZ` to `rect`. - public static func setClipRectsInRange(_ rect: Graphics.Rect, startZ: Int, endZ: Int) { - spriteAPI.setClipRectsInRange.unsafelyUnwrapped(rect.cValue, Int32(startZ), Int32(endZ)) - } - - public static func clearClipRectsInRange(startZ: Int, endZ: Int) { - spriteAPI.clearClipRectsInRange.unsafelyUnwrapped(Int32(startZ), Int32(endZ)) - } - - // MARK: - Behavior flags - - /// Whether the sprite's update function is called by `updateAndDrawAll()`. - public var updatesEnabled: Bool { - get { spriteAPI.updatesEnabled.unsafelyUnwrapped(pointer) != 0 } - set { spriteAPI.setUpdatesEnabled.unsafelyUnwrapped(pointer, newValue ? 1 : 0) } - } - - public var collisionsEnabled: Bool { - get { spriteAPI.collisionsEnabled.unsafelyUnwrapped(pointer) != 0 } - set { spriteAPI.setCollisionsEnabled.unsafelyUnwrapped(pointer, newValue ? 1 : 0) } - } - - public var isVisible: Bool { - get { spriteAPI.isVisible.unsafelyUnwrapped(pointer) != 0 } - set { spriteAPI.setVisible.unsafelyUnwrapped(pointer, newValue ? 1 : 0) } - } - - /// Marking a sprite opaque tells the system it does not need to redraw - /// anything behind it. - public func setOpaque(_ flag: Bool) { - spriteAPI.setOpaque.unsafelyUnwrapped(pointer, flag ? 1 : 0) - } - - /// Forces the sprite to redraw this frame. - public func markDirty() { - spriteAPI.markDirty.unsafelyUnwrapped(pointer) - } - - /// Marks part of the sprite (in sprite-local coordinates) as needing - /// a redraw. - public func markDirty(rect: Playdate.Rect) { - spriteAPI.markDirtyRect.unsafelyUnwrapped(pointer, rect.cValue) - } - - /// An integer tag for identifying sprites (e.g. in collisions). - public var tag: UInt8 { - get { spriteAPI.getTag.unsafelyUnwrapped(pointer) } - set { spriteAPI.setTag.unsafelyUnwrapped(pointer, newValue) } - } - - /// When `true`, the sprite draws in screen coordinates, ignoring the - /// global draw offset. - public func setIgnoresDrawOffset(_ flag: Bool) { - spriteAPI.setIgnoresDrawOffset.unsafelyUnwrapped(pointer, flag ? 1 : 0) - } - - // MARK: - Callbacks - - /// Sets the function called by `updateAndDrawAll()` for this sprite. - public func setUpdateFunction(_ update: ((Sprite) -> Void)?) { - updateFunction = update - if update != nil { - spriteAPI.setUpdateFunction.unsafelyUnwrapped(pointer, { spritePointer in - guard let spritePointer else { return } - let sprite = Sprite.wrapper(for: spritePointer) - sprite.updateFunction?(sprite) - }) - } else { - spriteAPI.setUpdateFunction.unsafelyUnwrapped(pointer, nil) - } - } - - /// Sets a custom draw function, called when the sprite needs to draw. - /// `bounds` is the sprite's bounds; `drawRect` is the region that - /// needs redrawing. - public func setDrawFunction(_ draw: ((Sprite, _ bounds: Playdate.Rect, _ drawRect: Playdate.Rect) -> Void)?) { - drawFunction = draw - if draw != nil { - spriteAPI.setDrawFunction.unsafelyUnwrapped(pointer, { spritePointer, bounds, drawRect in - guard let spritePointer else { return } - let sprite = Sprite.wrapper(for: spritePointer) - sprite.drawFunction?(sprite, Playdate.Rect(bounds), Playdate.Rect(drawRect)) - }) - } else { - spriteAPI.setDrawFunction.unsafelyUnwrapped(pointer, nil) - } - } - - // MARK: - Collisions - - /// Clears the collision world. Call when changing scenes. - public static func resetCollisionWorld() { - spriteAPI.resetCollisionWorld.unsafelyUnwrapped() - } - - /// The rect (in sprite-local coordinates) used for collisions. - public var collideRect: Playdate.Rect { - get { Playdate.Rect(spriteAPI.getCollideRect.unsafelyUnwrapped(pointer)) } - set { spriteAPI.setCollideRect.unsafelyUnwrapped(pointer, newValue.cValue) } - } - - public func clearCollideRect() { - spriteAPI.clearCollideRect.unsafelyUnwrapped(pointer) - } - - /// Sets the function deciding how this sprite responds when it - /// collides with `other`. - public func setCollisionResponseFunction(_ filter: ((Sprite, _ other: Sprite) -> CollisionResponse)?) { - collisionResponseFunction = filter - if filter != nil { - spriteAPI.setCollisionResponseFunction.unsafelyUnwrapped(pointer, { spritePointer, otherPointer in - guard let spritePointer, let otherPointer else { return kCollisionTypeFreeze } - let sprite = Sprite.wrapper(for: spritePointer) - let other = Sprite.wrapper(for: otherPointer) - return sprite.collisionResponseFunction?(sprite, other).cValue ?? kCollisionTypeFreeze - }) - } else { - spriteAPI.setCollisionResponseFunction.unsafelyUnwrapped(pointer, nil) - } - } - - /// Converts and frees a C collision info array. - private static func collisionInfos(_ pointer: UnsafeMutablePointer?, - count: Int32) -> [CollisionInfo] { - guard let pointer else { return [] } - var infos = [CollisionInfo]() - infos.reserveCapacity(Int(count)) - for index in 0.. (actual: (x: Float, y: Float), collisions: [CollisionInfo]) { - var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0 - let result = spriteAPI.checkCollisions.unsafelyUnwrapped( - pointer, goalX, goalY, &actualX, &actualY, &count) - return ((actualX, actualY), Sprite.collisionInfos(result, count: count)) - } - - /// Moves the sprite toward (goalX, goalY), resolving collisions, and - /// returns where it ended up and what it hit. - @discardableResult - public func moveWithCollisions(goalX: Float, goalY: Float) - -> (actual: (x: Float, y: Float), collisions: [CollisionInfo]) { - var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0 - let result = spriteAPI.moveWithCollisions.unsafelyUnwrapped( - pointer, goalX, goalY, &actualX, &actualY, &count) - return ((actualX, actualY), Sprite.collisionInfos(result, count: count)) - } - - /// Converts and frees a C sprite pointer array. - private static func sprites(_ pointer: UnsafeMutablePointer?, - count: Int32) -> [Sprite] { - guard let pointer else { return [] } - var sprites = [Sprite]() - sprites.reserveCapacity(Int(count)) - for index in 0.. [Sprite] { - var count: Int32 = 0 - let result = spriteAPI.querySpritesAtPoint.unsafelyUnwrapped(x, y, &count) - return sprites(result, count: count) - } - - /// Sprites with collision rects intersecting the rect. - public static func query(inRect x: Float, _ y: Float, width: Float, height: Float) -> [Sprite] { - var count: Int32 = 0 - let result = spriteAPI.querySpritesInRect.unsafelyUnwrapped(x, y, width, height, &count) - return sprites(result, count: count) - } - - /// Sprites with collision rects intersecting the line segment. - public static func query(alongLine x1: Float, _ y1: Float, _ x2: Float, _ y2: Float) -> [Sprite] { - var count: Int32 = 0 - let result = spriteAPI.querySpritesAlongLine.unsafelyUnwrapped(x1, y1, x2, y2, &count) - return sprites(result, count: count) - } - - /// Like `query(alongLine:)`, with entry/exit information for each sprite. - public static func queryInfo(alongLine x1: Float, _ y1: Float, - _ x2: Float, _ y2: Float) -> [QueryInfo] { - var count: Int32 = 0 - guard let result = spriteAPI.querySpriteInfoAlongLine.unsafelyUnwrapped( - x1, y1, x2, y2, &count) else { return [] } - var infos = [QueryInfo]() - infos.reserveCapacity(Int(count)) - for index in 0.. Void)?) { + updateFunction = update + if update != nil { + spriteAPI.setUpdateFunction.unsafelyUnwrapped(pointer, { spritePointer in + guard let spritePointer else { return } + let sprite = Sprite.wrapper(for: spritePointer) + sprite.updateFunction?(sprite) + }) + } else { + spriteAPI.setUpdateFunction.unsafelyUnwrapped(pointer, nil) + } + } + + /// Sets a custom draw function, called when the sprite needs to draw. + /// `bounds` is the sprite's bounds; `drawRect` is the region that + /// needs redrawing. + public func setDrawFunction(_ draw: ((Sprite, _ bounds: Rect, _ drawRect: Rect) -> Void)?) { + drawFunction = draw + if draw != nil { + spriteAPI.setDrawFunction.unsafelyUnwrapped(pointer, { spritePointer, bounds, drawRect in + guard let spritePointer else { return } + let sprite = Sprite.wrapper(for: spritePointer) + sprite.drawFunction?(sprite, Rect(bounds), Rect(drawRect)) + }) + } else { + spriteAPI.setDrawFunction.unsafelyUnwrapped(pointer, nil) + } + } + + // MARK: - Collisions + + /// Clears the collision world. Call when changing scenes. + public static func resetCollisionWorld() { + spriteAPI.resetCollisionWorld.unsafelyUnwrapped() + } + + /// The rect (in sprite-local coordinates) used for collisions. + public var collideRect: Rect { + get { Rect(spriteAPI.getCollideRect.unsafelyUnwrapped(pointer)) } + set { spriteAPI.setCollideRect.unsafelyUnwrapped(pointer, newValue.cValue) } + } + + public func clearCollideRect() { + spriteAPI.clearCollideRect.unsafelyUnwrapped(pointer) + } + + /// Sets the function deciding how this sprite responds when it + /// collides with `other`. + public func setCollisionResponseFunction(_ filter: ((Sprite, _ other: Sprite) -> CollisionResponse)?) { + collisionResponseFunction = filter + if filter != nil { + spriteAPI.setCollisionResponseFunction.unsafelyUnwrapped(pointer, { spritePointer, otherPointer in + guard let spritePointer, let otherPointer else { return kCollisionTypeFreeze } + let sprite = Sprite.wrapper(for: spritePointer) + let other = Sprite.wrapper(for: otherPointer) + return sprite.collisionResponseFunction?(sprite, other).cValue ?? kCollisionTypeFreeze + }) + } else { + spriteAPI.setCollisionResponseFunction.unsafelyUnwrapped(pointer, nil) + } + } + + /// Converts and frees a C collision info array. + private static func collisionInfos(_ pointer: UnsafeMutablePointer?, + count: Int32) -> [CollisionInfo] { + guard let pointer else { return [] } + var infos = [CollisionInfo]() + infos.reserveCapacity(Int(count)) + for index in 0.. (actual: (x: Float, y: Float), collisions: [CollisionInfo]) { + var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0 + let result = spriteAPI.checkCollisions.unsafelyUnwrapped( + pointer, goalX, goalY, &actualX, &actualY, &count) + return ((actualX, actualY), Sprite.collisionInfos(result, count: count)) + } + + /// Moves the sprite toward (goalX, goalY), resolving collisions, and + /// returns where it ended up and what it hit. + @discardableResult + public func moveWithCollisions(goalX: Float, goalY: Float) + -> (actual: (x: Float, y: Float), collisions: [CollisionInfo]) { + var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0 + let result = spriteAPI.moveWithCollisions.unsafelyUnwrapped( + pointer, goalX, goalY, &actualX, &actualY, &count) + return ((actualX, actualY), Sprite.collisionInfos(result, count: count)) + } + + /// Converts and frees a C sprite pointer array. + private static func sprites(_ pointer: UnsafeMutablePointer?, + count: Int32) -> [Sprite] { + guard let pointer else { return [] } + var sprites = [Sprite]() + sprites.reserveCapacity(Int(count)) + for index in 0.. [Sprite] { + var count: Int32 = 0 + let result = spriteAPI.querySpritesAtPoint.unsafelyUnwrapped(x, y, &count) + return sprites(result, count: count) + } + + /// Sprites with collision rects intersecting the rect. + public static func query(inRect x: Float, _ y: Float, width: Float, height: Float) -> [Sprite] { + var count: Int32 = 0 + let result = spriteAPI.querySpritesInRect.unsafelyUnwrapped(x, y, width, height, &count) + return sprites(result, count: count) + } + + /// Sprites with collision rects intersecting the line segment. + public static func query(alongLine x1: Float, _ y1: Float, _ x2: Float, _ y2: Float) -> [Sprite] { + var count: Int32 = 0 + let result = spriteAPI.querySpritesAlongLine.unsafelyUnwrapped(x1, y1, x2, y2, &count) + return sprites(result, count: count) + } + + /// Like `query(alongLine:)`, with entry/exit information for each sprite. + public static func queryInfo(alongLine x1: Float, _ y1: Float, + _ x2: Float, _ y2: Float) -> [QueryInfo] { + var count: Int32 = 0 + guard let result = spriteAPI.querySpriteInfoAlongLine.unsafelyUnwrapped( + x1, y1, x2, y2, &count) else { return [] } + var infos = [QueryInfo]() + infos.reserveCapacity(Int(count)) + for index in 0.. 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)) } diff --git a/Tests/PlayDate/PlayDateTests.swift b/Tests/PlayDate/PlayDateTests.swift index 086a724..0ab71c2 100644 --- a/Tests/PlayDate/PlayDateTests.swift +++ b/Tests/PlayDate/PlayDateTests.swift @@ -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)