summaryrefslogtreecommitdiff
path: root/catbird/asset.scm
blob: 60848e219eebbaa28ffd8688b9d781f1d9508b43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
;;; Catbird Game Engine
;;; Copyright © 2022 David Thompson <davet@gnu.org>
;;;
;;; Licensed under the Apache License, Version 2.0 (the "License");
;;; you may not use this file except in compliance with the License.
;;; You may obtain a copy of the License at
;;;
;;;    http://www.apache.org/licenses/LICENSE-2.0
;;;
;;; Unless required by applicable law or agreed to in writing, software
;;; distributed under the License is distributed on an "AS IS" BASIS,
;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;;; See the License for the specific language governing permissions and
;;; limitations under the License.

;;; Commentary:
;;
;; Game data loaded from the file system, such as an image or audio
;; file.
;;
;;; Code:
(define-module (catbird asset)
  #:use-module (catbird config)
  #:use-module (catbird inotify)
  #:use-module (chickadee audio)
  #:use-module (chickadee graphics text)
  #:use-module (chickadee graphics texture)
  #:use-module (ice-9 match)
  #:use-module (oop goops)
  #:export (<asset>
            file-names
            loader
            artifact
            subscribers
            load!
            reload!
            ->asset
            subscribe
            unsubscribe
            on-asset-refresh
            define-asset
            define-font
            define-image
            define-tileset
            define-audio
            reload-modified-assets

            <asset-container>))

(define (absolute-file-name file-name)
  (if (absolute-file-name? file-name)
      file-name
      (string-append (getcwd) "/" file-name)))


;;;
;;; Base Asset
;;;

