Skip to content

Commit

Permalink
Merge pull request #92 from samdze/release/0.21.0
Browse files Browse the repository at this point in the history
Release/0.21.0
  • Loading branch information
ninovanhooff authored Dec 28, 2024
2 parents 1a752d3 + 2c4ce20 commit 405d0b2
Show file tree
Hide file tree
Showing 15 changed files with 251 additions and 57 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,3 @@ This project is a work in progress, here's what is missing right now:
- various playdate.sound funcionalities (but FilePlayer, SamplePlayer and SoundSequence are available)
- playdate.json, but you can use Nim std/json, which is very convenient
- advanced playdate.lua features, but basic Lua interop is available
- playdate.scoreboards, undocumented even in the official C API docs
2 changes: 1 addition & 1 deletion playdate.nimble
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Package

version = "0.20.0"
version = "0.21.0"
author = "Samuele Zolfanelli"
description = "Playdate Nim bindings with extra features."
license = "MIT"
Expand Down
8 changes: 7 additions & 1 deletion playdate_example/src/playdate_example.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import playdate/api
const FONT_PATH = "/System/Fonts/Asheville-Sans-14-Bold.pft"
const NIM_IMAGE_PATH = "/images/nim_logo"
const PLAYDATE_NIM_IMAGE_PATH = "/images/playdate_nim"
const BACKGROUND_MUSIC_PATH = "/audio/finally_see_the_light"
const BACKGROUND_MUSIC_SAMPLE_RATE = 48_000
const BACKGROUND_MUSIC_FADE_IN_SECONDS = 4.0
const BACKGROUND_MUSIC_FADE_IN_SAMPLES = (BACKGROUND_MUSIC_SAMPLE_RATE * BACKGROUND_MUSIC_FADE_IN_SECONDS).int32

var font: LCDFont

Expand Down Expand Up @@ -80,9 +84,11 @@ proc handler(event: PDSystemEvent, keycode: uint) {.raises: [].} =
except:
playdate.system.logToConsole(getCurrentExceptionMsg())
# Inline try/except
filePlayer = try: playdate.sound.newFilePlayer("/audio/finally_see_the_light") except: nil
filePlayer = try: playdate.sound.newFilePlayer(BACKGROUND_MUSIC_PATH) except: nil

filePlayer.play(0)
fileplayer.volume = 0.0 # first set folume to 0%
filePlayer.fadeVolume(1.0, 1.0, BACKGROUND_MUSIC_FADE_IN_SAMPLES, nil) # then fade to 100%

