summaryrefslogtreecommitdiff
path: root/catbird/asset.scm
blob: a53048f35386510e7af8c25adb6cf29ef84fb4e6 (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
(define-module (catbird asset)
  #:use-module (catbird config)
  #:use-module (catbird inotify)
  #:use-module (ice-9 match)
  #:use-module (oop goops)
  #:export (<asset>
            file-names
            loader
            artifact
            subscribers
            load!
            ->asset
            subscribe
            unsubscribe
            on-asset-refresh
            define-asset
            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)))))


;;;
;;; 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))))