Swift 6.4 migration and exhancements (#1)
This PR contains the work done to update the library and the attached example project to use the Swift 6.4 computer as a minimum supported version and also, to use the latest features introduced in it. Reviewed-on: #1 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -4,9 +4,7 @@
|
||||
|
||||
Swift bindings to the [Playdate](https://play.date) C API.
|
||||
|
||||
The Playdate C API is delivered as a `PlaydateAPI*` struct of function pointers that the firmware hands to your game at launch. This package wraps that surface in idiomatic Swift: namespaced APIs, wrapper types with ownership semantics, closures instead of function-pointer/userdata pairs, `OptionSet` and `enum` types instead of raw constants, and typed `throws` for fallible calls.
|
||||
|
||||
All ten C subsystems are covered:
|
||||
Namespaces per subsystem, wrapper types that own their C objects, closures instead of function-pointer/userdata pairs, `OptionSet`s and `enum`s instead of raw constants, and typed `throws`. All ten C subsystems are covered:
|
||||
|
||||
| Namespace | Wraps | Highlights |
|
||||
|---|---|---|
|
||||
@@ -23,29 +21,24 @@ All ten C subsystems are covered:
|
||||
|
||||
## Requirements
|
||||
|
||||
- The [Playdate SDK](https://play.date/dev/) (3.1.1 or later). The SDK is not vendored into this repository.
|
||||
- Swift 6.3 tools or later.
|
||||
- [Playdate SDK](https://play.date/dev/) 3.1.1+ (not bundled).
|
||||
- Swift 6.4 tools.
|
||||
- Any macOS. `InlineArray` overloads are `@available(macOS 26, *)` in Simulator builds; unrestricted on device and Linux.
|
||||
|
||||
Device builds additionally need:
|
||||
Device builds also need:
|
||||
|
||||
- A [swift.org development snapshot toolchain](https://www.swift.org/install/macos/) — Xcode's toolchain does not ship the Embedded Swift stdlib for the device target (`armv7em-none-none-eabi`). The easiest way to get one is to install [Swiftly](https://www.swift.org/swiftly/), Swift's toolchain manager, and install the `main-snapshot` toolchain with it:
|
||||
|
||||
```sh
|
||||
swiftly install main-snapshot
|
||||
```
|
||||
- The [Arm GNU toolchain](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads) (`arm-none-eabi-gcc`) on your `PATH`, which the Playdate SDK's build support uses to compile and link the device binary.
|
||||
- A [swift.org toolchain](https://www.swift.org/install/macos/) (6.4 release or a snapshot); Xcode's lacks the Embedded Swift stdlib. With [Swiftly](https://www.swift.org/swiftly/): `swiftly install main-snapshot`.
|
||||
- The [Arm GNU toolchain](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads) (`arm-none-eabi-gcc`) on your `PATH`.
|
||||
|
||||
### One-time SDK setup
|
||||
|
||||
The `CPlaydate` target resolves `pd_api.h` through a `playdate` pkg-config module, so the package works as a normal SwiftPM dependency without unsafe build flags. Generate the pkg-config file once per machine:
|
||||
`CPlaydate` finds `pd_api.h` through a `playdate` pkg-config module. Create it once per machine:
|
||||
|
||||
```sh
|
||||
make setup
|
||||
```
|
||||
|
||||
The target locates the SDK via `PLAYDATE_SDK_PATH` (default `~/Developer/PlaydateSDK`) and writes `playdate.pc` into a directory SwiftPM searches by default (`/usr/local/lib/pkgconfig`). If you prefer another location on your `PKG_CONFIG_PATH`, run the underlying script directly with the destination as an argument: `Scripts/install-pkgconfig.sh <directory>`.
|
||||
|
||||
If Xcode had the package open before you ran the setup, make it re-read the manifest (File ▸ Packages ▸ Reset Package Caches) — Xcode caches package resolution and won't notice the new `.pc` file on its own.
|
||||
It reads `PLAYDATE_SDK_PATH` (default `~/Developer/PlaydateSDK`) and writes `/usr/local/lib/pkgconfig/playdate.pc`; pass another directory with `Scripts/install-pkgconfig.sh <directory>`. If Xcode had the package open, run File ▸ Packages ▸ Reset Package Caches.
|
||||
|
||||
## Adding the dependency
|
||||
|
||||
@@ -72,13 +65,13 @@ targets: [
|
||||
|
||||
## Getting started
|
||||
|
||||
A Playdate application or game has a single C entry point, `eventHandler`. Export it with `@_cdecl`, initialize the binding on the first event, and install an update callback:
|
||||
A Playdate game has one C entry point, `eventHandler`. Export it with `@c`, call `Playdate.initialize(with:)` on the first event, and install an update callback:
|
||||
|
||||
```swift
|
||||
import CPlaydate
|
||||
import PlaydateKit
|
||||
|
||||
@_cdecl("eventHandler")
|
||||
@c(eventHandler)
|
||||
func eventHandler(
|
||||
pointer: UnsafeMutableRawPointer,
|
||||
event: PDSystemEvent,
|
||||
@@ -111,6 +104,10 @@ final class Game {
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
System.log("paused")
|
||||
}
|
||||
|
||||
func update() {
|
||||
let (_, pushed, _) = System.buttonState
|
||||
if pushed.contains(.a) {
|
||||
@@ -123,14 +120,14 @@ final class Game {
|
||||
}
|
||||
```
|
||||
|
||||
`Playdate.initialize(with:)` stores the API pointer once; every wrapper in the module uses it from then on. Calling any wrapper before `initialize` is a programmer error and will crash.
|
||||
Calling any wrapper before `Playdate.initialize(with:)` crashes.
|
||||
|
||||
## Tour of the API
|
||||
|
||||
### System: input, time, menu
|
||||
|
||||
```swift
|
||||
// Buttons are an OptionSet: current (held), pushed and released this frame.
|
||||
// Buttons: held now, pushed this frame, released this frame.
|
||||
let (current, pushed, released) = System.buttonState
|
||||
if current.contains([.b, .down]) { /* charge shot */ }
|
||||
|
||||
@@ -140,11 +137,11 @@ if !System.isCrankDocked {
|
||||
spin(by: System.crankChange)
|
||||
}
|
||||
|
||||
// Accelerometer is a peripheral you enable first.
|
||||
// Enable the accelerometer before reading it.
|
||||
System.setPeripheralsEnabled(.accelerometer)
|
||||
let (x, y, z) = System.accelerometer
|
||||
|
||||
// System menu items take closures; the binding keeps them alive until removed.
|
||||
// Menu items stay alive until removed.
|
||||
System.addCheckmarkMenuItem(title: "music", isChecked: true) { item in
|
||||
Audio.musicEnabled = item.isChecked
|
||||
}
|
||||
@@ -152,15 +149,14 @@ System.addOptionsMenuItem(title: "mode", options: ["easy", "hard"]) { item in
|
||||
Game.shared.difficulty = item.value
|
||||
}
|
||||
|
||||
// Logging goes to the simulator console or device serial.
|
||||
// Logs go to the Simulator console or the device's serial port.
|
||||
System.log("spawned \(count) enemies")
|
||||
System.error("unrecoverable") // stops execution
|
||||
System.error("unrecoverable") // stops the game
|
||||
```
|
||||
|
||||
### Graphics: drawing, bitmaps, fonts
|
||||
|
||||
Fallible loads (`Bitmap(path:)`, `Font(path:)`, …) throw `PlaydateError`,
|
||||
which carries the message produced by the OS:
|
||||
Loads (`Bitmap(path:)`, `Font(path:)`, …) throw `PlaydateError` with the OS's message:
|
||||
|
||||
```swift
|
||||
let font = try Graphics.Font(path: "fonts/Asheville-Sans-14-Bold.pft")
|
||||
@@ -170,13 +166,14 @@ 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.
|
||||
// Colors are solid or 8×8 patterns. On macOS 26+, the device, and Linux,
|
||||
// `rows:` also takes an array literal.
|
||||
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.
|
||||
// Draw into a bitmap by pushing it as the drawing context.
|
||||
let logo = try Graphics.Bitmap(path: "images/logo")
|
||||
logo.draw(x: 168, y: 88)
|
||||
|
||||
@@ -194,7 +191,7 @@ ball.setImage(try Graphics.Bitmap(path: "images/ball"))
|
||||
ball.moveTo(x: 200, y: 120)
|
||||
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
|
||||
ball.add() // the display list keeps the sprite alive until it is removed
|
||||
|
||||
// In the update callback:
|
||||
let (actual, collisions) = ball.moveWithCollisions(goalX: goalX, goalY: goalY)
|
||||
@@ -202,23 +199,22 @@ for collision in collisions where collision.other.tag == Tags.brick {
|
||||
collision.other.remove()
|
||||
}
|
||||
|
||||
// Every collision/query API also has a visitor form that iterates the
|
||||
// results in place instead of building an array — useful in hot loops:
|
||||
// Collision and query APIs also have a visitor form that allocates no array:
|
||||
ball.moveWithCollisions(goalX: goalX, goalY: goalY) { collision in
|
||||
if collision.other.tag == Tags.brick { collision.other.remove() }
|
||||
}
|
||||
```
|
||||
|
||||
Sprite callbacks (`setUpdateFunction`, `setDrawFunction`, `setCollisionResponseFunction`) receive the Swift wrapper back. The C-level sprite userdata slot is reserved by the binding for that recovery — use the `userdata` property on `Sprite` for your own per-sprite storage instead.
|
||||
The binding owns the C userdata slot; store your own per-sprite data in `Sprite.userdata`.
|
||||
|
||||
### Sound
|
||||
|
||||
```swift
|
||||
// Stream music from disk.
|
||||
// Stream from disk.
|
||||
let music = try Sound.FilePlayer(path: "audio/theme")
|
||||
music.play(repeat: 0) // 0 = loop forever
|
||||
|
||||
// Play short effects from memory.
|
||||
// Play from memory.
|
||||
let blip = try Sound.SamplePlayer(path: "audio/blip")
|
||||
blip.play()
|
||||
|
||||
@@ -236,7 +232,7 @@ let filter = Sound.TwoPoleFilter(kind: .lowPass)
|
||||
filter.setFrequency(800)
|
||||
channel.addEffect(filter)
|
||||
|
||||
// Anything that takes a modulator accepts any SignalValue (LFO, Envelope, …).
|
||||
// Modulator properties accept any SignalValue (LFO, Envelope, …).
|
||||
let wobble = Sound.LFO(shape: .sine)
|
||||
wobble.setRate(2)
|
||||
synth.frequencyModulator = wobble
|
||||
@@ -245,7 +241,7 @@ synth.frequencyModulator = wobble
|
||||
### Files and JSON
|
||||
|
||||
```swift
|
||||
// Paths resolve against the game's Data directory and pdx per the open mode.
|
||||
// The open mode decides whether paths resolve in the Data directory or the pdx.
|
||||
let save = try File.Handle(path: "save.json", mode: .write)
|
||||
try save.write(JSON.encode(.table([
|
||||
"level": .int(3),
|
||||
@@ -265,7 +261,7 @@ try File.listFiles(at: "replays") { name in
|
||||
|
||||
### Network
|
||||
|
||||
Network access requires user permission per server:
|
||||
Each server needs the user's permission:
|
||||
|
||||
```swift
|
||||
let reply = Network.HTTPConnection.requestAccess(
|
||||
@@ -278,7 +274,7 @@ func fetch() {
|
||||
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 …
|
||||
// Keep `connection` referenced until this callback fires.
|
||||
}
|
||||
try? connection.get(path: "/daily.json")
|
||||
}
|
||||
@@ -286,8 +282,7 @@ func fetch() {
|
||||
|
||||
### Lua interop
|
||||
|
||||
Lua callbacks are C function pointers with no context, so they must be
|
||||
`@convention(c)` functions rather than capturing closures:
|
||||
Lua callbacks are C function pointers with no context, so they cannot capture:
|
||||
|
||||
```swift
|
||||
let double: Lua.CFunction = { _ in
|
||||
@@ -299,95 +294,80 @@ try Lua.addFunction(double, name: "mylib.double")
|
||||
|
||||
## Conventions
|
||||
|
||||
- **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: `PlaydateKit.System`.
|
||||
- **Properties vs. methods.** State the OS can report back is a property: read-write where the C API has a get/set pair (`Display.refreshRate`, `Source.volume`), get-only where it only has a getter (`Display.fps`). A `set…` method means the C API is write-only there (`Display.setScale`, `Synth.setAttackTime`) or setting takes extra arguments — a property getter never invents a value the OS can't return. Callbacks are installed with `set…Callback`/`set…Function` methods.
|
||||
- **Paths.** APIs that load a single file label the argument `path:` (`Bitmap(path:)`, `JSON.decodeFile(path:)`); directory operations use `at:` (`File.listFiles(at:)`). The POSIX-named `File.stat`, `File.mkdir`, and `File.unlink` take their path unlabeled, like their C namesakes.
|
||||
- **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 OS-owned objects (a `Bitmap` from a `BitmapTable`, a track from a `Sequence`, …) don't free them — keep the owner alive instead, as documented on each API. Resources a C object keeps referencing (a sprite's image, a synth's sample, modulators, menu-item option titles) are retained by the wrapper automatically.
|
||||
- **Callbacks.** Where the C API provides a userdata slot, closures are supported everywhere and delivered back with the right wrapper. A few C callbacks have no userdata (serial messages, headphone changes, scoreboard
|
||||
completions, `getServerTime`); those track one Swift closure at a time, as noted in their documentation.
|
||||
- **Threading.** The Playdate runtime is single-threaded (audio callbacks excepted); statics in the binding are `nonisolated(unsafe)` on that basis. Don't call the API from other threads.
|
||||
- **Namespaces.** Subsystems are top-level; only the bootstrap lives in `Playdate`. Qualify on a clash: `PlaydateKit.System`.
|
||||
- **Properties vs. methods.** Readable state is a property; `set…` methods are write-only or take extra arguments. Callbacks: `set…Callback` / `set…Function`.
|
||||
- **Paths.** `path:` for files, `at:` for directories; `File.stat`/`mkdir`/`unlink` are unlabeled like C.
|
||||
- **Errors.** Typed throws: `PlaydateError`, or `Network.NetError` for network I/O.
|
||||
- **Buffers.** `Span`/`MutableSpan`, valid only during the call. Mono audio gets an empty `right`.
|
||||
- **Ownership.** Wrappers free what they create on `deinit` and retain what their C object references. Objects vended by the OS (a table's `Bitmap`, a `Sequence` track) need their owner alive. `File.Handle` is non-copyable and closes at end of scope or on `close()`.
|
||||
- **Callbacks.** C callbacks without a userdata slot (serial, headphones, scoreboards, `getServerTime`) keep one closure at a time.
|
||||
- **Threading.** Single-threaded except audio callbacks; don't call the API from other threads.
|
||||
|
||||
## Building for the simulator and device
|
||||
|
||||
This package builds as a plain Swift library, which is how you develop and unit-test game logic on the host (`swift test` works out of the box).
|
||||
Game logic builds and tests on the host with `swift build` / `swift test`. For a `.pdx`:
|
||||
|
||||
Shipping a `.pdx` needs the Playdate toolchain on top:
|
||||
- **Simulator:** a host dylib inside the pdx; see `Examples/HelloPlaydate/build.sh`.
|
||||
- **Device:** Embedded Swift for `armv7em-none-none-eabi` (`-fshort-enums`), linked by the SDK's make rules.
|
||||
|
||||
- **Simulator** builds compile your game as a host dylib placed in the pdx (`Examples/HelloPlaydate/build.sh` shows the SwiftPM-based flow).
|
||||
- **Device** builds cross-compile with Embedded Swift for ARM Cortex-M7 (`-enable-experimental-feature Embedded`, triple `armv7em-none-none-eabi`, `-fshort-enums` to match the firmware ABI) and link through the SDK's own make infrastructure.
|
||||
[`Examples/swift.mk`](Examples/swift.mk) runs the device pipeline; a game adds a short Makefile like [`Examples/HelloPlaydate/Makefile`](Examples/HelloPlaydate/Makefile), and `make` builds a `.pdx` with both binaries. Swift compiles `-Osize` (~15% smaller than `-O`); override with `make SWIFT_OPT=-O`. Adapted from Apple's [swift-playdate-examples](https://github.com/apple/swift-playdate-examples).
|
||||
|
||||
[`Examples/swift.mk`](Examples/swift.mk) packages the device pipeline: it locates the SDK and a Swift snapshot toolchain, compiles the `PlaydateKit` wrapper as a module-aliased Embedded Swift module, and hooks the game objects into the SDK's `common.mk`. A game needs only a short Makefile on top — [`Examples/HelloPlaydate/Makefile`](Examples/HelloPlaydate/Makefile) is the template, and `make` in that directory produces a `.pdx` containing both the device binary and the simulator dylib. Swift code is compiled `-Osize` by default (measured ~15% smaller than `-O` on the example); override with `make SWIFT_OPT=-O`. The setup follows Apple's [swift-playdate-examples](https://github.com/apple/swift-playdate-examples), adapted to this package.
|
||||
|
||||
The wrappers are written within the Embedded Swift subset for exactly this reason: no Foundation, no reflection, no untyped throws. That claim is enforced, not aspirational: the check cross-compiles the whole module for `armv7em-none-none-eabi` with Embedded Swift enabled and the device's `-fshort-enums` ABI. Running it needs the same snapshot toolchain and Arm GNU toolchain headers:
|
||||
|
||||
```sh
|
||||
make embedded
|
||||
```
|
||||
|
||||
The target picks up a snapshot toolchain installed at `~/Library/Developer/Toolchains` automatically; point it at another one with `make embedded SWIFT_BIN=<path to swift>`.
|
||||
The library stays within Embedded Swift (no Foundation, reflection, or untyped throws). `make embedded` checks this by compiling the module for the device; it uses `~/Library/Developer/Toolchains/swift-latest.xctoolchain` unless `SWIFT_BIN=<path to swift>` is set.
|
||||
|
||||
## Make targets
|
||||
|
||||
A root Makefile fronts the development lifecycle; a bare `make` (or `make help`) lists the targets. `build`, `test`, `docs`, and `consumer-test` need only Xcode's toolchain (after the one-time `make setup`); `embedded` and the example targets additionally need the device toolchains above.
|
||||
`embedded` and `example*` need the device toolchains; the rest need only Xcode and `make setup`.
|
||||
|
||||
| Target | Effect |
|
||||
|---|---|
|
||||
| `make setup` | One-time: point the `playdate` pkg-config module at the SDK |
|
||||
| `make setup` | Point the `playdate` pkg-config module at the SDK (once per machine) |
|
||||
| `make build` / `make test` | Build the bindings for the host / run the unit tests |
|
||||
| `make outdated` / `make upgrade` | Show / apply updates to the SwiftPM dependencies |
|
||||
| `make embedded` | Compile-only device check (Embedded Swift, `armv7em-none-none-eabi`) |
|
||||
| `make consumer-test` | Build and run a scratch package depending on playdate-kit |
|
||||
| `make check` | The full verification suite: build, test, embedded, consumer-test |
|
||||
| `make check` | `build`, `test`, `embedded`, and `consumer-test` |
|
||||
| `make docs` / `make docs-preview` | Generate / preview the DocC documentation |
|
||||
| `make example` / `make example-run` | Build the HelloPlaydate example / open it in the Playdate Simulator |
|
||||
| `make clean` | Remove build products of the package and the example |
|
||||
|
||||
## Example
|
||||
|
||||
[`Examples/HelloPlaydate`](Examples/HelloPlaydate) is a complete minimal game — bouncing box, crank needle, button handling, a system menu item — that builds into a runnable `.pdx`:
|
||||
[`Examples/HelloPlaydate`](Examples/HelloPlaydate): a bouncing box, crank needle, buttons, and a menu item.
|
||||
|
||||
```sh
|
||||
cd Examples/HelloPlaydate
|
||||
|
||||
# Simulator only (plain SwiftPM, no extra toolchains):
|
||||
# Simulator only (SwiftPM, no extra toolchains):
|
||||
./build.sh
|
||||
open -a "$HOME/Developer/PlaydateSDK/bin/Playdate Simulator.app" HelloPlaydate.pdx
|
||||
|
||||
# Device + simulator (snapshot toolchain and arm-none-eabi-gcc required):
|
||||
# Device and Simulator (swift.org toolchain and arm-none-eabi-gcc required):
|
||||
make
|
||||
```
|
||||
|
||||
Sideload the device build from the Playdate Simulator (Device ▸ Upload Game to Device) or with the SDK's `pdutil`.
|
||||
Install on a device with Device ▸ Upload Game to Device in the Simulator, or `pdutil`.
|
||||
|
||||
## Documentation
|
||||
|
||||
The API reference is a DocC catalog. Generate it locally with:
|
||||
|
||||
```sh
|
||||
make docs
|
||||
```
|
||||
|
||||
serve it in a local web server with `make docs-preview`, or browse it with Xcode's documentation viewer (Product ▸ Build Documentation).
|
||||
DocC: `make docs`, `make docs-preview`, or Xcode's Product ▸ Build Documentation.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
Examples/
|
||||
swift.mk Shared make rules for device builds: toolchain/SDK discovery and Embedded Swift compile flags
|
||||
HelloPlaydate/ Minimal game buildable into a .pdx for the simulator (build.sh) or simulator + device (make)
|
||||
swift.mk Device build rules
|
||||
HelloPlaydate/ Example game
|
||||
Scripts/
|
||||
install-pkgconfig.sh One-time setup: points the "playdate" pkg-config module at your SDK installation
|
||||
build-embedded.sh Compile-only device check: Embedded Swift for armv7em-none-none-eabi with the device ABI
|
||||
consumer-test.sh Builds a scratch package depending on playdate-kit to prove settings propagate to consumers
|
||||
install-pkgconfig.sh Writes the playdate pkg-config module
|
||||
build-embedded.sh Device compile check
|
||||
consumer-test.sh Builds a package that depends on playdate-kit
|
||||
Sources/
|
||||
CPlaydate/ System library target: module map + umbrella header importing pd_api.h from the SDK, plus inline shims for the variadic log/error functions
|
||||
PlaydateKit/ The Swift bindings, one folder per subsystem (System, Graphics, Sound, ...) holding one type per file, grouped by kind (Classes, Structures, Enumerations, Aliases);
|
||||
Sound is further split into the Source, Signal, Effect, and Synth subdomains. Also holds the shared Support.swift helpers and the PlaydateKit.docc documentation catalog
|
||||
CPlaydate/ pd_api.h module and log/error shims
|
||||
PlaydateKit/ Bindings: a folder per subsystem, a type per file, plus the DocC catalog
|
||||
Tests/
|
||||
PlaydateKit/ Host-runnable tests for the pure value types
|
||||
PlaydateKit/ Tests against a mock PlaydateAPI
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE). The Playdate SDK itself is licensed separately by Panic, Inc. and is not distributed with this package.
|
||||
MIT ([LICENSE](LICENSE)). The Playdate SDK is licensed separately by Panic, Inc.
|
||||
|
||||
Reference in New Issue
Block a user