# Add a checkmark menu item that plays a sound when switched and unpaused
discard playdate.system.addCheckmarkMenuItem("Checkmark", false,
Expand Down
4 changes: 2 additions & 2 deletions src/playdate/api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import std/importutils
import bindings/api
export api

import graphics, system, file, sprite, display, sound, lua, json, utils, types, nineslice
export graphics, system, file, sprite, display, sound, lua, json, utils, types, nineslice
import graphics, system, file, sprite, display, sound, scoreboards, lua, json, utils, types, nineslice
export graphics, system, file, sprite, display, sound, scoreboards, lua, json, utils, types, nineslice

macro initSDK*() =
return quote do:
Expand Down
3 changes: 2 additions & 1 deletion src/playdate/bindings/api.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{.push raises: [].}

import graphics, system, file, display, sprite, sound, lua
import graphics, system, file, display, sprite, sound, scoreboards, lua

type PlaydateAPI* {.importc: "PlaydateAPI", header: "pd_api.h".} = object
system* {.importc: "system".}: ptr PlaydateSys
Expand All @@ -9,6 +9,7 @@ type PlaydateAPI* {.importc: "PlaydateAPI", header: "pd_api.h".} = object
sprite* {.importc: "sprite".}: ptr PlaydateSprite
display* {.importc: "display".}: ptr PlaydateDisplay
sound* {.importc: "sound".}: ptr PlaydateSound
scoreboards* {.importc: "scoreboards".}: ptr PlaydateScoreboards
lua* {.importc: "lua".}: ptr PlaydateLua
# json* {.importc: "json".}: ptr PlaydateJSON # Unavailable, use std/json

Expand Down
81 changes: 44 additions & 37 deletions src/playdate/bindings/scoreboards.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,52 @@

import utils

type PDScore* {.importc: "PDScore", header: "pd_api_scoreboards.h", bycopy.} = object
rank* {.importc: "rank".}: uint32
value* {.importc: "value".}: uint32
player* {.importc: "player".}: cstring

type PDScoresList* {.importc: "PDScoresList", header: "pd_api_scoreboards.h", bycopy.} = object
boardID* {.importc: "boardID".}: cstring
count* {.importc: "count".}: cuint
lastUpdated* {.importc: "lastUpdated".}: uint32
playerIncluded* {.importc: "playerIncluded".}: cint
limit* {.importc: "limit".}: cuint
scores* {.importc: "scores".}: ptr PDScore

type PDBoard* {.importc: "PDBoard", header: "pd_api_scoreboards.h", bycopy.} = object
boardID* {.importc: "boardID".}: cstring
name* {.importc: "name".}: cstring

type PDBoardsList* {.importc: "PDBoardsList", header: "pd_api_scoreboards.h", bycopy.} = object
count* {.importc: "count".}: cuint
lastUpdated* {.importc: "lastUpdated".}: uint32
boards* {.importc: "boards".}: ptr PDBoard

type AddScoreCallback* = proc (score: ptr PDScore; errorMessage: cstring) {.cdecl.}
type PersonalBestCallback* = proc (score: ptr PDScore; errorMessage: cstring) {.cdecl.}
type BoardsListCallback* = proc (boards: ptr PDBoardsList; errorMessage: cstring) {.cdecl.}
type ScoresCallback* = proc (scores: ptr PDScoresList; errorMessage: cstring) {.cdecl.}
type
PDScoreRaw* {.importc: "PDScore", header: "pd_api.h", bycopy.} = object
rank* {.importc: "rank".}: cuint
value* {.importc: "value".}: cuint
player* {.importc: "player".}: cstring

PDScorePtr* = ptr PDScoreRaw

PDScoresListRaw* {.importc: "PDScoresList", header: "pd_api.h", bycopy.} = object
boardID* {.importc: "boardID".}: cstring
count* {.importc: "count".}: cuint
lastUpdated* {.importc: "lastUpdated".}: cuint
playerIncluded* {.importc: "playerIncluded".}: cuint
limit* {.importc: "limit".}: cuint
scores* {.importc: "scores".}: ptr UncheckedArray[PDScoreRaw]

PDScoresListPtr* = ptr PDScoresListRaw

PDBoardRaw* {.importc: "PDBoard", header: "pd_api.h", bycopy.} = object
boardID* {.importc: "boardID".}: cstring
name* {.importc: "name".}: cstring

PDBoardsListRaw* {.importc: "PDBoardsList", header: "pd_api.h", bycopy.} = object
count* {.importc: "count".}: cuint
lastUpdated* {.importc: "lastUpdated".}: cuint
boards* {.importc: "boards".}: ptr UncheckedArray[PDBoardRaw]

PDBoardsListPtr* = ptr PDBoardsListRaw

PersonalBestCallbackRaw* {.importc: "PersonalBestCallback", header: "pd_api.h".} = proc (score: PDScorePtr; errorMessage: cstring) {.cdecl.}
AddScoreCallbackRaw* {.importc: "AddScoreCallback", header: "pd_api.h".} = proc (score: PDScorePtr; errorMessage: cstring) {.cdecl.}
BoardsListCallbackRaw* = proc (boards: ptr PDBoardsListRaw; errorMessage: cstring) {.cdecl.}
ScoresCallbackRaw* = proc (scores: ptr PDScoresListRaw; errorMessage: cstring) {.cdecl.}

sdktype:
type PlaydateScoreboards* {.importc: "const struct playdate_scoreboards", header: "pd_api.h".} = object
addScore* {.importc: "addScore".}: proc (boardId: cstring; value: uint32;
callback: AddScoreCallback): cint {.cdecl.}
getPersonalBest* {.importc: "getPersonalBest".}: proc (boardId: cstring;
callback: PersonalBestCallback): cint {.cdecl.}
freeScore* {.importc: "freeScore".}: proc (score: ptr PDScore) {.cdecl.}
getScoreboards* {.importc: "getScoreboards".}: proc (
callback: BoardsListCallback): cint {.cdecl.}
getPersonalBestBinding* {.importc: "getPersonalBest".}: proc (boardId: cstring;
callback: PersonalBestCallbackRaw): cint {.cdecl, raises: [].}
addScoreBinding* {.importc: "addScore".}: proc (boardId: cstring; value: cuint;
callback: AddScoreCallbackRaw): cint {.cdecl, raises: [].}
freeScore* {.importc: "freeScore".}: proc (score: PDScorePtr) {.cdecl, raises: [].}
getScoreboardsBinding* {.importc: "getScoreboards".}: proc (
callback: BoardsListCallbackRaw): cint {.cdecl, raises: [].}
freeBoardsList* {.importc: "freeBoardsList".}: proc (
boardsList: ptr PDBoardsList) {.cdecl.}
getScores* {.importc: "getScores".}: proc (boardId: cstring;
callback: ScoresCallback): cint {.cdecl.}
boardsList: PDBoardsListPtr) {.cdecl, raises: [].}
getScoresBinding* {.importc: "getScores".}: proc (boardId: cstring;
callback: ScoresCallbackRaw): cint {.cdecl, raises: [].}
freeScoresList* {.importc: "freeScoresList".}: proc (
scoresList: ptr PDScoresList) {.cdecl.}
scoresList: PDScoresListPtr) {.cdecl, raises: [].}
8 changes: 4 additions & 4 deletions src/playdate/bindings/sound.nim
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ type PlaydateSoundFileplayer {.importc: "const struct playdate_sound_fileplayer"
# start: cfloat; `end`: cfloat) {.cdecl.}
# didUnderrun* {.importc: "didUnderrun".}: proc (player: ptr FilePlayer): cint {.cdecl.}
setFinishCallback* {.importc: "setFinishCallback".}: proc (
player: FilePlayerPtr; callback: PDSndCallbackProcRaw, userData: pointer = nil) {.cdecl, raises: [].}
player: FilePlayerPtr; callback: PDSndCallbackProcRaw, userdata: pointer = nil) {.cdecl, raises: [].}
# setLoopCallback* {.importc: "setLoopCallback".}: proc (player: ptr FilePlayer;
# callback: SndCallbackProc) {.cdecl.}
getOffset {.importc: "getOffset".}: proc (player: FilePlayerPtr): cfloat {.cdecl, raises: [].}
# getRate* {.importc: "getRate".}: proc (player: ptr FilePlayer): cfloat {.cdecl.}
# setStopOnUnderrun* {.importc: "setStopOnUnderrun".}: proc (
# player: ptr FilePlayer; flag: cint) {.cdecl.}
# fadeVolume* {.importc: "fadeVolume".}: proc (player: ptr FilePlayer; left: cfloat;
# right: cfloat; len: int32T; finishCallback: SndCallbackProc) {.cdecl.}
fadeVolume* {.importc: "fadeVolume".}: proc (player: FilePlayerPtr; left: cfloat;
right: cfloat; len: cint; finishCallback: PDSndCallbackProcRaw, userdata: pointer = nil) {.cdecl, raises:[].}
# setMP3StreamSource* {.importc: "setMP3StreamSource".}: proc (
# player: ptr FilePlayer; dataSource: proc (data: ptr uint8T; bytes: cint;
# userdata: pointer): cint {.cdecl.}; userdata: pointer; bufferLen: cfloat) {.
Expand Down Expand Up @@ -90,7 +90,7 @@ type PlaydateSoundSampleplayer {.importc: "const struct playdate_sound_samplepla
setPlayRange* {.importc: "setPlayRange".}: proc (player: SamplePlayerPtr;
start: cint; `end`: cint) {.cdecl, raises: [].}
setFinishCallback* {.importc: "setFinishCallback".}: proc (
player: SamplePlayerPtr; callback: PDSndCallbackProcRaw, userData: pointer = nil) {.cdecl, raises: [].}
player: SamplePlayerPtr; callback: PDSndCallbackProcRaw, userdata: pointer = nil) {.cdecl, raises: [].}
# setLoopCallback* {.importc: "setLoopCallback".}: proc (player: ptr SamplePlayer;
# callback: SndCallbackProc) {.cdecl.}
getOffset* {.importc: "getOffset".}: proc (player: SamplePlayerPtr): cfloat {.cdecl , raises: [].}
Expand Down
7 changes: 7 additions & 0 deletions src/playdate/bindings/utils.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import macros

