aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJuan J. Martinez <jjm@usebox.net>2022-09-16 07:34:44 +0100
committerJuan J. Martinez <jjm@usebox.net>2022-09-16 12:05:09 +0100
commit1ef0d697a62eff28115d6642c850ba4d01ef6a89 (patch)
tree23d7fdb24453a3a1378f81d021567616492bbdbd
parent290c74b70661bcde314f73fde2be888e5aed47e0 (diff)
downloadubox-msx-lib-1ef0d697a62eff28115d6642c850ba4d01ef6a89.tar.gz
ubox-msx-lib-1ef0d697a62eff28115d6642c850ba4d01ef6a89.zip
Added CAS support to the example game
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--CHANGES.md2
-rw-r--r--Makefile6
-rw-r--r--README.md22
-rw-r--r--TODO.md1
-rw-r--r--docs/tools.md32
-rw-r--r--game/Makefile5
-rw-r--r--game/data/loading.pngbin0 -> 8188 bytes
-rw-r--r--game/src/Makefile26
-rw-r--r--game/src/cas/loader.bas1
-rw-r--r--game/src/cas/loader.z80219
-rw-r--r--game/src/crt0.z80115
-rwxr-xr-xtools/mkcas.py164
-rwxr-xr-xtools/png2scr.py140
14 files changed, 675 insertions, 59 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6c232e7..3147c8b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -14,6 +14,7 @@ compile:
stage: build
script:
- make game
+ - make game-cas
test:
stage: test
diff --git a/CHANGES.md b/CHANGES.md
index 28436bc..421f4c8 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,7 @@
## Release 1.1.12 - 2022-??-??
+ - Added CAS support to the example game, introducing new tools: `mkcas.py` and
+ `png2scr.py`.
- Fix in `png2sprites.py` and `png2tiles.py`: ensure the order of the colours
is always the same
diff --git a/Makefile b/Makefile
index e97a66c..b467286 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,10 @@ game: bin libs
make -C tools
make -C game all
+game-cas: bin libs
+ make -C tools
+ make -C game cas
+
test: game
make -C tests test
@@ -32,7 +36,7 @@ ap:
bin/apultra: bin
make -C tools ../bin/apultra
-.PHONY: clean cleanall docs libs game
+.PHONY: clean cleanall docs libs game game-cas
clean:
make -C src/ubox clean
make -C src/spman clean
diff --git a/README.md b/README.md
index dfbd8f3..301e856 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,28 @@ An example game is included with the libraries and it can be built with:
After a successful build, the game ROM should be in `./bin`.
+#### CAS support
+
+Although the focus is cartridge ROMs, CAS files (and audio) is still one of the
+cheapest ways of loading homebrew games on a real MSX.
+
+Optionally, is possible to generate a CAS file of the example game running:
+
+ make game-cas
+
+After a successful build, the game CAS should be in `./bin`.
+
+The CAS support has some limitations:
+
+ - It requires 32K of extra RAM.
+ - The compressed ROM must be less than 24576 bytes.
+ - The loader uses the BIOS, so it won't be fast.
+ - Machines with disk must have it disabled (by pressing shift on boot), to
+ have more memory available.
+
+Despite these limitations, it is worth considering releasing your game in CAS
+format as well as cartridge ROM.
+
### Building the docs
The documentation is available at
diff --git a/TODO.md b/TODO.md
index cc75e31..e919fa8 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,7 +3,6 @@ Short/mid term:
- Add util tools/libs (e.g. compression)
- Improve MSX 1 support
- Support 48K ROMs
- - CAS support
- Some sort of cookie-cutter support to easily start new projects
Long term:
diff --git a/docs/tools.md b/docs/tools.md
index 19705a7..67b797a 100644
--- a/docs/tools.md
+++ b/docs/tools.md
@@ -197,6 +197,38 @@ The entities are encoded in a stream that ends with the byte `0xff` as terminato
Refer to the example game and `init_map_entities()` in `game.c` for a full example.
+### mkcas.py
+
+This is a simple tool to make CAS files to be used in different MSX emulators.
+
+It supports the following block types:
+
+ - binary: binary executable data, include loading and execution addresses.
+ - basic: tokenized BASIC code.
+ - ascii: ASCII text, can be BASIC for example. In case of BASIC code, the CR
+ LF is expected as end of line.
+ - custom-header: no block type, header with loading address and block length
+ followed by the data.
+ - custom: no block type, data stored "as-is".
+
+Use `-h` flag to get command line help, and check the example game for sample on
+how to build a CAS file with a loading screen.
+
+### png2scr.py
+
+This tools converts an PNG RGB image into a Screen 2 SCR image.
+
+The image is expected to use an approximation to RGB for the colours in the
+Toshiba palette (see `png2tiles.py` for the palette values).
+
+The image must be 256x192 pixels.
+
+The SCR file is 768 background tiles (6144 bytes) followed by the
+background colours for the 3 areas of the screen (6144 bytes), that can be
+uploaded to the VDP "as is".
+
+Check the example game for a sample on how to use it.
+
## Build helpers
### chksize
diff --git a/game/Makefile b/game/Makefile
index 3fe43f3..347d37b 100644
--- a/game/Makefile
+++ b/game/Makefile
@@ -6,6 +6,9 @@ all: generated bin build
cd src && ../../tools/mkdeps.py -b ../build -I ../generated:../../include ./ Makefile.deps
make -C src all
+cas: all
+ make -C src cas
+
generated:
mkdir -p ./generated
@@ -18,7 +21,7 @@ build:
openmsx: all
make -C src openmsx
-.PHONY: all clean default
+.PHONY: all cas clean default
clean:
rm -rf ./generated ./bin ./build
make -C src clean
diff --git a/game/data/loading.png b/game/data/loading.png
new file mode 100644
index 0000000..ce196ef
--- /dev/null
+++ b/game/data/loading.png
Binary files differ
diff --git a/game/src/Makefile b/game/src/Makefile
index b05ef4e..3ca267a 100644
--- a/game/src/Makefile
+++ b/game/src/Makefile
@@ -32,6 +32,9 @@ all: $(TMPDIR)/$(TARGET).rom
@$(CHKSIZE) $(ROM_MAX) 4000 $(TMPDIR)/$(TARGET).map
@cp $(TMPDIR)/$(TARGET).rom $(OUTDIR)
+cas: $(TMPDIR)/$(TARGET).cas
+ @cp $(TMPDIR)/$(TARGET).cas $(OUTDIR)
+
openmsx: all
openmsx -carta $(OUTDIR)/$(TARGET).rom -machine msx1
@@ -51,10 +54,31 @@ $(TMPDIR)/$(TARGET).rom: $(OBJS) $(TMPDIR)/crt0.rel $(UBOX_LIBS)
hex2bin -e bin -p 00 -l $(ROM_MAX) $(TMPDIR)/$(TARGET).ihx
@cp $(TMPDIR)/$(TARGET).bin $(TMPDIR)/$(TARGET).rom
+LOADER_ADDR=0xe000
+LOADER_WORK_ADDR=0x8000
+$(TMPDIR)/$(TARGET).cas: $(TMPDIR)/$(TARGET).rom cas/loader.bas $(TMPDIR)/loader.bin $(TMPDIR)/loading.ap
+ apultra -v $(TMPDIR)/$(TARGET).rom $(TMPDIR)/$(TARGET).ap
+ ../../tools/mkcas.py --name $(TARGET) $@ ascii cas/loader.bas
+ ../../tools/mkcas.py -a --name loader --addr $(LOADER_ADDR) --exec $(LOADER_ADDR) $@ binary $(TMPDIR)/loader.bin
+ ../../tools/mkcas.py -a --addr $(LOADER_WORK_ADDR) $@ custom-header $(TMPDIR)/loading.ap
+ ../../tools/mkcas.py -a --addr $(LOADER_WORK_ADDR) $@ custom-header $(TMPDIR)/$(TARGET).ap
+
+$(TMPDIR)/loader.bin: $(TMPDIR)/loader.rel
+ $(CC) $(CFLAGS) $(LDFLAGS) --code-loc $(LOADER_ADDR) --data-loc 0 -lap $< -o $(TMPDIR)/loader.ihx
+ hex2bin -p 00 $(TMPDIR)/loader.ihx
+
+$(TMPDIR)/loading.ap: ../data/loading.png
+ ../../tools/png2scr.py ../data/loading.png > $(TMPDIR)/loading.bin
+ apultra -v $(TMPDIR)/loading.bin $(TMPDIR)/loading.ap
+
+$(TMPDIR)/loader.rel: cas/loader.z80
+ $(AS) -g -o $@ $<
+
clean:
rm -f $(TMPDIR)/*
rm -f $(OUTDIR)/$(TARGET).rom
+ rm -f $(OUTDIR)/$(TARGET).cas
-.PHONY: all clean
+.PHONY: all cas clean
include Makefile.deps
diff --git a/game/src/cas/loader.bas b/game/src/cas/loader.bas
new file mode 100644
index 0000000..c94a478
--- /dev/null
+++ b/game/src/cas/loader.bas
@@ -0,0 +1 @@
+10 BLOAD"cas:",R
diff --git a/game/src/cas/loader.z80 b/game/src/cas/loader.z80
new file mode 100644
index 0000000..d7b058c
--- /dev/null
+++ b/game/src/cas/loader.z80
@@ -0,0 +1,219 @@
+;
+; TAPE LOADER USING THE BIOS
+;
+
+TAPION = 0x00e1
+TAPIN = 0x00e4
+TAPIOF = 0x00e7
+DISSCR = 0x0041
+ENASCR = 0x0044
+LDIRVM = 0x005c
+CHGMOD = 0x005f
+CHGCLR = 0x0062
+FORCLR = 0xf3e9
+BAKCLR = 0xf3ea
+BDRCLR = 0xf3eb
+CHPUT = 0x00a2
+TOTEXT = 0x00d2
+ENASLT = 0x0024
+RSLREG = 0x0138
+
+ .area _CODE
+ di
+ ; init the stack
+ ld sp, #0xf380
+
+ ; sslot fix
+ ld a, (0xffff)
+ cpl
+ and #0xf0
+ ld c, a
+ rrca
+ rrca
+ rrca
+ rrca
+ or c
+ ld (0xffff),a
+
+ in a, (#0xa8)
+
+ ; map RAM on 0x8000
+ and #0xcf
+ ld c, #16
+ ld hl, #0x8000
+ call select_ram
+
+ ; map RAM on 0x4000
+ and #0xf3
+ ld c, #4
+ ld hl, #0x4000
+ call select_ram
+
+ ei
+
+ ; now we have: ROM RAM RAM RAM
+
+ ; set these to black
+ ld a, #1
+ ld (FORCLR), a
+ ld (BAKCLR), a
+ ld (BDRCLR), a
+ call CHGCLR
+
+ ; load the loading screen
+ call load_custom_block
+
+ ld hl, (block_addr)
+ ld de, #0x4000
+ call ap_uncompress
+
+ call upload_screen
+
+ ; load the code
+ call load_custom_block
+
+ ld hl, (block_addr)
+ ld de, #0x4000
+ call ap_uncompress
+
+ ; cas main
+ jp 0x4010
+
+upload_screen:
+ ld a, #2
+ call CHGMOD
+
+ call DISSCR
+
+ ld hl, #0x4000
+ ld de, #0
+ ld bc, #256 * 8
+ call LDIRVM
+
+ ld hl, #0x4000 + 256 * 8
+ ld de, #256 * 8
+ ld bc, #256 * 8
+ call LDIRVM
+
+ ld hl, #0x4000 + 256 * 8 * 2
+ ld de, #256 * 8 * 2
+ ld bc, #256 * 8
+ call LDIRVM
+
+ ld hl, #0x4000 + 256 * 8 * 3
+ ld de, #0x2000
+ ld bc, #256 * 8
+ call LDIRVM
+
+ ld hl, #0x4000 + 256 * 8 * 4
+ ld de, #0x2000 + 256 * 8
+ ld bc, #256 * 8
+ call LDIRVM
+
+ ld hl, #0x4000 + 256 * 8 * 5
+ ld de, #0x2000 + 256 * 8 * 2
+ ld bc, #256 * 8
+ call LDIRVM
+
+ jp ENASCR
+
+load_custom_block:
+ call TAPION
+ jp c, tape_error
+
+ ld bc, #4
+ ld hl, #block_addr
+ call load_block
+
+ ld bc, (block_len)
+ ld hl, (block_addr)
+ call load_block
+
+ jp TAPIOF
+
+load_block:
+ ld a, r
+ and #15
+ out (#0x99), a
+ ld a, #0x87
+ nop
+ nop
+ out (#0x99), a
+
+ push bc
+ push hl
+ call TAPIN
+ pop hl
+ pop bc
+ jr c, tape_error
+
+ ld (hl), a
+ inc hl
+
+ dec bc
+ ld a, b
+ or c
+ jr nz, load_block
+
+ ld a, #1
+ out (#0x99), a
+ ld a, #0x87
+ nop
+ nop
+ out (#0x99), a
+ ret
+
+select_ram:
+ ld b, #4
+select_ram_loop:
+ out (#0xa8), a
+ ld (hl), a
+ cp (hl)
+ ret z
+ add a, c
+ djnz select_ram_loop
+
+memory_error:
+ ld hl, #mem_err_message
+ jp display_error
+
+tape_error:
+ ld hl, #tape_err_message
+
+display_error:
+ push hl
+ call TOTEXT
+
+ ld a, #1
+ ld (BAKCLR), a
+ ld (BDRCLR), a
+ ld a, #6
+ ld (FORCLR), a
+ call CHGCLR
+
+ pop hl
+print_loop:
+ ld a, (hl)
+ or a
+ jr z, halt0
+ inc hl
+ call CHPUT
+ jr print_loop
+halt0:
+ halt
+ jr halt0
+
+tape_err_message:
+ .str "TAPE READ ERROR"
+ .db 0
+mem_err_message:
+ .str "MEMORY INIT ERROR"
+ .db 0
+
+ .area _DATA
+
+block_addr:
+ .dw 2
+block_len:
+ .dw 2
+
diff --git a/game/src/crt0.z80 b/game/src/crt0.z80
index 81763d0..c8bd167 100644
--- a/game/src/crt0.z80
+++ b/game/src/crt0.z80
@@ -1,82 +1,87 @@
.module crt0
-.globl _main
-.area _HOME
-.area _CODE
-.area _INITIALIZER
+.globl _main
+
+.area _HOME
+.area _CODE
+.area _INITIALIZER
.area _GSINIT
.area _GSFINAL
-.area _DATA
-.area _INITIALIZED
-.area _BSEG
+.area _DATA
+.area _INITIALIZED
+.area _BSEG
.area _BSS
.area _HEAP
-.area _CODE
-
ENASLT = 0x0024
RSLREG = 0x0138
CLIKSW = 0xf3db
- ; ROM header
- .str "AB"
- .dw _main_init
- .db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+.area _CODE
+ ; ROM header
+ .str "AB"
+ .dw _main_init
+ .db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+
+ ; entry point skipping the init code that is used
+ ; when starting from cassette
+ jp _cas_main
_main_init::
- ; init the stack
- di
- ld sp, #0xf380
- ei
+ ; init the stack
+ di
+ ld sp, #0xf380
+ ei
- ; setup memory
- ; ref: https://www.msx.org/forum/msx-talk/development/memory-pages-again
+ ; setup memory
+ ; ref: https://www.msx.org/forum/msx-talk/development/memory-pages-again
- call RSLREG
- rrca
- rrca
- and #3
- ld c, a
- add a, #0xc1
- ld l, a
- ld h, #0xfc
- ld a, (hl)
- and #0x80
- or c
- ld c, a
- inc l
- inc l
- inc l
- inc l
- ld a, (hl)
- and #0x0c
- or c
- ld h, #0x80
- call ENASLT
+ call RSLREG
+ rrca
+ rrca
+ and #3
+ ld c, a
+ add a, #0xc1
+ ld l, a
+ ld h, #0xfc
+ ld a, (hl)
+ and #0x80
+ or c
+ ld c, a
+ inc l
+ inc l
+ inc l
+ inc l
+ ld a, (hl)
+ and #0x0c
+ or c
+ ld h, #0x80
+ call ENASLT
- ; disable key click sound
- xor a
- ld (CLIKSW), a
+_cas_main::
+ ; disable key click sound
+ xor a
+ ld (CLIKSW), a
- call gsinit
- call _main
+ call gsinit
+ call _main
halt0:
- halt
- jr halt0
+ halt
+ jr halt0
.area _GSINIT
gsinit::
- ld bc, #l__INITIALIZER
- ld a, b
- or a, c
- jr Z, gsinit_next
- ld de, #s__INITIALIZED
- ld hl, #s__INITIALIZER
- ldir
+ ld bc, #l__INITIALIZER
+ ld a, b
+ or a, c
+ jr Z, gsinit_next
+ ld de, #s__INITIALIZED
+ ld hl, #s__INITIALIZER
+ ldir
gsinit_next:
.area _GSFINAL
- ret
+ ret
diff --git a/tools/mkcas.py b/tools/mkcas.py
new file mode 100755
index 0000000..267270d
--- /dev/null
+++ b/tools/mkcas.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2022 by Juan J. Martinez <jjm@usebox.net>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+__version__ = "1.1"
+
+import os
+from argparse import ArgumentParser
+
+DEF_ADDR = 0x4000
+TYPES = ("binary", "basic", "ascii", "custom-header", "custom")
+TYPE_BLOCK = {
+ "binary": bytes((0xD0 for _ in range(10))),
+ "basic": bytes((0xD3 for _ in range(10))),
+ "ascii": bytes((0xEA for _ in range(10))),
+}
+
+BLOCK_ID = bytes((0x1F, 0xA6, 0xDE, 0xBA, 0xCC, 0x13, 0x7D, 0x74))
+
+
+def write_word(fd, word):
+ fd.write(bytes((word & 0xFF, word >> 8)))
+
+
+def auto_int(value):
+ return int(value, 0)
+
+
+def main():
+
+ parser = ArgumentParser(
+ description="Make a CAS file for the MSX",
+ epilog="Copyright (C) 2020-2022 Juan J Martinez <jjm@usebox.net>",
+ )
+
+ parser.add_argument(
+ "--version", action="version", version="%(prog)s " + __version__
+ )
+ parser.add_argument(
+ "-a",
+ "--add",
+ dest="add",
+ action="store_true",
+ help="append to the existing CAS file instead of creating a new one",
+ )
+ parser.add_argument(
+ "--name",
+ dest="name",
+ default=None,
+ type=str,
+ help="name to use for the file (limit 6 chars, defaults to the file name)",
+ )
+ parser.add_argument(
+ "--addr",
+ dest="addr",
+ default=DEF_ADDR,
+ type=auto_int,
+ help="address to load if binary file (default: 0x%04x)" % DEF_ADDR,
+ )
+ parser.add_argument(
+ "--exec",
+ dest="exec",
+ default=DEF_ADDR,
+ type=auto_int,
+ help="address to exec if binary file (default: 0x%04x)" % DEF_ADDR,
+ )
+
+ parser.add_argument("output", help="target .CAS file")
+ parser.add_argument("type", help="file type", choices=TYPES)
+ parser.add_argument("file", help="input file")
+
+ args = parser.parse_args()
+
+ if args.add:
+ flags = "ab"
+ else:
+ flags = "wb"
+
+ with open(args.output, flags) as out:
+
+ if args.name:
+ name = args.name
+ else:
+ name = os.path.basename(args.file)
+
+ with open(args.file, "rb") as inf:
+ data = inf.read()
+
+ out.write(BLOCK_ID)
+
+ if args.type == "ascii":
+
+ out.write(TYPE_BLOCK[args.type])
+ out.write(name[:6].ljust(6).encode("ascii"))
+
+ for b in range(0, len(data), 256):
+ out.write(BLOCK_ID)
+ out.write(data[b : b + 256])
+
+ padding = 256 - (len(data) % 256)
+ if padding == 256:
+ out.write(BLOCK_ID)
+ out.write(bytes((0x1A for _ in range(padding))))
+
+ elif args.type == "basic":
+
+ out.write(TYPE_BLOCK[args.type])
+ out.write(name[:6].ljust(6).encode("ascii"))
+ out.write(BLOCK_ID)
+ out.write(data)
+
+ elif args.type == "binary":
+
+ addr = args.addr
+ exec_addr = args.exec
+ end_addr = addr + len(data) - 1
+
+ if end_addr > 0xFFFF:
+ parser.error("Binary doesn't fit in memory")
+
+ out.write(TYPE_BLOCK[args.type])
+ out.write(name[:6].ljust(6).encode("ascii"))
+
+ out.write(BLOCK_ID)
+ write_word(out, addr)
+ write_word(out, end_addr)
+ write_word(out, exec_addr)
+
+ out.write(data)
+
+ elif args.type == "custom-header":
+
+ addr = args.addr
+ length = len(data)
+
+ write_word(out, addr)
+ write_word(out, length)
+ out.write(data)
+
+ else:
+ # custom
+ out.write(data)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/png2scr.py b/tools/png2scr.py
new file mode 100755
index 0000000..5d08915
--- /dev/null
+++ b/tools/png2scr.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2022 by Juan J. Martinez <jjm@usebox.net>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+__version__ = "1.0"
+
+import sys
+from argparse import ArgumentParser
+from PIL import Image
+from collections import defaultdict
+
+DEF_W = 8
+DEF_H = 8
+
+# TOSHIBA palette
+MSX_COLS = [
+ (255, 0, 255),
+ (0, 0, 0),
+ (102, 204, 102),
+ (136, 238, 136),
+ (68, 68, 221),
+ (119, 119, 255),
+ (187, 85, 85),
+ (119, 221, 221),
+ (221, 102, 102),
+ (255, 119, 119),
+ (204, 204, 85),
+ (238, 238, 136),
+ (85, 170, 85),
+ (187, 85, 187),
+ (204, 204, 204),
+ (238, 238, 238),
+]
+
+
+def main():
+
+ parser = ArgumentParser(
+ description="PNG to SCR file",
+ epilog="Copyright (C) 2018-2022 Juan J Martinez <jjm@usebox.net>",
+ )
+
+ parser.add_argument(
+ "--version", action="version", version="%(prog)s " + __version__
+ )
+
+ parser.add_argument("image", help="image to convert")
+
+ args = parser.parse_args()
+
+ try:
+ image = Image.open(args.image)
+ except IOError:
+ parser.error("failed to open the image")
+
+ if image.mode != "RGB":
+ parser.error("not a RGB image")
+
+ (w, h) = image.size
+
+ if w % 256 or h % 192:
+ parser.error("%s size is not 256x192" % args.image)
+
+ data = image.getdata()
+
+ color_idx = defaultdict(list)
+ color = []
+ out = []
+ ntiles = 0
+ for y in range(0, h, DEF_H):
+ for x in range(0, w, DEF_W):
+ # tile data
+ tile = [
+ data[x + i + ((y + j) * w)] for j in range(DEF_H) for i in range(DEF_W)
+ ]
+
+ # get the attibutes of the tile
+ # FIXME: this may not be right
+ for i in range(0, len(tile), DEF_W):
+ cols = list(sorted(set(tile[i : i + DEF_W])))
+
+ if len(cols) > 2:
+ parser.error(
+ "tile %d (%d, %d) has more than two colors: %r"
+ % (ntiles, x, y, cols)
+ )
+ elif len(cols) == 1:
+ cols.append(MSX_COLS[1])
+
+ for c in cols:
+ if c not in MSX_COLS:
+ parser.error(
+ "tile %d (%d, %d) has a color not in the MSX palette: %r"
+ % (ntiles, x, y, c)
+ )
+
+ # each tile has two coloributes per row
+ color_idx[ntiles * DEF_H + i // DEF_W] = cols
+ color.append((MSX_COLS.index(cols[1]) << 4) | MSX_COLS.index(cols[0]))
+
+ frame = []
+ for i in range(0, len(tile), 8):
+ byte = 0
+ p = 7
+ for k in range(8):
+ # 0 or 1 is determined by the order in the coloributes for that row
+ byte |= (
+ color_idx[ntiles * DEF_H + i // DEF_W].index(tile[i + k]) << p
+ )
+ p -= 1
+
+ frame.append(byte)
+ ntiles += 1
+ out.extend(frame)
+
+ sys.stdout.buffer.write(bytes(out))
+ sys.stdout.buffer.write(bytes(color))
+
+
+if __name__ == "__main__":
+ main()