Promote subsystem namespaces to the top level of the module

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

Breaking change for any existing consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 11:10:25 +02:00
co-authored by Claude Fable 5
parent 3ee071e098
commit 6485997edc
22 changed files with 801 additions and 822 deletions
+70 -67
View File
@@ -15,16 +15,16 @@ All ten C subsystems are covered:
| Namespace | Wraps | Highlights |
|---|---|---|
| `Playdate.System` | `playdate->system` | input, time, menu items, logging |
| `Playdate.Display` | `playdate->display` | refresh rate, scale, mosaic, flip |
| `Playdate.Graphics` | `playdate->graphics` | drawing, `Bitmap`, `Font`, `TileMap`, video |
| `Playdate.Sprite` | `playdate->sprite` | display list, collisions, custom draw |
| `Playdate.Sound` | `playdate->sound` | players, synths, sequences, effects |
| `Playdate.File` | `playdate->file` | `Handle`, directory operations |
| `Playdate.JSON` | `playdate->json` | `Value` tree decode/encode |
| `Playdate.Lua` | `playdate->lua` | C functions, classes, stack access |
| `Playdate.Scoreboards` | `playdate->scoreboards` | online leaderboards |
| `Playdate.Network` | `playdate->network` | wifi, `HTTPConnection`, `TCPConnection` |
| `System` | `playdate->system` | input, time, menu items, logging |
| `Display` | `playdate->display` | refresh rate, scale, mosaic, flip |
| `Graphics` | `playdate->graphics` | drawing, `Bitmap`, `Font`, `TileMap`, video |
| `Sprite` | `playdate->sprite` | display list, collisions, custom draw |
| `Sound` | `playdate->sound` | players, synths, sequences, effects |
| `File` | `playdate->file` | `Handle`, directory operations |
| `JSON` | `playdate->json` | `Value` tree decode/encode |
| `Lua` | `playdate->lua` | C functions, classes, stack access |
| `Scoreboards` | `playdate->scoreboards` | online leaderboards |
| `Network` | `playdate->network` | wifi, `HTTPConnection`, `TCPConnection` |
## Requirements
@@ -82,7 +82,7 @@ import PlayDate
func eventHandler(pointer: UnsafeMutableRawPointer,
event: PDSystemEvent,
argument: UInt32) -> Int32 {
switch Playdate.SystemEvent(event: event, argument: argument) {
switch SystemEvent(event: event, argument: argument) {
case .initialize:
Playdate.initialize(with: pointer) // must happen before anything else
Game.shared.start()
@@ -97,25 +97,25 @@ func eventHandler(pointer: UnsafeMutableRawPointer,
final class Game {
nonisolated(unsafe) static let shared = Game()
var player = Playdate.Sprite()
var player = Sprite()
func start() {
Playdate.Display.setRefreshRate(50)
Display.setRefreshRate(50)
Playdate.System.setUpdateCallback {
System.setUpdateCallback {
self.update()
return true // true = redraw the display this frame
}
}
func update() {
let (_, pushed, _) = Playdate.System.buttonState
let (_, pushed, _) = System.buttonState
if pushed.contains(.a) {
Playdate.System.log("A pressed at \(Playdate.System.currentTimeMilliseconds)ms")
System.log("A pressed at \(System.currentTimeMilliseconds)ms")
}
Playdate.Sprite.updateAndDrawAll()
Playdate.System.drawFPS()
Sprite.updateAndDrawAll()
System.drawFPS()
}
}
```
@@ -130,68 +130,68 @@ a programmer error and will crash.
```swift
// Buttons are an OptionSet: current (held), pushed and released this frame.
let (current, pushed, released) = Playdate.System.buttonState
let (current, pushed, released) = System.buttonState
if current.contains([.b, .down]) { /* charge shot */ }
// Crank.
if !Playdate.System.isCrankDocked {
aim(degrees: Playdate.System.crankAngle)
spin(by: Playdate.System.crankChange)
if !System.isCrankDocked {
aim(degrees: System.crankAngle)
spin(by: System.crankChange)
}
// Accelerometer is a peripheral you enable first.
Playdate.System.setPeripheralsEnabled(.accelerometer)
let (x, y, z) = Playdate.System.accelerometer
System.setPeripheralsEnabled(.accelerometer)
let (x, y, z) = System.accelerometer
// System menu items take closures; the binding keeps them alive until removed.
Playdate.System.addCheckmarkMenuItem(title: "music", isChecked: true) { item in
System.addCheckmarkMenuItem(title: "music", isChecked: true) { item in
Audio.musicEnabled = item.isChecked
}
Playdate.System.addOptionsMenuItem(title: "mode", options: ["easy", "hard"]) { item in
System.addOptionsMenuItem(title: "mode", options: ["easy", "hard"]) { item in
Game.shared.difficulty = item.value
}
// Logging goes to the simulator console or device serial.
Playdate.System.log("spawned \(count) enemies")
Playdate.System.error("unrecoverable") // stops execution
System.log("spawned \(count) enemies")
System.error("unrecoverable") // stops execution
```
### Graphics: drawing, bitmaps, fonts
Fallible loads (`Bitmap(path:)`, `Font(path:)`, …) throw `Playdate.Error`,
Fallible loads (`Bitmap(path:)`, `Font(path:)`, …) throw `PlaydateError`,
which carries the message produced by the OS:
```swift
let font = try Playdate.Graphics.Font(path: "fonts/Asheville-Sans-14-Bold.pft")
Playdate.Graphics.setFont(font)
let font = try Graphics.Font(path: "fonts/Asheville-Sans-14-Bold.pft")
Graphics.setFont(font)
Playdate.Graphics.clear(color: .white)
Playdate.Graphics.fillRect(x: 0, y: 0, width: 400, height: 32, color: .black)
Playdate.Graphics.drawText("Hëllo, Playdate", x: 8, y: 8)
Graphics.clear(color: .white)
Graphics.fillRect(x: 0, y: 0, width: 400, height: 32, color: .black)
Graphics.drawText("Hëllo, Playdate", x: 8, y: 8)
// Colors are solid or 8×8 patterns.
let checker = Playdate.Graphics.Pattern(rows: (0xAA, 0x55, 0xAA, 0x55,
0xAA, 0x55, 0xAA, 0x55))
Playdate.Graphics.fillEllipse(x: 100, y: 100, width: 64, height: 64,
color: .pattern(checker))
let checker = Graphics.Pattern(rows: (0xAA, 0x55, 0xAA, 0x55,
0xAA, 0x55, 0xAA, 0x55))
Graphics.fillEllipse(x: 100, y: 100, width: 64, height: 64,
color: .pattern(checker))
// Bitmaps draw themselves; draw into one by pushing it as the context.
let logo = try Playdate.Graphics.Bitmap(path: "images/logo")
let logo = try Graphics.Bitmap(path: "images/logo")
logo.draw(x: 168, y: 88)
let canvas = Playdate.Graphics.Bitmap(width: 64, height: 64)
Playdate.Graphics.pushContext(canvas)
Playdate.Graphics.drawLine(x1: 0, y1: 0, x2: 63, y2: 63, width: 2, color: .black)
Playdate.Graphics.popContext()
let canvas = Graphics.Bitmap(width: 64, height: 64)
Graphics.pushContext(canvas)
Graphics.drawLine(x1: 0, y1: 0, x2: 63, y2: 63, width: 2, color: .black)
Graphics.popContext()
```
### Sprites and collisions
```swift
let ball = Playdate.Sprite()
ball.setImage(try Playdate.Graphics.Bitmap(path: "images/ball"))
let ball = Sprite()
ball.setImage(try Graphics.Bitmap(path: "images/ball"))
ball.moveTo(x: 200, y: 120)
ball.collideRect = Playdate.Rect(x: 0, y: 0, width: 16, height: 16)
ball.collideRect = Rect(x: 0, y: 0, width: 16, height: 16)
ball.setCollisionResponseFunction { _, _ in .bounce }
ball.add() // adds to the display list; the binding keeps it alive while added
@@ -211,29 +211,29 @@ sprite userdata slot is reserved by the binding for that recovery — use the
```swift
// Stream music from disk.
let music = try Playdate.Sound.FilePlayer(path: "audio/theme")
let music = try Sound.FilePlayer(path: "audio/theme")
music.play(repeat: 0) // 0 = loop forever
// Play short effects from memory.
let blip = try Playdate.Sound.SamplePlayer(path: "audio/blip")
let blip = try Sound.SamplePlayer(path: "audio/blip")
blip.play()
// Synthesis.
let synth = Playdate.Sound.Synth(waveform: .square)
let synth = Sound.Synth(waveform: .square)
synth.setAttackTime(0.01)
synth.setReleaseTime(0.2)
synth.playMIDINote(Playdate.Sound.noteC4, velocity: 0.8, length: 0.5)
synth.playMIDINote(Sound.noteC4, velocity: 0.8, length: 0.5)
// Channels mix sources and effects.
let channel = Playdate.Sound.Channel()
let channel = Sound.Channel()
channel.add()
channel.addSource(synth)
let filter = Playdate.Sound.TwoPoleFilter(kind: .lowPass)
let filter = Sound.TwoPoleFilter(kind: .lowPass)
filter.setFrequency(800)
channel.addEffect(filter)
// Anything that takes a modulator accepts any SignalValue (LFO, Envelope, ).
let wobble = Playdate.Sound.LFO(shape: .sine)
let wobble = Sound.LFO(shape: .sine)
wobble.setRate(2)
synth.frequencyModulator = wobble
```
@@ -242,20 +242,20 @@ synth.frequencyModulator = wobble
```swift
// Paths resolve against the game's Data directory and pdx per the open mode.
let save = try Playdate.File.Handle(path: "save.json", mode: .write)
try save.write(Playdate.JSON.encode(.table([
let save = try File.Handle(path: "save.json", mode: .write)
try save.write(JSON.encode(.table([
"level": .int(3),
"name": .string("Röck"),
])))
try save.close()
let loaded = try Playdate.JSON.decodeFile(at: "save.json")
let loaded = try JSON.decodeFile(at: "save.json")
if case .table(let entries) = loaded, case .int(let level)? = entries["level"] {
Game.shared.level = level
}
try Playdate.File.listFiles(at: "replays") { name in
Playdate.System.log("found \(name)")
try File.listFiles(at: "replays") { name in
System.log("found \(name)")
}
```
@@ -264,14 +264,14 @@ try Playdate.File.listFiles(at: "replays") { name in
Network access requires user permission per server:
```swift
let reply = Playdate.Network.HTTPConnection.requestAccess(
let reply = Network.HTTPConnection.requestAccess(
server: "example.com", purpose: "Fetching daily puzzles") { allowed in
guard allowed else { return }
Puzzles.fetch()
}
func fetch() {
guard let connection = Playdate.Network.HTTPConnection(server: "example.com") else { return }
guard let connection = Network.HTTPConnection(server: "example.com") else { return }
connection.setRequestCompleteCallback { connection in
let body = try? connection.read(length: connection.bytesAvailable)
// keep `connection` referenced somewhere until this fires
@@ -286,19 +286,22 @@ Lua callbacks are C function pointers with no context, so they must be
`@convention(c)` functions rather than capturing closures:
```swift
let double: Playdate.Lua.CFunction = { _ in
Playdate.Lua.push(Playdate.Lua.intArgument(at: 1) * 2)
let double: Lua.CFunction = { _ in
Lua.push(Lua.intArgument(at: 1) * 2)
return 1 // number of return values pushed
}
try Playdate.Lua.addFunction(double, name: "mylib.double")
try Lua.addFunction(double, name: "mylib.double")
```
## Conventions
- **Namespaces.** Everything lives under `Playdate`. Games that find that
verbose can alias: `typealias Graphics = Playdate.Graphics`.
- **Errors.** Fallible operations use typed throws — `throws(Playdate.Error)`
generally, `throws(Playdate.Network.NetError)` for network I/O — so `catch`
- **Namespaces.** The subsystem namespaces (`System`, `Graphics`, `Sound`,
…) live at the top level of the module; only the raw C API bootstrap
stays under `Playdate` (`Playdate.initialize(with:)`, `Playdate.api`).
On a name collision with another module, qualify with the module name:
`PlayDate.System`.
- **Errors.** Fallible operations use typed throws — `throws(PlaydateError)`
generally, `throws(Network.NetError)` for network I/O — so `catch`
gives you a concrete type, and no `any Error` existentials are needed.
- **Ownership.** A wrapper that *creates* a C object frees it on `deinit`;
keep the wrapper referenced for as long as you use it. Wrappers vending