iterator items*[T](rawField: ptr UncheckedArray[T], len: Natural): T =
## iterate through a C array
## To convert to a Nim seq:
## `cArray.items(count).toSeq`
for i in 0..<len:
yield rawField[i]

func toNimSymbol(typeSymbol: string): string =
case typeSymbol:
of "cint":
Expand Down
16 changes: 11 additions & 5 deletions src/playdate/build/config.nim
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ when not compiles(task):
import system/nimscript

const headlessTesting = defined(simulator) and declared(test)
const nimbleTesting = not defined(simulator) and not defined(devide) and declared(test)
const nimbleTesting = not defined(simulator) and not defined(device) and declared(test)
const testing = headlessTesting or nimbleTesting

# Use the host OS for compilation. This is useful when running supporting development tools that import the playdate SDK or where the os module needs to be available.
# This does not make the playdate api callable, only the types (header files) are available.
const useHostOS = defined(useHostOS)

# Path to the playdate src directory when checked out locally
const localPlaydatePath = currentSourcePath / "../../../../src"

Expand All @@ -24,7 +28,7 @@ let nimblePlaydatePath =
else:
gorgeEx("nimble path playdate").output.split("\n")[0]

if not testing:
if not testing and not useHostOS:
switch("noMain", "on")
switch("backend", "c")
switch("mm", "arc")
Expand All @@ -45,7 +49,9 @@ switch("passC", "-Wno-unknown-pragmas")
switch("passC", "-Wdouble-promotion")
switch("passC", "-I" & sdkPath() / "C_API")

