diff --git a/README.md b/README.md index 6c8da4f..3cc26e5 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,19 @@ I built this project on macOS. The instructions should be pretty easy to follow The build pipeline has dependencies on [node.js](http://nodejs.org) and [SDCC](http://sdcc.sourceforge.net). You'll need to install them. ### Dependencies -`cd` into `ihx2gb/` and run `npm install`. - `cd` into `img2gb/` and run `npm install`. `cd` into `stringc/` and run `npm install`. ### Building the Game `cd` into `game/` and run `make` or `make debug`. + +### Building the Game and Dependencies +Just run `make`, `make debug` or `make run` in the root folder. + +### Building with an SDCC Snapshot +*This is for versions of SDCC that aren't installed* +``` +SDCC_HOME=/path/to/sdcc/share/sdcc/ +make SDCCBIN=${SDCC_HOME}../../bin/ +``` \ No newline at end of file diff --git a/game/makefile b/game/makefile index fe32ea9..50a3f9d 100644 --- a/game/makefile +++ b/game/makefile @@ -1,8 +1,15 @@ -CC = sdcc -ASMC = sdasgb +SDCCBIN = +CC = $(SDCCBIN)sdcc +AS = $(SDCCBIN)sdasgb +MKROM = $(SDCCBIN)makebin +# https://github.com/LIJI32/SameBoy or any other emulator +EMU = sameboy +# https://github.com/bbbbbr/romusage +ROMUSE = romusage +NODE = node CFLAGS = -c -mgbz80 -ASMFLAGS = -plosgffjw -LINKFLAGS = -mgbz80 --no-std-crt0 --data-loc 0xc0a0 +ASFLAGS = -plosgffjw +LDFLAGS = -mgbz80 --no-std-crt0 --data-loc 0xc0a0 GFXS = $(notdir $(shell find data/gfx -name '*.png')) GFXSRC = $(patsubst %.png,data_gfx_%.c,$(GFXS)) @@ -13,44 +20,60 @@ OBJS = $(SRCS:.c=.o) SRCFILES = $(patsubst %.c,src/%.c,$(SRCS)) OBJFILES = $(patsubst %.s,obj/%.rel,$(ASMS)) $(patsubst %.c,obj/%.rel,$(SRCS)) -build: obj/dependencies bin/BubbleFactory.gb +ROM = BubbleFactory -debug: CFLAGS += -DDEBUG +.PHONY: build debug run clean spaceleft statistics + +build: obj/dependencies bin/$(ROM).gb + +debug: CFLAGS += -DDEBUG --debug +debug: LDFLAGS += -Wl-y debug: build src/data_strings.c: data/strings.json - node ../stringc data/stringsMap.json $< $@ + $(NODE) ../stringc data/stringsMap.json $< $@ src/data_gfx_%.c: data/gfx/%.png - node ../img2gb -n $(notdir $(basename $<)) $< $@ + $(NODE) ../img2gb -n $(notdir $(basename $<)) $< $@ -obj/dependencies: $(SRCFILES) +obj/dependencies: $(SRCFILES) | obj/ @echo "regenerate dependency file" - @mkdir -p obj - @rm -f obj/dependencies + @$(RM) -f obj/dependencies @for srcFile in $(SRCFILES) ; do \ { printf "obj/"; $(CC) -MM $$srcFile; } >> obj/dependencies ; \ done -include obj/dependencies -bin/BubbleFactory.gb: obj/game.ihx - @mkdir -p bin - node ../ihx2gb --name BUBBLEFACT --licensee DH --cartridge 0x1B --ram 1 --version 1 $< $@ +bin/$(ROM).gb: obj/game.ihx | bin/ + $(MKROM) -Z -yn BUBBLEFACT -yk DH -yt 0x1B -ya 1 -yS -yp0x014C=1 $< $@ + cp obj/game.sym bin/$(ROM).sym + cp obj/game.noi bin/$(ROM).noi + test \! -s obj/game.cdb || cp obj/game.cdb bin/$(ROM).cdb -obj/game.ihx: $(OBJFILES) - @mkdir -p obj - $(CC) $(LINKFLAGS) $^ -o $@ +obj/game.ihx: $(OBJFILES) | obj/ + $(CC) $(LDFLAGS) $^ -o $@ -obj/%.rel: src/%.c - @mkdir -p obj +obj/%.rel: src/%.c | obj/ $(CC) $(CFLAGS) $< -o $@ -obj/%.rel: src/%.s - @mkdir -p obj - $(ASMC) $(ASMFLAGS) $@ $< +obj/%.rel: src/%.s | obj/ + $(AS) $(ASFLAGS) $@ $< + +%/: + @mkdir -p $@ + +spaceleft: build + $(ROMUSE) bin/$(ROM).noi -g -E + +statistics: build + test \! -s bin/$(ROM).cdb || $(ROMUSE) bin/$(ROM).cdb -g + $(ROMUSE) bin/$(ROM).noi -G -E -sH -a + +run: build + $(EMU) bin/$(ROM).gb clean: - rm -rf src/data_* - rm -rf obj - rm -rf bin + $(RM) -rf src/data_* + $(RM) -rf obj + $(RM) -rf bin diff --git a/ihx2gb/ihx2gb.js b/ihx2gb/ihx2gb.js deleted file mode 100644 index 7445dc9..0000000 --- a/ihx2gb/ihx2gb.js +++ /dev/null @@ -1,257 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const commander = require("commander"); -const Overflow = require("overflow-js").Overflow; - -function parseHexPair(string, index) { - return parseInt(string.substr(index, 2), 16); -} - -function writeLogo(buffer) { - const logoBuffer = Buffer.from([ - 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, - 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, - 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E - ]); - - logoBuffer.copy(buffer, 0x0104); -} - -function writeTitle(buffer, title) { - if(title.length > 11) { - console.warn(`warning: title longer than 11 characters, later characters will be ignored`); - } - - if(/^[A-Z0-9 ]*$/g.test(title) == false) { - console.error("title may only have uppercase letters, digits, and spaces"); - process.exit(1); - } - - const startAddress = 0x0134; - buffer.fill(0x00, startAddress, startAddress + 11); - buffer.write(title, startAddress, Math.min(title.length, 11), "ascii"); -} - -function writeManufacturerCode(buffer, code) { - if(code.length > 4) { - console.warn(`warning: manufacturer code longer than 4 characters, later characters will be ignored`); - } - - if(/^[A-Z0-9 ]*$/g.test(code) == false) { - console.error("manufacturer code may only have uppercase letters, digits, and spaces"); - process.exit(1); - } - - const startAddress = 0x013F; - buffer.fill(0x00, startAddress, startAddress + 4); - buffer.write(code, startAddress, Math.min(code.length, 4), "ascii"); -} - -function writeCGBFlag(buffer, flag) { - buffer.writeUInt8(flag, 0x0143); -} - -function writeLicenseeCode(buffer, code) { - if(code.length > 2) { - console.warn(`warning: licensee code longer than 2 characters, later characters will be ignored`); - } - - if(/^[a-zA-Z0-9 ]*$/g.test(code) == false) { - console.error("licensee code may only have letters, digits, and spaces"); - process.exit(1); - } - - const startAddress = 0x0144; - buffer.fill(0xFF, startAddress, startAddress + 2); - buffer.write(code, startAddress, Math.min(code.length, 2), "ascii"); -} - -function writeSGBFlag(buffer, flag) { - buffer.writeUInt8(flag, 0x0146); -} - -function writeCartridgeType(buffer, type) { - buffer.writeUInt8(type, 0x0147); -} - -function writeROMSize(buffer, size) { - buffer.writeUInt8(size, 0x0148); -} - -function writeRAMSize(buffer, size) { - buffer.writeUInt8(size, 0x0149); -} - -function writeDestinationCode(buffer, code) { - buffer.writeUInt8(code, 0x014A); -} - -function writeOldLicenseeCode(buffer, code) { - buffer.writeUInt8(code, 0x014B); -} - -function writeROMVersion(buffer, version) { - buffer.writeUInt8(version, 0x014C); -} - -function writeHeaderChecksum(buffer) { - let x = Overflow.ubyte(0); - for(let index = 0x0134; index <= 0x014C; index++) { - x = x.minus(buffer[index]).minus(1); - } - buffer.writeUInt8(x.value, 0x014D); -} - -function writeROMChecksum(buffer) { - let x = Overflow.ushort(0); - for(let index = 0x00; index < buffer.length; index++) { - if(index == 0x014E || index == 0x014F) { - continue; - } - x = x.plus(buffer[index]); - } - buffer.writeUInt16BE(x.value, 0x014E); -} - -function processMap(mapFilePath, symOutputPath) { - if(fs.existsSync(mapFilePath) == false) { - return; - } - - const regex = /^[\s]+([0-9A-F]{8}) _([a-zA-Z0-9_]+)/; - - const text = fs.readFileSync(mapFilePath, "utf8"); - const lines = text.split(/[\n\r]+/g); - const addresses = []; - lines.forEach((line) => { - const match = regex.exec(line); - if(match == null) { - return; - } - const address = match[1]; - const symbol = match[2]; - - addresses.push({ - "address" : address, - "symbol" : symbol - }); - }); - - const symbolLines = [ - "; Generated by IHX2GB", - "" - ]; - addresses.forEach((address) => { - symbolLines.push(`00:${address.address.substr(4)} ${address.symbol}`); - }); - - fs.writeFileSync(symOutputFilePath, symbolLines.join("\n")); -} - -commander - .usage("[options] ") - .option("-n, --name [value]", "The name in the header. Default: \"MY GAME\"", "MY GAME") - .option("-m, --manufacturer [code]", "The manufacturer code. Default: \"\"", "") - .option("-c, --cgb [flag]", "The decimal Game Boy Color support flag. 128 for CGB+GB, 192 for CGB-only. Default: 0", "0", parseInt) - .option("-l, --licensee [code]", "The ASCII licensee code. Default: \"00\"", "00") - .option("-s, --sgb [flag]", "The decimal Super Game Boy support flag. 3 to enable. Default: 0", "0", parseInt) - .option("-t, --cartridge [type]", "The decimal cartridge type. Default: 0", "0", parseInt) - .option("-r, --rom [code]", "The decimal ROM size code. File will be 32KB << value, except for 82, 83, and 84. Default: 0", "0", parseInt) - .option("-a, --ram [code]", "The decimal external RAM size code. Default: 0", "0", parseInt) - .option("-d, --destination [code]", "The destination code. 0 for Japan, 1 for international. Default: 1", "1", parseInt) - .option("--oldlicensee [code]", "The decimal old licensee code. 51 to use new licensee code. SGB will not work if != 51. Default: 51", "51", parseInt) - .option("-v, --version [version]", "The ROM version. Default: 0", "0", parseInt) - -commander.on("--help", () => { - console.log("Go to http://gbdev.gg8.se/wiki/articles/The_Cartridge_Header for header details, including values for Cartridge Type, ROM Size, and RAM Size. That site lists codes in hex, but must be provided here in decimal."); - console.log(""); -}); - -commander.parse(process.argv); - -if(commander.rom > 8) { - let n = 32768; - for(let i=0; i < commander.rom; i++) { - n *= 2; - } - console.error("rom size codes > 8 unsupported. Fun fact, you asked me to make a file " + (n) + " bytes big! Wow!"); - process.exit(1); -} - -if(commander.args.length != 2) { - console.error("must provide both an input and output file"); - process.exit(1); -} - -const inputFilePath = path.resolve(commander.args[0]); -const outputFilePath = path.resolve(commander.args[1]); - -const inputFileDirectory = path.dirname(inputFilePath); -const inputFileBasename = path.basename(inputFilePath, path.extname(inputFilePath)); -const mapFilePath = path.join(inputFileDirectory, inputFileBasename + ".map"); - -const outputFileDirectory = path.dirname(outputFilePath); -const outputFileBasename = path.basename(outputFilePath, path.extname(outputFilePath)); -const symOutputFilePath = path.join(outputFileDirectory, outputFileBasename + ".sym"); - -processMap(mapFilePath, symOutputFilePath); - -const ihxText = fs.readFileSync(inputFilePath, { "encoding" : "utf8" }); -const ihxRecords = ihxText.split(/[\n\r]+/g).filter((record) => { return record.length > 0; }); -const gbBuffer = Buffer.alloc(32768 << commander.rom, 0xFF); - -let baseAddress = 0; - -ihxRecords.forEach((record) => { - let head = 0; - - if(record[head] != ":") { - console.error("not an ihx file"); - process.exit(1); - } - - head +=1; - - const dataLength = parseHexPair(record, head); - head += 2; - - const address = baseAddress + (parseHexPair(record, head) << 8) + parseHexPair(record, head + 2); - head += 4; - - const type = parseHexPair(record, head); - head += 2; - - if(type == 0) { - // Data Record, copy bytes - for(let index = 0; index < dataLength; index++) { - const byte = parseHexPair(record, head); - head += 2; - - gbBuffer[address + index] = byte; - } - } else if(type == 1) { - // EOF Record. - } else { - console.error("unsupported record type"); - process.exit(1); - } -}); - -writeLogo(gbBuffer); -writeTitle(gbBuffer, commander.name); -writeManufacturerCode(gbBuffer, commander.manufacturer); -writeCGBFlag(gbBuffer, commander.cgb); -writeLicenseeCode(gbBuffer, commander.licensee); -writeSGBFlag(gbBuffer, commander.sgb); -writeCartridgeType(gbBuffer, commander.cartridge); -writeROMSize(gbBuffer, commander.rom); -writeRAMSize(gbBuffer, commander.ram); -writeDestinationCode(gbBuffer, commander.destination); -writeOldLicenseeCode(gbBuffer, commander.oldlicensee); -writeROMVersion(gbBuffer, commander.version); -writeHeaderChecksum(gbBuffer); -writeROMChecksum(gbBuffer); - -fs.writeFileSync(outputFilePath, gbBuffer); diff --git a/ihx2gb/package-lock.json b/ihx2gb/package-lock.json deleted file mode 100644 index 78345e8..0000000 --- a/ihx2gb/package-lock.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "ihx2gb", - "version": "0.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "requires": { - "graceful-readlink": ">= 1.0.0" - } - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" - }, - "overflow-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/overflow-js/-/overflow-js-1.0.0.tgz", - "integrity": "sha1-G8S//0Qg5Rc5ZuvWd8SASR7phPA=" - } - } -} diff --git a/ihx2gb/package.json b/ihx2gb/package.json deleted file mode 100644 index 2aa81c8..0000000 --- a/ihx2gb/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "ihx2gb", - "description": "converts intel ihx files to game boy format", - "version": "0.0.0", - "main": "ihx2gb", - "dependencies": { - "commander": "~2.9.0", - "overflow-js": "~1.0.0" - } -} diff --git a/img2gb/package-lock.json b/img2gb/package-lock.json index 1995d27..f1f5bd5 100644 --- a/img2gb/package-lock.json +++ b/img2gb/package-lock.json @@ -1,7 +1,7 @@ { "name": "img2gb", "version": "0.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, "dependencies": { "commander": { diff --git a/makefile b/makefile new file mode 100644 index 0000000..4648552 --- /dev/null +++ b/makefile @@ -0,0 +1,19 @@ +.PHONY: build debug tools run clean + +build: tools + $(MAKE) -C game/ $@ + +debug: tools + $(MAKE) -C game/ $@ + +run: tools + $(MAKE) -C game/ $@ + +clean: + $(MAKE) -C game/ $@ + $(RM) -rf img2gb/node_modules stringc/node_modules + +tools: img2gb/node_modules stringc/node_modules + +%/node_modules: + cd $* && npm install \ No newline at end of file diff --git a/stringc/package-lock.json b/stringc/package-lock.json index 64f85c0..900a929 100644 --- a/stringc/package-lock.json +++ b/stringc/package-lock.json @@ -1,7 +1,7 @@ { "name": "stringc", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, "dependencies": { "commander": {