million-perfect-letters/src/storage.a

445 lines
14 KiB
Plaintext

;license:MIT
;(c) 2020-2 by 4am
;
; disk functions to load puzzles, and load & save puzzle completion status
;
; Public functions:
; - MaybeLoadWorldFromDisk
; - PreParseWorldData
; - LoadProgressFromDisk
; - LoadProgressFromMemory
; - LoadPuzzleFromMemory
; - MarkPuzzleCompleted
; - FindPackedProgress
;
; Public variables:
; - gWorldID
; - gPuzzleID
;
; Public constants:
; - kPuzzleWidths
;
gWorldID ; a world is a group of 100 similar puzzles (12 worlds in total, 0-based)
!byte $FD ; 0x00..0x0B
gPuzzleID ; ID of current puzzle within the current world (100 puzzles per world, 0-based)
!byte $FD ; 0x00..0x63
kPuzzleWidths ; width in letters of the puzzles in each world (index=world)
!byte 4,5,6,7 ; note: every puzzle in a world is the same width
!byte 4,5,6,7
!byte 4,5,6,7
checksum = $FB
packedvalue = $FC
bitcount = $FD
;------------------------------------------------------------------------------
; MaybeLoadWorldFromDisk
; load a world (100 puzzles) from a file on disk
; caches the last world loaded to reduce unnecessary disk access
;
; in: A = world ID
; out: C clear if file loaded and parsed successfully (or it was already in memory)
; C set if file could not be loaded and/or parsed
; other registers/flags clobbered
;------------------------------------------------------------------------------
MaybeLoadWorldFromDisk
cmp WorldFileLoaded
bne +
@success clc
rts
+ sta WorldFileLoaded
jsr $ff58
jsr OpenFile
!word @filename
!word WORLDFILEBUFFER
bcs @failure
sta @refnum
lda WorldFileLoaded
asl
asl
tax
lda data_index, x
ldy data_index+1, x
tax
lda @refnum
jsr SeekFile
lda WorldFileLoaded
asl
asl
tax
lda data_index+2, x
sta @readlen
lda data_index+3, x
sta @readlen+1
jsr ReadFile
@refnum !byte $FD ; SMC
!word compressed_data ; data address
@readlen !word $FDFD ; data length
php
lda @refnum
jsr CloseFile
plp
bcs @failure
lda #<compressed_data
sta get_crunched_byte+1
lda #>compressed_data
sta get_crunched_byte+2
jsr decrunch
jsr PreParseWorldData
cpx #100
beq @success
@failure lda WorldFileLoaded
jsr CloseFile ; ignore error, if any
lda #$FF
sta WorldFileLoaded
sec
rts
@filename
!raw 8,"MPL.DATA"
WorldFileLoaded
!byte $FF ; no file
;------------------------------------------------------------------------------
; PreParseWorldData
; build internal list of pointers to the start of each puzzle within a world,
; after the raw world data has been loaded into memory
;
; in: WORLDDATA contains raw world data from disk
; out: X = number of puzzles found
; other registers & flags clobbered
; $FE/$FF clobbered
;------------------------------------------------------------------------------
PreParseWorldData
lda #<(WORLDDATA-1)
sta $FE
lda #>(WORLDDATA-1)
sta $FF ; ($FE) -> one byte before start of file data
ldy #$00
@newLine ldx #$00 ; X = puzzle ID
beq @emptyline ; always branches
@skipLine ; skip to CR
jsr @IncAndGetChar
cmp #$0A ; CR
bne @skipLine
@emptyline
jsr @IncAndGetChar
cmp #$0A ; CR in first position (blank line) -> skip
beq @emptyline
cmp #$23 ; '#' starts a comment -> skip to CR
beq @skipLine
cmp #$5B ; '[' ends the parsing
beq @exit
lda $FE
sta PUZZLELO, x
lda $FF
sta PUZZLEHI, x
inx
bne @skipLine ; always branches
@IncAndGetChar
; in: Y = 0
+INCADDR $FE
lda ($FE),y
cmp #$0D ; CR - hide it
beq @IncAndGetChar
@exit rts
;------------------------------------------------------------------------------
; LoadProgressFromDisk
; load PROGRESS file into memory
;
; see comments in MarkPuzzleCompleted for file format
;
; in: none
; out: C clear if file was read successfully
; C set if error occurred
;------------------------------------------------------------------------------
LoadProgressFromDisk
jsr LoadFile1Shot
!word progressFile
!word PACKEDPROGRESS ; data address
!word $00C0 ; data length
!word PROGRESSFILEBUFFER
rts
progressFile
!raw 12,"MPL.PROGRESS"
;------------------------------------------------------------------------------
; LoadProgressFromMemory
; unpacks one world's progress into a 1-byte-per-puzzle array for easier access
;
; in: progress file must be in memory (call MaybeLoadProgressFromDisk and
; check C)
; out: A = next puzzle ID
;------------------------------------------------------------------------------
LoadProgressFromMemory
lda gWorldID
jsr FindPackedProgress
; ($FE) -> start of the 16-byte structure for this world
; zero destination array
ldx #103
lda #0
- sta PROGRESS, x
dex
bpl -
; unpack values
ldy #0 ; index into ($FE) (source data)
ldx #0 ; index into PROGRESS (destination data)
-- lda #8
sta bitcount
lda ($FE), y
- lsr
rol PROGRESS, x ; note: PROGRESS structure is 104 bytes (a multiple of 8),
inx ; even though only the first 100 bytes are used
dec bitcount
bne -
iny
cpy #$0D
bne --
lda ($FE), y ; A = next puzzle ID
rts
;------------------------------------------------------------------------------
; LoadPuzzleFromMemory
; loads one puzzle from the in-memory cache of the world file, into the
; 'now playing this puzzle' data structures
;
; in: X = puzzle ID
; world file must be in memory (call MaybeLoadWorldFromDisk and check C)
; out: all registers and flags clobbered
;------------------------------------------------------------------------------
LoadPuzzleFromMemory
lda PUZZLELO, x
sta $FE
lda PUZZLEHI, x
sta $FF
bne @addLine ; always branches
@lineLoop
+INCADDR $FE
@addLine
; ($FE) -> characters for 1 line of this puzzle (no prefix)
jsr AddLineToPuzzle
ldx gWorldID
lda kPuzzleWidths, x
clc
adc $FE
sta $FE
bcc +
inc $FF
+ ldy #0
lda ($FE), y
cmp #$2C ; ','
beq @lineLoop ; if next byte is ',' then there is another line so loop back to parse it
; otherwise the next byte is a delimiter between lines and target words
; (probably '|' but we don't actually check it)
@targetWordLoop
+INCADDR $FE
; ($FE) -> characters for 1 target word of this puzzle (no prefix)
jsr AddTargetWordToPuzzle
ldx gWorldID
lda kPuzzleWidths, x
clc
adc $FE
sta $FE
bcc +
inc $FF
+ ldy #0
lda ($FE), y
cmp #$2C ; ','
beq @targetWordLoop ; if next byte is ',' then there is another target word so loop back to parse it
cmp #$7C ; if next byte is '|' then there is a message, otherwise we're done
bne @exit
+INCADDR $FE
@messageLoop
lda ($FE), y
cmp #$0A
beq @setMessage
cmp #$0D
beq @setMessage
iny
sta @messageBuffer, y
bne @messageLoop
@setMessage
sty @messageBuffer
+LDADDR @messageBuffer
jsr SetNextMessage
@exit rts
@messageBuffer
!byte 40
!raw " "
;------------------------------------------------------------------------------
; MarkPuzzleCompleted/MarkPuzzleSkipped
;
; in: X = puzzle ID
; out: A = next puzzle ID
; X = number of puzzles completed in this world (0x01..0x64)
;------------------------------------------------------------------------------
MarkPuzzleCompleted
; Completion status for the current world (100 puzzles) is stored unpacked in
; memory for easy access
; Mark this puzzle as completed in the unpacked table
lda #1
!byte $2C
MarkPuzzleSkipped
lda #0
sta PROGRESS, x
; Find the next uncompleted puzzle
clc
beq @next
+HIDE_NEXT_BYTE
-- sec
- lda PROGRESS, x
beq @done
@next inx
cpx #100
bne -
ldx #0 ; start over at the puzzle 0
bcc --
; If we fall through here, it means all puzzles in this world
; have been completed (yay!), and we'll leave X at 0.
; If we branch here, we found the next uncompleted puzzle
; and X contains that puzzle ID.
@done txa
pha ; push next puzzle ID
;
; Completion status for all puzzles across all worlds is stored in packed
; bitfields, 1 bit per puzzle, so that the file on disk fits in a single
; block (actually a single sector).
;
; This is the file format (0xC0 bytes):
; +0x00 bit 0 - completion status for world 0x00, puzzle 0x00
; bit 1 - completion status for world 0x00, puzzle 0x01
; bit 2 - completion status for world 0x00, puzzle 0x02
; bit 3 - completion status for world 0x00, puzzle 0x03
; bit 4 - completion status for world 0x00, puzzle 0x04
; bit 5 - completion status for world 0x00, puzzle 0x05
; bit 6 - completion status for world 0x00, puzzle 0x06
; bit 7 - completion status for world 0x00, puzzle 0x07
; ...
; +0x0C bit 0 - completion status for world 0x00, puzzle 0x60
; bit 1 - completion status for world 0x00, puzzle 0x61
; bit 2 - completion status for world 0x00, puzzle 0x62
; bit 3 - completion status for world 0x00, puzzle 0x63
; bit 4-7 always 0
; +0x0D next puzzle to play in world 0x00 (0x00..0x63)
; +0x0E percent complete (0x00..0x64)
; +0x0F XOR of bytes +0x00..+0x0E
; +0x10..+0x1F same format but for world 0x01
; +0x20..+0x2F same format but for world 0x02
; +0x30..+0x3F same format but for world 0x03
; +0x40..+0x4F same format but for world 0x04
; +0x50..+0x5F same format but for world 0x05
; +0x60..+0x6F same format but for world 0x06
; +0x70..+0x7F same format but for world 0x07
; +0x80..+0x8F same format but for world 0x08
; +0x90..+0x9F same format but for world 0x09
; +0xA0..+0xAF same format but for world 0x0A
; +0xB0..+0xBF same format but for world 0x0B
;
; Among other things, this means that a file filled with zeroes is valid
; and means that no puzzles have been completed in any world and you should
; start at the beginning. So that's nice.
;
lda gWorldID
jsr FindPackedProgress
; ($FE) -> start of the 16-byte structure for this world
; zero out all 16 bytes
ldy #$0F
lda #$00
- sta ($FE), y
dey
bpl -
sta checksum
; build packed values
ldy #0
ldx #0
-- lda #8
sta bitcount
lda #0
sta packedvalue
- lda PROGRESS, x
ror
ror packedvalue
inx
dec bitcount
bne -
lda packedvalue
sta ($FE), y
eor checksum
sta checksum
iny
cpy #$0D
bne --
pla ; A = next puzzle ID (pushed earlier)
pha
sta ($FE), y
eor checksum
sta checksum
; count number of completed puzzles
ldy #0
ldx #99
- lda PROGRESS, x
beq +
iny
+ dex
bpl -
tya ; A = number of puzzles completed (out of 100, so also the percent complete)
pha
ldy #$0E
sta ($FE), y
eor checksum
iny
sta ($FE), y
jsr SaveFile1Shot
!word progressFile
!byte 4
!word 0
!word PACKEDPROGRESS
!word $00C0
!word PROGRESSFILEBUFFER
pla
tax
pla
rts
;------------------------------------------------------------------------------
; FindPackedProgress
; find address of a world's packed progress data
;
; in: A = world ID
; out: ($FE) points to first byte of this world's packed progress (16-byte structure)
; A/Y clobbered
; X preserved
; all flags clobbered
;------------------------------------------------------------------------------
FindPackedProgress
pha
+LDADDR PACKEDPROGRESS
+ST16 $FE
pla
asl
asl
asl
asl
clc
adc $FE
sta $FE
bcc +
inc $FF
+ rts
get_crunched_byte
lda compressed_data
inc get_crunched_byte+1
bne +
inc get_crunched_byte+2
+ rts