(define-root-class <asset> ()
  (file-names #:getter file-names #:init-keyword #:file-names)
  (loader #:getter loader #:init-keyword #:loader)
  (artifact #:accessor %artifact #:init-value #f)
  (subscribers #:getter subscribers #:init-form (make-weak-key-hash-table)))

(define-method (initialize (asset <asset>) initargs)
  (next-method)
  ;; Convert relative file names to absolute file names for
  ;; consistency and ease of use later.
  (slot-set! asset 'file-names (map absolute-file-name (file-names asset))))

;; Allow any object to be wrapped in an asset.
(define-method (->asset x)
  (make <asset>
    #:file-names '()
    #:loader (lambda () x)))

(define-method (->asset (asset <asset>))
  asset)

(define-method (subscribe (asset <asset>) obj context)
  (let ((subs (subscribers asset)))
    (hashq-set! subs obj (cons context (hashq-ref subs obj '())))))

(define-method (unsubscribe (asset <asset>) obj context)
  (let* ((subs (subscribers asset))
         (contexts (delq context (hashq-ref subs obj '()))))
    (if (null? contexts)
        (hashq-remove! subs obj)
        (hashq-set! subs obj contexts))))

(define-method (on-asset-refresh obj context)
  #t)

(define-method (notify-refresh (asset <asset>))
  (hash-for-each (lambda (subscriber contexts)
                   (for-each (lambda (context)
                               (on-asset-refresh subscriber context))
                             contexts))
                 (subscribers asset)))

(define-method (load! (asset <asset>))
  (let ((value (apply (loader asset) (file-names asset))))
    (set! (%artifact asset) value)
    (notify-refresh asset)
    value))

(define-method (reload! (asset <asset>))
  (load! asset))

(define-method (unload! (asset <asset>))
  (set! (%artifact asset) #f))

(define-method (artifact (asset <asset>))
  (or (%artifact asset)
      (load! asset)))


;;;
;;; Auto-reloading Asset
;;;

(define-class <auto-reload-asset> (<asset>)
  ;; Do not create inotify handle until it is needed.
  (inotify #:allocation #:class #:init-form (delay (make-inotify)))
  ;; List of all auto-reloadable assets stored as a weak key hash
  ;; table
  (assets #:allocation #:class #:init-thunk make-weak-key-hash-table))

(define (asset-inotify)
  (force (class-slot-ref <auto-reload-asset> 'inotify)))

(define (auto-reload-assets)
  (class-slot-ref <auto-reload-asset> 'assets))

(define (register-auto-reload-asset! asset)
  (hashq-set! (auto-reload-assets) asset #t))

(define-method (load! (asset <auto-reload-asset>))
  ;; These are both no-ops if the asset and file are already being
  ;; watched.
  (register-auto-reload-asset! asset)
  (for-each (lambda (file-name)
              (inotify-add-watch! (asset-inotify) file-name '(close-write)))
            (file-names asset))
  (next-method))

(define (assets-for-event event)
  (let ((f (inotify-watch-file-name (inotify-event-watch event))))
    (hash-fold (lambda (asset dummy-value memo)
                 (if (member f (file-names asset))
                     (cons asset memo)
                     memo))
               '()
               (auto-reload-assets))))

;; Needs to be called periodically in the game loop to reload modified
;; assets.
(define (reload-modified-assets)
  "Reload all assets whose files have been modified."
  (let ((inotify (asset-inotify)))
    (while (inotify-pending-events? inotify)
      (let* ((event (inotify-read-event inotify))
             (assets (assets-for-event event)))
        (if (null? assets)
            ;; There are no assets associated with this file anymore
            ;; (they've been redefined with new file names or GC'd),
            ;; so remove the watch.
            (inotify-watch-remove! (inotify-event-watch event))
            ;; Reload all assets associated with the file.
            (for-each reload! assets))))))


;;;
;;; Syntax
;;;

(define-syntax-rule (define-asset (name (var file-name) ...) body ...)
  (define name
    (let ((file-names (list file-name ...))
          (proc (lambda (var ...) body ...)))
      (if (and (defined? 'name) (is-a? name <asset>))
          (begin
            (initialize name
                        #:file-names file-names
                        #:loader proc)
            name)
          (make (if developer-mode? <auto-reload-asset> <asset>)
            #:file-names file-names
            #:loader proc)))))

(define-syntax-rule (define-font name file-name size args ...)
  (define-asset (name (f file-name))
    (load-font f size args ...)))

(define-syntax-rule (define-image name file-name args ...)
  (define-asset (name (f file-name))
    (load-image f args ...)))

(define-syntax-rule (define-tileset name file-name tw th args ...)
  (define-asset (name (f file-name))
    (load-tileset f tw th args ...)))

(define-syntax-rule (define-audio name file-name args ...)
  (define-asset (name (f file-name))
    (load-audio f args ...)))


;;;
;;; Asset Metaclass
;;;

(define-class <asset-slot-class> (<catbird-metaclass>))

(define-method (asset-slot? (slot <slot>))
  (get-keyword #:asset? (slot-definition-options slot)))

(define (slot-ref* obj slot-name)
  (and (slot-bound? obj slot-name)
       (slot-ref obj slot-name)))

(define-method (compute-getter-method (class <asset-slot-class>) slot)
  (if (asset-slot? slot)
      ;; Wrap the original getter procedure with a new procedure that
      ;; extracts the current value from the asset object.
      (make <method>
        #:specializers (list class)
        #:procedure (let ((slot-name (slot-definition-name slot))
                          (proc (method-procedure (next-method))))
                      (lambda (obj)
                        (artifact (proc obj)))))
      (next-method)))

(define-method (compute-setter-method (class <asset-slot-class>) slot)
  (if (asset-slot? slot)
      ;; Wrap the original setter procedure with a new procedure that
      ;; manages asset update notifications.
      (make <method>
        #:specializers (list class <top>)
        #:procedure (let ((slot-name (slot-definition-name slot))
                          (proc (method-procedure (next-method))))
                      (lambda (obj new)
                        (let ((old (slot-ref* obj slot-name))
                              (new* (->asset new)))
                          (unless (eq? old new)
                            (when old
                              (unsubscribe old obj slot-name))
                            (subscribe new* obj slot-name)
                            (proc obj new*))))))
      (next-method)))

(define (map-initargs proc initargs)
  (let loop ((initargs initargs))
    (match initargs
      (() '())
      ((slot-name value . rest)
       (cons* slot-name (proc slot-name value) (loop rest))))))

(define (for-each-initarg proc initargs)
  (let loop ((initargs initargs))
    (match initargs
      (() '())
      ((slot-name value . rest)
       (proc slot-name value)
       (loop rest)))))

(define (coerce-asset obj slot-name)
  (let ((value (slot-ref* obj slot-name)))
    (if (is-a? value <asset>)
        value
        (let ((asset (->asset value)))
          (slot-set! obj slot-name asset)
          asset))))

(define-class <asset-container> ()
  #:metaclass <asset-slot-class>)

(define-method (initialize (instance <asset-container>) initargs)
  (next-method)
  ;; Subscribe for updates to all asset slots.
  (for-each (lambda (slot)
              (when (asset-slot? slot)
                (let* ((slot-name (slot-definition-name slot))
                       (value (coerce-asset instance slot-name)))
                  (subscribe value instance slot-name))))
            (class-slots (class-of instance))))