├── in_art ├── .gitignore ├── bg.pal ├── fg.pal ├── bg.chr ├── fg.chr ├── info.nam ├── play.nam ├── title.nam ├── tracks.nam └── art.txt ├── in_nsf ├── .gitignore ├── brahms.nsf ├── debussy.nsf ├── sundried.nsf └── tracks.txt ├── py65emu ├── .gitignore ├── py65emu.py ├── readme.txt ├── __init__.py ├── mmu.py └── cpu.py ├── .github └── funding.yml ├── cc65 ├── .gitignore └── cc65.txt ├── .gitignore ├── demo.nes ├── mod.inc ├── nsf.cfg ├── mod.cfg ├── nsfe.s ├── base.inc ├── region.s ├── nes.cfg ├── nsfe.cfg ├── ramp.s ├── nsf.s ├── readme.txt ├── package.py ├── in_code └── custom.s ├── base.s └── nsfspider.py /in_art/.gitignore: -------------------------------------------------------------------------------- 1 | NESst.exe 2 | -------------------------------------------------------------------------------- /in_art/bg.pal: -------------------------------------------------------------------------------- 1 |  %& ) -------------------------------------------------------------------------------- /in_art/fg.pal: -------------------------------------------------------------------------------- 1 | *  ,#' -------------------------------------------------------------------------------- /in_nsf/.gitignore: -------------------------------------------------------------------------------- 1 | *.nsf.deb 2 | -------------------------------------------------------------------------------- /py65emu/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | patreon: rainwarrior -------------------------------------------------------------------------------- /cc65/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !cc65.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out_src 2 | out_mod 3 | out_info 4 | out_build 5 | temp 6 | -------------------------------------------------------------------------------- /demo.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/demo.nes -------------------------------------------------------------------------------- /py65emu/py65emu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /in_art/bg.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/in_art/bg.chr -------------------------------------------------------------------------------- /in_art/fg.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/in_art/fg.chr -------------------------------------------------------------------------------- /in_art/info.nam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/in_art/info.nam -------------------------------------------------------------------------------- /in_art/play.nam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/in_art/play.nam -------------------------------------------------------------------------------- /in_art/title.nam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/in_art/title.nam -------------------------------------------------------------------------------- /in_art/tracks.nam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/in_art/tracks.nam -------------------------------------------------------------------------------- /in_nsf/brahms.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/in_nsf/brahms.nsf -------------------------------------------------------------------------------- /in_nsf/debussy.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/in_nsf/debussy.nsf -------------------------------------------------------------------------------- /in_nsf/sundried.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbbradsmith/zensf/HEAD/in_nsf/sundried.nsf -------------------------------------------------------------------------------- /cc65/cc65.txt: -------------------------------------------------------------------------------- 1 | Get CC65 windows snapshot and unzip it here. 2 | 3 | http://cc65.github.io/cc65/ -------------------------------------------------------------------------------- /py65emu/readme.txt: -------------------------------------------------------------------------------- 1 | py65emu 2 | Jeremy Neiman 3 | (very permissively licensed) 4 | 5 | Downloaded from: 6 | https://github.com/docmarionum1/py65emu 7 | 2018-08-09 8 | -------------------------------------------------------------------------------- /py65emu/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jeremy Neiman' 5 | __email__ = 'docmarionum1@gmail.com' 6 | __version__ = '0.0.0' 7 | -------------------------------------------------------------------------------- /mod.inc: -------------------------------------------------------------------------------- 1 | ; mod.inc 2 | ; 3 | ; include for modifications 4 | ; 5 | ; 6 | ; https://github.com/bbbradsmith/zensf 7 | 8 | ; exports for mod.cfg to know where ramp.s resides 9 | .include "out_info/build.inc" 10 | .export RAM_LOW 11 | 12 | ; dummy exports for ramp.s 13 | base_nmi = $8000 14 | base_banks = $8000 15 | BASE_BANK = $FF 16 | .export base_nmi 17 | .export base_banks 18 | .exportzp BASE_BANK 19 | 20 | ; bankswitch replacements (from ramp.s) 21 | 22 | .import sta_5FF8 23 | .import sta_5FF9 24 | .import sta_5FFA 25 | .import sta_5FFB 26 | .import sta_5FFC 27 | .import sta_5FFD 28 | .import sta_5FFE 29 | .import sta_5FFF 30 | 31 | .import sty_5FFA 32 | .import sty_5FFB 33 | 34 | ; vectors (from ramp.s) 35 | 36 | .import ramp_nmi 37 | .import ramp_reset 38 | .import ramp_irq 39 | 40 | .macro VECTORS 41 | .assert * = $FFFA, error, "VECTORS must be at $FFFA" 42 | .word ramp_nmi 43 | .word ramp_reset 44 | .word ramp_irq 45 | .endmacro 46 | 47 | ; end of file 48 | -------------------------------------------------------------------------------- /in_art/art.txt: -------------------------------------------------------------------------------- 1 | SCREEN Title title.nam bg.chr fg.chr bg.pal fg.pal 2 | SCREEN Info info.nam bg.chr fg.chr bg.pal fg.pal 3 | SCREEN Tracks tracks.nam bg.chr fg.chr bg.pal fg.pal 4 | SCREEN Play play.nam bg.chr fg.chr bg.pal fg.pal 5 | 6 | ZP $80 7 | RAM $500 8 | BANKS 32 9 | 10 | NSF_TITLE "ZENSF Test" 11 | NSF_ARTIST "bradsmith" 12 | NSF_COPYRIGHT "github.com/bbbradsmith/zensf" 13 | 14 | # SCREEN [Name] [nametable] [chr0] [chr1] [pal0] [pal1] 15 | # A screen is a combination of nametable, CHR, and palettes. 16 | # Up to 256 different source files can be used. 17 | # Nametables must be 1k. CHR 4k. Palettes 16 bytes. 18 | # 19 | # ZP [address] 20 | # Lowest Zero Page address to use. Everything below this the NSFs are allowed to use. 21 | # 22 | # RAM [address] 23 | # Lowest RAM address to use. Everything below this the NSFs are allowed to use. 24 | # 25 | # BANKS [count] 26 | # Power of 2, up to 256, gives the count of 4k banks. 27 | # 28 | # NSF_TITLE [title] 29 | # NSF_ARTIST [artist] 30 | # NSF_COPYRIGHT [copyright] 31 | # Strings to identify the NSF. Maximum 32 characters long for NSF, unlimited for NSFe. 32 | # 33 | # # 34 | # Text following a # will be treated as a comment until the end of line. 35 | -------------------------------------------------------------------------------- /nsf.cfg: -------------------------------------------------------------------------------- 1 | # https://github.com/bbbradsmith/zensf 2 | 3 | SYMBOLS 4 | { 5 | BANK_STUB: type = import, addrsize = abs; 6 | ZP_LOW: type = import, addrsize = abs; 7 | ZP_HIGH: type = import, addrsize = abs; 8 | RAM_LOW: type = import, addrsize = abs; 9 | 10 | NSFS_SIZE: type = weak, addrsize = abs, value = BANK_STUB * $1000; 11 | } 12 | 13 | MEMORY 14 | { 15 | RAM_ZP: start = ZP_LOW, size = ZP_HIGH - ZP_LOW, type = rw, file = ""; 16 | RAM_STACK: start = $0100, size = $80, type = rw, file = ""; 17 | RAM_MAIN: start = RAM_LOW, size = $800 - RAM_LOW, type = rw, file = ""; 18 | 19 | NSF_HEADER: start = $0000, size = $80, type = ro, file = %O, fill = yes, fillval = 0; 20 | ROM_NSFS: start = $0000, size = NSFS_SIZE, type = ro, file = %O, fill = yes, fillval = 0; 21 | ROM_STUB: start = $8000, size = $1000, type = ro, file = %O, fill = no; 22 | } 23 | 24 | SEGMENTS 25 | { 26 | HEADER: load = NSF_HEADER, type = ro; 27 | 28 | BASE_ZP: load = RAM_ZP, type = zp, define = yes, optional = yes; 29 | STACK: load = RAM_STACK, type = bss, define = yes, optional = yes; 30 | RAMP_CODE: load = ROM_STUB, type = ro, define = yes, run = RAM_MAIN; 31 | RAMP_RAM: load = RAM_MAIN, type = rw, define = yes; 32 | BASE_RAM: load = RAM_MAIN, type = bss, define = yes, optional = yes; 33 | 34 | NSFS: load = ROM_NSFS, type = ro, define = yes; 35 | STUB: load = ROM_STUB, type = ro, define = yes; 36 | } 37 | -------------------------------------------------------------------------------- /mod.cfg: -------------------------------------------------------------------------------- 1 | # https://github.com/bbbradsmith/zensf 2 | 3 | SYMBOLS 4 | { 5 | RAM_LOW: type = import; 6 | } 7 | 8 | MEMORY 9 | { 10 | RAM_MAIN: start = RAM_LOW, size = $800 - RAM_LOW, type = rw, file = ""; 11 | ROM_DUMMY: start = $8000, size = $1000, type = ro, file = ""; 12 | 13 | PRG8: start = $8000, size = $1000, type = ro, file = %O, fill = no; 14 | PRG9: start = $9000, size = $1000, type = ro, file = %O, fill = no; 15 | PRGA: start = $A000, size = $1000, type = ro, file = %O, fill = no; 16 | PRGB: start = $B000, size = $1000, type = ro, file = %O, fill = no; 17 | PRGC: start = $C000, size = $1000, type = ro, file = %O, fill = no; 18 | PRGD: start = $D000, size = $1000, type = ro, file = %O, fill = no; 19 | PRGE: start = $E000, size = $1000, type = ro, file = %O, fill = no; 20 | PRGF: start = $F000, size = $1000, type = ro, file = %O, fill = no; 21 | } 22 | 23 | SEGMENTS 24 | { 25 | RAMP_CODE: load = ROM_DUMMY, run = RAM_MAIN, type = ro; 26 | RAMP_RAM: load = RAM_MAIN, type = rw; 27 | 28 | M8000: load = PRG8, type = ro, optional = yes; 29 | M9000: load = PRG9, type = ro, optional = yes; 30 | MA000: load = PRGA, type = ro, optional = yes; 31 | MB000: load = PRGB, type = ro, optional = yes; 32 | MC000: load = PRGC, type = ro, optional = yes; 33 | MD000: load = PRGD, type = ro, optional = yes; 34 | ME000: load = PRGE, type = ro, optional = yes; 35 | MF000: load = PRGF, type = ro, optional = yes; 36 | } 37 | -------------------------------------------------------------------------------- /nsfe.s: -------------------------------------------------------------------------------- 1 | ; nsfe.s 2 | ; 3 | ; usd to generate NSFe compilation 4 | ; 5 | ; 6 | ; https://github.com/bbbradsmith/zensf 7 | 8 | .import stub_init 9 | .import stub_play 10 | .importzp TRACK_ORDER_LENGTH 11 | .import BANK_STUB 12 | 13 | .include "out_info/strings.inc" 14 | 15 | .segment "NSFE_HEAD" 16 | .byte "NSFE" 17 | 18 | .segment "NSFE_INFO" 19 | .import __NSFE_INFO_SIZE__ 20 | .dword __NSFE_INFO_SIZE__ - 8 21 | .byte "INFO" 22 | .word $8000 ; LOAD 23 | .word stub_init ; INIT 24 | .word stub_play ; PLAY 25 | .byte %00000010 ; PAL/NTSC bits 26 | .byte %00000000 ; expansion bits 27 | .byte TRACK_ORDER_LENGTH ; songs 28 | .byte 0 ; starting song 29 | 30 | .segment "NSFE_BANK" 31 | .import __NSFE_BANK_SIZE__ 32 | .dword __NSFE_BANK_SIZE__ - 8 33 | .byte "BANK" 34 | .repeat 8 35 | .byte b['start'] and start+length < b['start']+b['length']) or 48 | (b['start']+b['length'] > start and b['start']+b['length'] < start+length)): 49 | raise MemoryRangeError() 50 | 51 | newBlock = { 52 | 'start': start, 'length': length, 'readonly': readonly, 53 | 'memory': array.array('B', [0]*length) 54 | } 55 | 56 | # TODO: implement initialization value 57 | if type(value) == list: 58 | for i in range(len(value)): 59 | newBlock['memory'][i+romOffset] = value[i] 60 | 61 | elif value is not None: 62 | a = array.array('B') 63 | a.fromstring(value.read()) 64 | for i in range(len(a)): 65 | newBlock['memory'][i+romOffset] = a[i] 66 | 67 | self.blocks.append(newBlock) 68 | 69 | def getBlock(self, addr): 70 | """ 71 | Get the block associated with the given address. 72 | """ 73 | 74 | for b in self.blocks: 75 | if addr >= b['start'] and addr < b['start']+b['length']: 76 | return b 77 | 78 | raise IndexError 79 | 80 | def getIndex(self, block, addr): 81 | """ 82 | Get the index, relative to the block, of the address in the block. 83 | """ 84 | return addr-block['start'] 85 | 86 | def write(self, addr, value): 87 | """ 88 | Write a value to the given address if it is writeable. 89 | """ 90 | b = self.getBlock(addr) 91 | if b['readonly']: 92 | raise ReadOnlyError() 93 | 94 | i = self.getIndex(b, addr) 95 | 96 | b['memory'][i] = value & 0xff 97 | 98 | def read(self, addr): 99 | """ 100 | Return the value at the address. 101 | """ 102 | b = self.getBlock(addr) 103 | i = self.getIndex(b, addr) 104 | return b['memory'][i] 105 | 106 | def readWord(self, addr): 107 | return (self.read(addr+1) << 8) + self.read(addr) 108 | -------------------------------------------------------------------------------- /ramp.s: -------------------------------------------------------------------------------- 1 | ; ramp.s 2 | ; 3 | ; code placed in RAM to run the NSF 4 | ; 5 | ; 6 | ; external dependencies: 7 | ; base_nmi 8 | ; BASE_BANK 9 | ; 10 | ; when compiling the ROM, these are in base.s 11 | ; when compiling mods, these are in mod.inc 12 | ; 13 | ; 14 | ; ramp_play must be called from $8000 bank 15 | ; base_nmi must reside in $8000 bank 16 | ; base_banks must reside in $8000 bank 17 | ; 18 | ; BASE_BANK will be loaded automatically into $8000 before base_nmi is called 19 | ; $9000, $A000, $B000 must be pre-banked by the caller before entering ramp_play 20 | ; or returning from base_nmi 21 | ; (these values will be cached in bank_9000, bank_A000, bank_B000) 22 | ; 23 | ; 24 | ; https://github.com/bbbradsmith/zensf 25 | 26 | .export ramp_play ; begins playing track (call INIT, nsf_playing=1) 27 | .export ramp_nmi 28 | .export ramp_irq 29 | .export ramp_reset 30 | .export ramp_nsf_init 31 | .export ramp_nsf_play 32 | 33 | .export sta_5FF8 ; mod replacement for STA $5FF8 34 | .export sta_5FF9 35 | .export sta_5FFA 36 | .export sta_5FFB 37 | .export sta_5FFC 38 | .export sta_5FFD 39 | .export sta_5FFE 40 | .export sta_5FFF 41 | 42 | .export sty_5FFA 43 | .export sty_5FFB 44 | 45 | .export bank_add ; re-location of original NSF banks 46 | .export bank_8000 ; rembers the last bank required by NSF (so they can be temporarily replaced) 47 | .export bank_9000 48 | .export bank_A000 49 | .export bank_B000 50 | 51 | ; ramp_play parameters 52 | .export nsf_init 53 | .export nsf_play 54 | .export nsf_init_a ; NSF song 55 | .export nsf_init_x ; 0/1 = NTSC/PAL mode 56 | .export nsf_adjust ; 0 = none, 1 = double every 5th frame, 2 = FT PAL pitch adjust hack + double 5th 57 | .export nsf_playing ; 1 = call PLAY on every NMI 58 | 59 | .import base_nmi 60 | .import base_banks 61 | 62 | .include "out_info/build.inc" 63 | 64 | .segment "RAMP_RAM" 65 | bank_add: .res 1 66 | bank_8000: .res 1 67 | bank_9000: .res 1 68 | bank_A000: .res 1 69 | bank_B000: .res 1 70 | nsf_init: .res 2 71 | nsf_play: .res 2 72 | nsf_init_a: .res 1 73 | nsf_init_x: .res 1 74 | nsf_adjust: .res 1 75 | nsf_playing: .res 1 76 | ramp_nmi_now: .res 1 ; prevents NMI re-entry 77 | 78 | .segment "RAMP_CODE" 79 | 80 | ramp_play: 81 | ; swap out BASE bank 82 | lda bank_8000 83 | sta $5FF8 84 | jsr ramp_nsf_init 85 | ; enable music playback in NMI 86 | lda #1 87 | sta nsf_playing 88 | ; return to BASE bank 89 | lda #BANK_BASE 90 | sta $5FF8 91 | rts 92 | 93 | ramp_nmi: 94 | pha 95 | txa 96 | pha 97 | tya 98 | pha 99 | ; 100 | lda ramp_nmi_now 101 | bne @skip 102 | lda #1 103 | sta ramp_nmi_now 104 | ; call base_nmi (needs at least $8000 at BASE) 105 | lda #BANK_BASE 106 | sta $5FF8 107 | .assert (base_nmi >= $8000 && base_nmi < $9000), error, "base_nmi must be in $8000 bank" 108 | jsr base_nmi 109 | lda nsf_playing 110 | beq @play_end 111 | lda bank_8000 112 | sta $5FF8 113 | jsr ramp_nsf_play 114 | @play_end: 115 | ; restore all BASE banks 116 | lda #BANK_BASE 117 | sta $5FF8 118 | jsr base_banks 119 | lda #0 120 | sta ramp_nmi_now 121 | @skip: 122 | pla 123 | tay 124 | pla 125 | tax 126 | pla 127 | ; rti 128 | ramp_irq: 129 | rti 130 | 131 | ramp_reset: 132 | lda #$FF 133 | sta $5FFF 134 | jmp ($FFFC) 135 | 136 | ramp_nsf_init: 137 | ; call NSF INIT 138 | lda nsf_init_a 139 | ldx nsf_init_x 140 | ldy #0 141 | jsr do_nsf_init 142 | lda nsf_adjust 143 | cmp #2 144 | bne :+ 145 | ; FT pitch adjust hack: 146 | ; On FT, the pitch table pointer usually resides at $0C. 147 | ; PAL pitch table is 192 bytes past the NTSC pitch table 148 | lda $0C 149 | clc 150 | adc #<192 151 | sta $0C 152 | lda $0D 153 | adc #>192 154 | sta $0D 155 | lda #1 156 | sta nsf_adjust 157 | : 158 | rts 159 | 160 | ramp_nsf_play: 161 | ; call NSF PLAY 162 | jsr do_nsf_play 163 | ; if nsf_adjust, double every 5th frame (NTSC to PAL speed conversion) 164 | lda nsf_adjust 165 | beq :+ 166 | inc nsf_adjust 167 | lda nsf_adjust 168 | cmp #6 169 | bcc :+ 170 | jsr do_nsf_play 171 | lda #1 172 | sta nsf_adjust 173 | : 174 | rts 175 | 176 | ; trampolines for playback/init 177 | 178 | do_nsf_init: 179 | jmp (nsf_init) 180 | 181 | do_nsf_play: 182 | jmp (nsf_play) 183 | 184 | ; STA ABS bank switching 185 | ; (add more of these if needed for STX, STY, indexed, etc.) 186 | 187 | sta_5FF8: 188 | php 189 | pha 190 | clc 191 | adc bank_add 192 | sta bank_8000 193 | sta $5FF8 194 | pla 195 | plp 196 | rts 197 | 198 | sta_5FF9: 199 | php 200 | pha 201 | clc 202 | adc bank_add 203 | sta bank_9000 204 | sta $5FF9 205 | pla 206 | plp 207 | rts 208 | 209 | sta_5FFA: 210 | php 211 | pha 212 | fin_5FFA_: 213 | clc 214 | adc bank_add 215 | sta bank_A000 216 | sta $5FFA 217 | pla 218 | plp 219 | rts 220 | 221 | sta_5FFB: 222 | php 223 | pha 224 | fin_5FFB_: 225 | clc 226 | adc bank_add 227 | sta bank_B000 228 | sta $5FFB 229 | pla 230 | plp 231 | rts 232 | 233 | sta_5FFC: 234 | php 235 | pha 236 | clc 237 | adc bank_add 238 | sta $5FFC 239 | pla 240 | plp 241 | rts 242 | 243 | sta_5FFD: 244 | php 245 | pha 246 | clc 247 | adc bank_add 248 | sta $5FFD 249 | pla 250 | plp 251 | rts 252 | 253 | sta_5FFE: 254 | php 255 | pha 256 | clc 257 | adc bank_add 258 | sta $5FFE 259 | pla 260 | plp 261 | rts 262 | 263 | sta_5FFF: 264 | php 265 | pha 266 | clc 267 | adc bank_add 268 | sta $5FFF 269 | pla 270 | plp 271 | rts 272 | 273 | sty_5FFA: 274 | php 275 | pha 276 | tya 277 | jmp fin_5FFA_ 278 | 279 | sty_5FFB: 280 | php 281 | pha 282 | tya 283 | jmp fin_5FFB_ 284 | 285 | 286 | ; end of file 287 | -------------------------------------------------------------------------------- /nsf.s: -------------------------------------------------------------------------------- 1 | ; nsf.s 2 | ; 3 | ; used to generate NSF compilation 4 | ; 5 | ; 6 | ; https://github.com/bbbradsmith/zensf 7 | 8 | .include "out_info/build.inc" 9 | .include "out_info/strings.inc" 10 | .include "out_info/nsfs.inc" 11 | BANK_STUB = BANK_ART 12 | 13 | ; used by cfg 14 | .export ZP_LOW : abs 15 | .export ZP_HIGH : abs 16 | .export RAM_LOW : abs 17 | .export BANK_STUB : abs 18 | 19 | .segment "HEADER" 20 | .byte 'N', 'E', 'S', 'M', $1A ; ID 21 | .byte $01 ; version 22 | .byte TRACK_ORDER_LENGTH ; songs 23 | .byte 1 ; starting song 24 | .word $8000 ; LOAD 25 | .word stub_init ; INIT 26 | .word stub_play ; PLAY 27 | .byte NSF_TITLE 28 | .res 32 - .strlen(NSF_TITLE) 29 | .byte NSF_ARTIST 30 | .res 32 - .strlen(NSF_ARTIST) 31 | .byte NSF_COPYRIGHT 32 | .res 32 - .strlen(NSF_COPYRIGHT) 33 | .assert * = $6E, error, "NSF strings may be too long?" 34 | .word 16639 ; NTSC speed 35 | .repeat 8 36 | .byte BANK_STUB ; bankswitch init 37 | .endrepeat 38 | .word 19997 ; PAL speed 39 | .byte %00000010 ; PAL/NTSC bits 40 | .byte %00000000 ; expansion bits 41 | .byte 0,0,0,0 ; pad to $80 42 | .assert * = $80, error, "NSF header has incorrect length." 43 | 44 | .segment "BASE_ZP" : zeropage 45 | ; temporary pointers for copying 46 | psrc: .res 2 47 | pdst: .res 2 48 | 49 | .segment "STUB" 50 | 51 | .include "out_info/tracks.inc" 52 | 53 | ZP_HIGH = TRACK_RESERVE_ZP 54 | 55 | .assert TRACK_HIGH_ZP < ZP_LOW, error, "Embedded NSFs have conflicting ZP use." 56 | .assert TRACK_HIGH_RAM < RAM_LOW, error, "Embedded NSFs have conflicting RAM use." 57 | .assert __RAMP_CODE_LOAD__ 215 | sta psrc+1 216 | lda #<__RAMP_CODE_RUN__ 217 | sta pdst+0 218 | lda #>__RAMP_CODE_RUN__ 219 | sta pdst+1 220 | ldy #0 221 | @loop: 222 | lda (psrc), Y 223 | sta (pdst), Y 224 | inc psrc+0 225 | bne :+ 226 | inc psrc+1 227 | : 228 | inc pdst+0 229 | bne :+ 230 | inc pdst+1 231 | : 232 | lda pdst+0 233 | cmp #<(__RAMP_CODE_RUN__ + __RAMP_CODE_SIZE__) 234 | lda pdst+1 235 | sbc #>(__RAMP_CODE_RUN__ + __RAMP_CODE_SIZE__) 236 | bcc @loop 237 | rts 238 | 239 | .segment "RAMP_CODE" 240 | 241 | stub_finish: 242 | lda bank_8000 243 | sta $5FF8 244 | jsr ramp_nsf_init 245 | stub_return: 246 | lda #BANK_STUB 247 | sta $5FF8 248 | rts 249 | 250 | stub_play_ram: 251 | ; could also just point PLAY at ramp_nsf_play directly 252 | ; but the PowerPak does not appear to correctly re-initialize 253 | ; banks when switching tracks, so unless the stub bank is restored 254 | ; after every PLAY, the INIT for the next track will fail. 255 | lda bank_8000 256 | sta $5FF8 257 | jsr ramp_nsf_play 258 | jmp stub_return 259 | 260 | .assert stub_finish > ramp_nsf_init, error, "nsf.o must be linked after ramp.o (mods will rely on its address)" 261 | 262 | ; end of file 263 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | # ZENSF 2 | 3 | Difficult to use tools for making an NES ROM album from NSF files. 4 | 5 | This is mostly for my personal use, though I offer it freely for anyone to use. 6 | Comments and documentation are written for my own benefit, but if you're lucky they'll be enough for you. 7 | This is not a tutorial. If you'd like something easier to use, try EZNSF instead: 8 | https://github.com/bbbradsmith/eznsf 9 | 10 | For a demonstration example, see: demo.nes 11 | 12 | The NES mapper used by this project is a newer design and only supported by recently made emulators. More info: 13 | https://wiki.nesdev.com/w/index.php/INES_Mapper_031 14 | 15 | 16 | Instructions: 17 | 18 | 0. See below for dependencies; in particular make sure cc65 is placed in the right folder before beginning 19 | 1. Place NSFs in in_nsf/ folder, edit tracks.txt to build the track list 20 | 2. Run nsfspider.py to build in_nsf/tracks.txt and work through any exceptions you get 21 | 2.1. Input: 22 | 2.1.1. in_nsf/tracks.txt 23 | 2.1.2. in_nsf/*.nsf - if named in tracks.txt 24 | 2.2. Output: 25 | 2.2.1. out_src/*.bin - 4k .bin files of each bank from the NSFs 26 | 2.2.2. out_src/*.s - disassembly of any bank that needs modifications 27 | 2.2.3. out_info/binlist.txt - list of 4k banks to be prepared 28 | 2.2.4. out_info/modlist.txt - list of detected modifications to be done 29 | 2.2.5. out_info/tracks.inc - track data tables 30 | 2.2.6. out_info/nsfe.inc - NSFe data tables 31 | 2.2.7. out_info/nsfspider.txt - diagnostic output 32 | Note: this may take a few minutes to analyze the NSFs. 33 | In nsfspider.py, debug_track_skip can be used for faster iteration. 34 | All of the data will still be valid, but it will fail to identify some mods: 35 | - modlist.txt will be incomplete. 36 | - Some disassemblies will not be produced. 37 | This is OK if you've already identified the mods previously. 38 | (The contents of out_mod are not touched.) 39 | 40 | 3. Prepare modified banks. 41 | 3.1. Copy disassembled .s files from out_src/ to out_mod/ 42 | 3.2. Review out_info/modlist.txt for things you need to modify with replacments from ramp.s: 43 | 3.2.1. Any bank F: comment out last 6 bytes and replace with VECTORS macro 44 | 3.2.2. Any bankswitch write needs to be replaced (sta $5FFB -> jsr sta_5FFB, etc.) 45 | 3.2.3. A bankswitch from bank "$-1" means it came from RAM. This has to be resolved with manual debugging. 46 | 3.2.4. If ZP or RAM usage is too high you will probably need to patch the NSF directly to fix this. 47 | Some engines with this problem merely clear RAM during INIT, so this can just be replaced with NOP, 48 | and after patching the NSF re-running nsfpider will find the true RAM usage instead. 49 | (The disassemble_all option in nsfspider.py may assist in locating the memory clear code.) 50 | The RESERVE option is to accomodate engines (e.g. OFGS) that use the high bytes of ZP, 51 | ZENSF's ZP usage will be in the middle between ZP (in_art/art.txt) and RESERVE (in_nsf/tracks.txt). 52 | 3.3. Any extra banks needed can be manually added to binlist.txt 53 | 4. Place nmt/chr/pal art files in in_art/ folder, edit art.txt to provide an art list and other build paramters 54 | 5. Run package.py to build out_mod/ and in_art/ and work through any exceptions you get 55 | 5.1. Input: 56 | 5.1.1. in_art/art.txt 57 | 5.1.2. out_info/binlist.txt 58 | 5.1.3. out_mod/*.s - if named in binlist.txt and this file exists 59 | 5.1.4. in_art/*.nmt - if named in art.txt 60 | 5.1.5. in_art/*.chr - if named in chr.txt 61 | 5.1.6. in_art/*.pal - if named in pal.txt 62 | 5.1.7. ramp.s 63 | 5.1.8. mod.cfg 64 | 5.2. Output: 65 | 5.2.1. out_info/art.inc 66 | 5.2.2. out_info/screen.inc 67 | 5.2.3. out_info/screen_enum.inc 68 | 5.2.4. out_info/build.inc 69 | 5.2.5. out_info/strings.inc 70 | 5.2.6. out_mod/*.bin 71 | 5.2.7. out_info/nsfs.inc 72 | 5.2.8. out_info/package.txt 73 | 5.2.9. out_build/*.o (temporary) 74 | 5.2.10. out_build/command_temp.txt (temporary) 75 | 76 | 6. Build NSF with build_nsf.bat 77 | 7. Build NSFe with build_nsfe.bat (test this and/or the NSF to verify that everything done so far is correct) 78 | 79 | 8. Prepare in_code/custom.s to do your custom stuff. All the menus and stuff are handled in here. 80 | 9. Build NES with build_nes.bat 81 | 82 | Good luck! 83 | 84 | Other notes: 85 | - Some NSF files do not use all startup banks. These will be replaced with the ROM's high bank ($FF). 86 | - All input text files will be interpreted as UTF-8. 87 | - Any changes to ramp.s will require package.py to be re-run to rebuild the mod banks. 88 | - If adding NSFs late, add to the end of the list in in_nfs/tracks.txt and use the ORDER to specify the playback order. (In nsfspider.py debug_track_skip can be used to skip analysis for NSFs you've already modified.) 89 | - This project cannot support Famicom expansion audio. Don't ask for it. Nothing is impossible, but expansion audio is beyond the scope of this thing. 90 | 91 | 92 | Dependencies: 93 | 94 | python 3 - install it 95 | https://www.python.org/ 96 | 97 | cc65 - place in cc65/ folder 98 | http://cc65.github.io/cc65/ 99 | 100 | py65emu - already included in py65emu/ 101 | https://github.com/docmarionum1/py65emu 102 | 103 | If you're on a host platform other than Windows, you can probably figure out how to replace those batch files with whatever script you think is appropriate. 104 | 105 | 106 | Other Utilities: 107 | 108 | If you need a player for NSF/NSFe files: 109 | https://github.com/bbbradsmith/nsfplay 110 | 111 | Some emulators that can debug NSF: 112 | https://www.mesen.ca/ 113 | http://www.fceux.com/ 114 | 115 | If you need a way to edit the files in in_art/ try Shiru's NES Screen Tool: 116 | https://shiru.untergrund.net/software.shtml 117 | 118 | It may help to be able to identify NSF engines used. This NSFID tool can do that: 119 | https://forums.nesdev.com/viewtopic.php?f=6&t=14358 120 | 121 | 122 | Licenses: 123 | 124 | Go ahead and use this code, fork it, modify it, do whatever you like with it, commercial or otherwise. I'd prefer some credit, but I'm not that picky. 125 | 126 | The project contained in py65emu/ was not written by me, but it has a license compatible with the above. 127 | 128 | The code condainted in region.s has its own license which requires any source code releases to retain its license/copyright notice, but otherwise may be freely distributed. See that file for details. It was written by Damian Yerrick, and the original version is available here: 129 | http://wiki.nesdev.com/w/index.php/Detect_TV_system 130 | 131 | The 3 NSFs included under in_nsf/ are not included in this license. See below. 132 | 133 | 134 | NSF License exception: 135 | 136 | The 3 NSF files contained under in_nsf/ are my own and may not be reused or modified without my permission, but may be resdistributed freely with this project. If you don't intend to properly credit me for them, replace them with your own NSF files instead. 137 | 138 | Two of the NSFs are from my album of classical music for the NES: 139 | http://rainwarrior.ca/music/classic_chips.html 140 | 141 | The other NSF was composed for Famicompo Mini Vol. 8 and is available here: 142 | http://rainwarrior.ca/music/index.html 143 | 144 | 145 | 146 | https://github.com/bbbradsmith/zensf 147 | -------------------------------------------------------------------------------- /package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | assert sys.version_info[0] >= 3, "Python 3 required." 4 | 5 | import datetime 6 | import glob 7 | import os 8 | import shlex 9 | import subprocess 10 | 11 | # package.py 12 | # 13 | # This program prepares art data, NSF binary banks, 14 | # and modifications. 15 | # 16 | # Input: 17 | # /out_mod/*.s 18 | # /in_info/binlist.txt 19 | # /in_art/*.nmt 20 | # /in_art/*.chr 21 | # /in_art/*.pal 22 | # /in_art/art.txt 23 | # ramp.s 24 | # mod.cfg 25 | # mod.inc 26 | # 27 | # Output: 28 | # /out_info/art.inc 29 | # /out_info/screen.inc 30 | # /out_info/screen_enum.inc 31 | # /out_info/build.inc 32 | # /out_info/strings.inc 33 | # /out_mod/*.bin 34 | # /out_info/nsfs.inc 35 | # /out_info/package.txt 36 | # /out_build/*.o (temporary) 37 | # /out_build/command_temp.txt (temporary) 38 | # 39 | # See art.txt and readme.txt for information. 40 | # 41 | # 42 | # https://github.com/bbbradsmith/zensf 43 | 44 | delete_output = True # delete output files before beginning 45 | 46 | dir_art = "in_art" 47 | dir_src = "out_src" 48 | dir_mod = "out_mod" 49 | dir_info = "out_info" 50 | dir_build = "out_build" 51 | 52 | in_art = "art.txt" 53 | in_bins = "binlist.txt" 54 | in_ramp = "ramp" 55 | 56 | out_build = "build.inc" 57 | out_nsfs = "nsfs.inc" 58 | out_art = "art.inc" 59 | out_screen = "screen.inc" 60 | out_screen_enum = "screen_enum.inc" 61 | out_strings = "strings.inc" 62 | out_result = "package.txt" 63 | out_command = "command.txt" 64 | 65 | ca65 = os.path.join("cc65","bin","ca65") 66 | ld65 = os.path.join("cc65","bin","ld65") 67 | ca65_args = " %s -o %s" 68 | ld65_args = " -o %s -C mod.cfg %s %s" 69 | 70 | now_string = datetime.datetime.now().strftime("%a %b %d %H:%M:%S %Y") 71 | 72 | def parse_address(s): 73 | if (s.startswith("$")): 74 | return int(s[1:],16) 75 | else: 76 | return int(s,10) 77 | 78 | def file_size(s): 79 | try: 80 | return os.stat(s).st_size 81 | except: 82 | return 0 83 | 84 | def command(cline): 85 | global result 86 | result += cline + "\n" 87 | print(cline) 88 | out_command = os.path.join(dir_build,"command_temp.txt") 89 | try: 90 | output = open(out_command,"wt") 91 | returncode = subprocess.call(cline,stdout=output,stderr=output) 92 | output.close() 93 | except: 94 | raise Exception("Command failed to execute.\n> " + cline) 95 | if returncode != 0: 96 | print(open(out_command,"rt").read()) 97 | raise Exception("Command returned failure.\n> " + cline) 98 | 99 | 100 | # ==== 101 | # MAIN 102 | # ==== 103 | 104 | screens = [] 105 | bins = [] 106 | zp = 0x30 107 | ram = 0x100 108 | banks = 64 109 | result = "" 110 | 111 | nsf_title = "untitled (ZENSF)" 112 | nsf_artist = "unknown" 113 | nsf_copyright = "github.com/bbbradsmith/zensf" 114 | 115 | if delete_output: 116 | s = "Removing files." 117 | result += s + "\n" 118 | print(s) 119 | def rm(filename): 120 | if '*' in filename: 121 | #debug("removing glob: " + filename) 122 | for f in glob.glob(filename): 123 | rm(f) 124 | return 125 | #debug("rm: " + filename) 126 | try: 127 | os.remove(filename) 128 | s = "removed: " + filename 129 | global result 130 | result += s + "\n" 131 | print(s) 132 | except: 133 | #debug("not removed: " + filename) 134 | pass 135 | rm(os.path.join(dir_mod,"*.bin")) 136 | rm(os.path.join(dir_info,out_build)) 137 | rm(os.path.join(dir_info,out_nsfs)) 138 | rm(os.path.join(dir_info,out_art)) 139 | rm(os.path.join(dir_info,out_screen)) 140 | rm(os.path.join(dir_info,out_screen_enum)) 141 | rm(os.path.join(dir_info,out_strings)) 142 | rm(os.path.join(dir_info,out_result)) 143 | result += "\n" 144 | print() 145 | 146 | # parse art.txt 147 | 148 | in_line_count = 0 149 | for line in open(os.path.join(dir_art,in_art),"rt",encoding="UTF-8").readlines(): 150 | try: 151 | args = shlex.split(line,comments=True) 152 | except Exception as e: 153 | raise Exception(("Parsing error. (%d)\n" % in_line_count) + str(e) + "\n>" + line) 154 | in_line_count += 1 155 | if len(args) <= 0: 156 | continue 157 | elif args[0] == "SCREEN": 158 | if len(args) != 7: 159 | raise Execption("SCREEN requires 6 arguments. (%d)" % in_line_count) 160 | screens.append((args[1],args[2],args[3],args[4],args[5],args[6])) 161 | continue 162 | elif args[0] == "ZP": 163 | if len(args) != 2: 164 | raise Exception("ZP requires 1 argument. (%d)" % in_line_count) 165 | zp = parse_address(args[1]) 166 | continue 167 | elif args[0] == "RAM": 168 | if len(args) != 2: 169 | raise Exception("RAM requires 1 argument. (%d)" % in_line_count) 170 | ram = parse_address(args[1]) 171 | continue 172 | elif args[0] == "BANKS": 173 | if len(args) != 2: 174 | raise Exception("ZP requires 1 argument. (%d)" % in_line_count) 175 | banks = parse_address(args[1]) 176 | continue 177 | elif args[0] == "NSF_TITLE": 178 | if len(args) != 2: 179 | raise Exception("NSF_TITLE requires 1 argument. (%d)" % in_line_count) 180 | nsf_title = args[1] 181 | continue 182 | elif args[0] == "NSF_ARTIST": 183 | if len(args) != 2: 184 | raise Exception("NSF_ARTIST requires 1 argument. (%d)" % in_line_count) 185 | nsf_artist = args[1] 186 | continue 187 | elif args[0] == "NSF_COPYRIGHT": 188 | if len(args) != 2: 189 | raise Exception("NSF_COPYRIGHT requires 1 argument. (%d)" % in_line_count) 190 | nsf_copyright = args[1] 191 | continue 192 | else: 193 | raise Exception("Unknown statement. (%d)" % in_line_count) 194 | s = "ZP: $%02X\n" % zp 195 | s += "RAM: $%04X\n" % ram 196 | s += "BANKS: %d\n" % banks 197 | result += s + "\n" 198 | print(s) 199 | 200 | # parse binlist.txt 201 | in_line_count = 0 202 | for line in open(os.path.join(dir_info,in_bins),"rt",encoding="UTF-8").readlines(): 203 | args = shlex.split(line) 204 | in_line_count += 1 205 | if len(args) <= 0: 206 | continue 207 | elif args[0] == "#": 208 | continue 209 | elif len(args) != 1: 210 | raise Exception("BIN list should contain a single entry per line. (%d)" % in_line_count) 211 | bins.append(args[0]) 212 | s = "NSF BINS: %d" % len(bins) 213 | result += s + "\n" 214 | print(s) 215 | 216 | # output art.inc / screen.inc / screen_enum.inc / strings.inc 217 | 218 | art_nmt = [] 219 | art_chr = [] 220 | art_pal = [] 221 | for (name, nmt, chr0, chr1, pal0, pal1) in screens: 222 | if nmt not in art_nmt: 223 | art_nmt.append(nmt) 224 | if chr0 not in art_chr: 225 | art_chr.append(chr0) 226 | if chr1 not in art_chr: 227 | art_chr.append(chr1) 228 | if pal0 not in art_pal: 229 | art_pal.append(pal0) 230 | if pal1 not in art_pal: 231 | art_pal.append(pal1) 232 | 233 | art = "; generated by package.py " + now_string + "\n\n" 234 | art += ".segment \"ART\"\n" 235 | art_count = 0 236 | art_size = 0 237 | art_index = {} 238 | 239 | # Note: 240 | # Art data blocks are all power-of-two sizes, 241 | # sorted in decreasing order of size. 242 | # This ensures that no art block will cross a 4k bank boundary. 243 | 244 | for chr_ in art_chr: 245 | path = os.path.join(dir_art,chr_) 246 | if file_size(path) != 0x1000: 247 | raise Exception("CHR file must be 4kb: " + path) 248 | art_index[chr_] = art_count 249 | art += "art_data%02X: .incbin \"%s\"\n" % (art_count,path) 250 | s = "ART %02X: %s" % (art_count, path) 251 | result += s + "\n" 252 | print(s) 253 | art_size += 0x1000 254 | art_count += 1 255 | for nmt in art_nmt: 256 | path = os.path.join(dir_art,nmt) 257 | if file_size(path) != 0x400: 258 | raise Exception("NMT file must be 1kb: " + path) 259 | art_index[nmt] = art_count 260 | art += "art_data%02X: .incbin \"%s\"\n" % (art_count,path) 261 | s = "ART %02X: %s" % (art_count, path) 262 | result += s + "\n" 263 | print(s) 264 | art_size += 0x400 265 | art_count += 1 266 | for pal in art_pal: 267 | path = os.path.join(dir_art,pal) 268 | if file_size(path) != 0x10: 269 | raise Exception("PAL file must be 16b: " + path) 270 | art_index[pal] = art_count 271 | art += "art_data%02X: .incbin \"%s\"\n" % (art_count,path) 272 | s = "ART %02X: %s" % (art_count, path) 273 | result += s + "\n" 274 | print(s) 275 | art_size += 0x10 276 | art_count += 1 277 | art += "\n" 278 | art += "; end of file\n" 279 | open(os.path.join(dir_info,out_art),"wt",encoding="UTF-8").write(art) 280 | 281 | if art_count > 128: 282 | raise Exception("Art file count may not be more than 128. (%d)" % art_count) 283 | if art_size > 0x10000: 284 | raise Exception("Art data may not be more than 64k! (%d bytes)" % art_size) 285 | 286 | screen = "; generated by package.py " + now_string + "\n\n" 287 | screen_enum = "; generated by package.py " + now_string + "\n\n" 288 | screen_table = "screen_table:\n" 289 | count = 0 290 | for (name, nmt, chr0, chr1, pal0, pal1) in screens: 291 | screen_table += ".byte $%02X, $%02X, $%02X, $%02X, $%02X, 0, 0, 0 ; %s\n" % \ 292 | (art_index[nmt], 293 | art_index[chr0], 294 | art_index[chr1], 295 | art_index[pal0], 296 | art_index[pal1], 297 | name) 298 | screen_enum += "%s = %d\n" % ("SCREEN_"+name, count) 299 | s = "SCREEN %d: %s" % (count, name) 300 | result += s + "\n" 301 | print(s) 302 | count += 1 303 | 304 | screen_art = "art_table:\n" 305 | count = 0 306 | for (name, index) in sorted(art_index.items(), key=lambda i: i[1]): 307 | screen_art += ".word art_data%02X ; %s\n" % (index,name) 308 | if index != count: 309 | raise Exception("Unexpected error: sorted art index out of order?") 310 | count += 1 311 | 312 | screen += screen_table + "\n" 313 | screen += screen_art + "\n" 314 | screen += "; end of file\n" 315 | open(os.path.join(dir_info,out_screen),"wt",encoding="UTF-8").write(screen) 316 | 317 | screen_enum += "\n" 318 | screen_enum += "; end of file\n" 319 | open(os.path.join(dir_info,out_screen_enum),"wt",encoding="UTF-8").write(screen_enum) 320 | 321 | s = "ART SIZE: $%X bytes in %d files\n" % (art_size, art_count) 322 | s += "SCREENS: %d\n" % len(screens) 323 | result += s + "\n" 324 | print(s) 325 | 326 | s = "; generated by package.py " + now_string + "\n\n" 327 | s += ".define NSF_TITLE \"" + nsf_title + "\"\n" 328 | s += ".define NSF_ARTIST \"" + nsf_artist + "\"\n" 329 | s += ".define NSF_COPYRIGHT \"" + nsf_copyright + "\"\n" 330 | s += "\n" 331 | s += "; end of file\n" 332 | open(os.path.join(dir_info,out_strings),"wt",encoding="UTF-8").write(s) 333 | 334 | s = "NSF_TITLE \"" + nsf_title + "\"\n" 335 | s += "NSF_ARTIST \"" + nsf_artist + "\"\n" 336 | s += "NSF_COPYRIGHT \"" + nsf_copyright + "\"\n" 337 | result += s + "\n" 338 | print(s) 339 | 340 | # calculate bank structure, output build.inc 341 | 342 | if (banks & (banks-1)) != 0: 343 | raise Exception("BANKS must be a power of two.") 344 | 345 | bank_art = len(bins) # BANK_ART is first bank after NSF bins 346 | bank_art_end = bank_art + (art_size >> 12) 347 | if (art_size & 0x0FFF) != 0: 348 | bank_art_end += 1 349 | 350 | bank_base = banks - 4 # ideal BANK_BASE is 4 from the end 351 | while bank_base < bank_art_end: 352 | bank_base += 1 # shrink if there's not enough for 4 353 | 354 | s = "BANKS: %d\n" % banks 355 | s += "BANK_ART: %d to %d\n" % (bank_art,bank_art_end-1) 356 | s += "BANK_BASE: %d (%d total)\n" % (bank_base, banks-bank_base) 357 | result += s + "\n" 358 | print(s) 359 | 360 | if (banks - bank_base) < 1: 361 | raise Exception("No banks remain for BANK_BASE. (Minimum banks: %d)" % (bank_base+1)) 362 | 363 | inc = "; generated by package.py " + now_string + "\n\n" 364 | inc += "BANK_ART = $%02X\n" % bank_art 365 | inc += "BANK_BASE = $%02X\n" % bank_base 366 | inc += "BANK_END = $%02X\n" % banks 367 | inc += "ZP_LOW = $%02X\n" % zp 368 | inc += "RAM_LOW = $%04X\n" % ram 369 | inc += "\n" 370 | inc += "; end of file\n" 371 | open(os.path.join(dir_info,out_build),"wt",encoding="UTF-8").write(inc) 372 | 373 | # build ramp.s to use with mods 374 | 375 | ramp_s = in_ramp + ".s" 376 | ramp_o = os.path.join(dir_build, in_ramp + ".o") 377 | cline = ca65 + ca65_args % (ramp_s, ramp_o) 378 | command(cline) 379 | 380 | # build the mods 381 | 382 | for b in bins: 383 | mod_s = os.path.join(dir_mod, b + ".s") 384 | mod_o = os.path.join(dir_build, b + ".o") 385 | mod_bin = os.path.join(dir_mod, b + ".bin") 386 | if os.path.exists(mod_s): 387 | cline = ca65 + ca65_args % (mod_s, mod_o) 388 | command(cline) 389 | cline = ld65 + ld65_args % (mod_bin, mod_o, ramp_o) 390 | command(cline) 391 | mod_size = file_size(mod_bin) 392 | if mod_size != 0x1000: 393 | raise Exception("Compiled mod bin files must be 4k. (%d bytes)" % mod_size) 394 | 395 | result += "\n" 396 | print() 397 | 398 | # output nsfs.inc 399 | 400 | nsfs = "; generated by package.py " + now_string + "\n\n" 401 | nsfs += ".segment \"NSFS\"\n" 402 | count = 0 403 | for b in bins: 404 | name = b + ".bin" 405 | path_src = os.path.join(dir_src,name) 406 | path_mod = os.path.join(dir_mod,name) 407 | path_use = path_src 408 | if os.path.exists(path_mod): 409 | path_use = path_mod 410 | nsfs += ".incbin \"" + path_use + "\" ; %02X\n" % count 411 | s = "BANK $%02X: %s" % (count, path_use) 412 | result += s + "\n" 413 | print(s) 414 | if file_size(path_use) != 0x1000: 415 | raise Exception("BIN file must be 4kb: " + path_use) 416 | count += 1 417 | nsfs += "\n" 418 | result += "\n" 419 | print() 420 | nsfs += "; end of file\n" 421 | open(os.path.join(dir_info,out_nsfs),"wt",encoding="UTF-8").write(nsfs) 422 | 423 | #end 424 | open(os.path.join(dir_info,out_result ),"wt",encoding="UTF-8").write(result) 425 | print("Finished!") 426 | -------------------------------------------------------------------------------- /in_code/custom.s: -------------------------------------------------------------------------------- 1 | ; custom.s 2 | ; 3 | ; A basic set of menus to demonstrate how to implement them on top of the base layer. 4 | ; 5 | ; 6 | ; https://github.com/bbbradsmith/zensf 7 | 8 | .feature force_range ; required for negative values in .byte 9 | .macpack longbranch 10 | 11 | .include "../base.inc" 12 | .export custom_main 13 | .export custom_nmi 14 | 15 | .import nsf_playing ; ramp.s 16 | 17 | TRACKS = TRACK_ORDER_LENGTH 18 | 19 | .include "../out_info/screen_enum.inc" 20 | 21 | .segment "CUSTOM_ZP" : zeropage 22 | ; probably don't want to add any more ZP burden 23 | ; this is not high performance code anyway 24 | 25 | .segment "CUSTOM_RAM" 26 | gamepad_last: .res 1 27 | gamepad_new: .res 1 28 | title_pos: .res 1 29 | track: .res 1 30 | paused: .res 1 31 | 32 | .segment "CUSTOM" 33 | 34 | custom_main: 35 | jsr sfx_setup 36 | jmp menu_title 37 | 38 | custom_nmi: 39 | jmp sfx_tick 40 | 41 | ; sfx 42 | 43 | .segment "CUSTOM_RAM" 44 | sfx__lda_abs_y: .res 1 45 | sfx_ptr: .res 2 46 | sfx__rts: .res 1 47 | sfx_on: .res 1 48 | sfx_pos: .res 1 49 | sfx_wait: .res 1 50 | 51 | .segment "CUSTOM" 52 | 53 | sfx_read = sfx__lda_abs_y ; reads a byte from sfx_ptr + Y 54 | 55 | sfx_setup: 56 | ; sfx_read is a bit of RAM code around the pointer 57 | ; (saves having to put the pointer on ZP at the expense of 2 more RAM bytes) 58 | lda #$B9 ; LDA ABS, Y 59 | sta sfx__lda_abs_y 60 | lda #$60 ; RTS 61 | sta sfx__rts 62 | rts 63 | 64 | sfx_tick: 65 | lda sfx_on 66 | beq @end 67 | lda sfx_wait 68 | beq :+ 69 | dec sfx_wait 70 | jmp @end 71 | : 72 | ldy sfx_pos 73 | @loop: 74 | jsr sfx_read ; fake equivalent of: lda (sfx_ptr), Y 75 | cmp #$20 76 | bcs :+ 77 | ; command <$20 = direct write to $40XX register 78 | tax 79 | iny 80 | jsr sfx_read 81 | sta $4000, X 82 | iny 83 | jmp @loop 84 | : 85 | cmp #$21 86 | bcs :+ 87 | ; command $20 = finish frame and wait (0 = next frame) 88 | iny 89 | jsr sfx_read 90 | sta sfx_wait 91 | iny 92 | jmp @finish 93 | : 94 | ; command $FF (or anything > $20) = finish sfx 95 | lda #0 96 | sta sfx_on 97 | jmp @end 98 | ; 99 | @finish: 100 | sty sfx_pos 101 | @end: 102 | rts 103 | 104 | sfx_play: 105 | lda #0 106 | sta sfx_on 107 | sta sfx_wait 108 | sta sfx_pos 109 | lda ptr+0 110 | sta sfx_ptr+0 111 | lda ptr+1 112 | sta sfx_ptr+1 113 | lda #1 114 | sta sfx_on 115 | rts 116 | 117 | sfx_cursor_move: 118 | .byte $15, $01 ; enable square 0 119 | .byte $00, $B4 ; square, constant volume 4 120 | .byte $02, $FF ; low frequency $FF 121 | .byte $03, $F0 ; enable channel, high freq $0 122 | .byte $01, $8B ; sweep up 123 | .byte $20, 6 ; wait a few frames 124 | .byte $15, $00 ; disable 125 | .byte $FF ; end 126 | 127 | sfx_cursor_act: 128 | .byte $15, $02 ; enable square 1 129 | .byte $04, $B5 ; square, constant volume 5 130 | .byte $05, $07 ; disable sweep 131 | .byte $06, $E0 ; low frequency $EO 132 | .byte $07, $F0 ; enable channel, high freq $0 133 | .byte $20, 2 ; wait 134 | .byte $06, $D0 ; 135 | .byte $20, 2 ; 136 | .byte $06, $F0 ; 137 | .byte $20, 2 ; 138 | .byte $04, $31 ; narrow pulse, constant volume 1 (echo) 139 | .byte $06, $E0 ; 140 | .byte $20, 2 ; 141 | .byte $06, $D0 ; 142 | .byte $20, 2 ; 143 | .byte $06, $F0 ; 144 | .byte $20, 2 ; 145 | .byte $15, $00 ; disable 146 | .byte $FF ; end 147 | 148 | .macro SFX addr 149 | LOAD_PTR addr 150 | jsr sfx_play 151 | .endmacro 152 | 153 | ; utilities 154 | 155 | .macro LOAD_PTR addr 156 | lda #addr 159 | sta ptr+1 160 | .endmacro 161 | 162 | .macro LOAD_NMT addr 163 | lda #addr 166 | sta nmt_addr+1 167 | .endmacro 168 | 169 | ffade_in: ; fastest fade_in 170 | ldx #0 171 | jmp fade_in 172 | 173 | ffade_out: ; fastest fade_out 174 | ldx #0 175 | jmp fade_out 176 | 177 | gamepad_poll_new: 178 | ; return/gamepad_new = only buttons that are newly pressed on this poll 179 | jsr gamepad_poll 180 | eor gamepad_last 181 | and gamepad 182 | sta gamepad_new 183 | ; store last value 184 | lda gamepad 185 | sta gamepad_last 186 | ; return new 187 | lda gamepad_new 188 | rts 189 | 190 | gamepad_wait_release: 191 | : 192 | jsr gamepad_poll 193 | bne :- 194 | rts 195 | 196 | rainbow17: 197 | ; only do this 1/8 frames 198 | lda nmi_count 199 | and #7 200 | beq :+ 201 | rts 202 | : 203 | ; cycle hue of palette 17 by -1 204 | lda palette+17 205 | pha 206 | and #$F0 207 | sta temp 208 | pla 209 | and #$0F 210 | beq @combine 211 | cmp #$0D 212 | bcs @combine 213 | clc 214 | adc #$0B ; -1 hue 215 | : 216 | cmp #$0D 217 | bcc :+ 218 | sec 219 | sbc #$0C 220 | jmp :- 221 | : 222 | @combine: 223 | ora temp 224 | sta palette+17 225 | rts 226 | 227 | ; sprites 228 | 229 | .macro SPRITE addr 230 | LOAD_PTR addr 231 | jsr sprite 232 | .endmacro 233 | 234 | .macro SPRITEXY addr, x_, y_ 235 | LOAD_PTR addr 236 | ldx #x_ 237 | ldy #y_ 238 | jsr sprite 239 | .endmacro 240 | 241 | ; sprite definitions: [x y tile attr] 128=end 242 | sprite_title: .byte 0, 0, $0C, $00 243 | .byte 55, 0, $0C, $40, 128 244 | sprite_tracks: .byte -3, 0, $0C, $00, 128 245 | sprite_info0: .byte 0, -2, $10, $00, 128 246 | sprite_info1: .byte 1, -2, $11, $00, 128 247 | sprite_info2: .byte 1, -2, $12, $00, 128 248 | sprite_info3: .byte 1, -2, $13, $00, 128 249 | sprite_infu0: .byte 0, -1, $10, $01, 128 250 | sprite_infu1: .byte 1, -1, $11, $01, 128 251 | sprite_infu2: .byte 1, -1, $12, $01, 128 252 | sprite_infu3: .byte 1, -1, $13, $01, 128 253 | sprite_num0: .byte 0, -1, $00, $00, 128 254 | sprite_num1: .byte 0, -1, $01, $00, 128 255 | sprite_num2: .byte 0, -1, $02, $00, 128 256 | sprite_num3: .byte 0, -1, $03, $00, 128 257 | sprite_num4: .byte 0, -1, $04, $00, 128 258 | sprite_num5: .byte 0, -1, $05, $00, 128 259 | sprite_num6: .byte 0, -1, $06, $00, 128 260 | sprite_num7: .byte 0, -1, $07, $00, 128 261 | sprite_num8: .byte 0, -1, $08, $00, 128 262 | sprite_num9: .byte 0, -1, $09, $00, 128 263 | sprite_colon: .byte 0, -1, $0A, $00, 128 264 | sprite_play: .byte 0, -1, $0B, $00, 128 265 | sprite_loop: .byte 0, -1, $0C, $00, 128 266 | sprite_pause: .byte 0, -1, $0D, $00, 128 267 | 268 | sprite_num_table: 269 | .word sprite_num0 270 | .word sprite_num1 271 | .word sprite_num2 272 | .word sprite_num3 273 | .word sprite_num4 274 | .word sprite_num5 275 | .word sprite_num6 276 | .word sprite_num7 277 | .word sprite_num8 278 | .word sprite_num9 279 | 280 | load_sprite_num: 281 | ; A = number 0-9 282 | ; clobbers X 283 | asl 284 | tax 285 | lda sprite_num_table+0, X 286 | sta ptr+0 287 | lda sprite_num_table+1, X 288 | sta ptr+1 289 | rts 290 | 291 | ; menu screens 292 | 293 | menu_title_redraw: 294 | jsr sprite_begin 295 | lda title_pos 296 | asl 297 | asl 298 | asl 299 | asl 300 | clc 301 | adc #(15*8) 302 | tay 303 | ldx #(12*8) 304 | SPRITE sprite_title 305 | jsr sprite_finish 306 | jmp rainbow17 307 | 308 | menu_title: 309 | lda #SCREEN_Title 310 | jsr load_screen 311 | jsr menu_title_redraw 312 | jsr ffade_in 313 | @loop: 314 | jsr gamepad_poll_new 315 | ;lda gamepad_new 316 | and #(PAD_L | PAD_R | PAD_U | PAD_D | PAD_SELECT) 317 | beq :+ 318 | lda title_pos 319 | eor #1 320 | sta title_pos 321 | SFX sfx_cursor_move 322 | jmp @finish 323 | : 324 | lda gamepad_new 325 | and #(PAD_A | PAD_B | PAD_START) 326 | beq :+ 327 | SFX sfx_cursor_act 328 | jsr ffade_out 329 | lda title_pos 330 | jne menu_info 331 | jmp menu_tracks 332 | : 333 | @finish: 334 | jsr menu_title_redraw 335 | jsr ppu_update 336 | jmp @loop 337 | 338 | menu_info_bob_: 339 | clc 340 | adc nmi_count 341 | lsr 342 | lsr 343 | lsr 344 | lsr 345 | lsr 346 | tay 347 | lda @anim, Y 348 | tay 349 | rts 350 | @anim: 351 | .byte 0, -1, -2, -1, 0, 1, 2, 1 352 | 353 | .macro MENU_INFO_BOB angle, x_, y_, spr_ 354 | lda #angle 355 | jsr menu_info_bob_ 356 | clc 357 | adc #(y_ * 8) 358 | tay 359 | ldx #(x_ * 8) 360 | SPRITE spr_ 361 | .endmacro 362 | 363 | menu_info_redraw: 364 | jsr sprite_begin 365 | ; INFO letters bobbing up and down 366 | MENU_INFO_BOB 0, 8, 20, sprite_info0 367 | MENU_INFO_BOB 53, 13, 19, sprite_info1 368 | MENU_INFO_BOB 106, 18, 22, sprite_info2 369 | MENU_INFO_BOB 159, 23, 21, sprite_info3 370 | ; shadow underlay 371 | MENU_INFO_BOB 0, 8, 20, sprite_infu0 372 | MENU_INFO_BOB 53, 13, 19, sprite_infu1 373 | MENU_INFO_BOB 106, 18, 22, sprite_infu2 374 | MENU_INFO_BOB 159, 23, 21, sprite_infu3 375 | jsr sprite_finish 376 | jmp rainbow17 377 | 378 | menu_info: 379 | lda #SCREEN_Info 380 | jsr load_screen 381 | jsr menu_info_redraw 382 | jsr ffade_in 383 | @loop: 384 | jsr gamepad_poll_new 385 | beq :+ 386 | SFX sfx_cursor_act 387 | jsr ffade_out 388 | jmp menu_title 389 | : 390 | jsr menu_info_redraw 391 | jsr ppu_update 392 | jmp @loop 393 | 394 | menu_tracks_redraw: 395 | ; update artist 396 | LOAD_NMT ($2000+6+(23*32)) 397 | lda track 398 | jsr load_track_artist 399 | jsr ppu_string_buffer 400 | lda #$00 401 | ldx nmt_count ; fill with spaces to 20 characters wide 402 | : 403 | cpx #20 404 | bcs :+ 405 | sta nmt_buffer, X 406 | inx 407 | jmp :- 408 | : 409 | stx nmt_count 410 | ; draw track cursor 411 | jsr sprite_begin 412 | lda track 413 | asl 414 | asl 415 | asl 416 | clc 417 | adc #(6*8) 418 | tay 419 | ldx #(5*8) 420 | SPRITE sprite_tracks 421 | jsr sprite_finish 422 | jmp rainbow17 423 | 424 | menu_tracks: 425 | lda #SCREEN_Tracks 426 | jsr load_screen 427 | ; fill track names 428 | LOAD_NMT ($2000+6+(6*32)) 429 | lda #0 430 | : 431 | pha 432 | jsr load_track_title_short 433 | jsr ppu_string 434 | lda nmt_addr+0 435 | clc 436 | adc #<32 437 | sta nmt_addr+0 438 | lda nmt_addr+1 439 | adc #>32 440 | sta nmt_addr+1 441 | pla 442 | clc 443 | adc #1 444 | cmp #TRACKS 445 | bcc :- 446 | jsr menu_tracks_redraw 447 | jsr ppu_update 448 | jsr ffade_in 449 | @loop: 450 | jsr gamepad_poll_new 451 | ;lda gamepad_new 452 | and #(PAD_R | PAD_D) 453 | beq :+ 454 | lda track 455 | cmp #(TRACKS-1) 456 | bcs :+ 457 | inc track 458 | SFX sfx_cursor_move 459 | : 460 | lda gamepad_new 461 | and #(PAD_L | PAD_U) 462 | beq :+ 463 | lda track 464 | beq :+ 465 | dec track 466 | SFX sfx_cursor_move 467 | : 468 | lda gamepad_new 469 | and #(PAD_SELECT) 470 | beq :+ 471 | SFX sfx_cursor_act 472 | jsr ffade_out 473 | jmp menu_title 474 | : 475 | lda gamepad_new 476 | and #(PAD_B) 477 | beq :+ 478 | lda #1 479 | sta nsf_looping 480 | jmp menu_tracks_play 481 | : 482 | lda gamepad_new 483 | and #(PAD_A | PAD_START) 484 | beq :+ 485 | lda #0 486 | sta nsf_looping 487 | jmp menu_tracks_play 488 | : 489 | jsr menu_tracks_redraw 490 | jsr ppu_update 491 | jmp @loop 492 | 493 | menu_tracks_play: 494 | jsr ffade_out 495 | lda #0 496 | sta sfx_on 497 | jmp menu_play 498 | 499 | .macro SPRITENUM num, x_, y_ 500 | lda num 501 | jsr load_sprite_num 502 | ldx #x_ 503 | ldy #y_ 504 | jsr sprite 505 | .endmacro 506 | 507 | menu_play_redraw: 508 | jsr sprite_begin 509 | SPRITENUM time_d0, 140, 144 510 | SPRITENUM time_d1, 132, 144 511 | SPRITENUM time_d2, 116, 144 512 | SPRITENUM time_d3, 108, 144 513 | SPRITEXY sprite_colon, 124, 144 514 | lda nsf_playing 515 | bne :+ 516 | LOAD_PTR sprite_pause 517 | jmp @mode 518 | : 519 | lda nsf_looping 520 | beq :+ 521 | LOAD_PTR sprite_loop 522 | jmp @mode 523 | : 524 | LOAD_PTR sprite_play 525 | ; 526 | @mode: 527 | ldx #124 528 | ldy #156 529 | jsr sprite 530 | jsr sprite_finish 531 | jmp rainbow17 532 | 533 | menu_play: 534 | lda #SCREEN_Play 535 | jsr load_screen 536 | LOAD_NMT ($2000+6+(6*32)) 537 | lda track 538 | jsr load_track_artist 539 | jsr ppu_string 540 | LOAD_NMT ($2000+6+(8*32)) 541 | lda track 542 | jsr load_track_title 543 | jsr ppu_string 544 | jsr menu_play_restart_ 545 | jsr menu_play_redraw 546 | jsr ffade_in 547 | @loop: 548 | jsr gamepad_poll_new 549 | ;lda gamepad_new 550 | and #(PAD_SELECT) 551 | beq :+ 552 | jsr menu_play_stop_ 553 | jmp menu_tracks 554 | : 555 | lda gamepad_new 556 | and #(PAD_B) 557 | beq :+ 558 | lda #1 559 | sta nsf_looping 560 | lda nsf_playing 561 | bne :+ 562 | lda paused 563 | bne @unpause 564 | jsr menu_play_restart_ 565 | : 566 | lda gamepad_new 567 | and #(PAD_A) 568 | beq :+ 569 | lda #0 570 | sta nsf_looping 571 | lda nsf_playing 572 | bne :+ 573 | lda paused 574 | bne @unpause 575 | jsr menu_play_restart_ 576 | : 577 | lda gamepad_new 578 | and #(PAD_START) 579 | beq @pause_end 580 | lda paused 581 | beq :+ 582 | @unpause: 583 | ; unpause 584 | lda #$0F 585 | sta $4015 ; re-enable channels 586 | lda #1 587 | sta nsf_playing 588 | lda #0 589 | sta paused 590 | jmp @pause_end 591 | : 592 | lda nsf_playing 593 | beq :+ 594 | ; pause 595 | lda #$00 596 | sta nsf_playing 597 | sta $4015 ; disable channels 598 | lda #1 599 | sta paused 600 | jmp @pause_end 601 | : 602 | ; restart 603 | jsr menu_play_restart_ 604 | ; 605 | @pause_end: 606 | lda gamepad_new 607 | and #(PAD_L | PAD_U) 608 | beq @track_prev_end 609 | lda track 610 | bne :+ 611 | jsr menu_play_restart_ 612 | jmp @track_prev_end 613 | : 614 | jsr menu_play_stop_ 615 | dec track 616 | jmp menu_play 617 | @track_prev_end: 618 | lda gamepad_new 619 | and #(PAD_R | PAD_D) 620 | beq :+ 621 | lda track 622 | cmp #(TRACKS-1) 623 | bcs :+ 624 | jsr menu_play_stop_ 625 | inc track 626 | jmp menu_play 627 | : 628 | ; auto-advance if track finished and not looping 629 | lda nsf_looping 630 | bne :+ 631 | lda advance 632 | beq :+ 633 | lda track 634 | cmp #(TRACKS-1) 635 | bcs :+ 636 | jsr menu_play_stop_ 637 | inc track 638 | jmp menu_play 639 | : 640 | ; redraw and go to next frame 641 | jsr menu_play_redraw 642 | jsr ppu_update 643 | jmp @loop 644 | 645 | menu_play_stop_: 646 | ; silence and fade out 647 | lda #$00 648 | sta nsf_playing 649 | sta $4015 650 | jmp ffade_out 651 | 652 | menu_play_restart_: 653 | lda #0 654 | sta paused 655 | ldx track 656 | jsr play_track 657 | rts 658 | 659 | ; end of file 660 | -------------------------------------------------------------------------------- /base.s: -------------------------------------------------------------------------------- 1 | ; base.s 2 | ; 3 | ; common functionality for NES ROM, stuff that doesn't need to be "custom" 4 | ; handles direct interaction with ramp.s (intermediary layer between it and custom.s) 5 | ; 6 | ; 7 | ; https://github.com/bbbradsmith/zensf 8 | 9 | .macpack longbranch 10 | 11 | .import ramp_play 12 | .import ramp_nmi 13 | .import ramp_irq 14 | 15 | .import bank_add 16 | .import bank_8000 17 | .import bank_9000 18 | .import bank_A000 19 | .import bank_B000 20 | .import sta_5FFC 21 | .import sta_5FFD 22 | .import sta_5FFE 23 | .import sta_5FFF 24 | 25 | .import nsf_init 26 | .import nsf_play 27 | .import nsf_init_a 28 | .import nsf_init_x 29 | .import nsf_adjust 30 | .import nsf_playing 31 | .import ramp_play_exit 32 | 33 | .import getTVSystem ; region.s 34 | 35 | .import custom_main 36 | .import custom_nmi 37 | 38 | .export base_nmi 39 | .export base_banks 40 | 41 | .include "base.inc" ; exports RAM and utilities for custom.s 42 | 43 | ; build info, NSF collection, art collection 44 | 45 | .include "out_info/build.inc" 46 | .include "out_info/nsfs.inc" 47 | .include "out_info/art.inc" 48 | 49 | ; used by cfg 50 | .export ZP_LOW : abs 51 | .export ZP_HIGH : abs 52 | .export RAM_LOW : abs 53 | .export BANK_ART : abs 54 | .export BANK_BASE : abs 55 | .export BANK_END : abs 56 | 57 | ; RAM usage 58 | 59 | .segment "BASE_ZP" : zeropage 60 | nmi_count: .res 1 61 | nmi_ready: .res 1 62 | region: .res 1 63 | gamepad: .res 1 64 | ppu_ctrl: .res 1 65 | ppu_mask: .res 1 66 | ppu_scroll_x: .res 1 67 | ppu_scroll_y: .res 1 68 | oam_pos: .res 1 69 | nmt_count: .res 1 70 | nmt_addr: .res 2 71 | ptr: .res 2 72 | temp: .res 2 73 | 74 | .segment "STACK" 75 | nmt_buffer: .res 64 76 | 77 | .segment "BASE_RAM" 78 | palette: .res 32 79 | time_fps: .res 1 80 | time_frame: .res 1 81 | time_seconds: .res 2 82 | time_d0: .res 1 83 | time_d1: .res 1 84 | time_d2: .res 1 85 | time_d3: .res 1 86 | nsf_looping: .res 1 87 | nsf_track: .res 1 88 | advance: .res 1 89 | 90 | 91 | .segment "OAM" 92 | .align $100 93 | oam: .res $100 94 | 95 | ; iNES header 96 | 97 | .segment "HEADER" 98 | 99 | INES_MAPPER = 31 100 | INES_MIRROR = 0 ; 0=vertical nametables, 1=horizontal 101 | INES_PRG_16K = BANK_END / 4 102 | INES_CHR_8K = 0 103 | INES_BATTERY = 0 104 | INES2 = %00001000 ; NES 2.0 flag for bit 7 105 | INES2_SUBMAPPER = 0 106 | INES2_PRGRAM = 0 ; x: 2^(6+x) bytes (0 for none) 107 | INES2_PRGBAT = 0 108 | INES2_CHRRAM = 7 109 | INES2_CHRBAT = 0 110 | INES2_REGION = 2 ; 0=NTSC, 1=PAL, 2=Dual 111 | 112 | .byte 'N', 'E', 'S', $1A ; ID 113 | .byte >8) 119 | .byte ((INES_CHR_8K >> 8) << 4) | (INES_PRG_16K >> 8) 120 | .byte (INES2_PRGBAT << 4) | INES2_PRGRAM 121 | .byte (INES2_CHRBAT << 4) | INES2_CHRRAM 122 | .byte INES2_REGION 123 | .byte $00 ; VS system 124 | .byte $00, $00 ; padding/reserved 125 | .assert * = 16, error, "NES header must be 16 bytes." 126 | 127 | ; reset stub 128 | 129 | .segment "RESET" 130 | reset: 131 | sei 132 | lda #0 133 | sta $2000 134 | lda #BANK_BASE 135 | sta $5FF8 136 | .assert (base_main < $9000), error, "base_main must be in $8000 bank" 137 | jmp base_main 138 | 139 | ; vectors 140 | 141 | .segment "VECTORS" 142 | .word ramp_nmi 143 | .word reset 144 | .word ramp_irq 145 | 146 | ; base_low 147 | ; these routines are safe to run when only $8000 is banked in 148 | 149 | .segment "BASE_LOW" 150 | .import __BASE_LOW_LOAD__ 151 | .import __BASE_LOW_SIZE__ 152 | .assert (__BASE_LOW_LOAD__ + __BASE_LOW_SIZE__) <= $9000, error, "BASE_LOW must fit in $8000 bank." 153 | 154 | base_banks: 155 | ldx #BANK_BASE+1 156 | stx $5FF9 157 | inx 158 | stx $5FFA 159 | inx 160 | stx $5FFB 161 | rts 162 | 163 | base_unbanks: 164 | ldx bank_B000 165 | stx $5FFB 166 | ldx bank_A000 167 | stx $5FFA 168 | ldx bank_9000 169 | stx $5FF9 170 | rts 171 | 172 | base_main: 173 | ;sei 174 | cld 175 | ldx #$ff 176 | txs 177 | jsr base_banks 178 | jmp base_main_ 179 | 180 | base_nmi: 181 | jsr base_banks 182 | jsr base_nmi_ 183 | jmp base_unbanks 184 | 185 | base_ramp_play: 186 | lda ppu_ctrl 187 | and #%01111111 188 | sta $2000 ; disable NMI (NMI has to restore BASE banks, will interfere with INIT) 189 | jsr base_unbanks 190 | jsr ramp_play 191 | bit $2002 192 | lda ppu_ctrl 193 | sta $2000 ; restore NMI 194 | jmp base_banks 195 | 196 | ; main routines 197 | 198 | .segment "BASE" 199 | 200 | base_main_: 201 | ;sei 202 | ;cld 203 | ;ldx #$ff 204 | ;txs 205 | ldx #$00 206 | ;stx $2000 ; disable NMI 207 | stx $2001 ; disable rendering 208 | stx $4010 ; disable DPCM IRQ 209 | stx $4015 ; mute APU 210 | lda #$40 211 | sta $4017 ; disable APU IRQ 212 | ; warmup frame 1 213 | bit $2002 214 | : 215 | bit $2002 216 | bpl :- 217 | ; clear memory 218 | ;ldx #$00 219 | txa 220 | : 221 | sta $00, X 222 | sta $100, X 223 | sta $200, X 224 | sta $300, X 225 | sta $400, X 226 | sta $500, X 227 | sta $600, X 228 | sta $700, X 229 | inx 230 | bne :- 231 | ; warmup frame 2 232 | : 233 | bit $2002 234 | bpl :- 235 | ; copy RAM code from ramp.s 236 | .import __RAMP_CODE_LOAD__ 237 | .import __RAMP_CODE_RUN__ 238 | .import __RAMP_CODE_SIZE__ 239 | __RAMP_CODE_END__ = __RAMP_CODE_RUN__ + __RAMP_CODE_SIZE__ 240 | @src = nmt_addr ; temporarily alias these pointer variables 241 | @dst = ptr 242 | lda #<__RAMP_CODE_LOAD__ 243 | sta @src+0 244 | lda #>__RAMP_CODE_LOAD__ 245 | sta @src+1 246 | lda #<__RAMP_CODE_RUN__ 247 | sta @dst+0 248 | lda #>__RAMP_CODE_RUN__ 249 | sta @dst+1 250 | ldy #0 251 | @ramp_loop: 252 | lda (@src), Y 253 | sta (@dst), Y 254 | inc @src+0 255 | bne :+ 256 | inc @src+1 257 | : 258 | inc @dst+0 259 | bne :+ 260 | inc @dst+1 261 | : 262 | lda @dst+0 263 | cmp #<__RAMP_CODE_END__ 264 | lda @dst+1 265 | sbc #>__RAMP_CODE_END__ 266 | bcc @ramp_loop 267 | ; permanently enable NMI 268 | lda #%10001000 269 | sta ppu_ctrl 270 | sta $2000 271 | ; detect platform 272 | jsr getTVSystem 273 | cmp #3 274 | bcc :+ 275 | lda #0 ; 3+ = unknown: defaults to NTSC 276 | : 277 | sta region 278 | tax 279 | lda REGION_FPS, X 280 | sta time_fps 281 | ; begin 282 | jmp custom_main 283 | 284 | REGION_FPS: 285 | .byte 60, 50, 50 286 | ; nmi handler 287 | 288 | base_nmi_: 289 | inc nmi_count 290 | lda nmi_ready 291 | beq @ppu_end 292 | ; 293 | lda #0 294 | sta nmi_ready 295 | ; apply PPU mask 296 | lda ppu_mask 297 | sta $2001 298 | beq @ppu_end ; rendering is off 299 | ; OAM DMA 300 | lda #0 301 | sta $2003 302 | lda #>oam 303 | sta $4014 304 | ; palettes 305 | bit $2002 306 | lda #>$3F00 307 | sta $2006 308 | lda #<$3F00 309 | sta $2006 310 | ldx #0 311 | : 312 | lda palette, X 313 | sta $2007 314 | inx 315 | cpx #32 316 | bcc :- 317 | ; nametables 318 | ldy nmt_count 319 | beq @ppu_scroll 320 | lda ppu_ctrl ; sets direction of update 321 | sta $2000 322 | lda nmt_addr+1 323 | sta $2006 324 | lda nmt_addr+0 325 | sta $2006 326 | tsx 327 | txa 328 | ldx #<(nmt_buffer-1) 329 | txs 330 | tax 331 | ; X = stack pointer 332 | ; Y = nmt_count 333 | ; stack points at first nmt_buffer byte 334 | : 335 | pla 336 | sta $2007 337 | dey 338 | bne :- 339 | txs ; restore stack 340 | sty nmt_count ; = 0 341 | @ppu_scroll: 342 | lda ppu_ctrl 343 | sta $2000 344 | lda ppu_scroll_x 345 | sta $2005 346 | lda ppu_scroll_y 347 | sta $2005 348 | @ppu_end: 349 | jsr custom_nmi 350 | lda nsf_playing 351 | jeq @play_timer_end 352 | ldx #0 353 | inc time_frame 354 | lda time_frame 355 | cmp time_fps 356 | bcc @play_timer_end 357 | stx time_frame 358 | ; internal timer 359 | inc time_seconds+0 360 | bne :+ 361 | inc time_seconds+1 362 | bne :+ ; max out as 65535 seconds 363 | lda #$FF 364 | sta time_seconds+0 365 | sta time_seconds+1 366 | : 367 | ; automatic stop 368 | ldy nsf_track 369 | lda nsf_looping 370 | beq :+ 371 | lda track_loop, Y 372 | bne :++ ; track is loopable, and nsf_looping is true, play forever 373 | : 374 | tya 375 | asl 376 | tay 377 | lda time_seconds+0 378 | cmp track_time+0, Y 379 | lda time_seconds+1 380 | sbc track_time+1, Y 381 | bcc :+ 382 | stx nsf_playing ; stop playback if not looping and track finished 383 | stx $4015 ; silence all channels 384 | lda #1 385 | sta advance ; signal that track may advance 386 | : 387 | ; display counter 388 | inc time_d0 ; seconds 389 | lda time_d0 390 | cmp #10 391 | bcc :+ 392 | stx time_d0 393 | inc time_d1 394 | lda time_d1 395 | cmp #6 396 | bcc :+ 397 | stx time_d1 398 | inc time_d2 ; minutes 399 | lda time_d2 400 | cmp #10 401 | bcc :+ 402 | stx time_d2 403 | inc time_d3 404 | lda time_d3 405 | cmp #6 406 | bcc :+ ; max at 59:59 407 | ldx #5 408 | sta time_d1 409 | sta time_d3 410 | ldx #9 411 | sta time_d0 412 | sta time_d2 413 | : 414 | @play_timer_end: 415 | rts 416 | 417 | ; player utilities called by custom.s 418 | 419 | .include "out_info/tracks.inc" 420 | 421 | .exportzp TRACK_ORDER_LENGTH 422 | ZP_HIGH = TRACK_RESERVE_ZP 423 | 424 | ; ensure there's enough RAM/ZP in the open areas to run the NSFs 425 | .assert TRACK_HIGH_ZP < ZP_LOW, error, "Embedded NSFs have conflicting ZP use." 426 | .assert TRACK_HIGH_RAM < RAM_LOW, error, "Embedded NSFs have conflicting RAM use." 427 | .import __STACK_LOAD__ 428 | .import __STACK_SIZE__ 429 | STACK_SAFETY = 16 ; a few extra bytes of stack as a safety buffer 430 | .assert (TRACK_LOW_STACK - STACK_SAFETY) >= (__STACK_LOAD__ + __STACK_SIZE__), error, "Stack area too small for embedded NSFs." 431 | 432 | .include "out_info/screen.inc" 433 | 434 | play_track: 435 | ; in: X = track 436 | lda #0 437 | sta nsf_playing 438 | sta advance 439 | sta time_frame 440 | sta time_seconds+0 441 | sta time_seconds+1 442 | sta time_d0 443 | sta time_d1 444 | sta time_d2 445 | sta time_d3 446 | lda track_order, X 447 | sta nsf_track 448 | ; NSF init 449 | .assert $0200 473 | sta ptr+1 474 | ldy #0 475 | @clear_loop: 476 | tya ; A = Y = 0 477 | sta (ptr), Y 478 | inc ptr+0 479 | bne :+ 480 | inc ptr+1 481 | : 482 | lda ptr+0 483 | cmp #RAM_LOW 486 | bcc @clear_loop 487 | ; setup APU 488 | ; A = Y = 0 489 | : 490 | sta $4000, Y 491 | iny 492 | cpy #$14 493 | bcc :- 494 | lda #$0F 495 | sta $4015 496 | lda #$40 497 | sta $4017 498 | ; setup INIT parameter X / region adjustment 499 | ldy nsf_track 500 | ldx region 501 | cpx #2 502 | bne :+ 503 | ; 2 = Dendy 504 | lda #1 ; 1/5 frame doubling 505 | sta nsf_adjust 506 | lda #0 507 | sta nsf_init_x ; play as if NTSC 508 | jmp @init_x_set 509 | : 510 | cpx #1 511 | bne @init_x_ntsc 512 | ; PAL 513 | lda track_pal_adjust, Y 514 | sta nsf_adjust 515 | beq :+ 516 | dex ; X=0 if adjust 1/2 517 | : 518 | stx nsf_init_x ; adjust 0 = play as PAL (x=1), 1/2 = play as NTSC (x=0) 519 | jmp @init_x_set 520 | ; 521 | @init_x_ntsc: 522 | ; 0 = NTSC 523 | lda #0 524 | sta nsf_adjust 525 | sta nsf_init_x 526 | ; 527 | @init_x_set: 528 | ; rest of track data 529 | lda track_song, Y 530 | sta nsf_init_a 531 | lda track_bank_offset, Y 532 | sta bank_add 533 | tya 534 | asl 535 | tax ; X = track * 2 (word data) 536 | lda track_init_addr+0, X 537 | sta nsf_init+0 538 | lda track_init_addr+1, X 539 | sta nsf_init+1 540 | lda track_play_addr+0, X 541 | sta nsf_play+0 542 | lda track_play_addr+1, X 543 | sta nsf_play+1 544 | txa 545 | asl 546 | asl 547 | tax ; X = track * 8 (8 byte data) 548 | ; top 4 banks apply immediately 549 | lda track_bank_start+7, X 550 | jsr sta_5FFF 551 | lda track_bank_start+6, X 552 | jsr sta_5FFE 553 | lda track_bank_start+5, X 554 | jsr sta_5FFD 555 | lda track_bank_start+4, X 556 | jsr sta_5FFC 557 | ; bottom 4 banks are applied by the base_low $8000 bank / RAM code 558 | lda track_bank_start+3, X 559 | clc 560 | adc bank_add 561 | sta bank_B000 562 | lda track_bank_start+2, X 563 | clc 564 | adc bank_add 565 | sta bank_A000 566 | lda track_bank_start+1, X 567 | clc 568 | adc bank_add 569 | sta bank_9000 570 | lda track_bank_start+0, X 571 | clc 572 | adc bank_add 573 | sta bank_8000 574 | jsr base_ramp_play 575 | 576 | ; other utilities 577 | 578 | gamepad_poll_: ; standard single read 579 | lda #1 580 | sta $4016 581 | lda #0 582 | sta $4016 583 | ldx #8 584 | : 585 | pha 586 | lda $4016 587 | and #%00000011 588 | cmp #%00000001 589 | pla 590 | ror 591 | dex 592 | bne :- 593 | sta gamepad 594 | rts 595 | 596 | gamepad_poll: ; DPCM safe read-until-consistent 597 | jsr gamepad_poll_ 598 | : 599 | lda gamepad 600 | pha 601 | jsr gamepad_poll_ 602 | pla 603 | cmp gamepad 604 | bne :- 605 | cmp #0 ; refresh flags 606 | rts 607 | 608 | ppu_off: 609 | lda #0 610 | sta ppu_mask 611 | jmp ppu_update 612 | ppu_on: 613 | lda #%00011110 614 | sta ppu_mask 615 | ;jmp ppu_update 616 | ppu_update: 617 | lda #1 618 | sta nmi_ready 619 | ;jmp ppu_skip 620 | ppu_skip: 621 | lda nmi_count 622 | : 623 | cmp nmi_count 624 | beq :- 625 | rts 626 | 627 | ppu_skip_frames: 628 | ; X = number of frames to skip 629 | cpx #0 630 | beq :++ 631 | : 632 | jsr ppu_skip 633 | dex 634 | bne :- 635 | : 636 | rts 637 | 638 | ppu_string: 639 | ; load null-terminated string to nmt_addr immediately, '\' for newline 640 | ; in: ptr = string 641 | ; in: nmt_addr = address 642 | ; clobbers: A, Y, ptr, nmt_addr 643 | ldy #0 644 | bit $2002 645 | @restart: 646 | lda nmt_addr+1 647 | sta $2006 648 | lda nmt_addr+0 649 | sta $2006 650 | : 651 | lda (ptr), Y 652 | beq @end 653 | iny 654 | cmp #'\' 655 | beq @next_line 656 | sta $2007 657 | jmp :- 658 | @next_line: 659 | lda nmt_addr+0 660 | clc 661 | adc #<32 662 | sta nmt_addr+0 663 | lda nmt_addr+1 664 | adc #>32 665 | sta nmt_addr+1 666 | jmp @restart 667 | @end: 668 | rts 669 | 670 | ppu_string_buffer: 671 | ; load null-terminated string to nmt_buffer 672 | ; in: ptr = string 673 | ; out: nmt_count 674 | ; clobbers: A, X, Y, ptr 675 | ldx #0 676 | ldy #0 677 | : 678 | lda (ptr), Y 679 | beq :+ 680 | sta nmt_buffer, X 681 | inx 682 | iny 683 | jmp :- 684 | : 685 | stx nmt_count 686 | rts 687 | 688 | sprite_begin: 689 | ; reserve sprite 0 and place offscreen 690 | lda #$FF 691 | sta oam+0 692 | ; set oam_pos to start at sprite 1 693 | lda #4 694 | sta oam_pos 695 | rts 696 | 697 | sprite_finish: 698 | ldx oam_pos 699 | beq :++ 700 | lda #$FF 701 | : 702 | sta oam+0, X 703 | inx 704 | inx 705 | inx 706 | inx 707 | bne :- 708 | : 709 | rts 710 | 711 | sprite: 712 | ; adds a sprite to OAM 713 | ; in: ptr = sprite data (list of 4-byte tiles) 714 | ; [ x, y, tile, attribute ] 715 | ; x=128 marks the end of list 716 | ; in: X = sprite x 717 | ; in: Y = sprite y 718 | ; out: oam_pos 719 | ; clobbers: A, X, Y, temp 720 | @sprite_x = temp+0 721 | @sprite_y = temp+1 722 | stx @sprite_x 723 | sty @sprite_y 724 | ldy #0 725 | ldx oam_pos 726 | beq @finish ; OAM full 727 | @tile: 728 | lda (ptr), Y 729 | iny 730 | cmp #128 731 | beq @finish 732 | clc 733 | adc @sprite_x 734 | sta oam+3, X 735 | lda (ptr), Y 736 | iny 737 | clc 738 | adc @sprite_y 739 | sta oam+0, X 740 | lda (ptr), Y 741 | iny 742 | sta oam+1, X 743 | lda (ptr), Y 744 | iny 745 | sta oam+2, X 746 | inx 747 | inx 748 | inx 749 | inx 750 | stx oam_pos 751 | bne @tile 752 | @finish: 753 | rts 754 | 755 | fade_copy_palette_: 756 | ; temporarily copy palette to nmt_buffer for fade 757 | ldx #32 758 | : 759 | lda palette-1, X 760 | sta nmt_buffer+31, X 761 | dex 762 | bne :- 763 | rts 764 | 765 | fade_apply_: 766 | ; in: A = value to subtract from palette, copying from nmt_buffer 767 | @sub = temp+0 768 | sta @sub 769 | ldx #32 770 | @loop: 771 | lda nmt_buffer+31, X 772 | sec 773 | sbc @sub 774 | bcs :++ 775 | : 776 | lda #$0F 777 | : 778 | cmp #$0D ; replace sub-black $0D with black $0F 779 | beq :-- 780 | sta palette-1, X 781 | dex 782 | bne @loop 783 | rts 784 | 785 | fade_step_: 786 | ; in: A = value to subtract from palette 787 | ; in: @time = frames-1 to wait 788 | @time = temp+1 789 | jsr fade_apply_ 790 | jsr ppu_update 791 | ldx @time 792 | jsr ppu_skip_frames 793 | rts 794 | 795 | fade_out: 796 | ; in: X+1 = frames to wait on each fade level 797 | ; clobbers: A, X, temp 798 | @time = temp+1 799 | stx @time 800 | jsr fade_copy_palette_ 801 | lda #$10 802 | jsr fade_step_ 803 | lda #$20 804 | jsr fade_step_ 805 | lda #$30 806 | jsr fade_step_ 807 | jsr ppu_off 808 | lda #$00 809 | jsr fade_apply_ ; restore original palette now that rendering is off 810 | rts 811 | 812 | fade_in: 813 | ; in: X+1 = frames to wait on each fade level 814 | ; clobbers: A, X, temp 815 | @time = temp+1 816 | stx @time 817 | jsr fade_copy_palette_ 818 | lda #$30 819 | jsr fade_apply_ 820 | jsr ppu_on 821 | ldx @time 822 | jsr ppu_skip_frames 823 | lda #$20 824 | jsr fade_step_ 825 | lda #$10 826 | jsr fade_step_ 827 | lda #$00 828 | jsr fade_apply_ 829 | jsr ppu_update 830 | rts 831 | 832 | art_prepare_: 833 | ; in: A = art 834 | ; out: bank $C000 contains art, and (ptr) points to it 835 | ; clobbers: Y 836 | asl 837 | tay 838 | lda art_table+0, Y 839 | sta ptr+0 840 | lda art_table+1, Y 841 | sta ptr+1 842 | ; high nybble selects bank 843 | lsr 844 | lsr 845 | lsr 846 | lsr 847 | clc 848 | adc #BANK_ART 849 | sta $5FFC 850 | ; remap to $C000 851 | lda ptr+1 852 | and #$0F 853 | ora #$C0 854 | sta ptr+1 855 | rts 856 | 857 | art_load_ppu_4k_: 858 | jsr art_load_ppu_1k_ 859 | jsr art_load_ppu_1k_ 860 | jsr art_load_ppu_1k_ 861 | art_load_ppu_1k_: 862 | jsr art_load_ppu_256b_ 863 | jsr art_load_ppu_256b_ 864 | jsr art_load_ppu_256b_ 865 | art_load_ppu_256b_: 866 | ldy #0 867 | : 868 | lda (ptr), Y 869 | sta $2007 870 | iny 871 | bne :- 872 | inc ptr+1 873 | rts 874 | 875 | load_screen: 876 | ; in: A = screen to load 877 | ; out: palette, CHR/nametables 878 | ; clobbers: A, X, Y, ptr 879 | asl 880 | asl 881 | asl 882 | tax ; X = screen * 8 883 | ; latch PPU address to 0 884 | bit $2002 885 | lda #0 886 | sta $2006 887 | sta $2006 888 | ; CHR $0000 889 | lda screen_table+1, X 890 | jsr art_prepare_ 891 | jsr art_load_ppu_4k_ 892 | ; CHR $1000 893 | lda screen_table+2, X 894 | jsr art_prepare_ 895 | jsr art_load_ppu_4k_ 896 | ; NMT $2000 897 | lda screen_table+0, X 898 | jsr art_prepare_ 899 | jsr art_load_ppu_1k_ 900 | ; palette BG 901 | lda screen_table+3, X 902 | jsr art_prepare_ 903 | ldy #0 904 | : 905 | lda (ptr), Y 906 | sta palette+0, Y 907 | iny 908 | cpy #16 909 | bcc :- 910 | ; palette FG 911 | lda screen_table+4, X 912 | jsr art_prepare_ 913 | ldy #0 914 | : 915 | lda (ptr), Y 916 | sta palette+16, Y 917 | iny 918 | cpy #16 919 | bcc :- 920 | rts 921 | 922 | track_index_: 923 | tax 924 | lda track_order, X 925 | rts 926 | 927 | load_track_title: 928 | ; in: A = track 929 | ; out: ptr 930 | ; clobbers A, X 931 | jsr track_index_ 932 | asl 933 | tax 934 | lda track_title_list+0, X 935 | sta ptr+0 936 | lda track_title_list+1, X 937 | sta ptr+1 938 | rts 939 | 940 | load_track_title_short: 941 | ; in: A = track 942 | ; out: ptr 943 | ; clobbers A, X 944 | jsr track_index_ 945 | asl 946 | tax 947 | lda track_title_short_list+0, X 948 | sta ptr+0 949 | lda track_title_short_list+1, X 950 | sta ptr+1 951 | rts 952 | 953 | load_track_artist: 954 | ; in: A = track 955 | ; out: ptr 956 | ; clobbers A, X 957 | jsr track_index_ 958 | asl 959 | tax 960 | lda track_artist_list+0, X 961 | sta ptr+0 962 | lda track_artist_list+1, X 963 | sta ptr+1 964 | rts 965 | 966 | ; end of file 967 | -------------------------------------------------------------------------------- /nsfspider.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | assert sys.version_info[0] >= 3, "Python 3 required." 4 | 5 | import datetime 6 | import glob 7 | import os 8 | import shlex 9 | import shutil 10 | import struct 11 | import subprocess 12 | 13 | import py65emu.cpu 14 | 15 | # nsfspider.py 16 | # 17 | # This program executes and analyzes a collection of NSF files. 18 | # 19 | # Input: 20 | # /in_nsf/*.nsf 21 | # /in_nsf/tracks.txt 22 | # 23 | # Output: 24 | # /out_src/*.bin 25 | # /out_src/*.s 26 | # /out_info/binlist.txt 27 | # /out_info/modlist.txt 28 | # /out_info/tracks.inc 29 | # /out_info/nsfspider.txt 30 | # 31 | # See tracks.txt and readme.txt for information. 32 | # 33 | # 34 | # https://github.com/bbbradsmith/zensf 35 | 36 | debug_track_skip = 0 # skips the full play/analysis of tracks below this number for faster iteration (if you don't need the mod info) 37 | delete_output = True # delete output files before beginning 38 | disassemble_all = False # may be useful for other patching 39 | 40 | in_dir = "in_nsf" 41 | in_list = "tracks.txt" 42 | 43 | out_dir_bin = "out_src" 44 | out_dir_mod = "out_mod" 45 | out_dir_info = "out_info" 46 | out_dir_build = "out_build" 47 | 48 | out_binlist = "binlist.txt" 49 | out_modlist = "modlist.txt" 50 | out_result = "nsfspider.txt" 51 | out_inc = "tracks.inc" 52 | out_nsfe = "nsfe.inc" 53 | 54 | FPS = 60 55 | NSF_TIMEOUT_INIT = 20000 * 120 # cycles allowed for INIT 56 | NSF_TIMEOUT_PLAY = 10000 # cycles allowed for PLAY 57 | gap = 2 # seconds to add to timer for gap 58 | 59 | da65 = os.path.join("cc65","bin","da65") 60 | da65_args = " -o %s -v --comments 4 -g -S $%04X %s" # % (output, address, input) 61 | 62 | now_string = datetime.datetime.now().strftime("%a %b %d %H:%M:%S %Y") 63 | 64 | DEBUG = False 65 | 66 | def debug(message): 67 | if DEBUG: 68 | print (message) 69 | 70 | class NSF: 71 | '''Parses an NSF file and unpack its ROM data.''' 72 | 73 | class Invalid(Exception): 74 | '''Invalid NSF data.''' 75 | 76 | def __init__(self, data): 77 | # read the header 78 | if (len(data) < 0x080): 79 | raise NSF.Invalid("Incomplete header.") 80 | if data[0x000:0x005] != b"NESM\x1A": 81 | raise NSF.Invalid("Missing NESM ID.") 82 | if data[0x005] != 1: 83 | raise NSF.Invalid("Unknown version %d." % data[0x005]) 84 | self.songs = data[0x006] 85 | self.start = data[0x007] 86 | self.addr_load = struct.unpack(" len(self.rom): 108 | raise NSF.Invalid("Too much data in file. (%d bytes)" % (len(data)-0x80)) 109 | pad = 0 110 | if not self.bankswitch: 111 | pad = self.addr_load - 0x8000 112 | if (pad < 0): 113 | raise NSF.Invalid("Low LOAD address unsupported: $%04X" % (self.addr_load)) 114 | else: 115 | pad = self.addr_load & 0x0FFF 116 | for b in data[0x080:]: 117 | self.rom[pad] = b 118 | pad += 1 119 | # calculate used banks 120 | self.bank_begin = 0 # first bank with data 121 | if not self.bankswitch: 122 | self.bank_begin = (self.addr_load - 0x8000) >> 12 123 | self.bank_end = ((pad-1) >> 12) # last bank with data 124 | self.bank_count = (self.bank_end + 1) - self.bank_begin 125 | 126 | @classmethod 127 | def open(cls, filename): 128 | return cls(open(filename,"rb").read()) 129 | 130 | def __str__(self): 131 | return \ 132 | ("title: " + self.title) + "\n" + \ 133 | ("artist: " + self.artist) + "\n" + \ 134 | ("copyright: " + self.copyright) + "\n" + \ 135 | ("song: %d / %d" % (self.start, self.songs)) + "\n" + \ 136 | ("data size: $%06X = %d bytes" % (self.size, self.size)) + "\n" + \ 137 | ("LOAD: $%04X / INIT: $%04X / PLAY: $%04X" % (self.addr_load, self.addr_init, self.addr_play)) + "\n" + \ 138 | ("region: %02X / expansion: %02X" % (self.region, self.expansion)) + "\n" + \ 139 | ("speed: $%05X / $%05X" % (self.speed_ntsc, self.speed_pal)) + "\n" + \ 140 | ("banks:" + "".join((" %02X" % b) for b in self.banks)) + "\n" 141 | 142 | 143 | class NSFSpiderMMU: 144 | '''MMU for py65emu.cpu that logs important information for NSF playback.''' 145 | 146 | def __init__(self, nsf): 147 | self.nsf = nsf 148 | self.rom = nsf.rom 149 | 150 | def bank(self,b,v,ranged=True): 151 | #debug("bank: %d, %d" % (b,v)) 152 | self.bank_addr[v] = 0x8000 | (b * 0x1000) 153 | if self.pc >= 0: 154 | if self.pc >= 0x8000: 155 | pcb = (self.pc >> 12) - 0x8 156 | self.stat_bank_write.add((self.banks[pcb],self.pc)) # bank write from ROM 157 | else: 158 | self.stat_bank_write.add((-1,self.pc)) # bank write from RAM 159 | if (b == 7): 160 | if v <= self.nsf.bank_end: 161 | self.stat_bank_f.add(v) 162 | if ranged and (v > self.nsf.bank_end): 163 | raise Exception("Bank F (%d) outside file range. Pad NSF with zeros to fill this bank." % v) 164 | self.banks[b] = v * 0x1000 165 | 166 | # required interface for py65emu MMU 167 | # - reset 168 | # - write(addr,value) 169 | # - read(addr) 170 | # - readWord(addr) - only used by NMI/IRQ/BRK 171 | 172 | def reset(self): 173 | self.pc = -1 # set this before every cpu step 174 | self.ram = [0] * 0x800 175 | self.exram = [0] * 0x2000 176 | self.banks = [0] * 8 177 | self.bank_addr = {} 178 | self.stat_bank_f = set() 179 | self.stat_bank_write = set() 180 | start_banks = self.nsf.banks[:] 181 | if not self.nsf.bankswitch: 182 | start_banks = [0,1,2,3,4,5,6,7] 183 | for i in range(8): 184 | self.bank(i,start_banks[i],False) 185 | self.stat_highzp = 0 186 | self.stat_highram = 0 187 | self.stat_lowstack = 0x1FF 188 | self.stat_exram = False 189 | self.brk = False # BRK notify will be used to signal end of routine 190 | if (self.nsf.expansion != 0): 191 | raise Exception("Expansion audio not supported. (%02X)" % (self.nsf.expansion)) 192 | self.last_4015 = 0x0F 193 | 194 | def write(self, addr, value): 195 | if (addr < 0x0800): 196 | self.ram[addr] = value 197 | self.stat_highram = max(self.stat_highram, addr) 198 | if (addr < 0x100): 199 | global reserve 200 | if (addr < reserve): 201 | self.stat_highzp = max(self.stat_highzp, addr) 202 | elif (addr < 0x200): 203 | self.stat_lowstack = min(self.stat_lowstack, addr) 204 | if (addr == 0x100): 205 | raise Exception("Write to stack at $100. Overflow?") 206 | elif (addr >= 0x5FF8) and (addr < 0x6000): 207 | if not self.nsf.bankswitch: 208 | raise Exception("Banskwitching in non-bankswitched NSF.") 209 | b = addr - 0x5FF8 210 | self.bank(b,value) 211 | elif (addr >= 0x6000) and (addr < 0x8000): 212 | self.stat_exram = True 213 | self.exram[addr - 0x6000] = value 214 | elif (addr >= 0x4000) and (addr < 0x4020): 215 | if addr == 0x4015: 216 | # workaround treating $4015 writes as if DPCM is always "finished" 217 | # correctly emulating $4015 reads is a lot more complicated 218 | # (requires simulation of parts of all 5 APU channels) 219 | # but the one engine I saw reading this only used it to avoid 220 | # retriggering DPCM, so this is sufficient. 221 | self.last_4015 = value & 0x0F 222 | #debug("APU write: $%04X = $%02X" % (addr, value)) 223 | pass # Could also add a whitelist of expansion audio addresses 224 | else: 225 | raise Exception("Write to unexpected address: $%04X" % (addr)) 226 | 227 | def read(self, addr): 228 | if (addr < 0x0800): 229 | return self.ram[addr & 0x1FFF] 230 | elif (addr >= 0x6000) and (addr < 0x8000): 231 | return self.exram[addr - 0x6000] 232 | elif (addr >= 0x8000): 233 | b = (addr >> 12) - 0x8 234 | mo = self.banks[b] 235 | mb = mo >> 12 236 | #debug("read: %04X = %01X : %06X" % (addr, b, mo | (addr & 0xFFF))) 237 | if mb < self.nsf.bank_begin or mb > self.nsf.bank_end: 238 | if self.pc != 0x01FF: # don't check this on the BRK 239 | raise Exception("Read from padding bank: $%02X ($%04X)" % (mb,addr)) 240 | return self.rom[ mo | (addr & 0xFFF) ] 241 | elif (addr == 0x4015): 242 | return self.last_4015 243 | else: 244 | raise Exception("Read to unexpected address: $%04X" % (addr)) 245 | 246 | def readWord(self, addr): 247 | if self.pc != 0x01FF: 248 | raise Exception("Unexpected interrupt?") 249 | self.brk = True # in the absence of IRQs readWord is only called in response to BRK 250 | return (self.read(addr+1) << 8) + self.read(addr) 251 | 252 | def trace(cpu): 253 | o0 = cpu.mmu.read(cpu.r.pc ) 254 | o1 = cpu.mmu.read(cpu.r.pc+1) 255 | o2 = cpu.mmu.read(cpu.r.pc+2) 256 | opcode = o0 257 | opname = "---" 258 | opargs = "--" 259 | found = False 260 | for op in cpu._ops: 261 | for (args, cy, codes, flags) in op[2]: 262 | if o0 in codes: 263 | opname = op[0] 264 | opargs = args 265 | found = True 266 | break 267 | if found: 268 | break 269 | stack = "" 270 | for i in range(cpu.r.s+1, 0x100): 271 | stack += " %02X" % cpu.mmu.read(0x100+i) 272 | print(str(cpu.r) + (" > %02X %02X %02X %3s %2s" % (o0,o1,o2,opname,opargs)) + stack) 273 | 274 | def execute(cpu, addr, timeout): 275 | #debug("execute: $%04X" % (addr)) 276 | cpu.mmu.write(0x01FF,0x00) # BRK 277 | cpu.mmu.write(0x01FE,0x01) 278 | cpu.mmu.write(0x01FD,0xFE) # stack return to BRK 279 | cpu.r.s = 0xFC # RTS will return to BRK on the stack 280 | cpu.r.pc = addr 281 | cycles = NSF_TIMEOUT_INIT 282 | cpu.mmu.brk = False 283 | while (cycles > 0): 284 | #trace(cpu) # for debugging 285 | cpu.mmu.pc = cpu.r.pc 286 | cpu.step() 287 | cycles -= cpu.cc 288 | if cpu.mmu.brk: 289 | break 290 | if (cpu.r.p & 4) == 0: 291 | raise Exception("NSF has enabled IRQ!") 292 | if cycles < 0: 293 | raise Exception("Subroutine at $%04X exceeded %d instructions!") 294 | #debug("RTS after: %d cycles" % (NSF_TIMEOUT_INIT - cycles)) 295 | 296 | def run_nsf(nsf,song,frames): 297 | mmu = NSFSpiderMMU(nsf) 298 | cpu = py65emu.cpu.CPU(mmu) 299 | #cpu.reset() # reset by constructor 300 | assert song>0 301 | cpu.r.a = song-1 302 | execute(cpu, nsf.addr_init, NSF_TIMEOUT_INIT) 303 | for f in range(frames): 304 | execute(cpu, nsf.addr_play, NSF_TIMEOUT_PLAY) 305 | return mmu 306 | 307 | def parse_address(s): 308 | if (s.startswith("$")): 309 | return int(s[1:],16) 310 | else: 311 | return int(s,10) 312 | 313 | def verify_dir(dir): 314 | if not os.path.exists(dir): 315 | os.mkdir(dir) 316 | 317 | # ==== 318 | # MAIN 319 | # ==== 320 | 321 | result = "" 322 | binlist = "" 323 | mods = "" 324 | inc = "" 325 | order = [] 326 | reserve = 0x100 327 | allcaps = False 328 | text_map = {} 329 | 330 | def text_remap(s): 331 | global text_map 332 | if allcaps: 333 | s = s.upper() 334 | for (symbol,replacement) in text_map.items(): 335 | s = s.replace(symbol,replacement) 336 | return s 337 | 338 | if delete_output: 339 | s = "Removing files." 340 | result += s + "\n" 341 | print(s) 342 | def rm(filename): 343 | if '*' in filename: 344 | #debug("removing glob: " + filename) 345 | for f in glob.glob(filename): 346 | rm(f) 347 | return 348 | #debug("rm: " + filename) 349 | try: 350 | os.remove(filename) 351 | s = "removed: " + filename 352 | global result 353 | result += s + "\n" 354 | print(s) 355 | except: 356 | #debug("not removed: " + filename) 357 | pass 358 | rm(os.path.join(out_dir_bin,"*.s")) 359 | rm(os.path.join(out_dir_bin,"*.bin")) 360 | rm(os.path.join(out_dir_info,out_binlist)) 361 | rm(os.path.join(out_dir_info,out_modlist)) 362 | rm(os.path.join(out_dir_info,out_inc)) 363 | rm(os.path.join(out_dir_info,out_nsfe)) 364 | rm(os.path.join(out_dir_info,out_result)) 365 | result += "\n" 366 | print() 367 | 368 | verify_dir(out_dir_bin) 369 | verify_dir(out_dir_mod) 370 | verify_dir(out_dir_info) 371 | verify_dir(out_dir_build) 372 | 373 | # gather entries from in_list 374 | track = 0 375 | entries = [] 376 | in_line_count = 0 377 | for line in open(os.path.join(in_dir,in_list),"rt",encoding="UTF-8").readlines(): 378 | try: 379 | args = shlex.split(line,comments=True) 380 | except Exception as e: 381 | raise Exception(("Parsing error. (%d)\n" % in_line_count) + str(e) + "\n>" + line) 382 | in_line_count += 1 383 | if len(args) <= 0: 384 | continue 385 | elif args[0] == "ORDER": 386 | for a in args[1:]: 387 | order.append(int(a,10)) 388 | continue 389 | elif args[0] == "ALLCAPS": 390 | if len(args) != 1: 391 | raise Exception("Unexpected argument after ALLCAPS. (%d)" % in_line_count) 392 | allcaps = True 393 | continue 394 | elif args[0] == "MAP": 395 | if len(args) != 3: 396 | raise Exception("MAP must have two arguments. (%d)" % in_line_count) 397 | if args[1] in text_map: 398 | raise Exception("Duplicate MAP? \"%s\" (%d)" % (args[1], in_line_count)) 399 | text_map[args[1]] = args[2] 400 | continue 401 | elif args[0] == "GAP": 402 | if len(args) != 2: 403 | raise Exception("One argument expected after GAP. (%d)" % in_line_count) 404 | gap = int(args[1]) 405 | continue 406 | elif args[0] == "RESERVE": 407 | if len(args) != 2: 408 | raise Exception("One argument expected after RESERVE. (%d)" % in_line_count) 409 | reserve = parse_address(args[1]) 410 | continue 411 | if len(args) < 7 or len(args) > 8: 412 | raise Exception(("Incorrect number of arguments. (%d)\n> " % in_line_count) + line) 413 | nsf_filename = os.path.join(in_dir,args[0]) 414 | nsf_song = int(args[1]) 415 | nsf_time = args[2].split(":") 416 | nsf_min = int(nsf_time[0]) 417 | nsf_sec = int(nsf_time[1]) 418 | nsf_pal_adjust = int(args[3]) 419 | nsf_loop = int(args[4]) 420 | nsf_artist = args[5] 421 | nsf_title = args[6] 422 | nsf_title_short = None 423 | if len(args) >= 8: 424 | nsf_title_short = args[7] 425 | entry = (nsf_filename, nsf_song, nsf_min, nsf_sec, nsf_pal_adjust, nsf_loop, nsf_artist, nsf_title, nsf_title_short) 426 | entries.append(entry) 427 | entry_str = "%02d: %-40s %d %02d:%02d %d%d %-25s %s" % (track, nsf_filename, nsf_song, nsf_min, nsf_sec, nsf_pal_adjust, nsf_loop, nsf_artist, nsf_title) 428 | print(entry_str) 429 | result += entry_str + "\n" 430 | track += 1 431 | 432 | if len(order) == 0: # default order 433 | order = [(x+1) for x in range(len(entries))] 434 | s = "Order:" 435 | for o in order: 436 | if (o > len(entries)): 437 | raise Exception("Order (%d) too high!" % o) 438 | if (o < 1): 439 | raise Exception("Order 0 too low!") 440 | s += " %02d" % o 441 | result += s + "\n\n" 442 | print(s) 443 | print() 444 | 445 | if len(entries) > 32: 446 | raise Exception("Too many tracks (%d), only 32 supported." % len(entries)) 447 | # The main limiting factor here is how the starting bank data is fetched. 448 | # Look for uses of track_bank_start if you want to increase this limit. 449 | 450 | # analyze NSFs 451 | track = 0 452 | analyzed = [] 453 | highest_zp = 0 454 | highest_ram = 0 455 | lowest_stack = 0x1FF 456 | for (nsf_filename, nsf_song, nsf_min, nsf_sec, nsf_pal_adjust, nsf_loop, nsf_artist, nsf_title, nsf_title_short) in entries: 457 | # parse the file 458 | s_track = ("File %d: " % track) + nsf_filename 459 | print(s_track) 460 | result += s_track + "\n" 461 | nsf = NSF.open(nsf_filename) 462 | result += str(nsf) + "\n" 463 | print(nsf) 464 | # simulate the NSF 465 | frames = ((nsf_min * 60) + nsf_sec + gap) * FPS 466 | if track < debug_track_skip: 467 | frames = 5 468 | mmu = run_nsf(nsf,nsf_song,frames) 469 | # analysis 470 | mod = "Track %02d: %s\n" % (track,nsf_title) 471 | mod += "High ZP: $%02X\n" % mmu.stat_highzp 472 | mod += "High RAM: $%04X\n" % mmu.stat_highram 473 | if mmu.stat_exram != False: 474 | raise Exception("External RAM required!") 475 | mod += "EXTRA RAM\n" 476 | for b in mmu.stat_bank_f: 477 | mod += "BANK F: $%02X\n" % b 478 | for (offset,pc) in mmu.stat_bank_write: 479 | mod += "BANK WRITE: $%02X:$%04X\n" % (offset>>12,pc) 480 | highest_zp = max(mmu.stat_highzp, highest_zp) 481 | highest_ram = max(mmu.stat_highram, highest_ram) 482 | lowest_stack = min(mmu.stat_lowstack, lowest_stack) 483 | print(mod) 484 | mods += mod + "\n" 485 | result += mod + "\n" 486 | analyzed.append((nsf,mmu)) 487 | track += 1 488 | 489 | # output binaries and analyzed data 490 | 491 | assert(len(entries) == len(analyzed)) 492 | 493 | inc = "; generated by nsfspider.py " + now_string + "\n\n" 494 | inc_init = "track_init_addr:\n" 495 | inc_play = "track_play_addr:\n" 496 | inc_bank_offset = "track_bank_offset: ; bank location of NSF in ROM\n" 497 | inc_bank_start = "track_bank_start: ; starting banks for NSF\n" 498 | inc_bank_start += "; 8000 9000 A000 B000 C000 D000 E000 F000\n" 499 | inc_song = "track_song: ; song index for NSF -1\n" 500 | inc_pal_adjust = "track_pal_adjust: ; double every 5th frame if in PAL\n; " 501 | inc_loop = "track_loop: ; whether track loops\n; " 502 | inc_time = "track_time: ; time to play each track in seconds\n" 503 | inc_artist = "" 504 | inc_title = "" 505 | inc_title_short = "" 506 | 507 | inc += "TRACK_LENGTH = %d\n" % len(entries) 508 | inc += "TRACK_ORDER_LENGTH = %d\n" % len(order) 509 | inc += "TRACK_HIGH_ZP = $%02X\n" % highest_zp 510 | inc += "TRACK_HIGH_RAM = $%04X\n" % highest_ram 511 | inc += "TRACK_LOW_STACK = $%04X\n" % lowest_stack 512 | inc += "TRACK_RESERVE_ZP = $%04X\n" % reserve 513 | inc += "\n" 514 | 515 | mod_bins = "BIN numbers:\n" 516 | 517 | outbank = 0 518 | generated = [] 519 | disassembled = [] 520 | for track in range(len(analyzed)): 521 | (nsf_filename, nsf_song, nsf_min, nsf_sec, nsf_pal_adjust, nsf_loop, nsf_artist, nsf_title, nsf_title_short) = entries[track] 522 | (nsf,mmu) = analyzed[track] 523 | track_outbank = outbank 524 | bank_f = mmu.stat_bank_f.copy() 525 | bank_write = mmu.stat_bank_write.copy() 526 | track_write = track 527 | # check if track already present, 528 | reuse = False 529 | for (r_filename, r_track, r_outbank, r_bank_f, r_bank_write) in generated: 530 | if nsf_filename == r_filename: 531 | if not reuse: 532 | track_write = r_track # take the number of the first used track with this NSF 533 | track_outbank = r_outbank 534 | reuse = True 535 | bank_f = bank_f.union(r_bank_f) 536 | bank_write = bank_write.union(r_bank_write) 537 | mod_bins += (" Track %02d = %02X bin - " % (track,track_write)) + nsf_title + "\n" 538 | # export banks 539 | for b in range(nsf.bank_begin, nsf.bank_begin + nsf.bank_count): 540 | addr = -1 541 | bname = "%02X_%02X" % (track_write,b) 542 | # create a mod disassembly if needed 543 | if b in bank_f: 544 | addr = 0xF000 545 | for (pco,pc) in bank_write: 546 | pcb = pco >> 12 547 | if b == pcb: 548 | pca = pc & 0xF000 549 | if addr < 0: 550 | addr = pca 551 | elif pca != addr: 552 | raise Exception("Bank modification needed in two places! " + bname) 553 | if addr < 0 and disassemble_all: 554 | if b in mmu.bank_addr: 555 | addr = mmu.bank_addr[b] 556 | else: 557 | addr = 0x8000 558 | # split the banks 559 | bfile = os.path.join(out_dir_bin,bname+".bin") 560 | if not reuse: 561 | block = nsf.rom[b*0x1000:(b+1)*0x1000] 562 | open(bfile,"wb").write(bytes(block)) 563 | # disassemble the banks to be nodified 564 | if (addr >= 0) and (bname not in disassembled): 565 | mfile = os.path.join(out_dir_bin,bname+".s") 566 | cline = da65 + da65_args % (mfile,addr,bfile) 567 | result += cline + "\n" 568 | print (cline) 569 | try: 570 | subprocess.call(cline) 571 | except: 572 | raise Exception("Disassembly failure.\n> " + cline) 573 | # prepend .segment to the generated file 574 | f = open(mfile,"r+") 575 | mtext = f.read() 576 | f.seek(0) 577 | f.write(".include \"../mod.inc\"\n") 578 | f.write(".segment \"M%04X\"\n" % addr) 579 | f.write(".org $%04X\n\n" % addr) 580 | f.write(mtext) 581 | f.close() 582 | disassembled.append(bname) 583 | binlist_entry = bname 584 | if not reuse: 585 | #result += binlist_entry + "\n" 586 | #print(binlist_entry) 587 | binlist += binlist_entry + "\n" 588 | outbank += 1 589 | generated.append((nsf_filename, track, track_outbank, mmu.stat_bank_f, mmu.stat_bank_write)) 590 | # track data 591 | bank_add = (track_outbank - nsf.bank_begin) & 0xFF 592 | inc_init += ".word $%04X ; %02X %s\n" % (nsf.addr_init, track, nsf_title) 593 | inc_play += ".word $%04X ; %02X %s\n" % (nsf.addr_play, track, nsf_title) 594 | inc_bank_offset += ".byte $%02X ; $%02X - $%02X > %02X %s\n" % ((track_outbank - nsf.bank_begin) & 255, track_outbank, nsf.bank_begin, track, nsf_title) 595 | start_banks = bytearray(nsf.banks) 596 | if not nsf.bankswitch: 597 | start_banks = [0,1,2,3,4,5,6,7] 598 | for i in range(8): 599 | # replace any unused banks with $FF 600 | if start_banks[i] < nsf.bank_begin or start_banks[i] > nsf.bank_end: 601 | start_banks[i] = (0xFF - bank_add) & 0xFF 602 | inc_bank_start += ".byte $%02X, $%02X, $%02X, $%02X, $%02X, $%02X, $%02X, $%02X ; %02X %s\n" % \ 603 | (start_banks[0], start_banks[1], start_banks[2], start_banks[3], \ 604 | start_banks[4], start_banks[5], start_banks[6], start_banks[7], track, nsf_title) 605 | inc_song += ".byte %02X ; %02X %s\n" % (nsf_song-1, track, nsf_title) 606 | inc_pal_adjust += "%02X " % track 607 | inc_loop += "%02X " % track 608 | inc_time += ".word (%2d*60)+%2d+%d ; %02X %s\n" % (nsf_min, nsf_sec, gap, track, nsf_title) 609 | inc_artist += "track_artist_%02X: .asciiz \"%s\"\n" % (track, text_remap(nsf_artist)) 610 | inc_title += "track_title_%02X: .asciiz \"%s\"\n" % (track, text_remap(nsf_title)) 611 | if nsf_title_short == None: 612 | inc_title_short += "track_title_short_%02X = track_title_%02X\n" % (track, track) 613 | else: 614 | inc_title_short += "track_title_short_%02X: .asciiz \"%s\"\n" % (track, text_remap(nsf_title_short)) 615 | 616 | inc_order = "track_order:\n.byte" 617 | for o in order: 618 | inc_order += " $%02X," % (o-1) 619 | inc_order += " 0\n" 620 | 621 | s = "Disassembled banks: %d\n" % len(disassembled) 622 | result += s 623 | print(s) 624 | 625 | mods += mod_bins + "\n" 626 | print(mod_bins) 627 | 628 | # second pass for generated data 629 | inc_pal_adjust += "end\n.byte " 630 | inc_loop += "end\n.byte " 631 | inc_artist += "track_artist_list:\n" 632 | inc_title += "track_title_list:\n" 633 | inc_title_short += "track_title_short_list:\n" 634 | for track in range(len(analyzed)): 635 | (nsf_filename, nsf_song, nsf_min, nsf_sec, nsf_pal_adjust, nsf_loop, nsf_artist, nsf_title, nsf_title_short) = entries[track] 636 | (nsf,mmu) = analyzed[track] 637 | inc_pal_adjust += "%d, " % nsf_pal_adjust 638 | inc_loop += "%d, " % nsf_loop 639 | inc_artist += ".word track_artist_%02X\n" % track 640 | inc_title += ".word track_title_%02X\n" % track 641 | inc_title_short += ".word track_title_short_%02X\n" % track 642 | inc_pal_adjust += "0\n" 643 | inc_loop += "0\n" 644 | 645 | # finish generating table inc 646 | inc += inc_init + "\n" 647 | inc += inc_play + "\n" 648 | inc += inc_bank_offset + "\n" 649 | inc += inc_bank_start + "\n" 650 | inc += inc_song + "\n" 651 | inc += inc_pal_adjust + "\n" 652 | inc += inc_loop + "\n" 653 | inc += inc_time + "\n" 654 | inc += inc_order + "\n" 655 | inc += inc_artist + "\n" 656 | inc += inc_title + "\n" 657 | inc += inc_title_short + "\n" 658 | inc += "; end of file\n" 659 | 660 | # nsfe 661 | nsfe = "; generated by nsfspider.py " + now_string + "\n\n" 662 | nsfe += "SONG_COUNT = %d\n" % len(entries) 663 | nsfe += "PLAYLIST_COUNT = %d\n" % len(order) 664 | nsfe += "\n" 665 | nsfe_time = ".segment \"NSFE_time\"\n" 666 | nsfe_fade = ".segment \"NSFE_fade\"\n" 667 | nsfe_tlbl = ".segment \"NSFE_tlbl\"\n" 668 | nsfe_taut = ".segment \"NSFE_taut\"\n" 669 | for o in order: 670 | track = o-1 671 | (nsf_filename, nsf_song, nsf_min, nsf_sec, nsf_pal_adjust, nsf_loop, nsf_artist, nsf_title, nsf_title_short) = entries[track] 672 | nsfe_time += ".dword ((%2d*60)+%2d)*1000 ; %02X %s\n" % (nsf_min, nsf_sec, track, nsf_title) 673 | nsfe_fade += ".dword %d*1000\n" % gap 674 | title = nsf_title.replace('\\'," ") 675 | nsfe_tlbl += ".asciiz \"%s\" ; %02X\n" % (title, track) 676 | nsfe_taut += ".asciiz \"%s\" ; %02X\n" % (nsf_artist, track) 677 | nsfe += nsfe_time + "\n" 678 | nsfe += nsfe_fade + "\n" 679 | nsfe += nsfe_tlbl + "\n" 680 | nsfe += nsfe_taut + "\n" 681 | nsfe += "; end of file\n" 682 | 683 | # end 684 | open(os.path.join(out_dir_info,out_inc ),"wt",encoding="UTF-8").write(inc) 685 | open(os.path.join(out_dir_info,out_nsfe ),"wt",encoding="UTF-8").write(nsfe) 686 | open(os.path.join(out_dir_info,out_binlist),"wt",encoding="UTF-8").write(binlist) 687 | open(os.path.join(out_dir_info,out_modlist),"wt",encoding="UTF-8").write(mods) 688 | open(os.path.join(out_dir_info,out_result ),"wt",encoding="UTF-8").write(result) 689 | print("Finished!") 690 | -------------------------------------------------------------------------------- /py65emu/cpu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import math 4 | import functools 5 | 6 | 7 | class Registers: 8 | """ An object to hold the CPU registers. """ 9 | def __init__(self, pc=0): 10 | self.reset(pc) 11 | 12 | def reset(self, pc=0): 13 | self.a = 0 # Accumulator 14 | self.x = 0 # General Purpose X 15 | self.y = 0 # General Purpose Y 16 | self.s = 0xff # Stack Pointer 17 | self.pc = pc # Program Counter 18 | 19 | self.flagBit = { 20 | 'N': 128, # N - Negative 21 | 'V': 64, # V - Overflow 22 | 'B': 16, # B - Break Command 23 | 'D': 8, # D - Decimal Mode 24 | 'I': 4, # I - IRQ Disable 25 | 'Z': 2, # Z - Zero 26 | 'C': 1 # C - Carry 27 | } 28 | self.p = 0b00100100 # Flag Pointer - N|V|1|B|D|I|Z|C 29 | 30 | def getFlag(self, flag): 31 | return bool(self.p & self.flagBit[flag]) 32 | 33 | def setFlag(self, flag, v=True): 34 | if v: 35 | self.p = self.p | self.flagBit[flag] 36 | else: 37 | self.clearFlag(flag) 38 | 39 | def clearFlag(self, flag): 40 | self.p = self.p & (255 - self.flagBit[flag]) 41 | 42 | def clearFlags(self): 43 | self.p = 0 44 | 45 | def ZN(self, v): 46 | """ 47 | The criteria for Z and N flags are standard. Z gets set if the 48 | value is zero and N gets set to the same value as bit 7 of the value. 49 | """ 50 | self.setFlag('Z', v == 0) 51 | self.setFlag('N', v & 0x80) 52 | 53 | def __repr__(self): 54 | return "A: %02x X: %02x Y: %02x S: %02x PC: %04x P: %s" % ( 55 | self.a, self.x, self.y, self.s, self.pc, bin(self.p)[2:].zfill(8) 56 | ) 57 | 58 | 59 | class CPU: 60 | 61 | def __init__(self, mmu=None, pc=None, stack_page=0x1, magic=0xee): 62 | """ 63 | Parameters 64 | ---------- 65 | mmu: An instance of MMU 66 | pc: The starting address of the pc (program counter) 67 | stack_page: The index of the page which contains the stack. The default for 68 | a 6502 is page 1 (the stack from 0x0100-0x1ff) but in some varients the 69 | stack page may be elsewhere. 70 | magic: A value needed for on of the illegal opcodes, XAA. This value differs 71 | between different versions, even of the same CPU. The default is 0xee. 72 | """ 73 | self.mmu = mmu 74 | self.r = Registers() 75 | self.cc = 0 76 | # Which page the stack is in. 0x1 means that the stack is from 77 | # 0x100-0x1ff. In the 6502 this is always true but it's different 78 | # for other 65* varients. 79 | self.stack_page = stack_page 80 | self.magic = magic 81 | self.reset() 82 | 83 | if pc: 84 | self.r.pc = pc 85 | else: 86 | # if pc is none get the address from $FFFD,$FFFC 87 | pass 88 | 89 | self._create_ops() 90 | 91 | def reset(self): 92 | self.r.reset() 93 | self.mmu.reset() 94 | 95 | self.running = True 96 | 97 | def step(self): 98 | self.cc = 0 99 | # pc = self.r.pc 100 | opcode = self.nextByte() 101 | self.ops[opcode]() 102 | 103 | def execute(self, instruction): 104 | """ 105 | Execute a single instruction independent of the program in memory. 106 | instruction is an array of bytes. 107 | """ 108 | pass 109 | 110 | def nextByte(self): 111 | v = self.mmu.read(self.r.pc) 112 | self.r.pc += 1 113 | return v 114 | 115 | def nextWord(self): 116 | low = self.nextByte() 117 | high = self.nextByte() 118 | return (high << 8) + low 119 | 120 | def stackPush(self, v): 121 | self.mmu.write(self.stack_page*0x100 + self.r.s, v) 122 | self.r.s = (self.r.s - 1) & 0xff 123 | 124 | def stackPushWord(self, v): 125 | self.stackPush(v >> 8) 126 | self.stackPush(v & 0xff) 127 | 128 | def stackPop(self): 129 | v = self.mmu.read(self.stack_page*0x100 + ((self.r.s + 1) & 0xff)) 130 | self.r.s = (self.r.s + 1) & 0xff 131 | return v 132 | 133 | def stackPopWord(self): 134 | return self.stackPop() + (self.stackPop() << 8) 135 | 136 | def fromBCD(self, v): 137 | return (((v & 0xf0) // 0x10) * 10) + (v & 0xf) 138 | 139 | def toBCD(self, v): 140 | return int(math.floor(v/10))*16 + (v % 10) 141 | 142 | def fromTwosCom(self, v): 143 | return (v & 0x7f) - (v & 0x80) 144 | 145 | interrupts = { 146 | "ABORT": 0xfff8, 147 | "COP": 0xfff4, 148 | "IRQ": 0xfffe, 149 | "BRK": 0xfffe, 150 | "NMI": 0xfffa, 151 | "RESET": 0xfffc 152 | } 153 | 154 | def interruptAddress(self, i): 155 | return self.mmu.readWord(self.interrupts[i]) 156 | 157 | # Addressing modes 158 | def z_a(self): 159 | return self.nextByte() 160 | 161 | def zx_a(self): 162 | return (self.nextByte() + self.r.x) & 0xff 163 | 164 | def zy_a(self): 165 | return (self.nextByte() + self.r.y) & 0xff 166 | 167 | def a_a(self): 168 | return self.nextWord() 169 | 170 | def ax_a(self): 171 | o = self.nextWord() 172 | a = o + self.r.x 173 | if math.floor(o/0xff) != math.floor(a/0xff): 174 | self.cc += 1 175 | 176 | return a & 0xffff 177 | 178 | def ay_a(self): 179 | o = self.nextWord() 180 | a = o + self.r.y 181 | if math.floor(o/0xff) != math.floor(a/0xff): 182 | self.cc += 1 183 | 184 | return a & 0xffff 185 | 186 | def i_a(self): 187 | """Only used by indirect JMP""" 188 | i = self.nextWord() 189 | # Doesn't carry, so if the low byte is in the XXFF position 190 | # Then the high byte will be XX00 rather than XY00 191 | if i & 0xff == 0xff: 192 | j = i - 0xff 193 | else: 194 | j = i + 1 195 | 196 | return ((self.mmu.read(j) << 8) + self.mmu.read(i)) & 0xffff 197 | 198 | def ix_a(self): 199 | i = (self.nextByte() + self.r.x) & 0xff 200 | return ((self.mmu.read((i + 1) & 0xff) << 8) + self.mmu.read(i)) & 0xffff 201 | 202 | def iy_a(self): 203 | i = self.nextByte() 204 | o = (self.mmu.read((i + 1) & 0xff) << 8) + self.mmu.read(i) 205 | a = o + self.r.y 206 | 207 | if math.floor(o/0xff) != math.floor(a/0xff): 208 | self.cc += 1 209 | 210 | return a & 0xffff 211 | 212 | # Return values based on the addressing mode 213 | def im(self): 214 | return self.nextByte() 215 | 216 | def z(self): 217 | return self.mmu.read(self.z_a()) 218 | 219 | def zx(self): 220 | return self.mmu.read(self.zx_a()) 221 | 222 | def zy(self): 223 | return self.mmu.read(self.zy_a()) 224 | 225 | def a(self): 226 | return self.mmu.read(self.a_a()) 227 | 228 | def ax(self): 229 | return self.mmu.read(self.ax_a()) 230 | 231 | def ay(self): 232 | return self.mmu.read(self.ay_a()) 233 | 234 | def i(self): 235 | return self.mmu.read(self.i_a()) 236 | 237 | def ix(self): 238 | return self.mmu.read(self.ix_a()) 239 | 240 | def iy(self): 241 | return self.mmu.read(self.iy_a()) 242 | 243 | # Operators 244 | # All the operations. For each operation have the name of the operation, 245 | # whether it acts on values, "v" or on addresses, "a" and a list of 4-tuples 246 | # containing the valid addressing modes for the operation, the 247 | # base number of cycles it takes, the opcode, and a target register, if valid. 248 | _ops = [ 249 | ("ADC", "v", [ 250 | ("im", 2, [0x69], None), 251 | ("z", 3, [0x65], None), 252 | ("zx", 4, [0x75], None), 253 | ("a", 4, [0x6d], None), 254 | ("ax", 4, [0x7d], None), 255 | ("ay", 4, [0x79], None), 256 | ("ix", 6, [0x61], None), 257 | ("iy", 5, [0x71], None) 258 | ]), 259 | ("AND", "v", [ 260 | ("im", 2, [0x29], None), 261 | ("z", 3, [0x25], None), 262 | ("zx", 4, [0x35], None), 263 | ("a", 4, [0x2d], None), 264 | ("ax", 4, [0x3d], None), 265 | ("ay", 4, [0x39], None), 266 | ("ix", 6, [0x21], None), 267 | ("iy", 5, [0x31], None) 268 | ]), 269 | ("ASL", "a", [ 270 | ("im", 2, [0x0a], "a"), 271 | ("z", 5, [0x06], None), 272 | ("zx", 6, [0x16], None), 273 | ("a", 6, [0x0e], None), 274 | ("ax", 7, [0x1e], None) 275 | ]), 276 | ("B", "v", [ 277 | ("PL", 2, [0x10], ("N", False)), 278 | ("MI", 2, [0x30], ("N", True)), 279 | ("VC", 2, [0x50], ("V", False)), 280 | ("VC", 2, [0x70], ("V", True)), 281 | ("CC", 2, [0x90], ("C", False)), 282 | ("CS", 2, [0xb0], ("C", True)), 283 | ("NE", 2, [0xd0], ("Z", False)), 284 | ("EQ", 2, [0xf0], ("Z", True)) 285 | ]), 286 | ("BIT", "v", [ 287 | ("z", 3, [0x24], None), 288 | ("a", 4, [0x2c], None) 289 | ]), 290 | ("BRK", "v", [ 291 | ("im", 7, [0x00], 1) 292 | ]), 293 | ("CMP", "v", [ 294 | ("im", 2, [0xc9], None), 295 | ("z", 3, [0xc5], None), 296 | ("zx", 4, [0xd5], None), 297 | ("a", 4, [0xcd], None), 298 | ("ax", 4, [0xdd], None), 299 | ("ay", 4, [0xd9], None), 300 | ("ix", 6, [0xc1], None), 301 | ("iy", 5, [0xd1], None) 302 | ]), 303 | ("CPX", "v", [ 304 | ("im", 2, [0xe0], None), 305 | ("z", 3, [0xe4], None), 306 | ("a", 4, [0xec], None) 307 | ]), 308 | ("CPY", "v", [ 309 | ("im", 2, [0xc0], None), 310 | ("z", 3, [0xc4], None), 311 | ("a", 4, [0xcc], None) 312 | ]), 313 | ("DEC", "a", [ 314 | ("z", 5, [0xc6], None), 315 | ("zx", 6, [0xd6], None), 316 | ("a", 6, [0xce], None), 317 | ("ax", 7, [0xde], None) 318 | ]), 319 | ("DEX", "a", [ 320 | ("im", 2, [0xca], 1) 321 | ]), 322 | ("DEY", "a", [ 323 | ("im", 2, [0x88], 1) 324 | ]), 325 | ("EOR", "v", [ 326 | ("im", 2, [0x49], None), 327 | ("z", 3, [0x45], None), 328 | ("zx", 4, [0x55], None), 329 | ("a", 4, [0x4d], None), 330 | ("ax", 4, [0x5d], None), 331 | ("ay", 4, [0x59], None), 332 | ("ix", 6, [0x41], None), 333 | ("iy", 5, [0x51], None) 334 | ]), 335 | ("CL", "v", [ 336 | ("C", 2, [0x18], "C"), 337 | ("I", 2, [0x58], "I"), 338 | ("V", 2, [0xb8], "V"), 339 | ("D", 2, [0xd8], "D") 340 | ]), 341 | ("SE", "v", [ 342 | ("C", 2, [0x38], "C"), 343 | ("I", 2, [0x78], "I"), 344 | ("D", 2, [0xf8], "D") 345 | ]), 346 | ("INC", "a", [ 347 | ("z", 5, [0xe6], None), 348 | ("zx", 6, [0xf6], None), 349 | ("a", 6, [0xee], None), 350 | ("ax", 7, [0xfe], None) 351 | ]), 352 | ("INX", "a", [ 353 | ("im", 2, [0xe8], 1) 354 | ]), 355 | ("INY", "a", [ 356 | ("im", 2, [0xc8], 1) 357 | ]), 358 | ("JMP", "a", [ 359 | ("a", 3, [0x4c], None), 360 | ("i", 5, [0x6c], None) 361 | ]), 362 | ("JSR", "a", [ 363 | ("a", 6, [0x20], None) 364 | ]), 365 | ("LDA", "v", [ 366 | ("im", 2, [0xa9], None), 367 | ("z", 3, [0xa5], None), 368 | ("zx", 4, [0xb5], None), 369 | ("a", 4, [0xad], None), 370 | ("ax", 4, [0xbd], None), 371 | ("ay", 4, [0xb9], None), 372 | ("ix", 6, [0xa1], None), 373 | ("iy", 5, [0xb1], None) 374 | ]), 375 | ("LDX", "v", [ 376 | ("im", 2, [0xa2], None), 377 | ("z", 3, [0xa6], None), 378 | ("zy", 4, [0xb6], None), 379 | ("a", 4, [0xae], None), 380 | ("ay", 4, [0xbe], None) 381 | ]), 382 | ("LDY", "v", [ 383 | ("im", 2, [0xa0], None), 384 | ("z", 3, [0xa4], None), 385 | ("zx", 4, [0xb4], None), 386 | ("a", 4, [0xac], None), 387 | ("ax", 4, [0xbc], None) 388 | ]), 389 | ("LSR", "a", [ 390 | ("im", 2, [0x4a], "a"), 391 | ("z", 5, [0x46], None), 392 | ("zx", 6, [0x56], None), 393 | ("a", 6, [0x4e], None), 394 | ("ax", 7, [0x5e], None) 395 | ]), 396 | ("NOP", "v", [ 397 | # (imp)lied mode with opcode 0xea is the only documented 398 | # NOP, the rest are illegal opcodes. 399 | ("ip", 2, [0x1a, 0x3a, 0x5a, 0x7a, 0xda, 0xea, 0xfa], 1), 400 | ("im", 2, [0x80, 0x82, 0x89, 0xc2, 0xe2], None), 401 | ("z", 3, [0x04, 0x44, 0x64, ], None), 402 | ("zx", 4, [0x14, 0x34, 0x54, 0x74, 0xd4, 0xf4], None), 403 | ("a", 4, [0x0c], None), 404 | ("ax", 4, [0x1c, 0x3c, 0x5c, 0x7c, 0xdc, 0xfc], None) 405 | ]), 406 | ("ORA", "v", [ 407 | ("im", 2, [0x09], None), 408 | ("z", 3, [0x05], None), 409 | ("zx", 4, [0x15], None), 410 | ("a", 4, [0x0d], None), 411 | ("ax", 4, [0x1d], None), 412 | ("ay", 4, [0x19], None), 413 | ("ix", 6, [0x01], None), 414 | ("iy", 5, [0x11], None) 415 | ]), 416 | ("P", "a", [ 417 | ("PHA", 3, [0x48], ("PH", "a")), 418 | ("PLA", 4, [0x68], ("PL", "a")), 419 | ("PHP", 3, [0x08], ("PH", "p")), 420 | ("PLP", 4, [0x28], ("PL", "p")), 421 | ]), 422 | ("T", "a", [ 423 | ("AX", 2, [0xaa], ('a', 'x')), 424 | ("XA", 2, [0x8a], ('x', 'a')), 425 | ("AY", 2, [0xa8], ('a', 'y')), 426 | ("YA", 2, [0x98], ('y', 'a')), 427 | ("XS", 2, [0x9a], ('x', 's')), 428 | ("SX", 2, [0xba], ('s', 'x')) 429 | ]), 430 | ("ROL", "a", [ 431 | ("im", 2, [0x2a], "a"), 432 | ("z", 5, [0x26], None), 433 | ("zx", 6, [0x36], None), 434 | ("a", 6, [0x2e], None), 435 | ("ax", 7, [0x3e], None) 436 | ]), 437 | ("ROR", "a", [ 438 | ("im", 2, [0x6a], "a"), 439 | ("z", 5, [0x66], None), 440 | ("zx", 6, [0x76], None), 441 | ("a", 6, [0x6e], None), 442 | ("ax", 7, [0x7e], None) 443 | ]), 444 | ("RTI", "a", [ 445 | ("im", 6, [0x40], 1) 446 | ]), 447 | ("RTS", "a", [ 448 | ("im", 6, [0x60], 1) 449 | ]), 450 | ("SBC", "v", [ 451 | ("im", 2, [0xe9, 0xeb], None), 452 | ("z", 3, [0xe5], None), 453 | ("zx", 4, [0xf5], None), 454 | ("a", 4, [0xed], None), 455 | ("ax", 4, [0xfd], None), 456 | ("ay", 4, [0xf9], None), 457 | ("ix", 6, [0xe1], None), 458 | ("iy", 5, [0xf1], None) 459 | ]), 460 | ("STA", "a", [ 461 | ("z", 3, [0x85], None), 462 | ("zx", 4, [0x95], None), 463 | ("a", 4, [0x8d], None), 464 | ("ax", 5, [0x9d], None), 465 | ("ay", 5, [0x99], None), 466 | ("ix", 6, [0x81], None), 467 | ("iy", 6, [0x91], None) 468 | ]), 469 | ("STX", "a", [ 470 | ("z", 3, [0x86], None), 471 | ("zy", 4, [0x96], None), 472 | ("a", 4, [0x8e], None), 473 | ]), 474 | ("STY", "a", [ 475 | ("z", 3, [0x84], None), 476 | ("zx", 4, [0x94], None), 477 | ("a", 4, [0x8c], None) 478 | ]), 479 | # ***Ilegal Opcodes*** 480 | ("AAC", "v", [ 481 | ("im", 2, [0x0b, 0x2b], None) 482 | ]), 483 | ("AAX", "a", [ 484 | ("z", 3, [0x87], None), 485 | ("zy", 4, [0x97], None), 486 | ("a", 4, [0x8f], None), 487 | ("ix", 6, [0x83], None), 488 | ]), 489 | ("ARR", "v", [ 490 | ("im", 2, [0x6b], None) 491 | ]), 492 | ("ASR", "v", [ 493 | ("im", 2, [0x4b], None) 494 | ]), 495 | ("ATX", "v", [ 496 | ("im", 2, [0xab], None) 497 | ]), 498 | ("AXA", "a", [ 499 | ("ay", 5, [0x9f], None), 500 | ("iy", 6, [0x93], None) 501 | ]), 502 | ("AXS", "v", [ 503 | ("im", 2, [0xcb], None) 504 | ]), 505 | ("DCP", "a", [ 506 | ("z", 5, [0xc7], None), 507 | ("zx", 6, [0xd7], None), 508 | ("a", 6, [0xcf], None), 509 | ("ax", 7, [0xdf], None), 510 | ("ay", 7, [0xdb], None), 511 | ("ix", 8, [0xc3], None), 512 | ("iy", 8, [0xd3], None) 513 | ]), 514 | ("ISC", "a", [ 515 | ("z", 5, [0xe7], None), 516 | ("zx", 6, [0xf7], None), 517 | ("a", 6, [0xef], None), 518 | ("ax", 7, [0xff], None), 519 | ("ay", 7, [0xfb], None), 520 | ("ix", 8, [0xe3], None), 521 | ("iy", 8, [0xf3], None) 522 | ]), 523 | ("KIL", "v", [ 524 | ("im", 0, [0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 525 | 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2], 1) 526 | ]), 527 | ("LAR", "v", [ 528 | ("ay", 4, [0xbb], None) 529 | ]), 530 | ("LAX", "v", [ 531 | ("z", 3, [0xa7], None), 532 | ("zy", 4, [0xb7], None), 533 | ("a", 4, [0xaf], None), 534 | ("ay", 4, [0xbf], None), 535 | ("ix", 6, [0xa3], None), 536 | ("iy", 5, [0xb3], None) 537 | ]), 538 | ("RLA", "a", [ 539 | ("z", 5, [0x27], None), 540 | ("zx", 6, [0x37], None), 541 | ("a", 6, [0x2f], None), 542 | ("ax", 7, [0x3f], None), 543 | ("ay", 7, [0x3b], None), 544 | ("ix", 8, [0x23], None), 545 | ("iy", 8, [0x33], None) 546 | ]), 547 | ("RRA", "a", [ 548 | ("z", 5, [0x67], None), 549 | ("zx", 6, [0x77], None), 550 | ("a", 6, [0x6f], None), 551 | ("ax", 7, [0x7f], None), 552 | ("ay", 7, [0x7b], None), 553 | ("ix", 8, [0x63], None), 554 | ("iy", 8, [0x73], None) 555 | ]), 556 | ("SLO", "a", [ 557 | ("z", 5, [0x07], None), 558 | ("zx", 6, [0x17], None), 559 | ("a", 6, [0x0f], None), 560 | ("ax", 7, [0x1f], None), 561 | ("ay", 7, [0x1b], None), 562 | ("ix", 8, [0x03], None), 563 | ("iy", 8, [0x13], None) 564 | ]), 565 | ("SRE", "a", [ 566 | ("z", 5, [0x47], None), 567 | ("zx", 6, [0x57], None), 568 | ("a", 6, [0x4f], None), 569 | ("ax", 7, [0x5f], None), 570 | ("ay", 7, [0x5b], None), 571 | ("ix", 8, [0x43], None), 572 | ("iy", 8, [0x53], None) 573 | ]), 574 | ("SXA", "a", [ 575 | ("ay", 5, [0x9e], None) 576 | ]), 577 | ("SYA", "a", [ 578 | ("ax", 5, [0x9c], None) 579 | ]), 580 | ("XAA", "v", [ 581 | ("im", 2, [0x8b], None) 582 | ]), 583 | ("XAS", "a", [ 584 | ("ay", 5, [0x9b], None) 585 | ]) 586 | ] 587 | 588 | def _create_ops(self): 589 | 590 | def f(self, op_f, a_f, cc): 591 | op_f(a_f()) 592 | self.cc += cc 593 | 594 | def f_target(target): 595 | return target 596 | 597 | self.ops = [None]*0x100 598 | 599 | for op, atype, addrs in self._ops: 600 | op_f = getattr(self, op) 601 | for a, cc, opcode, target in addrs: 602 | if target: 603 | a_f = functools.partial(f_target, target) 604 | elif atype == 'v': 605 | a_f = getattr(self, a) 606 | else: 607 | a_f = getattr(self, "%s_a" % a) 608 | 609 | fp = functools.partial(f, self, op_f, a_f, cc) 610 | for o in opcode: 611 | if self.ops[o]: 612 | raise Exception("Opcode %s already defined" % hex(o)) 613 | self.ops[o] = fp 614 | 615 | def ADC(self, v2): 616 | v1 = self.r.a 617 | 618 | if self.r.getFlag('D'): # decimal mode 619 | d1 = self.fromBCD(v1) 620 | d2 = self.fromBCD(v2) 621 | r = d1 + d2 + self.r.getFlag('C') 622 | self.r.a = self.toBCD(r % 100) 623 | 624 | self.r.setFlag('C', r > 99) 625 | else: 626 | r = v1 + v2 + self.r.getFlag('C') 627 | self.r.a = r & 0xff 628 | 629 | self.r.setFlag('C', r > 0xff) 630 | 631 | self.r.ZN(self.r.a) 632 | self.r.setFlag('V', ((~(v1 ^ v2)) & (v1 ^ r) & 0x80)) 633 | 634 | def AND(self, v): 635 | self.r.a = (self.r.a & v) & 0xff 636 | self.r.ZN(self.r.a) 637 | 638 | def ASL(self, a): 639 | if a == 'a': 640 | v = self.r.a << 1 641 | self.r.a = v & 0xff 642 | else: 643 | v = self.mmu.read(a) << 1 644 | self.mmu.write(a, v) 645 | 646 | self.r.setFlag('C', v > 0xff) 647 | self.r.ZN(v & 0xff) 648 | 649 | def BIT(self, v): 650 | self.r.setFlag('Z', self.r.a & v == 0) 651 | self.r.setFlag('N', v & 0x80) 652 | self.r.setFlag('V', v & 0x40) 653 | 654 | def B(self, v): 655 | """ 656 | v is a tuple of (flag, boolean). For instance, BCC (Branch Carry Clear) 657 | will call B(('C', False)). 658 | """ 659 | d = self.im() 660 | if self.r.getFlag(v[0]) is v[1]: 661 | o = self.r.pc 662 | self.r.pc += self.fromTwosCom(d) 663 | if math.floor(o/0xff) == math.floor(self.r.pc/0xff): 664 | self.cc += 1 665 | else: 666 | self.cc += 2 667 | 668 | def BRK(self, _): 669 | self.r.setFlag('B') 670 | self.stackPushWord(self.r.pc+1) 671 | self.stackPush(self.r.p) 672 | self.r.setFlag('I') 673 | self.r.pc = self.interruptAddress('BRK') 674 | 675 | def CP(self, r, v): 676 | o = (r-v) & 0xff 677 | self.r.setFlag('Z', o == 0) 678 | self.r.setFlag('C', v <= r) 679 | self.r.setFlag('N', o & 0x80) 680 | 681 | def CMP(self, v): 682 | self.CP(self.r.a, v) 683 | 684 | def CPX(self, v): 685 | self.CP(self.r.x, v) 686 | 687 | def CPY(self, v): 688 | self.CP(self.r.y, v) 689 | 690 | def DEC(self, a): 691 | v = (self.mmu.read(a)-1) & 0xff 692 | self.mmu.write(a, v) 693 | self.r.ZN(v) 694 | 695 | def DEX(self, _): 696 | self.r.x = (self.r.x-1) & 0xff 697 | self.r.ZN(self.r.x) 698 | 699 | def DEY(self, _): 700 | self.r.y = (self.r.y-1) & 0xff 701 | self.r.ZN(self.r.y) 702 | 703 | def EOR(self, v): 704 | self.r.a = self.r.a ^ v 705 | self.r.ZN(self.r.a) 706 | 707 | """Flag Instructions.""" 708 | def SE(self, v): 709 | """Set the flag to True.""" 710 | self.r.setFlag(v) 711 | 712 | def CL(self, v): 713 | """Clear the flag to False.""" 714 | self.r.clearFlag(v) 715 | 716 | def INC(self, a): 717 | v = (self.mmu.read(a)+1) & 0xff 718 | self.mmu.write(a, v) 719 | self.r.ZN(v) 720 | 721 | def INX(self, _): 722 | self.r.x = (self.r.x+1) & 0xff 723 | self.r.ZN(self.r.x) 724 | 725 | def INY(self, _): 726 | self.r.y = (self.r.y+1) & 0xff 727 | self.r.ZN(self.r.y) 728 | 729 | def JMP(self, a): 730 | self.r.pc = a 731 | 732 | def JSR(self, a): 733 | self.stackPushWord(self.r.pc-1) 734 | self.r.pc = a 735 | 736 | def LDA(self, v): 737 | self.r.a = v 738 | self.r.ZN(self.r.a) 739 | 740 | def LDX(self, v): 741 | self.r.x = v 742 | self.r.ZN(self.r.x) 743 | 744 | def LDY(self, v): 745 | self.r.y = v 746 | self.r.ZN(self.r.y) 747 | 748 | def LSR(self, a): 749 | if a == 'a': 750 | self.r.setFlag('C', self.r.a & 0x01) 751 | self.r.a = v = self.r.a >> 1 752 | else: 753 | v = self.mmu.read(a) 754 | self.r.setFlag('C', v & 0x01) 755 | v = v >> 1 756 | self.mmu.write(a, v) 757 | 758 | self.r.ZN(v) 759 | 760 | def NOP(self, _): 761 | pass 762 | 763 | def ORA(self, v): 764 | self.r.a = self.r.a | v 765 | self.r.ZN(self.r.a) 766 | 767 | def P(self, v): 768 | """ 769 | Stack operations, PusH and PulL. v is a tuple where the 770 | first value is either PH or PL, specifying the action and 771 | the second is the source or target register, either A or P, 772 | meaning the Accumulator or the Processor status flag. 773 | """ 774 | a, r = v 775 | 776 | if a == "PH": 777 | self.stackPush(getattr(self.r, r)) 778 | else: 779 | setattr(self.r, r, self.stackPop()) 780 | 781 | if r == "a": 782 | self.r.ZN(self.r.a) 783 | elif r == "p": 784 | self.r.p = self.r.p | 0b00100000 785 | 786 | def ROL(self, a): 787 | if a == "a": 788 | v_old = self.r.a 789 | self.r.a = v_new = ((v_old << 1) + self.r.getFlag('C')) & 0xff 790 | else: 791 | v_old = self.mmu.read(a) 792 | v_new = ((v_old << 1) + self.r.getFlag('C')) & 0xff 793 | self.mmu.write(a, v_new) 794 | 795 | self.r.setFlag('C', v_old & 0x80) 796 | self.r.ZN(v_new) 797 | 798 | def ROR(self, a): 799 | if a == "a": 800 | v_old = self.r.a 801 | self.r.a = v_new = ((v_old >> 1) + self.r.getFlag('C')*0x80) & 0xff 802 | else: 803 | v_old = self.mmu.read(a) 804 | v_new = ((v_old >> 1) + self.r.getFlag('C')*0x80) & 0xff 805 | self.mmu.write(a, v_new) 806 | 807 | self.r.setFlag('C', v_old & 0x01) 808 | self.r.ZN(v_new) 809 | 810 | def RTI(self, _): 811 | self.r.p = self.stackPop() 812 | self.r.pc = self.stackPopWord() 813 | 814 | def RTS(self, _): 815 | self.r.pc = (self.stackPopWord() + 1) & 0xffff 816 | 817 | def SBC(self, v2): 818 | v1 = self.r.a 819 | if self.r.getFlag('D'): 820 | d1 = self.fromBCD(v1) 821 | d2 = self.fromBCD(v2) 822 | r = d1 - d2 - (not self.r.getFlag('C')) 823 | self.r.a = self.toBCD(r % 100) 824 | else: 825 | r = v1 - v2 - (not self.r.getFlag('C')) 826 | self.r.a = r & 0xff 827 | 828 | self.r.setFlag('C', r >= 0) 829 | self.r.setFlag('V', ((v1 ^ v2) & (v1 ^ r) & 0x80)) 830 | self.r.ZN(self.r.a) 831 | 832 | def STA(self, a): 833 | self.mmu.write(a, self.r.a) 834 | 835 | def STX(self, a): 836 | self.mmu.write(a, self.r.x) 837 | 838 | def STY(self, a): 839 | self.mmu.write(a, self.r.y) 840 | 841 | def T(self, a): 842 | """ 843 | Transfer registers 844 | a is a tuple with (source, destination) so TAX 845 | would be T(('a', 'x'))self. 846 | """ 847 | s, d = a 848 | setattr(self.r, d, getattr(self.r, s)) 849 | if d != 's': 850 | self.r.ZN(getattr(self.r, d)) 851 | 852 | """ 853 | Illegal Opcodes 854 | --------------- 855 | 856 | Opcodes which were not officially documented but still have 857 | and effect. The behavior for each of these is based on the following: 858 | 859 | -http://www.ataripreservation.org/websites/freddy.offenga/illopc31.txt 860 | -http://wiki.nesdev.com/w/index.php/Programming_with_unofficial_opcodes 861 | -www.ffd2.com/fridge/docs/6502-NMOS.extra.opcodes 862 | 863 | The behavior is not consistent across the various resources so I don't 864 | promise 100% hardware accuracy here. 865 | 866 | Other names for the opcode are in comments on the function defintion 867 | line. 868 | """ 869 | 870 | def AAC(self, v): # ANC 871 | self.AND(v) 872 | self.r.setFlag('C', self.r.getFlag('N')) 873 | 874 | def AAX(self, a): # SAX, AXS 875 | r = self.r.a & self.r.x 876 | self.mmu.write(a, r) 877 | # self.r.ZN(r) # There is conflicting information whether this effects P. 878 | 879 | def ARR(self, v): 880 | self.AND(v) 881 | self.ROR('a') 882 | self.r.setFlag('C', self.r.a & 0x40) 883 | self.r.setFlag('V', bool(self.r.a & 0x40) ^ bool(self.r.a & 0x20)) 884 | 885 | def ASR(self, v): # ALR 886 | self.AND(v) 887 | self.LSR('a') 888 | 889 | def ATX(self, v): # LXA, OAL 890 | self.AND(v) 891 | self.T(('a', 'x')) 892 | 893 | def AXA(self, a): # SHA 894 | """ 895 | There are a few illegal opcodes which and the high 896 | bit of the address with registers and write the values 897 | back into that address. These operations are 898 | particularly screwy. These posts are used as reference 899 | but I am unsure whether they are correct. 900 | - forums.nesdev.com/viewtopic.php?f=3&t=3831&start=30#p113343 901 | - forums.nesdev.com/viewtopic.php?f=3&t=10698 902 | """ 903 | o = (a - self.r.y) & 0xffff 904 | low = o & 0xff 905 | high = o >> 8 906 | if low + self.r.y > 0xff: # crossed page 907 | a = ((high & self.r.x) << 8) + low + self.r.y 908 | else: 909 | a = (high << 8) + low + self.r.y 910 | 911 | v = self.r.a & self.r.x & (high + 1) 912 | self.mmu.write(a, v) 913 | 914 | def AXS(self, v): # SBX, SAX 915 | o = self.r.a & self.r.x 916 | self.r.x = (o - v) & 0xff 917 | 918 | self.r.setFlag('C', v <= o) 919 | self.r.ZN(self.r.x) 920 | 921 | def DCP(self, a): # DCM 922 | self.DEC(a) 923 | self.CMP(self.mmu.read(a)) 924 | 925 | def ISC(self, a): # ISB, INS 926 | self.INC(a) 927 | self.SBC(self.mmu.read(a)) 928 | 929 | def KIL(self, _): # JAM, HLT 930 | self.running = False 931 | 932 | def LAR(self, v): # LAE, LAS 933 | self.r.a = self.r.x = self.r.s = self.r.s & v 934 | self.r.ZN(self.r.a) 935 | 936 | def LAX(self, v): 937 | self.r.a = self.r.x = v 938 | self.r.ZN(self.r.a) 939 | 940 | def RLA(self, a): 941 | self.ROL(a) 942 | self.AND(self.mmu.read(a)) 943 | 944 | def RRA(self, a): 945 | self.ROR(a) 946 | self.ADC(self.mmu.read(a)) 947 | 948 | def SLO(self, a): # ASO 949 | self.ASL(a) 950 | self.ORA(self.mmu.read(a)) 951 | 952 | def SRE(self, a): # LSE 953 | self.LSR(a) 954 | self.EOR(self.mmu.read(a)) 955 | 956 | def SXA(self, a): # SHX, XAS 957 | # See AXA 958 | o = (a - self.r.y) & 0xffff 959 | low = o & 0xff 960 | high = o >> 8 961 | if low + self.r.y > 0xff: # crossed page 962 | a = ((high & self.r.x) << 8) + low + self.r.y 963 | else: 964 | a = (high << 8) + low + self.r.y 965 | 966 | v = self.r.x & (high + 1) 967 | self.mmu.write(a, v) 968 | 969 | def SYA(self, a): # SHY, SAY 970 | # See AXA 971 | o = (a - self.r.x) & 0xffff 972 | low = o & 0xff 973 | high = o >> 8 974 | if low + self.r.x > 0xff: # crossed page 975 | a = ((high & self.r.y) << 8) + low + self.r.x 976 | else: 977 | a = (high << 8) + low + self.r.x 978 | 979 | v = self.r.y & (high + 1) 980 | self.mmu.write(a, v) 981 | 982 | def XAA(self, v): # ANE 983 | """ 984 | Another very wonky operation. It's fully described here: 985 | http://visual6502.org/wiki/index.php?title=6502_Opcode_8B_%28XAA,_ANE%29 986 | "magic" varies by version of the processor. 0xee seems to be common. 987 | The formula is: A = (A | magic) & X & imm 988 | """ 989 | self.r.a = (self.r.a | self.magic) & self.r.x & v 990 | self.r.ZN(self.r.a) 991 | 992 | def XAS(self, a): # SHS, TAS 993 | # First set the stack pointer's value 994 | self.r.s = self.r.a & self.r.x 995 | 996 | # Then write to memory using the new value of the stack pointer 997 | o = (a - self.r.y) & 0xffff 998 | low = o & 0xff 999 | high = o >> 8 1000 | if low + self.r.y > 0xff: # crossed page 1001 | a = ((high & self.r.s) << 8) + low + self.r.y 1002 | else: 1003 | a = (high << 8) + low + self.r.y 1004 | 1005 | v = self.r.s & (high + 1) 1006 | self.mmu.write(a, v) 1007 | --------------------------------------------------------------------------------