switch("os", "any")
if not useHostOS:
echo "Setting os to any"
switch("os", "any")
switch("define", "useMalloc")
switch("define", "standalone")
switch("threads", "off")
Expand Down Expand Up @@ -133,8 +139,8 @@ when defined(simulator):
switch("passC", "-DTARGET_SIMULATOR=1")
switch("passC", "-Wstrict-prototypes")

if nimbleTesting:
# Compiling for tests.
if useHostOS or nimbleTesting:
# Compiling for host system environment.
switch("define", "simulator")
switch("nimcache", nimcacheDir() / "simulator")

Expand Down
9 changes: 6 additions & 3 deletions src/playdate/graphics.nim
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ proc set*(view: var BitmapView, x, y: int, color: LCDSolidColor) =

proc getDebugBitmap*(this: ptr PlaydateGraphics): LCDBitmap =
privateAccess(PlaydateGraphics)
return LCDBitmap(resource: this.getDebugBitmap(), free: true) # Who should manage this memory? Not clear. Auto-managed.
return LCDBitmap(resource: this.getDebugBitmap(), free: false) # do not free: system owns this

proc copyFrameBufferBitmap*(this: ptr PlaydateGraphics): LCDBitmap =
privateAccess(PlaydateGraphics)
Expand Down Expand Up @@ -411,9 +411,12 @@ proc set*(this: var LCDBitmap, x, y: int, color: LCDSolidColor = kColorBlack) =
var data = this.getData
data.set(x, y, color)

proc setStencilImage*(this: ptr PlaydateGraphics, bitmap: LCDBitmap, tile: bool) =
proc setStencilImage*(this: ptr PlaydateGraphics, bitmap: LCDBitmap, tile: bool = false) =
privateAccess(PlaydateGraphics)
this.setStencilImage(bitmap.resource, if tile: 1 else: 0)
if bitmap == nil:
this.setStencilImage(nil, if tile: 1 else: 0)
else:
this.setStencilImage(bitmap.resource, if tile: 1 else: 0)

proc makeFont*(this: LCDFontData, wide: bool): LCDFont =
privateAccess(PlaydateGraphics)
Expand Down
117 changes: 117 additions & 0 deletions src/playdate/scoreboards.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
{.push raises: [].}

import std/[importutils, sequtils]

import types {.all.}
import bindings/[api, types, utils]
import bindings/scoreboards

# Only export public symbols, then import all
export scoreboards
{.hint[DuplicateModuleImport]: off.}
import bindings/scoreboards {.all.}

type
PDScore* = object of RootObj
value*, rank*: uint32
player*: string

PDScoresList* = object of RootObj
boardID*: string
lastUpdated*: uint32
scores*: seq[PDScore]
# these properties are not implemented yet in the Playdate API
# playerIncluded*: uint32
# limit*: uint32

PDBoard* = object of RootObj
boardID*, name*: string

PDBoardsList* = object of RootObj
lastUpdated*: uint32
boards*: seq[PDBoard]

PDResultKind* = enum
PDResultSuccess,
PDResultUnavailable,
## The operation completed successfully, but the response had no data
PDResultError,

PDResult*[T] = object
case kind*: PDResultKind
of PDResultSuccess: result*: T
of PDResultUnavailable: discard
of PDResultError: message*: string

PersonalBestCallback* = proc(result: PDResult[PDScore]) {.raises: [].}
AddScoreCallback* = proc(result: PDResult[PDScore]) {.raises: [].}
BoardsListCallback* = proc(result: PDResult[PDBoardsList]) {.raises: [].}
ScoresCallback* = proc(result: PDResult[PDScoresList]) {.raises: [].}

