diff --git a/.gitignore b/.gitignore index a40d59a..dc0e923 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,22 @@ oscar64/build/ __pycache__/ *.pyc +# python venvs +tools/img-convert/venv/ + +# mufflon source (not a submodule, from SVN repo) +mufflon/ + +# mufflon build output +build/mufflon + +# NUFLI generated files (BMP intermediates, .nuf output, result/error maps) +src/data/nufli/*.bmp +src/data/nufli/*.nuf + +# temp / comparison output +tmp/ + # editor / IDE .vscode/ .idea/ diff --git a/Makefile b/Makefile index 882389e..aba24d0 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) OSCAR64_DIR := $(ROOT)/oscar64 OSCAR64_BIN := $(OSCAR64_DIR)/bin/oscar64 BUILD_DIR := $(ROOT)/build +MUFFLON_DIR := $(ROOT)/mufflon +MUFFLON_BIN := $(BUILD_DIR)/mufflon +TOOLS_DIR := $(ROOT)/tools SRC := main.c PRG := $(BUILD_DIR)/nyuller.prg @@ -17,6 +20,10 @@ LOG := $(BUILD_DIR)/vice.log PID_FILE := $(BUILD_DIR)/vice.pid SRC_DIR := $(ROOT)/src +# --- NUFLI source images ----------------------------------- +NUFLI_SRC_DIR := $(ROOT)/source_images +NUFLI_OUT_DIR := $(SRC_DIR)/data/nufli + # --- oscar64 flags ---------------------------------------- # Override with: make OPT=O3 (or O0/O1/O2/Os/g) OPT ?= O1 @@ -31,7 +38,8 @@ HDR_FILES := $(wildcard $(SRC_DIR)/*.h) # --- phony targets ---------------------------------------- .PHONY: help compile run run-vice run-vice-cycle \ - play play-cycle kill clean ensure-build-dir ensure-oscar64 + play play-cycle kill clean ensure-build-dir ensure-oscar64 \ + ensure-mufflon nufli nufli-clean # ============================================================ # help (default) @@ -61,6 +69,10 @@ help: @echo " Clean" @echo " make clean remove build/ artifacts and VICE log/pid files" @echo "" + @echo " NUFLI images" + @echo " make nufli convert source PNGs → NUFLI assembly data" + @echo " make nufli-clean remove NUFLI generated files" + @echo "" @echo " Output (in build/)" @echo " nyuller.prg loadable C64 program" @echo " nyuller.d64 disk image wrapping nyuller.prg (for VICE autostart)" @@ -101,10 +113,74 @@ ensure-oscar64: exit 1; \ fi +# ============================================================ +# ensure-mufflon — download source + build if missing +# ============================================================ +ensure-mufflon: ensure-build-dir + @if [ ! -f "$(MUFFLON_DIR)/mufflon.c" ]; then \ + echo "mufflon source not found; downloading from CSDb..."; \ + mkdir -p "$(MUFFLON_DIR)"; \ + curl -sL "https://csdb.dk/getinternalfile.php/180585/Mufflon1.0-source+GUI+Bonus.zip" -o "$(BUILD_DIR)/mufflon.zip"; \ + cd "$(BUILD_DIR)" && unzip -qo mufflon.zip "Mufflon1.0-source+Bonus/mufflon-source+GUI/mufflon.c" "Mufflon1.0-source+Bonus/mufflon-source+GUI/mufflon.h"; \ + cp "$(BUILD_DIR)/Mufflon1.0-source+Bonus/mufflon-source+GUI/mufflon.c" "$(MUFFLON_DIR)/"; \ + cp "$(BUILD_DIR)/Mufflon1.0-source+Bonus/mufflon-source+GUI/mufflon.h" "$(MUFFLON_DIR)/"; \ + rm -rf "$(BUILD_DIR)/Mufflon1.0-source+Bonus" "$(BUILD_DIR)/mufflon.zip"; \ + fi + @if [ ! -f "$(MUFFLON_DIR)/mufflon.c" ]; then \ + echo "error: mufflon source download failed" >&2; \ + exit 1; \ + fi + @if [ ! -x "$(MUFFLON_BIN)" ]; then \ + echo "building mufflon -> $(MUFFLON_BIN)"; \ + cd "$(MUFFLON_DIR)" && gcc -o "$(MUFFLON_BIN)" -lm -O5 -ffast-math mufflon.c; \ + fi + @if [ ! -x "$(MUFFLON_BIN)" ]; then \ + echo "error: $(MUFFLON_BIN) is still missing after build" >&2; \ + exit 1; \ + fi + +# ============================================================ +# NUFLI image pipeline +# ============================================================ + +# Source screen images (PNG) +NUFLI_SCREENS := screen_title screen_waiting1 screen_waiting_2 screen_win_hare screen_win_scoot + +# Generated files +NUFLI_NUF_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .nuf,$(NUFLI_SCREENS))) +NUFLI_ASM_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .asm,$(NUFLI_SCREENS))) +NUFLI_HDR_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .h,$(NUFLI_SCREENS))) + +# Build all NUFLI images +nufli: $(NUFLI_ASM_FILES) $(NUFLI_HDR_FILES) + @echo "NUFLI images built in $(NUFLI_OUT_DIR)/" + +# Clean NUFLI artifacts +nufli-clean: + @rm -f $(NUFLI_OUT_DIR)/*.nuf $(NUFLI_OUT_DIR)/*.asm $(NUFLI_OUT_DIR)/*.h + @rm -f $(NUFLI_OUT_DIR)/*.bmp + @echo "cleaned NUFLI images" + +# PNG → BMP (Mufflon needs BMP input) +$(NUFLI_OUT_DIR)/%.bmp: $(NUFLI_SRC_DIR)/%.png | ensure-build-dir + @mkdir -p $(NUFLI_OUT_DIR) + @echo "converting $< -> $@" + @python3 -c "from PIL import Image; Image.open('$<').resize((320,200), Image.LANCZOS).save('$@')" + +# BMP → NUFLI (.nuf) +$(NUFLI_OUT_DIR)/%.nuf: $(NUFLI_OUT_DIR)/%.bmp | ensure-mufflon + @echo "converting $< -> $@" + @$(MUFFLON_BIN) $< -o $@ --shutup + +# NUFLI → Assembly (.asm + .h) +$(NUFLI_OUT_DIR)/%.asm $(NUFLI_OUT_DIR)/%.h: $(NUFLI_OUT_DIR)/%.nuf + @echo "converting $< -> $@ + $(basename $@).h" + @python3 $(TOOLS_DIR)/nuf_to_asm.py $< $(NUFLI_OUT_DIR)/$* + # ============================================================ # $(PRG) — compile main.c → nyuller.prg # ============================================================ -$(PRG): $(SRC_FILES) $(HDR_FILES) | ensure-build-dir ensure-oscar64 +$(PRG): $(SRC_FILES) $(HDR_FILES) $(NUFLI_ASM_FILES) | ensure-build-dir ensure-oscar64 @echo "compiling $(SRC) with $(OSCAR64_BIN) -> $(BUILD_DIR)/" cd "$(SRC_DIR)" && "$(OSCAR64_BIN)" -i="$(OSCAR64_DIR)/include" -o="$(PRG)" $(OPT_FLAGS) "$(SRC)" diff --git a/src/data/nufli/screen_title.h b/src/data/nufli/screen_title.h new file mode 100644 index 0000000..f9d2f2c --- /dev/null +++ b/src/data/nufli/screen_title.h @@ -0,0 +1,12 @@ +/* NUFLI image data - generated by nuf_to_asm.py */ +#ifndef SCREEN_TITLE_H +#define SCREEN_TITLE_H + +/* NUFLI data size: 23040 bytes */ +extern const unsigned char nufli_data[23040]; +extern const unsigned int nufli_size; + +/* Display NUFLI image */ +void nufli_display(void); + +#endif diff --git a/src/data/nufli/screen_waiting1.h b/src/data/nufli/screen_waiting1.h new file mode 100644 index 0000000..a74d486 --- /dev/null +++ b/src/data/nufli/screen_waiting1.h @@ -0,0 +1,12 @@ +/* NUFLI image data - generated by nuf_to_asm.py */ +#ifndef SCREEN_WAITING1_H +#define SCREEN_WAITING1_H + +/* NUFLI data size: 23040 bytes */ +extern const unsigned char nufli_data[23040]; +extern const unsigned int nufli_size; + +/* Display NUFLI image */ +void nufli_display(void); + +#endif diff --git a/src/data/nufli/screen_waiting_2.h b/src/data/nufli/screen_waiting_2.h new file mode 100644 index 0000000..9c221a7 --- /dev/null +++ b/src/data/nufli/screen_waiting_2.h @@ -0,0 +1,12 @@ +/* NUFLI image data - generated by nuf_to_asm.py */ +#ifndef SCREEN_WAITING_2_H +#define SCREEN_WAITING_2_H + +/* NUFLI data size: 23040 bytes */ +extern const unsigned char nufli_data[23040]; +extern const unsigned int nufli_size; + +/* Display NUFLI image */ +void nufli_display(void); + +#endif diff --git a/src/data/nufli/screen_win_hare.h b/src/data/nufli/screen_win_hare.h new file mode 100644 index 0000000..906d96b --- /dev/null +++ b/src/data/nufli/screen_win_hare.h @@ -0,0 +1,12 @@ +/* NUFLI image data - generated by nuf_to_asm.py */ +#ifndef SCREEN_WIN_HARE_H +#define SCREEN_WIN_HARE_H + +/* NUFLI data size: 23040 bytes */ +extern const unsigned char nufli_data[23040]; +extern const unsigned int nufli_size; + +/* Display NUFLI image */ +void nufli_display(void); + +#endif diff --git a/src/data/nufli/screen_win_scoot.h b/src/data/nufli/screen_win_scoot.h new file mode 100644 index 0000000..0e8daba --- /dev/null +++ b/src/data/nufli/screen_win_scoot.h @@ -0,0 +1,12 @@ +/* NUFLI image data - generated by nuf_to_asm.py */ +#ifndef SCREEN_WIN_SCOOT_H +#define SCREEN_WIN_SCOOT_H + +/* NUFLI data size: 23040 bytes */ +extern const unsigned char nufli_data[23040]; +extern const unsigned int nufli_size; + +/* Display NUFLI image */ +void nufli_display(void); + +#endif diff --git a/tasks.md b/tasks.md index 6479256..4b4cdfa 100644 --- a/tasks.md +++ b/tasks.md @@ -360,6 +360,73 @@ These items were done after Phase 8 was marked complete: --- +## NUFLI Integration Plan + +**Goal:** Replace the current multicolor bitmap screens with NUFLI format +for static screens (title, win, gameover). NUFLI provides 320×200 resolution +with ~10+ colors per block vs the current 160×200 with 4 colors per cell. + +### Architecture + +**Hybrid approach:** +- **Static screens** (title, win_hare, win_scoot, gameover): NUFLI format +- **Animated screens** (waiting1, waiting2, draw): Keep current multicolor bitmap + +**Why hybrid:** +- NUFLI consumes 100% CPU during display (no game logic possible) +- NUFLI uses all 8 sprites (no sprites for game objects) +- Static screens don't need game logic, so NUFLI is perfect + +### Implementation Steps + +#### Step 1: NUFLI display routine integration +- [ ] Create `src/nufli.h` with display function declarations +- [ ] Create `src/nufli.c` with display routine wrapper: + - `nufli_show(const unsigned char *data)` — loads data to $2000, calls SYS 12288 + - `nufli_exit()` — restores normal video mode +- [ ] Add `nufli_display.asm` to `src/` (6502 assembly for bank switching + JSR $3000) + +#### Step 2: Screen state machine updates +- [ ] Modify `game_enter_title()` to use NUFLI for title screen +- [ ] Modify `game_enter_win_p1/p2()` to use NUFLI for win screens +- [ ] Modify `game_enter_gameover()` to use NUFLI for gameover screen +- [ ] Keep existing multicolor for waiting/draw screens + +#### Step 3: Memory management +- [ ] Ensure NUFLI data ($2000-$7FFF) doesn't conflict with game code +- [ ] Verify screen RAM at $C400 doesn't overlap with NUFLI bitmap +- [ ] Test sprite pointer setup (NUFLI uses bank 3 sprites) + +#### Step 4: State transitions +- [ ] Implement `nufli_exit()` to restore VIC-II state before returning to game +- [ ] Ensure raster IRQ is re-enabled after NUFLI display +- [ ] Test TITLE→READY transition (NUFLI→multicolor) + +#### Step 5: Build pipeline integration +- [x] ✅ `make nufli` generates .asm files from source PNGs +- [x] ✅ NUFLI .asm files are build dependencies +- [ ] Add `#pragma embed` or linker includes for NUFLI data +- [ ] Verify total binary size fits in C64 memory + +### Memory Map (NUFLI mode) +``` +$2000-$7FFF: NUFLI data (bitmap + sprites + color tables) +$8000-$9FFF: Game code (oscar64 default) +$C000-$C3FF: Screen RAM (for multicolor screens) +$C400-$C7FF: Screen RAM (for NUFLI underlays) +$E000-$FFFF: Bitmap RAM (for multicolor screens) +``` + +### Verify +- `make nufli` generates all 5 screen .asm files +- `make compile` builds successfully with NUFLI data included +- `make run`: title screen displays in NUFLI quality (320×200, many colors) +- Press fire → transitions to multicolor waiting screen +- Win → displays NUFLI win screen +- Gameover → displays NUFLI gameover screen + +--- + ## Phase 10 — Code review fixes (pending) Findings from the second round of 4-agent parallel code review. diff --git a/tools/nuf_to_asm.py b/tools/nuf_to_asm.py new file mode 100644 index 0000000..6edf347 --- /dev/null +++ b/tools/nuf_to_asm.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Convert NUFLI .nuf binary to oscar64-compatible assembly/include files. + +Usage: + python3 nuf_to_asm.py input.nuf output_base + +Output files: + output_base.asm - Assembly data file + output_base.h - C header with extern declarations +""" + +import argparse +import sys +from pathlib import Path + + +def nuf_to_asm(nuf_data, output_base): + """Convert NUFLI binary to assembly data.""" + out_path = Path(output_base) + + # Skip 2-byte load address header + data = nuf_data[2:] + + # Assembly data file + with open(out_path.with_suffix('.asm'), 'w') as f: + f.write("; NUFLI image data - generated by nuf_to_asm.py\n") + f.write("; Load at $2000, display with SYS 12288 ($3000)\n") + f.write(f"; Total size: {len(data)} bytes\n\n") + f.write(".segment \"NUFLI_DATA\"\n\n") + + # Export the data + f.write(".export _nufli_data\n") + f.write(".export _nufli_size\n\n") + + f.write("_nufli_data:\n") + + # Write data in rows of 16 bytes + for i in range(0, len(data), 16): + chunk = data[i:i+16] + hex_bytes = ', '.join(f'${b:02x}' for b in chunk) + f.write(f" .byte {hex_bytes}\n") + + f.write(f"\n_nufli_size = {len(data)}\n") + + # C header file + with open(out_path.with_suffix('.h'), 'w') as f: + f.write("/* NUFLI image data - generated by nuf_to_asm.py */\n") + f.write(f"#ifndef {out_path.name.upper().replace('.', '_')}_H\n") + f.write(f"#define {out_path.name.upper().replace('.', '_')}_H\n\n") + f.write(f"/* NUFLI data size: {len(data)} bytes */\n") + f.write(f"extern const unsigned char nufli_data[{len(data)}];\n") + f.write(f"extern const unsigned int nufli_size;\n\n") + f.write("/* Display NUFLI image */\n") + f.write("void nufli_display(void);\n\n") + f.write("#endif\n") + + print(f"Wrote {out_path.with_suffix('.asm')} ({len(data)} bytes)") + print(f"Wrote {out_path.with_suffix('.h')}") + + +def main(argv=None): + ap = argparse.ArgumentParser( + description="Convert NUFLI .nuf binary to oscar64 assembly" + ) + ap.add_argument("input", help="Input .nuf file") + ap.add_argument("output_base", help="Output base path (no extension)") + args = ap.parse_args(argv) + + in_path = Path(args.input) + if not in_path.exists(): + print(f"Error: {in_path} not found", file=sys.stderr) + sys.exit(1) + + data = in_path.read_bytes() + nuf_to_asm(data, args.output_base) + + +if __name__ == "__main__": + main()