Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/dev' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
ninovanhooff committed Nov 16, 2024
2 parents 09c5e19 + 12177d5 commit 4acd21a
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 49 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
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
2 changes: 1 addition & 1 deletion 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
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)
17 changes: 16 additions & 1 deletion src/playdate/sound.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type
resource: AudioSamplePtr
AudioSample* = ref AudioSampleObj

PDSoundCallbackFunction* = proc(userData: pointer) {.raises: [].}
PDSoundCallbackFunction* = proc(userdata: pointer) {.raises: [].}

proc `=destroy`(this: var AudioSampleObj) =
privateAccess(PlaydateSound)
Expand Down Expand Up @@ -55,6 +55,7 @@ type SoundSource* = ref SoundSourceObj
type
FilePlayerObj = object of SoundSourceObj
finishCallback: PDFilePlayerCallbackFunction
fadeVolumeCallback: PDFilePlayerCallbackFunction
FilePlayer* = ref FilePlayerObj

PDFilePlayerCallbackFunction* = proc(player: FilePlayer) {.raises: [].}
Expand Down Expand Up @@ -139,6 +140,11 @@ proc privateFilePlayerFinishCallback(soundSource: SoundSourcePtr, userdata: poin
if filePlayer.finishCallback != nil:
filePlayer.finishCallback(filePlayer)

proc privateFilePlayerFadeVolumeCallback(soundSource: SoundSourcePtr, userdata: pointer) {.cdecl, raises: [].} =
let filePlayer = cast[FilePlayer](userdata)
if filePlayer.fadeVolumeCallback != nil:
filePlayer.fadeVolumeCallback(filePlayer)

proc setFinishCallback*(this: FilePlayer, callback: PDFilePlayerCallbackFunction) =
privateAccess(PlaydateSound)
privateAccess(PlaydateSoundFileplayer)
Expand All @@ -148,6 +154,15 @@ proc setFinishCallback*(this: FilePlayer, callback: PDFilePlayerCallbackFunction
else:
playdate.sound.fileplayer.setFinishCallback(this.resource, privateFilePlayerFinishCallback, cast[pointer](this))

proc fadeVolume*(this: FilePlayer, left, right: float32, len: int32, callback: PDFilePlayerCallbackFunction) =
privateAccess(PlaydateSound)
privateAccess(PlaydateSoundFileplayer)
this.fadeVolumeCallback = callback
if callback == nil:
playdate.sound.fileplayer.fadeVolume(this.resource, left.cfloat, right.cfloat, len.cint , nil, nil)
else:
playdate.sound.fileplayer.fadeVolume(this.resource, left.cfloat, right.cfloat, len.cint , privateFilePlayerFadeVolumeCallback, cast[pointer](this))

proc finishCallback*(this: FilePlayer): PDFilePlayerCallbackFunction =
return this.finishCallback

Expand Down
3 changes: 2 additions & 1 deletion tests/src/playdate_tests.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
##

import playdate/api
import ../[t_buttons, t_graphics, t_nineslice, t_files, t_midi]
import ../[t_buttons, t_graphics, t_nineslice, t_files, t_midi, t_scoreboards]

proc runTests() {.raises: [].} =
try:
Expand All @@ -14,6 +14,7 @@ proc runTests() {.raises: [].} =
execNineSliceTests(true)
execFilesTest()
execMidiTests(true)
execScoreboardTests()
except Exception as e:
quit(e.msg & "\n" & e.getStackTrace)

Expand Down
6 changes: 6 additions & 0 deletions tests/t_graphics.nim
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ proc execGraphicsTests*(runnable: bool) =
let img = playdate.graphics.newBitmap(10, 10, kColorWhite)
discard playdate.graphics.createPattern(img, 0, 0)

test "DebugBitmap should not be freed after use":
if runnable:
discard playdate.graphics.getDebugBitmap()
# if the bitmap was freed, this would crash
discard playdate.graphics.getDebugBitmap()

test "Bitmaps should be loadable from files":
if runnable:
let img = playdate.graphics.newBitmap("boxes.png")
Expand Down
Loading

0 comments on commit 4acd21a

Please sign in to comment.