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)