-
Notifications
You must be signed in to change notification settings - Fork 1
/
tilemap.asm
1335 lines (1153 loc) · 46.6 KB
/
tilemap.asm
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;====
; Tilemap
;
; Each tile in the tilemap consists of 2-bytes which describe which pattern to
; use and which modifier attributes to apply to it, such as flipping, layer and
; color palette
;====
.define tilemap.ENABLED 1
;====
; Settings
;
; Define these before including this file if you wish to override the defaults
;====
; The tilemap address in VRAM (default $3800)
.ifndef tilemap.VRAM_ADDRESS
.define tilemap.VRAM_ADDRESS $3800
.endif
;====
; Dependencies
;====
.ifndef utils.assert
.include "utils/assert.asm"
.endif
.ifndef utils.clobbers
.include "utils/clobbers.asm"
.endif
.ifndef utils.math
.include "utils/math.asm"
.endif
.ifndef utils.outiBlock
.include "utils/outiBlock.asm"
.endif
.ifndef utils.ram
.include "utils/ram.asm"
utils.ram.assertRamSlot
.endif
.ifndef utils.vdp
.include "utils/vdp.asm"
.endif
;====
; Tile attributes
; Combine using OR (|), i.e. (tilemap.HIGH_BIT | tilemap.FLIP_X)
;====
.define tilemap.HIGH_BIT %00000001 ; 9th bit for the pattern ref, allows refs 256+
.define tilemap.FLIP_X %00000010 ; Flip horizontally
.define tilemap.FLIP_Y %00000100 ; Flip vertically
.define tilemap.FLIP_XY %00000110 ; Flip horizontally and vertically
.define tilemap.SPRITE_PALETTE %00001000 ; Use palette 2 (sprite palette)
; Place in front of sprites. Color 0 acts as transparent
.define tilemap.PRIORITY %00010000
; Spare bits - unused by VDP but some games use them to hold custom attributes
; such as whether the tile is a hazard that costs the player health
.define tilemap.CUSTOM_1 %00100000
.define tilemap.CUSTOM_2 %01000000
.define tilemap.CUSTOM_3 %10000000
;====
; Constants
;====
.define tilemap.ROWS 28
.define tilemap.COLS 32
.define tilemap.TILES tilemap.ROWS * tilemap.COLS
.define tilemap.MAX_PATTERN_INDEX 511
; Min and max number of rows visible on screen. If the Y scroll offset is a
; multiple of 8 it's the minimum, otherwise there is an extra row (the bottom
; of the top row is still visible, as well as the top of the bottom row)
.define tilemap.MIN_VISIBLE_ROWS 24
.define tilemap.MAX_VISIBLE_ROWS tilemap.MIN_VISIBLE_ROWS + 1
.define tilemap.Y_PIXELS tilemap.ROWS * 8
; Number of pixels the X scroll is shifted by on initialisation. -8 means
; column 0 is visible on screen and populated by the left-most column, while
; column 31 is hidden and populated by the next column on the right
.define tilemap.X_OFFSET -8
.define tilemap.TILE_SIZE_BYTES 2
.define tilemap.COL_SIZE_BYTES tilemap.MAX_VISIBLE_ROWS * tilemap.TILE_SIZE_BYTES
.define tilemap.ROW_SIZE_BYTES tilemap.COLS * 2
; Masks to set/reset the Y scroll flags (00 = no scroll, 01 = up, 11 = down)
.define tilemap.SCROLL_Y_RESET_MASK %11111100 ; AND mask
.define tilemap.SCROLL_UP_SET_MASK %00000001 ; OR mask
.define tilemap.SCROLL_DOWN_SET_MASK %00000011 ; OR mask
; Masks to set/reset the X scroll flags (00 = no scroll, 10 = right, 11 = left)
.define tilemap.SCROLL_X_RESET_MASK %00111111 ; AND mask
.define tilemap.SCROLL_LEFT_SET_MASK %11000000 ; OR mask
.define tilemap.SCROLL_RIGHT_SET_MASK %10000000 ; OR mask
;====
; RAM
;====
.ramsection "tilemap.ram" slot utils.ram.SLOT
; VDP x-axis scroll register buffer
tilemap.ram.xScrollBuffer: db ; negate before writing to the VDP
; Scroll flags
tilemap.ram.flags: db ; see constants for flag definitions
; VDP y-axis scroll register buffer
tilemap.ram.yScrollBuffer: db
; VRAM write command/address for row scrolling
tilemap.ram.vramRowWrite: dw
; Address to call when writing the scrolling column
tilemap.ram.colWriteCall: dw
.ends
; Buffer of raw column tiles
.ramsection "tilemap.ram.colBuffer" slot utils.ram.SLOT
tilemap.ram.colBuffer: dsb tilemap.COL_SIZE_BYTES
.ends
;===
; Buffer of raw row tiles
; Align to 256 so low byte starts at 0 and can be set to the offset
;===
.ramsection "tilemap.ram.rowBuffer" slot utils.ram.SLOT align 256
tilemap.ram.rowBuffer: dsb tilemap.ROW_SIZE_BYTES
.ends
;====
; Public functions
;====
;====
; Set the tile index ready to write to
;
; @in index 0 is top left tile
;====
.macro "tilemap.setIndex" args index
utils.assert.range index 0, 895, "\.: Index should be between 0 and 895"
utils.vdp.prepWrite (tilemap.VRAM_ADDRESS + (index * tilemap.TILE_SIZE_BYTES))
.endm
;====
; Set the tile index ready to write to
;
; @in col column number (x)
; @in row row number (y)
;====
.macro "tilemap.setColRow" args colX rowY
utils.assert.range colX, 0, 31, "\.: colX should be between 0 and 31"
utils.assert.range rowY, 0, 27, "\.: rowY should be between 0 and 27"
tilemap.setIndex ((rowY * tilemap.COLS) + colX)
.endm
;====
; Set HL to the write address for the given row and column
;
; @in hl row * 32 + column (i.e. ------yy yyyxxxxx)
;
; @out hl the VRAM address with write command set
; @out c the port to output data to (using out, outi etc)
;====
.macro "tilemap.loadHLWriteAddress"
utils.clobbers "af"
; Multiply by 2 (2 bytes per tile)
add hl, hl
; Low byte of base address is 0, so we just need to manipulate high byte
; Set A to high byte of base address with the write command set
ld a, >tilemap.VRAM_ADDRESS | utils.vdp.commands.WRITE
or h ; combine bits with high byte of relative address
ld h, a ; set HL to the full address
utils.clobbers.end
.endm
;====
; Define tile data
;
; @in patternIndex the pattern index (0-511)
; @in attributes (optional) the tile attributes (see Tile attributes section).
; Note, if patternRef is greater than 255, tilemap.HIGH_BIT
; is set automatically
;====
.macro "tilemap.tile" args patternIndex attributes
utils.assert.range NARGS, 1, 2, "\.: Invalid number of arguments"
utils.assert.range patternIndex, 0, tilemap.MAX_PATTERN_INDEX, "tilemap.asm \.: Invalid patternIndex argument"
.ifndef attributes
.define attributes $00
.endif
; Set high bit attribute if pattern index is above 255
.ifgr patternIndex 255
.redefine attributes attributes | tilemap.HIGH_BIT
.endif
.db <(patternIndex) ; low byte of patternIndex
.db attributes
.endm
;====
; Write a tile to the current position in the tilemap
;
; @in patternIndex the pattern index (0-511)
; @in attributes (optional) the tile attributes (see Tile attributes section).
; Note, if patternRef is greater than 255, tilemap.HIGH_BIT
; is set automatically
;
; @in c VDP data port
;====
.macro "tilemap.writeTile" args patternIndex attributes
utils.assert.range patternIndex, 0, tilemap.MAX_PATTERN_INDEX, "\.: Invalid patternIndex argument"
.ifndef attributes
.define attributes $00
.else
utils.assert.range attributes, 0, 255, "\.: Invalid attributes argument"
.endif
; Set high bit attribute if pattern index is above 255
.ifgr patternIndex 255
.redefine attributes attributes | tilemap.HIGH_BIT
.endif
utils.clobbers "af"
; Load A with low-byte of pattern index
.if <(patternIndex) == 0
xor a ; set to 0
.else
ld a, <(patternIndex)
.endif
out (utils.vdp.DATA_PORT), a ; write pattern index
; Load A with attribute byte
.if attributes != <(patternIndex)
.if attributes == 0
xor a ; set A to 0
.else
ld a, attributes
.endif
.endif
out (utils.vdp.DATA_PORT), a ; write tile attributes
utils.clobbers.end
.endm
;====
; Outputs the given number of tiles to the current position in the tilemap
;
; @in hl pointer to the tile data
; @in number the number of tiles to write
; @in VRAM pointer to destination address with write command
;====
.macro "tilemap.writeTiles" args number
utils.assert.range number, 1, tilemap.TILES, "\.: Invalid number argument"
utils.outiBlock.write tilemap.TILE_SIZE_BYTES * number
.endm
;====
; Copies pattern ref bytes to VRAM until a terminator byte is reached
;
; @in terminator value that signifies the end of the data
; @in dataAddr address of the first byte of ASCII data
; @in [attributes] tile attributes to use for all the tiles (see tile
; attribute options at top). Defaults to 0
;====
.macro "tilemap.writeBytesUntil" args terminator dataAddr attributes
utils.assert.range terminator 0 255 "\.: terminator should be a byte value"
utils.assert.label dataAddr "\.: dataAddr should be a label"
utils.clobbers "af", "bc", "de", "hl"
ld d, terminator
ld hl, dataAddr
.ifdef attributes
utils.assert.range attributes 0 255 "\.: attributes should be a byte value"
ld b, attributes
.else
ld b, 0
.endif
call tilemap._writeBytesUntil
utils.clobbers.end
.endm
;====
; Write bytes of data representing tile pattern refs
;
; @in hl the address of the data to write
; @in b the number of bytes to write
; @in c tile attributes to use for all the tiles (see tile
; attribute options at top)
;====
.section "tilemap.writeBytes" free
_nextByte:
inc hl ; next byte
tilemap.writeBytes:
ld a, (hl) ; read byte
out (utils.vdp.DATA_PORT), a ; write pattern ref
ld a, c ; load attributes
out (utils.vdp.DATA_PORT), a ; write attributes
djnz _nextByte ; repeat until b = 0
ret
.ends
;====
; Writes bytes of data representing tile pattern refs. The patterns will all
; share the same attributes
;
; @in address the address of the data to write
; @in count the number of bytes to write
; @in attributes (optional) the attributes to use for each tile
; See tile attribute options at top. Defaults to $00
;====
.macro "tilemap.writeBytes" args address count attributes
utils.assert.label address "\.: Address should be a label"
utils.assert.range count 0 tilemap.TILES "\.: Count should be between 0 and {tilemap.TILES}"
utils.clobbers "af", "hl", "bc"
ld hl, address
ld b, count
.ifdef attributes
ld c, attributes
.else
ld c, 0
.endif
call tilemap.writeBytes
utils.clobbers.end
.endm
;====
; Load a row (32-tiles) of uncompressed data. Each tile is 2-bytes - the
; first is the patternRef and the second is the tile's attributes.
;
; @in hl pointer to the raw data
;====
.macro "tilemap.writeRow"
; Output 1 row of data
utils.outiBlock.write tilemap.ROW_SIZE_BYTES
.endm
;====
; Write tile data from an uncompressed map. Each tile is 2-bytes - the first is
; the tileRef and the second is the tile's attributes.
;
; @in d number of rows to write
; @in e the amount to increment the pointer by each row i.e. the number of
; columns in the full map * 2 (as each tile is 2-bytes)
; @in hl pointer to the first tile to write
;====
.macro "tilemap.writeRows"
utils.clobbers "af", "bc", "de"
call tilemap._writeRows
utils.clobbers.end
.endm
;====
; Private (see macro)
;====
.section "tilemap._writeRows"
_nextRow:
ld a, e ; write row width into A
utils.math.addHLA ; add 1 row to full tilemap pointer
tilemap._writeRows:
push hl ; preserve HL
tilemap.writeRow ; write a row of data
pop hl ; restore HL
dec d
jp nz, _nextRow
ret
.ends
;====
; Initialises the RAM buffers and scroll values to their starting state
;====
.macro "tilemap.reset"
\@_\.:
utils.clobbers "af"
call tilemap._reset
utils.clobbers.end
.endm
;====
; Adjusts the buffered tilemap xScroll value by a given number of pixels. If
; this results in a new column needing to be drawn it sets flags in RAM
; indicating whether the left or right column needs re-writing. You can
; interpret these flags using tilemap.ifColScroll.
;
; The scroll value won't apply until you call tilemap.writeScrollRegisters
;
; @in a the number of x pixels to adjust. Positive values scroll right in
; the game world (shifting the tiles left). Negative values scroll
; left (shifting the tiles right)
;====
.macro "tilemap.adjustXPixels"
\@_\.:
utils.clobbers "af" "bc" "hl"
call tilemap._adjustXPixels
utils.clobbers.end
.endm
;====
; Adjusts the buffered tilemap yScroll value by a given number of pixels. If
; this results in a new row needing to be drawn it sets flags in RAM indicating
; whether the top or bottom rows need re-writing. You can interpret these flags
; using tilemap.ifRowScroll.
;
; The scroll value won't apply until you call tilemap.writeScrollRegisters
;
; @in a the number of y pixels to adjust. Positive values scroll down in
; the game world (shifting the tiles up). Negative values scroll
; up (shifting the tiles down)
;====
.macro "tilemap.adjustYPixels"
\@_\.:
utils.clobbers "af" "bc" "hl"
call tilemap._adjustYPixels
utils.clobbers.end
.endm
;====
; When tilemap.ifRowScroll indicates an up scroll, but you detect this new row
; will be out of bounds of the tilemap, call this to cap the y pixel scrolling
; to the top of the current in-bounds row. Further calls to tilemap.ifRowScroll
; will indicate that no row scroll is required and thus prevent rendering an
; invalid row.
;
; Note: This should be called before calling tilemap.calculateScroll
;====
.macro "tilemap.stopUpRowScroll"
\@_\.:
utils.clobbers "af"
call tilemap._stopUpRowScroll
utils.clobbers.end
.endm
.section "tilemap._stopUpRowScroll" free
tilemap._stopUpRowScroll:
; Reset UP scroll flag
ld a, (tilemap.ram.flags) ; load flags
and tilemap.SCROLL_Y_RESET_MASK ; reset Y scroll flags
ld (tilemap.ram.flags), a ; store updated flags
; Round yScrollBuffer to top of previous row
ld a, (tilemap.ram.yScrollBuffer) ; load current value
add 8 ; add 8px to go back down one row
; Ensure value hasn't gone out of 0-223 range
cp tilemap.Y_PIXELS
jp c, +
; Sub screen height to bring back into range (i.e. 224 becomes 0)
sub tilemap.Y_PIXELS
+:
and %11111000 ; round to top pixel of that row
ld (tilemap.ram.yScrollBuffer), a ; update yScrollBuffer
ret
.ends
;====
; When tilemap.ifRowScroll indicates a down scroll, but you detect this new row
; will be out of bounds of the tilemap, call this to cap the y pixel scrolling
; to the top of the current in-bounds row. Further calls to tilemap.ifRowScroll
; will indicate that no row scroll is required and thus prevent rendering an
; invalid row.
;
; Note: This should be called before calling tilemap.calculateScroll
;====
.macro "tilemap.stopDownRowScroll"
\@_\.:
utils.clobbers "af"
call tilemap._stopDownRowScroll
utils.clobbers.end
.endm
.section "tilemap._stopDownRowScroll" free
tilemap._stopDownRowScroll:
; Reset Y scroll flags
ld a, (tilemap.ram.flags) ; load flags
and tilemap.SCROLL_Y_RESET_MASK ; reset Y scroll flags
ld (tilemap.ram.flags), a ; store updated flags
; Adjust yScrollBuffer to point to bottom pixel of previous row
ld a, (tilemap.ram.yScrollBuffer) ; load current value
sub 8 ; sub 8px to go back up one row
; Ensure value hasn't gone out of 0-223 range
jp nc, +
; Value dropped below 0 - bring back into range
add tilemap.Y_PIXELS ; -1 becomes 223
+:
; Round yScrollBuffer to bottom pixel of the row
or %00000111 ; set bits 0-2
ld (tilemap.ram.yScrollBuffer), a ; store result
ret
.ends
;====
; When tilemap.ifColScroll indicates a left scroll, but you detect this new row
; will be out of bounds of the tilemap, call this to cap the x pixel scrolling
; to the left edge of the current in-bounds column. Further calls to
; tilemap.ifColScroll will indicate that no column scroll is required and thus
; prevent rendering an invalid column.
;
; Note: This should be called before calling tilemap.calculateScroll
;====
.macro "tilemap.stopLeftColScroll"
\@_\.:
utils.clobbers "af"
call tilemap._stopLeftColScroll
utils.clobbers.end
.endm
.section "tilemap._stopLeftColScroll" free
tilemap._stopLeftColScroll:
; Reset column scroll flags
ld a, (tilemap.ram.flags) ; load flags
and tilemap.SCROLL_X_RESET_MASK ; reset x scroll flags
ld (tilemap.ram.flags), a ; store updated flags
; Round xScrollBuffer to left of previous column
ld a, (tilemap.ram.xScrollBuffer) ; load current scroll value
add 8 ; go right one column
and %11111000 ; set to left-most pixel of the col
ld (tilemap.ram.xScrollBuffer), a ; update xScrollBuffer
ret
.ends
;====
; When tilemap.ifColScroll indicates a right scroll, but you detect this new row
; will be out of bounds of the tilemap, call this to cap the x pixel scrolling
; to the right edge of the current in-bounds column. Further calls to
; tilemap.ifColScroll will indicate that no column scroll is required and thus
; prevent rendering an invalid column.
;
; Note: This should be called before calling tilemap.calculateScroll
;====
.macro "tilemap.stopRightColScroll"
\@_\.:
utils.clobbers "af"
call tilemap._stopRightColScroll
utils.clobbers.end
.endm
.section "tilemap._stopRightColScroll" free
tilemap._stopRightColScroll:
; Reset column scroll flags
ld a, (tilemap.ram.flags) ; load flags
and tilemap.SCROLL_X_RESET_MASK ; reset x scroll flags
ld (tilemap.ram.flags), a ; store updated flags
; Round xScrollBuffer to right of previous column
ld a, (tilemap.ram.xScrollBuffer) ; load current scroll value
sub 8 ; go left one column
or %00000111 ; set to right-most pixel of the col
ld (tilemap.ram.xScrollBuffer), a ; update xScrollBuffer
ret
.ends
;====
; Calculates the adjustments made with tilemap.adjustXPixels/adjustYPixels
; and applies them to the RAM variables
;====
.macro "tilemap.calculateScroll"
\@_\.:
utils.clobbers "af" "bc"
call tilemap._calculateScroll
utils.clobbers.end
.endm
;====
; Load DE with a pointer to the column buffer
;
; @out de pointer to the column buffer
;====
.macro "tilemap.loadDEColBuffer"
ld de, tilemap.ram.colBuffer
.endm
;====
; Load B with the number of bytes to write for the scrolling column
;
; @out b the number of bytes to write
;====
.macro "tilemap.loadBColBytes"
ld b, tilemap.COL_SIZE_BYTES ; number of bytes to write
.endm
;====
; Load BC with the number of bytes to write for the scrolling column. Note,
; this will always be a value <= 50 so only needs 8-bits, but this macro is
; provided for convenience for routines that use ldi and require a 16-bit
; counter in BC
;
; @out bc the number of bytes to write
;====
.macro "tilemap.loadBCColBytes"
ld bc, tilemap.COL_SIZE_BYTES ; number of bytes to write
.endm
;====
; Load DE with a pointer to the row buffer
;
; @out de pointer to the row buffer
;====
.macro "tilemap.loadDERowBuffer"
ld de, tilemap.ram.rowBuffer
.endm
;====
; Load B with the number of bytes to write for the scrolling row
;
; @out b the number of bytes to write
;====
.macro "tilemap.loadBRowBytes"
ld b, tilemap.ROW_SIZE_BYTES
.endm
;====
; Load BC with the number of bytes to write for the scrolling row. Note,
; this will always be a value <= 50 so only needs 8-bits, but this macro is
; provided for convenience for routines that use ldi and require a 16-bit
; counter in BC
;
; @out bc the number of bytes to write
;====
.macro "tilemap.loadBCRowBytes"
ld bc, tilemap.ROW_SIZE_BYTES
.endm
;====
; Jumps to the relevant label if a column scroll is needed after a call to
; tilemap.adjustXPixels. Must be called with either 3 arguments (left, right, else)
; or just else alone
;
; @in left (optional) continue to this label if left column needs loading
; @in right (optional) jump to this label if the right column needs loading
; @in else jump to this label if no columns need loading
;====
.macro "tilemap.ifColScroll" args left, right, else
\@_\.:
.if NARGS == 1
; Only one argument passed ('else' label)
utils.clobbers.withBranching "af"
ld a, (tilemap.ram.flags) ; load flags
rlca ; set carry to 7th bit
utils.clobbers.end.jpnc \1 ; jp to else if no col to scroll
utils.clobbers.end
; ...otherwise continue
.elif NARGS == 3
utils.clobbers.withBranching "af"
; 3 arguments passed (left, right, else)
ld a, (tilemap.ram.flags) ; load flags
rlca ; set C to 7th bit
utils.clobbers.end.jpnc else ; bit 7 was 0 - no col scroll
; Check right scroll flag
rlca ; set C to what was 6th bit
utils.clobbers.end.jpnc right ; bit 6 was 0 - scrolling right
; ...otherwise continue to left label
utils.clobbers.end
.else
.print "\ntilemap.ifColScroll requires 1 or 3 arguments (left/right/else, or just else alone)\n\n"
.fail
.endif
.endm
;====
; Returns if no column scroll is needed, otherwise jumps to the relevant
; 'left' or 'right' label depending on the column scroll direction
;
; @in left if scrolling left, will continue to this label
; @in right if scrolling right, will jump to this label
;====
.macro "tilemap.ifColScrollElseRet" args left, right
utils.assert.equals NARGS 2 "\. requires 2 arguments (left and right)"
utils.clobbers.withBranching "af"
ld a, (tilemap.ram.flags) ; load flags
rlca ; set carry to 7th bit
utils.clobbers.end.retnc ; ret if no col to scroll (bit 7 was 0)
; Check right scroll flag
rlca ; set carry to what was 6th bit
utils.clobbers.end.jpnc right ; jp if scrolling right (bit 6 was 0)
jp nc, right
; ...otherwise continue to 'left' label
utils.clobbers.end
.endm
;====
; Jumps to the relevant label if a row scroll is needed after a call to
; tilemap.adjustYPixels. Must be called with either 3 arguments (up, down, else)
; or just else alone
;
; @in up (optional) continue to this label if top row needs loading
; @in down (optional) jump to this label if the bottom row needs loading
; @in else jump to this label if no columns need loading
;====
.macro "tilemap.ifRowScroll" args up, down, else
.if NARGS == 1
utils.clobbers.withBranching "af"
; Only one argument passed ('else' label)
ld a, (tilemap.ram.flags) ; load flags
rrca ; set C to bit 0
utils.clobbers.end.jpnc \1 ; jp to else if no row scroll (bit 0 was 0)
; ...otherwise continue
utils.clobbers.end
.elif NARGS == 3
utils.clobbers.withBranching "af"
ld a, (tilemap.ram.flags) ; load flags
rrca ; set C to bit 0
utils.clobbers.end.jpnc else; no row to scroll (bit 0 was 0)
; Check down scroll flag
rrca ; set C to what was bit 1
utils.clobbers.end.jpc down ; jp if scrolling down (bit 1 was set)
; ...otherwise continue to 'up' label
utils.clobbers.end
.else
.print "\ntilemap.ifRowScroll requires 1 or 3 arguments (up/down/else, or just else alone)\n\n"
.fail
.endif
.endm
;====
; Returns if no row scroll is needed, otherwise jumps to the relevant
; 'up' or 'down' label depending on the row scroll direction
;
; @in up if scrolling up, will continue to this label
; @in down if scrolling down, will jump to this label
;====
.macro "tilemap.ifRowScrollElseRet" args up, down
utils.assert.equals NARGS 2 "\. requires 2 arguments (up and down)"
utils.clobbers.withBranching "af"
ld a, (tilemap.ram.flags) ; load flags
rrca ; set C to bit 0
utils.clobbers.end.retnc ; return if no row to scroll
; Check down scroll flag
rrca ; set C to what was bit 1
utils.clobbers.end.jpc down ; jp if down scroll (bit 1 was set)
; ...otherwise continue to 'up' label
utils.clobbers.end
.endm
;====
; Sends the buffered scroll register values to the VDP. This should be called
; when the display is off or during a V or H interrupt
;====
.macro "tilemap.writeScrollRegisters"
utils.clobbers "af"
ld a, (tilemap.ram.xScrollBuffer)
neg
utils.vdp.setRegister utils.vdp.SCROLL_X_REGISTER
ld a, (tilemap.ram.yScrollBuffer)
utils.vdp.setRegister utils.vdp.SCROLL_Y_REGISTER
utils.clobbers.end
.endm
;====
; Sets the VRAM write address to the row that requires updating. If no row needs
; scrolling, the write address will be set to the last row scrolled
;
; @out VRAM write address write address for the row (column 0)
; @out c VDP data port
;====
.macro "tilemap._setRowScrollIndex"
ld hl, (tilemap.ram.vramRowWrite)
utils.vdp.setCommandHL utils.vdp.commands.WRITE
ld c, utils.vdp.DATA_PORT
.endm
;====
; Write tile data to the scrolling column in VRAM, if required. The data should
; be a sequential list of tiles starting with top of the column visible on screen
;
; @in dataAddr (optional) pointer to the sequential tile data (top of the
; column). Defaults to the internal column buffer
;====
.macro "tilemap._writeScrollCol" isolated args dataAddr
; Calculate the column bits for the write address
; If no col scroll needed, skip to _continue
tilemap.ifColScroll, _left, _right, _continue
_left:
ld a, (tilemap.ram.xScrollBuffer) ; load X scroll
add 8 ; go right 1 column to correct
and %11111000 ; floor value to nearest 8
rrca ; divide by 2
rrca ; divide by 2 again (4)
; We would need to divide by 2 again (8 total) then multiply by 2
; as there are 2 bytes per tile, but these operations cancel each
; other out and so aren't required
jp +
_right:
ld a, (tilemap.ram.xScrollBuffer) ; load X scroll
and %11111000 ; floor value to nearest 8
rrca ; divide by 2
rrca ; divide by 2 again (4)
; We would need to divide by 2 again (8 total) then multiply by 2
; as there are 2 bytes per tile, but these operations cancel each
; other out and so aren't required
+:
;===
; Prep the call to the address stored in tilemap.ram.colWriteCall, which
; points to an iteration of tilemap._writeColumn
;===
; Set E to column address bits
ld e, a
; Set D to column bits ORed with 128, as required by tilemap._writeColumn
or 128 ; OR A by 128 to combine bits
ld d, a ; set D to value
; Set B to bytes to write (tilemap.COL_SIZE_BYTES), and C to utils.vdp.DATA_PORT
ld bc, (tilemap.COL_SIZE_BYTES * 256) + utils.vdp.DATA_PORT
; Set HL to the tile data to write
.ifdef dataAddr
ld hl, dataAddr
.else
ld hl, tilemap.ram.colBuffer
.endif
; Call (tilemap.ram.colWriteCall); This points to an iteration of
; tilemap._writeColumn
ld iy, (tilemap.ram.colWriteCall)
call tilemap._callIY
_continue:
.endm
;====
; Update the scroll registers and write the necessary col/row data to VRAM.
; This should be called when the display is off or during VBlank
;====
.macro "tilemap.writeScrollBuffers"
utils.clobbers "af" "bc" "de" "hl" "iy"
call tilemap.writeScrollBuffers
utils.clobbers.end
.endm
;====
; See tilemap.writeScrollBuffers macro alias
;====
.section "tilemap.writeScrollBuffers" free
tilemap.writeScrollBuffers:
; Set scroll registers
tilemap.writeScrollRegisters
; Write column tiles from buffer to VRAM if required
tilemap._writeScrollCol
; Detect whether the row buffer should be flushed
tilemap.ifRowScroll +
; Set VRAM address to the scrolling row
; Set C to the VDP output port
tilemap._setRowScrollIndex
;===
; First write
; Set B to bytes to write minus floor(xScroll / 4)
; Set HL to end of the buffer minus bytes to write
;===
_firstWrite:
ld h, >tilemap.ram.rowBuffer ; set H to high byte of rowBuffer
ld a, (tilemap.ram.xScrollBuffer)
sub tilemap.X_OFFSET ; cancel X offset
rrca ; divide by 2
rrca ; divide by 2 (4 total)
and %00111110 ; clean value; now equals col * 2 bytes
ld e, a ; preserve result in E
jp z, _secondWrite ; skip if the are no bytes in first write
ld b, a ; set B to bytes to write
; Set A to the last byte of the buffer
ld a, <(tilemap.ram.rowBuffer + tilemap.ROW_SIZE_BYTES)
; Subtract bytes we need to write
sub b
; Point HL to end of buffer minus bytes
ld l, a
; Copy bytes from buffer to VDP
utils.outiBlock.writeUpTo128Bytes
;===
; Second write
; Set B to ROW_SIZE_BYTES minus bytes written in first write
; Set HL to start of rowBuffer
;===
_secondWrite:
; Set HL to start of rowBuffer; No need to update H as the data
; is aligned
ld l, <(tilemap.ram.rowBuffer) ; set L to low byte of rowBuffer
; Bytes to write
ld a, tilemap.ROW_SIZE_BYTES
sub e ; subtract bytes written in first write
; Write bytes then return to caller
ld b, a ; set B to bytes to write
utils.outiBlock.writeUpTo128BytesThenReturn
+:
ret
.ends
;====
; Private/internal functions
;====
;====
; See tilemap.adjustXPixels
;
; @in a the number of x pixels to adjust. Positive values scroll right in
; the game world (shifting the tiles left). Negative values scroll
; left (shifting the tiles right)
;====
.section "tilemap._adjustXPixels" free
tilemap._adjustXPixels:
or a ; analyse A
jp z, _noColumnScroll ; if adjust is zero, no scroll needed
ld hl, tilemap.ram.xScrollBuffer ; point to xScrollBuffer
ld b, (hl) ; load current xScrollBuffer into B
jp p, _movingRight ; jump if xAdjust is positive
_movingLeft:
; Adjust xScrollBuffer
add a, b ; add xAdjust to xScrollBuffer
ld (hl), a ; store new xScrollBuffer
; Detect if left column needs updating (if upper 5 bits change)
xor b ; compare bits with old value in B
and %11111000 ; zero all but upper 5 bits
jp z, _noColumnScroll ; jp if zero (upper 5 bits were the same)
; Left column needs scrolling
inc hl ; point to flags
ld a, (hl) ; load flags into A
or tilemap.SCROLL_LEFT_SET_MASK ; set left scroll flags
ld (hl), a ; store flags
ret
_movingRight:
; Adjust xScrollBuffer
add a, b ; add xAdjust to xScrollBuffer
ld (hl), a ; store new xScrollBuffer
; Detect if right column needs updating (if upper 5 bits change)
xor b ; compare bits with old value in B
and %11111000 ; zero all but upper 5 bits
jp z, _noColumnScroll ; jp if zero (upper 5 bits were the same)
; Right column needs scrolling
inc hl ; point to flags
ld a, (hl) ; load flags into A
and tilemap.SCROLL_X_RESET_MASK ; reset previous x scroll flags
or tilemap.SCROLL_RIGHT_SET_MASK; set right scroll flag
ld (hl), a ; store flags
ret
; No scroll needed
_noColumnScroll:
ld hl, tilemap.ram.flags
ld a, tilemap.SCROLL_X_RESET_MASK
and (hl) ; reset X scroll flags with mask
ld (hl), a ; update flags
ret
.ends
;====
; See tilemap.adjustYPixels macro