var
# The sdk callbacks unfortunately don't provide a userdata field to tag the callback with eg. the boardID
# Scoreboard responses are handled in order of request, however, so if we keep track of request order everything should be fine.
# By inserting the callback at the start, it will be popped last: first in, first out
privatePersonalBestCallbacks = newSeq[PersonalBestCallback]()
privateAddScoreCallbacks = newSeq[AddScoreCallback]()
privateScoresCallbacks = newSeq[ScoresCallback]()
privateBoardsListCallbacks = newSeq[BoardsListCallback]()

template invokeCallback(callbackSeqs, value, errorMessage, freeValue, builder: untyped) =
type ResultType = typeof(builder)
let callback = callbackSeqs.pop()
if value == nil:
if errorMessage == nil:
callback(PDResult[ResultType](kind: PDResultUnavailable))
else:
callback(PDResult[ResultType](kind: PDResultError, message: $errorMessage))
else:
try:
let built = builder
callback(PDResult[ResultType](kind: PDResultSuccess, result: built))
finally:
freeValue(value)

proc scoreBuilder(score: PDScoreRaw | PDScorePtr): PDScore =
PDSCore(value: score.value.uint32, rank: score.rank.uint32, player: $score.player)

proc invokePersonalBestCallback(score: PDScorePtr, errorMessage: ConstChar) {.cdecl, raises: [].} =
invokeCallback(privatePersonalBestCallbacks, score, errorMessage, playdate.scoreboards.freeScore):
scoreBuilder(score)

proc invokeAddScoreCallback(score: PDScorePtr, errorMessage: ConstChar) {.cdecl, raises: [].} =
invokeCallback(privateAddScoreCallbacks, score, errorMessage, playdate.scoreboards.freeScore):
scoreBuilder(score)

proc invokeScoresCallback(scoresList: PDScoresListPtr, errorMessage: ConstChar) {.cdecl, raises: [].} =
invokeCallback(privateScoresCallbacks, scoresList, errorMessage, playdate.scoreboards.freeScoresList):
let scoresSeq = scoresList.scores.items(scoresList.count).toSeq.mapIt(scoreBuilder(it))
PDScoresList(boardID: $scoresList.boardID, lastUpdated: scoresList.lastUpdated, scores: scoresSeq)

proc invokeBoardsListCallback(boardsList: PDBoardsListPtr, errorMessage: ConstChar) {.cdecl, raises: [].} =
invokeCallback(privateBoardsListCallbacks, boardsList, errorMessage, playdate.scoreboards.freeBoardsList):
let boardsSeq = boardsList.boards.items(boardsList.count).toSeq
.mapIt(PDBoard(boardID: $it.boardID, name: $it.name))
PDBoardsList(lastUpdated: boardsList.lastUpdated, boards: boardsSeq)

proc getPersonalBest*(this: ptr PlaydateScoreboards, boardID: string, callback: PersonalBestCallback): int32 {.discardable.} =
## Responds with PDResultUnavailable if no score exists for the current player.
privateAccess(PlaydateScoreboards)
privatePersonalBestCallbacks.insert(callback) # by inserting the callback at the start, it will be popped last: first in, first out
return this.getPersonalBestBinding(boardID.cstring, invokePersonalBestCallback)

proc addScore*(this: ptr PlaydateScoreboards, boardID: string, value: uint32, callback: AddScoreCallback): int32 {.discardable.} =
## Responds with PDResultUnavailable if the score was queued for later submission. Probably, Wi-Fi is not available.
privateAccess(PlaydateScoreboards)
privateAddScoreCallbacks.insert(callback) # by inserting the callback at the start, it will be popped last: first in, first out
return this.addScoreBinding(boardID.cstring, value.cuint, invokeAddScoreCallback)

proc getScoreboards*(this: ptr PlaydateScoreboards, callback: BoardsListCallback): int32 {.discardable.} =
privateAccess(PlaydateScoreboards)
privateBoardsListCallbacks.insert(callback) # by inserting the callback at the start, it will be popped last: first in, first out
return this.getScoreboardsBinding(invokeBoardsListCallback)

proc getScores*(this: ptr PlaydateScoreboards, boardID: string, callback: ScoresCallback): int32 {.discardable.} =
privateAccess(PlaydateScoreboards)
privateScoresCallbacks.insert(callback) # by inserting the callback at the start, it will be popped last: first in, first out
return this.getScoresBinding(boardID.cstring, invokeScoresCallback)
Loading

0 comments on commit 405d0b2

Please sign in to comment.