From 461d000c13cd995e30cde1e9dadd32e273bb2cb6 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Fri, 28 Aug 2026 11:30:42 +0200 Subject: [PATCH 1/4] 6502: added USTY chunk with parsed style information - parse LANG and issue warnings on improper styles - rewrite LANG -> USTY for apple2, c64, aambox - -iftf-sys-* platform overrides - foreground text color (c64) - warning options (see usage for flags) --- readme.txt | 5 + src/6502/Makefile | 6 +- src/6502/a2_frontend.s | 1 + src/6502/aambox_frontend.s | 1 + src/6502/c64_frontend.s | 11 + src/6502/engine.s | 604 ++++---------------------------- src/Makefile | 8 +- src/aambundle.c | 318 ++++++++++++++--- src/aambundle.h | 87 ++++- src/aamshow.c | 148 +++++++- src/aavm.h | 22 ++ src/bundle_6502.c | 18 + src/bundle_apple2.c | 52 +-- src/bundle_c64.c | 39 +-- src/bundle_web.c | 16 +- src/gen_usty.c | 683 +++++++++++++++++++++++++++++++++++++ src/js/nodefrontend.js | 2 +- test/common.mk | 10 +- 18 files changed, 1367 insertions(+), 664 deletions(-) create mode 100644 src/bundle_6502.c create mode 100644 src/gen_usty.c diff --git a/readme.txt b/readme.txt index 8ecf268..24cdaf1 100644 --- a/readme.txt +++ b/readme.txt @@ -129,6 +129,11 @@ Project website: Release notes: + 1.0.4: + + Aambundle now replaces the LOOK chunk with an internal USTY + chunk for 6502 targets. Use --help-all to see new warnings. + 1.0.3: New Apple II interpreter: diff --git a/src/6502/Makefile b/src/6502/Makefile index 0d3edec..1c4cff0 100644 --- a/src/6502/Makefile +++ b/src/6502/Makefile @@ -1,7 +1,7 @@ VERSION=1.0.4 CFLAGS=-Wall -O2 -DVERSION=\"$(VERSION)\" -all: aambox6502 aambox_frontend.bin c64_crunched.bin c64_drivecode.bin c64_loader.prg a2.system a2_0boot.bin a2_sboot.bin +all: aambox6502 aambox_frontend.bin c64_crunched.bin c64_drivecode.bin c64_loader.prg a2.system a2_0boot.bin a2_sboot.bin check_xa: @if ! command -v xa >/dev/null 2>&1; then \ @@ -20,7 +20,7 @@ check_acme: exit 1; fi clean: - rm -rf aambox6502 cruncher labels c64.labels mkfont + rm -rf aambox6502 aambox_frontend.bin cruncher labels c64.labels mkfont rm -f a2_frontend.bin a2_prorwts2.bin a2.labels a2.system a2_0boot.bin a2_sboot.bin a2_sboot.labels mkfont: mkfont.c @@ -35,7 +35,7 @@ aambox_frontend.bin: aambox_frontend.s engine.s | check_xa xa -l aambox.labels -o $@ $< -DRAMTOP=49152 c64_frontend.bin: c64_frontend.s engine.s font.bin | check_xa - xa -l c64.labels -o $@ $< -DVERSION=\"$(VERSION)\" + xa -l c64.labels -P c64_frontend.lst -o $@ $< -DVERSION=\"$(VERSION)\" font.bin: fontdef.txt mkfont ./mkfont <$< >$@ diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index fe583c5..c69ee2c 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -109,6 +109,7 @@ HAVE_STATUS = 1 HAVE_STYLE = 0 ; reverse alone is not enough to qualify SAVERESTORE = 1 UNDO = 1 +FGCOLOR = 0 TRACE_INST = 0 TRACE_STORE = 0 diff --git a/src/6502/aambox_frontend.s b/src/6502/aambox_frontend.s index 68125c4..d54dfcb 100644 --- a/src/6502/aambox_frontend.s +++ b/src/6502/aambox_frontend.s @@ -19,6 +19,7 @@ PRSHIFT = 0 HAVE_QUIT = 1 HAVE_STATUS = 0 HAVE_STYLE = 0 +FGCOLOR = 0 wrappos = $00 xpos = $01 diff --git a/src/6502/c64_frontend.s b/src/6502/c64_frontend.s index 333937f..e5eff08 100644 --- a/src/6502/c64_frontend.s +++ b/src/6502/c64_frontend.s @@ -22,6 +22,7 @@ TRACE_STORE = 0 MEASURE_TIME = 0 UNDO = 1 SAVERESTORE = 1 +FGCOLOR = 1 DEFWIDTH = 40 PREXTRA = 8 @@ -510,8 +511,11 @@ io_mstyle ; 1 reverse, 2 bold, 4 italic ; Or on C64: ; 1 warm, 2 light, 4 blue + ; input x = fg color or $80 if unset .( + cpx #0 + bpl color_override pha jsr io_mflush pla @@ -520,6 +524,13 @@ io_mstyle lda palette,x sta currfg rts +color_override + txa + pha + jsr io_mflush + pla + sta currfg + rts palette .byt $0 ; normal = black .byt $1 ; reverse = white (warm) diff --git a/src/6502/engine.s b/src/6502/engine.s index d728bde..e680963 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -101,6 +101,9 @@ ramsz = $ce ; word, b-e zporg = $d0 ; 24 bytes of code +#if FGCOLOR +rfgcol = $f2 ; current fg color +#endif inittmp = $f3 ; word, b-e temp = $f5 pcmsb = $f6 @@ -128,8 +131,12 @@ chnklsb = HEAPEND+$0d0+0*CH_N chnkssb = HEAPEND+$0d0+1*CH_N chnkmsb = HEAPEND+$0d0+2*CH_N -databuf = HEAPEND+$0f0 ; 8 bytes -filesz = HEAPEND+$0f8 ; 3 bytes, b-e +; placed relative to the chunk table so that +; adding a chunk slot cannot silently +; overlap them; there are 48 bytes here and +; the three uses need 3*CH_N + 11. +databuf = HEAPEND+$0d0+3*CH_N ; 8 bytes +filesz = databuf+8 ; 3 bytes, b-e ; for each physical page, what ; virtual page owns it (initialized @@ -166,6 +173,7 @@ STY_MBOTTOM = 3 STY_STYON = 4 STY_STYOFF = 5 STY_FLAGS = 6 +STY_FG = 7 ; $80 = inherit STYF_RELW = $01 STYF_RELH = $02 @@ -534,6 +542,10 @@ unstyle lda stflag bne skip +#if FGCOLOR + lda #$80 + sta rfgcol +#endif lda #0 sta rstyle @@ -565,12 +577,22 @@ loop ldy #STY_STYON ora (phydata),y sta rstyle +#if FGCOLOR + ldy #STY_FG + lda (phydata),y + bmi nofgcol + sta rfgcol +nofgcol +#endif inx inx jmp loop done lda rstyle +#if FGCOLOR + ldx rfgcol +#endif jmp io_mstyle skip rts @@ -684,6 +706,10 @@ clrdone restartvm .( +#if FGCOLOR + lda #$80 + sta rfgcol +#endif lda #0 sta stflag sta rupper @@ -7801,6 +7827,9 @@ op_style lda operlsb+0 ora rstyle sta rstyle +#if FGCOLOR + ldx rfgcol +#endif jsr io_mstyle lda #SPC_SPACE sta rspc @@ -7812,6 +7841,9 @@ off eor #$ff and rstyle sta rstyle +#if FGCOLOR + ldx rfgcol +#endif jsr io_mstyle jmp ldyfetchnext .) @@ -9901,380 +9933,41 @@ done rts initengine4 - ; LOOK. - - .( - lda freeptr - sec - sbc #8 - sta phytmp - lda freeptr+1 - sbc #0 - sta phytmp+1 - - lda chnklsb+CH_LOOK + ; Read the style table from the USTY chunk. + ; The header is: + ; 0 tag 1 nclass + ; 2 nxsty 3 reserved + ; 4 totalwords (b-e) + ; 6 xstyoff (b-e) + ; followed by: + ; records[nclass * 8] + ; padded to totalwords*2 bytes. + + lda chnklsb+CH_USTY sta virdata+2 - lda chnkssb+CH_LOOK + lda chnkssb+CH_USTY sta virdata+1 - lda chnkmsb+CH_LOOK + lda chnkmsb+CH_USTY sta virdata+0 - lda #2 - jsr readdata - lda #0 - sta temp - lda databuf+1 - sta count - asl - rol temp - asl - rol temp - tax - ldy temp - jsr allocwords + lda #8 + jsr readdata ; read the header + + ldx databuf+5 ; totalwords + ldy databuf+4 + jsr allocwords ; allocate memory stx stybase sty stybase+1 -classloop - lda #0 - sta temp - - lda count - bne notdone + stx phydata + sty phydata+1 - jmp classdone -notdone + lda databuf+5 asl - rol temp - ;clc - adc chnklsb+CH_LOOK - sta virdata+2 - lda temp - adc chnkssb+CH_LOOK - sta virdata+1 - lda chnkmsb+CH_LOOK - adc #0 - sta virdata+0 - lda #2 - jsr readdata - - lda databuf+1 - clc - adc chnklsb+CH_LOOK - sta virdata+2 - lda databuf - adc chnkssb+CH_LOOK - sta virdata+1 - lda chnkmsb+CH_LOOK - adc #0 - sta virdata+0 - - ldy #7 - lda #0 -clrloop - sta (phytmp),y - dey - bpl clrloop -attrsloop - lda #1 - jsr readdata - lda databuf - bne attrsnotdone - - jmp attrsdone -attrsnotdone - ldx #0 -attrloop - cmp #$41 - bcc nocase - - cmp #$5b - bcs nocase - - eor #$20 -nocase - sta inpbuf,x - inx - txa - pha - lda #1 - jsr readdata - pla - tax - lda databuf - bne attrloop - - sta inpbuf,x -#if 0 - ldx #0 -ploop - lda inpbuf,x - beq pdone - - txa - pha - lda inpbuf,x - jsr vio_putc - pla - tax - inx - jmp ploop -pdone - lda #SPC_AUTO - sta rspc - jsr vio_line -#endif - ldy #0 - sty operlsb+0 -matchloop - ldx #0 -cmploop - lda csskeywords,y - beq cmpend - - cmp inpbuf,x - bne matchnext - - inx - iny - jmp cmploop -cmpskip1 - inx -cmpend - lda inpbuf,x - cmp #$20 - beq cmpskip1 - - cmp #$3a - beq matchfound - - dey -matchnext - iny - lda csskeywords,y - bne matchnext - - iny - inc operlsb+0 - lda operlsb - cmp #CSS_N - bcc matchloop - - jmp attrsloop -matchfound -cmpskip2 - inx - lda inpbuf,x - cmp #$20 - beq cmpskip2 - - ldy operlsb - bne nowidth - - jsr css_abs_rel - ldy #STY_WIDTH - sta (phytmp),y - bcc norelw - - ldy #STY_FLAGS - lda (phytmp),y - ora #STYF_RELW - sta (phytmp),y -norelw - jmp attrsloop -nowidth - dey - bne noheight - - jsr css_abs_rel - ldy #STY_HEIGHT - sta (phytmp),y - bcc norelh - - ldy #STY_FLAGS - lda (phytmp),y - ora #STYF_RELH - sta (phytmp),y -norelh - jmp attrsloop -noheight - dey - bne nofloat - - ldy #cssparam_left - jsr css_check_param - bcc noleft - - ldy #STY_FLAGS - lda (phytmp),y - ora #STYF_FLOATL - sta (phytmp),y - jmp attrsloop -noleft - ldy #cssparam_right - jsr css_check_param - bcc noright - - ldy #STY_FLAGS - lda (phytmp),y - ora #STYF_FLOATR - sta (phytmp),y -noright - jmp attrsloop -nofloat - dey - bne nofstyle - - ldy #cssparam_italic - jsr css_check_param - bcs italic - - ldy #cssparam_oblique - jsr css_check_param - bcc noitalic -italic - ldy #STY_STYON - lda (phytmp),y - ora #4 - sta (phytmp),y - jmp attrsloop -noitalic - ldy #cssparam_normal - jsr css_check_param - bcc nounitalic - - ldy #STY_STYOFF - lda (phytmp),y - ora #4 - sta (phytmp),y -nounitalic - jmp attrsloop -nofstyle - dey - bne nofweight - - ldy #cssparam_bold - jsr css_check_param - bcc nobold - - ldy #STY_STYON - lda (phytmp),y - ora #2 - sta (phytmp),y - jmp attrsloop -nobold - ldy #cssparam_normal - jsr css_check_param - bcc nounbold - - ldy #STY_STYOFF - lda (phytmp),y - ora #2 - sta (phytmp),y -nounbold - jmp attrsloop -nofweight - dey - bne noffamily -fixedloop1 - ldy #0 -fixedloop2 - lda inpbuf,x - beq nofixed - - cmp css_monospace,y - beq fixednext - - cmp #'m' - beq fixedloop1 - - inx - jmp fixedloop1 -fixednext - inx - iny - cpy #9 - bne fixedloop2 - - ldy #STY_STYON - lda (phytmp),y - ora #8 - sta (phytmp),y -nofixed - jmp attrsloop -noffamily - dey - bne noifrev - - ldy #cssparam_reverse - jsr css_check_param - bcc noreverse - - ldy #STY_STYON - lda (phytmp),y - ora #1 - sta (phytmp),y - jmp attrsloop -noreverse - ldy #cssparam_none - jsr css_check_param - bcc nounreverse - - ldy #STY_STYOFF - lda (phytmp),y - ora #1 - sta (phytmp),y -nounreverse - jmp attrsloop -noifrev - dey - bne nomtop - - jsr css_abs_rel - bcs badmtop - - ldy #STY_MTOP - sta (phytmp),y -badmtop - jmp attrsloop -nomtop - dey - bne nombottom - - jsr css_abs_rel - bcs badmbtm - - ldy #STY_MBOTTOM - sta (phytmp),y -badmbtm -nombottom - jmp attrsloop -attrsdone -#if 0 - ldy #0 -dumploop - lda (phytmp),y - jsr puthex - iny - cpy #8 - bne dumploop - - lda #SPC_AUTO - sta rspc - jsr vio_line -#endif - dec count - lda phytmp - sec - sbc #8 - sta phytmp - bcs noc1 - - dec phytmp+1 -noc1 - jmp classloop -classdone - .) + sta physize + lda databuf+4 + rol + sta physize+1 + jsr readdatato ; read the data - .( lda chnklsb+CH_CODE sec sbc #1 @@ -10285,129 +9978,10 @@ classdone lda chnkmsb+CH_CODE sbc #0 sta codeseg+0 - .) rts -css_abs_rel - ; input inpbuf = data - ; input x = data offset - ; output a = value - ; output c = relative - ; returns absolute 0 on error - - .( - lda #0 - sta quot - sta quot+1 -digloop - lda inpbuf,x - cmp #$30 - bcc nodig - - cmp #$3a - bcs nodig - - and #$0f - pha - lda #0 - sta denom+1 - lda #10 - sta denom - jsr mul16 - pla - clc - adc numer - sta quot - lda numer+1 - adc #0 - sta quot+1 - inx - jmp digloop -skip1 - inx - lda inpbuf,x - cmp #$30 - bcc nodig2 - - cmp #$3a - bcc skip1 -nodig - cmp #$2e - beq skip1 -nodig2 - cmp #'e' - beq got_e - - cmp #'c' - beq got_c - - cmp #$25 - bne noparse - - lda quot - ;sec - rts -got_e - lda inpbuf+1,x - cmp #'m' - beq gotabs - - cmp #'n' - beq gotabs - - jmp noparse -got_c - lda inpbuf+1,x - cmp #'h' - bne noparse -gotabs - lda quot - clc - rts -noparse - lda #0 - clc - rts - .) - -css_check_param - ; input inpbuf = input - ; input x = input offset - ; input y = keyword offset - ; output c = match - ; preserves x - - .( - txa - pha -loop - lda cssparams,y - beq end - - cmp inpbuf,x - bne fail - - inx - iny - jmp loop -skip - inx -end - lda inpbuf,x - cmp #$20 - beq skip - - cmp #0 - beq succeed -fail - clc -succeed - pla - tax - rts - .) - +;; list of chunks we can find CH_CODE = 0 CH_LANG = 1 CH_META = 2 @@ -10416,56 +9990,16 @@ CH_WRIT = 4 CH_TAGS = 5 CH_DICT = 6 CH_MAPS = 7 -CH_LOOK = 8 +CH_USTY = 8 CH_URLS = 9 CH_N = 10 chunknames - .byt "CLMIWTDMLU" - .byt "OAENRAIAOR" - .byt "DNTIIGCPOL" - .byt "EGATTSTSKS" - -CSS_WIDTH = 0 -CSS_HEIGHT = 1 -CSS_FLOAT = 2 -CSS_FONTSTYLE = 3 -CSS_FONTWEIGHT = 4 -CSS_FONTFAMILY = 5 -CSS_REVERSE = 6 -CSS_MARGINTOP = 7 -CSS_MARGINBTM = 8 -CSS_N = 9 -csskeywords - .byt "width",0 - .byt "height",0 - .byt "float",0 - .byt "font-style",0 - .byt "font-weight",0 - .byt "font-family",0 - .byt "-iftf-reverse-video",0 - .byt "margin-top",0 - .byt "margin-bottom",0 - -cssparams -cssparam_left = * - cssparams - .byt "left",0 -cssparam_right = * - cssparams - .byt "right",0 -cssparam_italic = * - cssparams - .byt "italic",0 -cssparam_oblique = * - cssparams - .byt "oblique",0 -cssparam_normal = * - cssparams - .byt "normal",0 -cssparam_bold = * - cssparams - .byt "bold",0 -cssparam_reverse = * - cssparams - .byt "reverse",0 -cssparam_none = * - cssparams - .byt "none",0 - -css_monospace - .byt "monospace" + .byt "CLMIWTDMUU" + .byt "OAENRAIASR" + .byt "DNTIIGCPTL" + .byt "EGATTSTSYS" + +;; allocwords ; input x = size lsb diff --git a/src/Makefile b/src/Makefile index 9985152..4f19bf5 100644 --- a/src/Makefile +++ b/src/Makefile @@ -52,7 +52,7 @@ distclean: clean uninstall aamshow: aamshow.c crc32.c aavm.c aavm.h crc32.h ${CC} ${CFLAGS} -o $@ aamshow.c crc32.c aavm.c -aambundle: aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c \ +aambundle: aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c bundle_6502.c gen_usty.c crc32.c \ table_a2terp.h \ table_a20boot.h \ table_a2sboot.h \ @@ -68,7 +68,7 @@ aambundle: aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c \ table_jquery.h \ table_play.h \ tables_6502font.h - ${CC} ${CFLAGS} -o $@ aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c + ${CC} ${CFLAGS} -o $@ aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c bundle_6502.c gen_usty.c crc32.c aamrun.linux: js/nodefrontend.js js/engine.js pkg -t linux -o aamrun.linux js/nodefrontend.js @@ -85,7 +85,7 @@ aamrun.exe: js/nodefrontend.js js/engine.js aamshow.exe: aamshow.c crc32.c aavm.c aavm.h crc32.h ${MINGW32} ${CFLAGS} -o $@ aamshow.c crc32.c aavm.c -aambundle.exe: aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c \ +aambundle.exe: aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c bundle_6502.c gen_usty.c crc32.c \ table_a2terp.h \ table_a20boot.h \ table_a2sboot.h \ @@ -101,7 +101,7 @@ aambundle.exe: aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c \ table_jquery.h \ table_play.h \ tables_6502font.h - ${MINGW32} ${CFLAGS} -o $@ aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c + ${MINGW32} ${CFLAGS} -o $@ aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c bundle_6502.c gen_usty.c crc32.c # Fall back rules when you make a blob from here # e.g. "make -C src aambundle" redirects to "make -C 6502" diff --git a/src/aambundle.c b/src/aambundle.c index 60160ff..fea392f 100644 --- a/src/aambundle.c +++ b/src/aambundle.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,53 @@ uint32_t storysize; static char *dirname; static char *storyfile; +int charset_warning_level = WARN_DEFAULT; +int input_warning_level = WARN_DEFAULT; +int style_warning_level = WARN_DEFAULT; + +int nwarning; +static int warnings_as_errors; +static int show_all_help; + +static unsigned int warned_mask; + +static const struct { + const char *category; + int *level; +} warn_info[WARN_COUNT] = { + {"error", NULL}, + {"charset", &charset_warning_level}, + {"input", &input_warning_level}, + {"style", &style_warning_level} +}; + +void vwarning(warn_id_t id, const char *fmt, va_list ap) { + char msg[1024]; + + // Did we turn this warning off? + if(id < WARN_COUNT && warn_info[id].level && *warn_info[id].level == WARN_NEVER) return; + + vsnprintf(msg, sizeof(msg), fmt, ap); + + fprintf(stderr, "%s %s\n", id==WARN_ERROR || warnings_as_errors ? "Error:" : "Warning:", msg); + if(id < WARN_COUNT && warn_info[id].level && *warn_info[id].level != WARN_ALWAYS) { + // Give a hint for the disable warning option the first time. + if(!(warned_mask & (1u << id))) { + fprintf(stderr, "(Use --no-warn-%s to disable %s warnings.)\n", + warn_info[id].category, warn_info[id].category); + warned_mask |= (1u << id); + } + } + nwarning++; +} + +void warning(warn_id_t id, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vwarning(id, fmt, ap); + va_end(ap); +} + static int append_name(char *storyname, int storynamesize, int snamelen, char ch) { if(snamelen < storynamesize - 1) { if((ch >= 'a' && ch <= 'z') @@ -94,7 +142,7 @@ void visit_chunks(char *storyname, int storynamesize, chunk_visitor_t chunk_visi } } } - + if(chunk_visitor) { // Backend-specific chunk handling chunk_visitor(head, dirname, chunk, size); } @@ -157,12 +205,12 @@ void warn_about_nonascii(uint8_t *dict, uint32_t dictsize, uint8_t *lang, uint32 exttable++; // Skip past number of extended characters uint32_t unichar; uint8_t aachar; - + for(uint16_t i = 0; i < nword; i++) { pointer = 2 + 3*i; // 2 bytes for number of words, then each word is 1 byte length, 2 bytes starting position wordlength = dict[pointer]; wordstart = (dict[pointer+1] << 8) | dict[pointer+2]; - + for(uint32_t j = wordstart; j < wordstart+wordlength; j++) { if(dict[j] > 0x7f) { // Problem! // We need to figure out what this character actually *is* to report it @@ -172,7 +220,7 @@ void warn_about_nonascii(uint8_t *dict, uint32_t dictsize, uint8_t *lang, uint32 (lang[exttable+5*aachar+3] << 8) | (lang[exttable+5*aachar+4]) ); - fprintf(stderr, "Warning: Extended character %d (%s, U+%04x) found in dictionary word '%s'. This word will not be recognized in user input.\n", + warning(WARN_INPUT, "Extended character %d (%s, U+%04x) found in dictionary word '%s'. This word will not be recognized in user input.", dict[j], unicode_to_utf8(unichar), unichar, @@ -183,55 +231,196 @@ void warn_about_nonascii(uint8_t *dict, uint32_t dictsize, uint8_t *lang, uint32 } } -void trim_chunks(int align_writ) { - uint32_t src = 12, dest = 12, size; - uint8_t *chunk; - char head[5]; +static uint32_t chunk_size(const uint8_t *chunk) { + return + (chunk[4] << 24) | + (chunk[5] << 16) | + (chunk[6] << 8) | + (chunk[7] << 0); +} + +uint8_t *find_chunk(const char *id, uint32_t *sizep) { + uint32_t pos = 12, size; + + while(pos + 8 <= storysize) { + size = chunk_size(story + pos); + if(pos + 8 + size > storysize) break; + if(!memcmp(story + pos, id, 4)) { + if(sizep) *sizep = size; + return story + pos + 8; + } + pos += (8 + size + 1) & ~1; + } + + return 0; +} + +// The output buffer that rewrite_chunks() assembles the new story in. +// It replaces the story buffer at the end of the pass, so it grows on demand. + +static uint8_t *out; +static uint32_t outsize, outalloc; + +static void out_reserve(uint32_t n) { + if(outsize + n > outalloc) { + outalloc = 2 * (outsize + n) + 0x1000; + out = realloc(out, outalloc); + if(!out) { + warning(WARN_ERROR, "Out of memory."); + exit(1); + } + } +} + +static void emit_chunk(const char *id, const uint8_t *data, uint32_t size) { + uint32_t total = (8 + size + 1) & ~1; + + out_reserve(total); + memcpy(out + outsize, id, 4); + out[outsize + 4] = (size >> 24) & 0xff; + out[outsize + 5] = (size >> 16) & 0xff; + out[outsize + 6] = (size >> 8) & 0xff; + out[outsize + 7] = (size >> 0) & 0xff; + memcpy(out + outsize + 8, data, size); + if(total > 8 + size) { + out[outsize + 8 + size] = 0; + } + outsize += total; +} + +// The padding is a " " chunk +static void emit_padding(uint32_t pad) { + assert(pad >= 8); + out_reserve(pad); + memcpy(out + outsize, " ", 4); + out[outsize + 4] = 0; + out[outsize + 5] = 0; + out[outsize + 6] = ((pad - 8) >> 8) & 0xff; + out[outsize + 7] = ((pad - 8) >> 0) & 0xff; + memset(out + outsize + 8, 0, pad - 8); + outsize += pad; +} + + +void rewrite_chunks(chunk_rewriter_t rewriter, int align_writ) { + uint32_t pos = 12, size, newsize; + uint8_t *chunk, *newdata; + char head[5], newid[5]; + chunk_action_t action; int pad; - while(src < storysize) { - chunk = story + src; + out = 0; + outalloc = 0; + outsize = 0; + out_reserve(12); + memcpy(out, story, 12); + outsize = 12; + + while(pos < storysize) { + chunk = story + pos; memcpy(head, chunk, 4); head[4] = 0; - size = - (chunk[4] << 24) | - (chunk[5] << 16) | - (chunk[6] << 8) | - (chunk[7] << 0); - size = (8 + size + 1) & ~1; - if(align_writ && !strcmp(head, "WRIT")) { - pad = 0x100 - (dest & 0xff); - assert(!(pad & 1)); - if(pad < 0xf0) { - if(pad < 8) pad += 0x100; - memmove(story + src + pad, story + src, storysize - src); - storysize += pad; - memcpy(chunk, " ", 4); - chunk[4] = 0; - chunk[5] = 0; - chunk[6] = (pad - 8) >> 8; - chunk[7] = (pad - 8) & 0xff; - memset(chunk + 8, 0, pad - 8); - size = pad; - } + size = chunk_size(chunk); + + memcpy(newid, head, 5); + newdata = chunk + 8; + newsize = size; + action = rewriter + ? rewriter(head, chunk + 8, size, newid, &newdata, &newsize) + : CHUNK_KEEP; + if(action == CHUNK_INSERT) { + // Emit a new chunk before the current one, then keep + // the current chunk as it is. newid/newdata/newsize + // hold the inserted chunk; they are copied into the + // output buffer by emit_chunk() before we fall through. + emit_chunk(newid, newdata, newsize); + memcpy(newid, head, 5); + newdata = chunk + 8; + newsize = size; + action = CHUNK_KEEP; + } + if(action != CHUNK_REPLACE) { + memcpy(newid, head, 5); + newdata = chunk + 8; + newsize = size; } - if(strcmp(head, "FILE")) { - if(dest != src) { - memmove(story + dest, story + src, size); + + if(action != CHUNK_DROP) { + if(align_writ && !memcmp(newid, "WRIT", 4)) { + // The 6502 engine expects the WRIT chunk to be page-aligned + pad = 0x100 - (outsize & 0xff); + assert(!(pad & 1)); + + // Only align if we can write at least 8 bytes + if(pad < 0xf0) { + if(pad < 8) pad += 0x100; + emit_padding(pad); + } } - dest += size; + emit_chunk(newid, newdata, newsize); } - src += size; + + pos += (8 + size + 1) & ~1; } - storysize = dest; + free(story); + story = out; + storysize = outsize; + out = 0; + outalloc = 0; + outsize = 0; + story[4] = ((storysize - 8) >> 24) & 0xff; story[5] = ((storysize - 8) >> 16) & 0xff; story[6] = ((storysize - 8) >> 8) & 0xff; story[7] = ((storysize - 8) >> 0) & 0xff; } -void usage(char *prgname) { +/* Writes data, then pads the file with zeros up to a multiple of padto + * bytes (padto = 1 means no padding). dirname may be NULL, in which + * case name is taken as-is relative to the current directory. */ +void writefile_padded(char *dirname, char *name, const uint8_t *data, size_t size, size_t padto) { + char *filename; + FILE *f; + size_t npad; + + if(dirname) { + filename = malloc(strlen(dirname) + strlen(name) + 2); + sprintf(filename, "%s/%s", dirname, name); + } else { + filename = malloc(strlen(name) + 1); + strcpy(filename, name); + } + + f = fopen(filename, "wb"); + if(!f) { + fprintf(stderr, "%s: %s\n", filename, strerror(errno)); + exit(1); + } + if(size != fwrite(data, 1, size, f)) { + fprintf(stderr, "%s: %s\n", filename, strerror(errno)); + exit(1); + } + npad = (padto - size % padto) % padto; + while(npad--) { + if(EOF == fputc(0, f)) { + fprintf(stderr, "%s: %s\n", filename, strerror(errno)); + exit(1); + } + size++; + } + fclose(f); + + printf("%-14s %7lu bytes\n", name, (unsigned long) size); + + free(filename); +} + +void writefile(char *dirname, char *name, const uint8_t *data, size_t size) { + writefile_padded(dirname, name, data, size, 1); +} + +void usage(char *prgname, int all) { fprintf(stderr, "Aa-machine tools " VERSION "\n"); fprintf(stderr, "Copyright 2019-2026 Linus Akesson and the Dialog Project contributors.\n"); fprintf(stderr, "\n"); @@ -241,24 +430,47 @@ void usage(char *prgname) { fprintf(stderr, "\n"); fprintf(stderr, "--version -V Display the program version.\n"); fprintf(stderr, "--help -h Display this information.\n"); + fprintf(stderr, "--help-all Display all options, including warnings.\n"); fprintf(stderr, "\n"); fprintf(stderr, "--output -o Set output directory/file name.\n"); fprintf(stderr, "--target -t Select target (web, c64, apple2, web:story).\n"); + if(all) { + fprintf(stderr, "\n"); + fprintf(stderr, "Warning options:\n"); + fprintf(stderr, "--warn-charset Always warn about codepoints the target cannot render.\n"); + fprintf(stderr, "--no-warn-charset Never warn about codepoints the target cannot render.\n"); + fprintf(stderr, "--warn-input Always warn about words the target cannot type.\n"); + fprintf(stderr, "--no-warn-input Never warn about words the target cannot type.\n"); + fprintf(stderr, "--warn-style Always warn about styles the target cannot support.\n"); + fprintf(stderr, "--no-warn-style Never warn about styles the target cannot support.\n"); + fprintf(stderr, "--warnings-as-errors Exit with a failure status if anything warned.\n"); + } fprintf(stderr, "\n"); fprintf(stderr, "Targets:\n"); fprintf(stderr, "web (default) Directory with web interpreter.\n"); fprintf(stderr, "c64 Directory with c64 disk image.\n"); fprintf(stderr, "apple2 Directory with apple2 disk images.\n"); fprintf(stderr, "web:story Just story.js for the web interpreter.\n"); + if(all) { + fprintf(stderr, "aambox Rewrite to .ustory format for the aambox test platform.\n"); + } exit(1); } int main(int argc, char **argv) { struct option longopts[] = { {"help", 0, 0, 'h'}, + {"help-all", 0, &show_all_help, 1}, {"version", 0, 0, 'V'}, {"output", 1, 0, 'o'}, {"target", 1, 0, 't'}, + {"warn-charset", 0, &charset_warning_level, WARN_ALWAYS}, + {"no-warn-charset", 0, &charset_warning_level, WARN_NEVER}, + {"warn-input", 0, &input_warning_level, WARN_ALWAYS}, + {"no-warn-input", 0, &input_warning_level, WARN_NEVER}, + {"warn-style", 0, &style_warning_level, WARN_ALWAYS}, + {"no-warn-style", 0, &style_warning_level, WARN_NEVER}, + {"warnings-as-errors", 0, &warnings_as_errors, 1}, {0, 0, 0, 0} }; char *prgname = argv[0]; @@ -271,9 +483,12 @@ int main(int argc, char **argv) { opt = getopt_long(argc, argv, "?hVo:t:", longopts, 0); switch(opt) { case 0: + // A long-only option stored its value through + // the longopts table; nothing more to do. + break; case '?': case 'h': - usage(prgname); + usage(prgname, show_all_help); break; case 'V': fprintf(stderr, "Aa-machine tools " VERSION "\n"); @@ -286,7 +501,7 @@ int main(int argc, char **argv) { break; default: if(opt >= 0) { - fprintf(stderr, "Unimplemented option '%c'\n", opt); + warning(WARN_ERROR, "Unimplemented option '%c'", opt); exit(1); } break; @@ -294,7 +509,7 @@ int main(int argc, char **argv) { } while(opt >= 0); if(optind >= argc) { - usage(prgname); + usage(prgname, show_all_help); } storyfile = argv[optind]; @@ -302,14 +517,17 @@ int main(int argc, char **argv) { if(strcmp(target, "web") && strcmp(target, "web:story") && strcmp(target, "c64") - && strcmp(target, "apple2")) { - fprintf(stderr, "Unsupported target \"%s\".\n", target); + && strcmp(target, "apple2") + && strcmp(target, "aambox")) { + warning(WARN_ERROR, "Unsupported target \"%s\".", target); exit(1); } if(!dirname) { if(!strcmp(target, "web:story")) { dirname = "story.js"; + } else if(!strcmp(target, "aambox")) { + dirname = "story.ustory"; } else { dirname = malloc(strlen(argv[optind]) + 8); strcpy(dirname, argv[optind]); @@ -331,13 +549,13 @@ int main(int argc, char **argv) { f = fopen(argv[optind], "rb"); if(!f) { - fprintf(stderr, "%s: %s\n", argv[optind], strerror(errno)); + warning(WARN_ERROR, "%s: %s", argv[optind], strerror(errno)); exit(1); } if(12 != fread(buf, 1, 12, f) || memcmp(buf, "FORM", 4) || memcmp(buf + 8, "AAVM", 4)) { - fprintf(stderr, "Error: Bad or missing file header.\n"); + warning(WARN_ERROR, "Bad or missing file header."); exit(1); } storysize = 8 + @@ -349,22 +567,24 @@ int main(int argc, char **argv) { story = malloc(storysize + 0x108); if(storysize != fread(story, 1, storysize, f)) { - fprintf(stderr, "Failed to read all of '%s': %s\n", argv[optind], strerror(errno)); + warning(WARN_ERROR, "Failed to read all of '%s': %s", argv[optind], strerror(errno)); exit(1); } fclose(f); if(story[20] > VER_MAJOR || (story[20] == VER_MAJOR && story[21] > VER_MINOR)) { - fprintf(stderr, "Unsupported story file version: %d.%d is more than %d.%d\n", story[20], story[21], VER_MAJOR, VER_MINOR); + warning(WARN_ERROR, "Unsupported story file version: %d.%d is more than %d.%d", story[20], story[21], VER_MAJOR, VER_MINOR); exit(1); } if(!strcmp(target, "web:story")) { bundle_web_story(dirname); + } else if(!strcmp(target, "aambox")) { + bundle_aambox(dirname); } else { if(mkdir(dirname, 0777) && errno != EEXIST) { - fprintf(stderr, "%s: %s\n", dirname, strerror(errno)); + warning(WARN_ERROR, "%s: %s", dirname, strerror(errno)); exit(1); } if(!strcmp(target, "web")) { @@ -376,5 +596,5 @@ int main(int argc, char **argv) { } } - return 0; + return (warnings_as_errors && nwarning)? 1 : 0; } diff --git a/src/aambundle.h b/src/aambundle.h index 9b3d819..7a9984a 100644 --- a/src/aambundle.h +++ b/src/aambundle.h @@ -1,15 +1,97 @@ - #if (defined(_WIN32) || defined(__WIN32__)) #define mkdir(Path, Mode) mkdir(Path) #endif +#include + typedef void (*chunk_visitor_t)(char *head, char *dirname, uint8_t *chunk, uint32_t size); extern uint8_t *story; extern uint32_t storysize; void visit_chunks(char *storyname, int storynamesize, chunk_visitor_t chunk_visitor); -void trim_chunks(int align_writ); + +/* Locate a chunk in the story, returning its payload and (if sizep is + * non-null) its size, or null if the story has no such chunk. + * + * The returned pointer is only valid until the next rewrite_chunks(), which + * replaces the story buffer wholesale. */ + +uint8_t *find_chunk(const char *id, uint32_t *sizep); + +/* Rewrite the story in a single pass. For every chunk, the rewriter says + * whether to keep it as it is, drop it, or replace it -- in which case it + * fills in newid (four characters, not terminated), newdata and newsize. A + * replacement buffer must stay alive until rewrite_chunks() returns. + * + * A null rewriter keeps everything. */ + +typedef enum { + CHUNK_KEEP, + CHUNK_DROP, + CHUNK_REPLACE, + CHUNK_INSERT +} chunk_action_t; + +typedef chunk_action_t (*chunk_rewriter_t)( + const char *id, + uint8_t *data, + uint32_t size, + char *newid, + uint8_t **newdata, + uint32_t *newsize); + +void rewrite_chunks(chunk_rewriter_t rewriter, int align_writ); + +/* Warnings, similar to how dialogc does them */ + +#define WARN_DEFAULT 0 +#define WARN_ALWAYS 1 +#define WARN_NEVER 2 + +extern int charset_warning_level; +extern int input_warning_level; +extern int style_warning_level; +extern int nwarning; + +/* One entry per warning kind; warn_info[] in aambundle.c maps a + * warn_id_t to the flag that disables it and to its level variable. */ +typedef enum { + WARN_ERROR, + WARN_CHARSET, + WARN_INPUT, + WARN_STYLE, + WARN_COUNT +} warn_id_t; + +/* Print a warning. Unless the warning was forced on with --warn-, + * a hint suggesting its disabling flag is printed after the message. */ +void warning(warn_id_t id, const char *fmt, ...); +void vwarning(warn_id_t id, const char *fmt, va_list ap); + +/* Style precomputation (USTY chunk), implemented in gen_usty.c. Call + * gen_usty_set_target() before rewrite_chunks() on the 8-bit targets. + */ +void gen_usty_set_target(const char *target); + +/* Call after rewrite_chunks() on a 6502 target: aborts if the interpreter + * needs a USTY table and none was emitted. + */ +void gen_usty_check(void); + +chunk_action_t rewrite_6502_sty( + const char *id, + uint8_t *data, + uint32_t size, + char *newid, + uint8_t **newdata, + uint32_t *newsize); + +/* main bundle routines */ + +void writefile_padded(char *dirname, char *name, const uint8_t *data, size_t size, size_t padto); +void writefile(char *dirname, char *name, const uint8_t *data, size_t size); + /* dirname may be NULL, meaning "relative to the current directory". */ uint8_t *unicode_to_utf8(const uint32_t unichar); void warn_about_nonascii(uint8_t *dict, uint32_t dictsize, uint8_t *lang, uint32_t langsize); @@ -18,3 +100,4 @@ void bundle_web(char *dirname); void bundle_c64(char *dirname); void bundle_apple2(char *dirname); void bundle_web_story(char *filename); +void bundle_aambox(char *filename); diff --git a/src/aamshow.c b/src/aamshow.c index 5c531ec..274d49e 100644 --- a/src/aamshow.c +++ b/src/aamshow.c @@ -175,6 +175,135 @@ void decode_look(struct chunk *ch) { } } +// USTY: bundler-generated style table for the 6502 engines. +// (USTY_VERSION). +// +// 0 tag ; high nibble target, low nibble format version +// 1 nclass +// 2 nxsty +// 3 reserved +// 4-5 totalwords ; b-e, words of heap the two arrays need +// 6-7 xstyoff ; b-e, body array offset from the record base +// class records[nclass * USTY_RECSIZE] +// xsty[] ; (index, datalen, data[datalen]) records, +// ; ended by $ff in an index byte +// pad ; 0 or 1 bytes, to totalwords * 2 +// +// Keep in step with the record layouts in gen_usty.c. + +static void put_style_bits(uint8_t bits) { + int first = 1; + + if(bits & AASTYLE_REVERSE) { printf("%sreverse", first? "" : " "); first = 0; } + if(bits & AASTYLE_BOLD) { printf("%sbold", first? "" : " "); first = 0; } + if(bits & AASTYLE_ITALIC) { printf("%sitalic", first? "" : " "); first = 0; } + if(bits & AASTYLE_FIXED) { printf("%sfixed", first? "" : " "); first = 0; } + if(first) printf("none"); +} + +static const char *float_names[] = {"none", "left", "right"}; + +static void decode_usty_records(uint8_t *d, uint8_t tag, uint32_t recoffs, + uint8_t nclass, int nrec, const uint32_t *xrecoffs) +{ + int i; + + printf("\nClass records (%d bytes each):\n", USTY_RECSIZE); + for(i = 0; i < nclass; i++) { + uint8_t *r = d + recoffs + i * USTY_RECSIZE; + uint8_t fl = r[USTY_F_FLAGS]; + int flo = (fl & USTY_FL_FLOATL)? 1 : 0; + + if(fl & USTY_FL_FLOATR) flo = 2; + + printf(" %04x:", i); + if(r[USTY_F_WIDTH]) { + printf(" width=%d%s", r[USTY_F_WIDTH], (fl & USTY_FL_RELW)? "%" : ""); + } + if(r[USTY_F_HEIGHT]) { + printf(" height=%d%s", r[USTY_F_HEIGHT], (fl & USTY_FL_RELH)? "%" : ""); + } + if(r[USTY_F_MTOP]) printf(" mtop=%d", r[USTY_F_MTOP]); + if(r[USTY_F_MBOTTOM]) printf(" mbottom=%d", r[USTY_F_MBOTTOM]); + if(flo) printf(" float=%s", float_names[flo]); + if(r[USTY_F_STYON]) { + printf(" on="); + put_style_bits(r[USTY_F_STYON]); + } + if(r[USTY_F_STYOFF]) { + printf(" off="); + put_style_bits(r[USTY_F_STYOFF]); + } + if(r[USTY_F_FG] != 0x80) printf(" fg=%02x", r[USTY_F_FG]); + // An all-default class still has a record here + if(!r[0] && !r[1] && !r[2] && !r[3] + && !r[4] && !r[5] && !r[6] && r[7] == 0x80) { + printf(" all defaults"); + } + printf("\n"); + } + + printf("\n"); +} + +static void decode_usty_ext(uint8_t *d, uint32_t size, uint8_t tag) { + uint8_t nclass, nxsty; + uint32_t recoffs, xstyoffs, totalwords, xstyrel; + uint32_t xrecoffs[256]; + int nrec = 0; // no xsty records yet + + if(size < USTY_HDRSIZE) { + printf("Chunk too small (%u bytes) to hold a USTY header.\n", size); + return; + } + + nclass = d[1]; + nxsty = d[2]; + totalwords = (d[4] << 8) | d[5]; + xstyrel = (d[6] << 8) | d[7]; + + recoffs = USTY_HDRSIZE; + xstyoffs = recoffs + xstyrel; + + printf("nclass: %d nxsty: %d\n", nclass, nxsty); + printf("Offsets: rec %u xsty %u (%u words resident)\n", + recoffs, xstyoffs, totalwords); + + decode_usty_records(d, tag, recoffs, nclass, nrec, xrecoffs); +} + +void decode_usty(struct chunk *ch) { + uint8_t *d = ch->data; + uint32_t size = ch->size; + uint8_t tag; + + if(size < 4) { + printf("Chunk too small (%u bytes) to be a USTY table.\n", size); + return; + } + + tag = d[0]; + + printf("Tag: %02x (", tag); + switch(tag & 0xf0) { + case 0x00: printf("aambox"); break; + case 0x10: printf("c64"); break; + case 0x20: printf("apple2"); break; + default: printf("unknown target"); break; + } + printf(", format version %d)\n", tag & 0x0f); + + switch(tag & 0x0f) { + case USTY_VERSION: + decode_usty_ext(d, size, tag); + break; + default: + printf("Cannot decode USTY format version %d; this aamshow knows %d.\n", + tag & 0x0f, USTY_VERSION); + break; + } +} + static int put_obj(struct chunk *tagsch, int num) { int ptr, count = 0; uint8_t c; @@ -857,6 +986,7 @@ struct decoder { {decode_head, "HEAD"}, {decode_meta, "META"}, {decode_look, "LOOK"}, + {decode_usty, "USTY"}, {decode_tags, "TAGS"}, {decode_lang, "LANG"}, {decode_writ, "WRIT"}, @@ -957,7 +1087,7 @@ int main(int argc, char **argv) { fprintf(stderr, "Error: First chunk must be HEAD.\n"); exit(1); } - + aavm_init(chunk[0].data[0]); // Wait until after reading the file before initializing the opcode database so that we can pass the major version if(chunk[0].data[0] > AAVM_FORMAT_MAJOR || (chunk[0].data[0] == 1 && chunk[0].data[1] > AAVM_FORMAT_MINOR)) { @@ -999,9 +1129,18 @@ int main(int argc, char **argv) { exit(1); } + int crc_valid = 1; actual_crc = 0xffffffff; if(!savefile) { - crc_chunk("LOOK"); + // A story bundled for a 6502 target has had LOOK replaced by the + // precomputed USTY table, so its absence is not a defect there. The + // CRC will not match HEAD either way once chunks have been rewritten. + if(findchunk("USTY")) { + crc_chunk("USTY"); // crc will fail anyway + crc_valid = 0; + } else { + crc_chunk("LOOK"); + } crc_chunk("LANG"); crc_chunk("MAPS"); crc_chunk("DICT"); @@ -1010,7 +1149,10 @@ int main(int argc, char **argv) { crc_chunk("WRIT"); actual_crc ^= 0xffffffff; - if(actual_crc != get32(chunk[0].data + 12)) { + if(!crc_valid) { + printf("Warning: This file was rewritten with a USTY chunk and CRC (%08x) was not recomputed.\n", + get32(chunk[0].data + 12)); + } else if(actual_crc != get32(chunk[0].data + 12)) { printf("Warning: CRC declared in header (%08x) does not match actual CRC (%08x).\n", get32(chunk[0].data + 12), actual_crc); diff --git a/src/aavm.h b/src/aavm.h index 50d0002..f5a77ac 100644 --- a/src/aavm.h +++ b/src/aavm.h @@ -176,6 +176,28 @@ #define AA_MAX_TEMP (REG_TMP - REG_X) // 48 +// USTY chunk (precomputed style table for the 6502 targets) +// The USTY version number is internal, mainly to prevent devs from confusing +// themselves, so it should be incremented often and can wrap (0..15) +#define USTY_VERSION 0 + +#define USTY_HDRSIZE 8 // USTY header size +#define USTY_RECSIZE 8 // USTY record size + +#define USTY_F_WIDTH 0 +#define USTY_F_HEIGHT 1 +#define USTY_F_MTOP 2 +#define USTY_F_MBOTTOM 3 +#define USTY_F_STYON 4 +#define USTY_F_STYOFF 5 +#define USTY_F_FLAGS 6 +#define USTY_F_FG 7 + +#define USTY_FL_RELW 0x01 +#define USTY_FL_RELH 0x02 +#define USTY_FL_FLOATL 0x40 +#define USTY_FL_FLOATR 0x80 + #define AASTYLE_REVERSE 1 #define AASTYLE_BOLD 2 #define AASTYLE_ITALIC 4 diff --git a/src/bundle_6502.c b/src/bundle_6502.c new file mode 100644 index 0000000..889478d --- /dev/null +++ b/src/bundle_6502.c @@ -0,0 +1,18 @@ +#include +#include +#include +#include +#include + +#include "aambundle.h" + +// The aambox target: no packaging at all, just the rewritten .aastory. + +void bundle_aambox(char *filename) { + char storyname[256]; + + visit_chunks(storyname, sizeof(storyname), 0); + gen_usty_set_target("aambox"); + rewrite_chunks(rewrite_6502_sty, 1); + writefile(NULL, filename, story, storysize); +} diff --git a/src/bundle_apple2.c b/src/bundle_apple2.c index bf6a069..7821f2b 100644 --- a/src/bundle_apple2.c +++ b/src/bundle_apple2.c @@ -47,6 +47,8 @@ * doesn't do a volume scan. */ +#define AAKBD_APPLE2 "\\_`{|}~[]" + static char storyname[48]; /* ProDOS on-disk layout. Blocks are 512 bytes and .po images store them @@ -111,44 +113,6 @@ static const int dos_order[SECTORS_PER_TRACK] = { * start on that boundary, at block 8. Block 7 is the cost of the alignment. */ #define BOOT0_FIRST_DATA BLOCKS_PER_TRACK -/* Writes data, then pads the file with zeros up to a multiple of padto - * bytes (padto = 1 means no padding). */ -static void writefile_padded(char *dirname, char *name, const uint8_t *data, size_t size, size_t padto) { - char *filename; - FILE *f; - size_t npad; - - filename = malloc(strlen(dirname) + strlen(name) + 2); - sprintf(filename, "%s/%s", dirname, name); - - f = fopen(filename, "wb"); - if(!f) { - fprintf(stderr, "%s: %s\n", filename, strerror(errno)); - exit(1); - } - if(size != fwrite(data, 1, size, f)) { - fprintf(stderr, "%s: %s\n", filename, strerror(errno)); - exit(1); - } - npad = (padto - size % padto) % padto; - while(npad--) { - if(EOF == fputc(0, f)) { - fprintf(stderr, "%s: %s\n", filename, strerror(errno)); - exit(1); - } - size++; - } - fclose(f); - - printf("%-14s %7lu bytes\n", name, (unsigned long) size); - - free(filename); -} - -static void writefile(char *dirname, char *name, const uint8_t *data, size_t size) { - writefile_padded(dirname, name, data, size, 1); -} - /* ---------------------------------------------------------------- writing */ /* A freshly formatted ProDOS volume, built up block by block. Blocks 0-1 @@ -511,7 +475,7 @@ void check_font_has_translit(uint8_t *lang, uint32_t size) { // Expects the LANG (lang[exttable+5*i+4]) ); if(!does_font_have_translit(unichar)) { - fprintf(stderr, "Warning: Extended character %d (%s, U+%04x) has no Apple II transliteration. It will display as '?'.\n", 0x80|i, unicode_to_utf8(unichar), unichar); + warning(WARN_CHARSET, "Extended character %d (%s, U+%04x) has no Apple II transliteration. It will display as '?'.", 0x80|i, unicode_to_utf8(unichar), unichar); } } } @@ -532,7 +496,7 @@ void apple2_chunk_visitor(char *head, char *dirname, uint8_t *chunk, uint32_t si } else { return; } - + if(langchunk && dictchunk) { warn_about_nonascii(dictchunk, dictsize, langchunk, langsize); } @@ -689,6 +653,7 @@ static const char readme_build[] = "\n" "When booting from real ProDOS, name the volume containing the story\n" "file AA.STORY because the interpreter will scan for it by volume path.\n" +"If it boots into BASIC, type -AAM.SYSTEM to start the interpeter.\n" "\n" ; @@ -729,7 +694,6 @@ static void write_readme(char *dirname, int mode, const char *single, const char sprintf(text + strlen(text), readme_800k, bigdisk); } strcat(text, readme_save); - strcat(text, readme_build); strcat(text, readme_emul); if(bigdisk) { @@ -737,6 +701,8 @@ static void write_readme(char *dirname, int mode, const char *single, const char } strcat(text, readme_emul_tail); + strcat(text, readme_build); + writefile(dirname, "readme.txt", (const uint8_t *) text, strlen(text)); free(text); } @@ -866,7 +832,9 @@ void bundle_apple2(char *dirname) { int mode; visit_chunks(storyname, sizeof(storyname), apple2_chunk_visitor); - trim_chunks(1); + gen_usty_set_target("apple2"); + rewrite_chunks(rewrite_6502_sty, 1); + gen_usty_check(); writefile(dirname, "AAM.SYSTEM", table_a2terp, sizeof(table_a2terp)); /* The interpreter reads STORY a page at a time, so round the file up diff --git a/src/bundle_c64.c b/src/bundle_c64.c index 9f600f3..6404cf8 100644 --- a/src/bundle_c64.c +++ b/src/bundle_c64.c @@ -14,6 +14,8 @@ #include "tables_6502font.h" +#define AAKBD_C64 "\\_`{|}~" + #define INTERLEAVE 11 static char storyname[48]; @@ -124,7 +126,7 @@ void check_font_has_glyphs(uint8_t *lang, uint32_t size) { // Expects the LANG c (lang[exttable+5*i+4]) ); if(!does_font_have_glyph(unichar)) { - fprintf(stderr, "Warning: Extended character %d (%s, U+%04x) has no font entry. It will display as '�'.\n", 0x80|i, unicode_to_utf8(unichar), unichar); + warning(WARN_CHARSET, "Extended character %d (%s, U+%04x) has no font entry. It will display as '�'.", 0x80|i, unicode_to_utf8(unichar), unichar); } } } @@ -140,7 +142,7 @@ void c64_chunk_visitor(char *head, char *dirname, uint8_t *chunk, uint32_t size) } else { return; } - + if(langchunk && dictchunk) { warn_about_nonascii(dictchunk, dictsize, langchunk, langsize); } @@ -151,7 +153,6 @@ void bundle_c64(char *dirname) { int fnsize, size; int i, j, pos; int terpsectors, terploc; - FILE *outf; j = 0; for(i = 0; i < 35; i++) { @@ -162,7 +163,9 @@ void bundle_c64(char *dirname) { memset(available, 1, j); visit_chunks(storyname, sizeof(storyname), c64_chunk_visitor); - trim_chunks(1); + gen_usty_set_target("c64"); + rewrite_chunks(rewrite_6502_sty, 1); + gen_usty_check(); fnsize = strlen(dirname) + strlen(storyname) + 64; filename = malloc(fnsize); @@ -242,24 +245,14 @@ void bundle_c64(char *dirname) { write_bam(); - snprintf(filename, fnsize, "%s/%s.d64", dirname, storyname); - outf = fopen(filename, "wb"); - if(!outf) { - fprintf(stderr, "%s: %s", filename, strerror(errno)); - exit(1); - } - fwrite(image, 1, sizeof(image), outf); - fclose(outf); - + snprintf(filename, fnsize, "%s.d64", storyname); + writefile(dirname, filename, (const uint8_t *)image, sizeof(image)); + + // Also write out the raw story file (the USTY-inserted .aastory that + // went into the .d64), so it can be inspected with aamshow or reused. + snprintf(filename, fnsize, "%s.c64.ustory", storyname); + writefile_padded(dirname, filename, story, storysize, 256); + // Add the license - snprintf(filename, fnsize, "%s/interpreter_license.txt", dirname); - if(!(outf = fopen(filename, "wb"))) { - fprintf(stderr, "%s: %s\n", filename, strerror(errno)); - exit(1); - } - if(1 != fwrite(table_c64license, sizeof(table_c64license), 1, outf)) { - fprintf(stderr, "%s: write error\n", filename); - exit(1); - } - fclose(outf); + writefile(dirname, "interpreter_license.txt", table_c64license, sizeof(table_c64license)); } diff --git a/src/bundle_web.c b/src/bundle_web.c index 71c0425..e65b2ab 100644 --- a/src/bundle_web.c +++ b/src/bundle_web.c @@ -102,13 +102,27 @@ void bundle_web(char *dirname) { bundle_web_story(filename); } +// FILE chunks are unpacked into resources/ by bundle_web(), so the story the +// interpreter loads does not need to carry them. + +static chunk_action_t drop_files( + const char *id, + uint8_t *data, + uint32_t size, + char *newid, + uint8_t **newdata, + uint32_t *newsize) +{ + return strcmp(id, "FILE")? CHUNK_KEEP : CHUNK_DROP; +} + void bundle_web_story(char *filename) { int i; uint32_t n, pos; uint8_t buf[3], out[4]; FILE *outf; - trim_chunks(0); + rewrite_chunks(drop_files, 0); outf = fopen(filename, "wb"); if(!outf) { diff --git a/src/gen_usty.c b/src/gen_usty.c new file mode 100644 index 0000000..d038671 --- /dev/null +++ b/src/gen_usty.c @@ -0,0 +1,683 @@ +#include +#include +#include +#include + +#include "aambundle.h" +#include "aavm.h" + +// ============================================================================ +// USTY chunk generation: precomputed style table for the 6502 engines. +// +// Parses the LOOK chunk (CSS declarations) once, in C, and emits the compact +// binary table defined in aamshow.c. USTY replaces LOOK on the 6502 +// targets which have no style sheet parser. +// +// The parser mirrors both the dialog compiler's effective CSS subset +// (~/if/dialog/src/frontend.c:2899-3000): case-insensitive keys, +// unknown properties ignored, width/height accept % (relative) +// and em/ch/en (absolute), margins absolute only, fractional units truncated. +// ============================================================================ + +// The high nibble of the tag byte names the target, the low nibble the +// format revision (0..15, internal only so it can recycle) +#define STY_TAG_AAMBOX 0x00 +#define STY_TAG_C64 0x10 +#define STY_TAG_APPLE2 0x20 + +#define STY_RELW 0x01 +#define STY_RELH 0x02 + +#define NOCOLOR 0x80 // "$80 = not set" sentinel for the sty fg field + +struct sty_target { + const char *name; + uint8_t tag; + int have_vic_color; // per-character fg color (c64) + uint8_t stymask; // AASTYLE_* bits the frontend can actually act on + int mincols, maxcols; // 40 for c64, 40-80 for apple2, 80 for aambox + int maxrows; // 20 for all +}; + +static const struct sty_target sty_aambox = { + "aambox", STY_TAG_AAMBOX, 0, + 0, + 80, 80, 20 +}; +static const struct sty_target sty_c64 = { + "c64", STY_TAG_C64, 1, + AASTYLE_REVERSE | AASTYLE_BOLD | AASTYLE_ITALIC, + 40, 40, 20 +}; +static const struct sty_target sty_apple2 = { + "apple2", STY_TAG_APPLE2, 0, + AASTYLE_REVERSE, + 40, 80, 20 +}; + +static const struct sty_target *sty_target; +static uint8_t *sty_payload; // malloc'd USTY chunk payload (leaked, tool lifetime) +static uint32_t sty_size; +static int sty_emitted; + +// Per-class record, one for every class in LOOK. +typedef struct { + uint8_t styon, styoff; // AASTYLE_* bits + uint8_t fg; // c64 palette index, NOCOLOR = inherit + uint8_t mtop, mbottom; // top/bottom margins in rows + uint8_t width, height; // width/height in columns + uint8_t flo; // 0 none, 1 left, 2 right + uint8_t flags; // STY_RELW | STY_RELH +} styclass; + +// ---------------------------------------------------------------------------- +// Color mapping. The C64 has a 16-color palette; web colors are reduced to +// the nearest VIC-II color by RGB distance. "inherit", "initial" and +// "transparent" (or alpha 0) leave the field unset. + +static const uint8_t c64_rgb[16][3] = { + {0, 0, 0}, {255, 255, 255}, {136, 0, 0}, {170, 255, 238}, + {204, 68, 204}, {0, 204, 85}, {0, 0, 170}, {238, 238, 119}, + {221, 136, 85}, {102, 68, 0}, {255, 119, 119}, {51, 51, 51}, + {119, 119, 119}, {170, 255, 102}, {0, 136, 255}, {187, 187, 187} +}; + +// CSS color names map by name to their obvious VIC-II counterparts +static const struct { + const char *name; + uint8_t vic; +} css2vic[] = { + // canonical names (0..15 in order) + {"black", 0}, + {"white", 1}, + {"red", 2}, + {"cyan", 3}, + {"purple", 4}, + {"green", 5}, + {"blue", 6}, + {"yellow", 7}, + {"orange", 8}, + {"brown", 9}, + {"pink", 10}, + {"darkgrey", 11}, + {"mediumgrey", 12}, + {"lightgreen", 13}, + {"lightblue", 14}, + {"lightgrey", 15}, + // synonyms + {"maroon", 2}, + {"aqua", 3}, + {"teal", 3}, + {"magenta", 4}, + {"fuchsia", 4}, + {"lime", 5}, + {"navy", 6}, + {"lightred", 10}, + {"gray", 11}, + {"grey", 11}, + {"darkgray", 11}, + {"lightgray", 15}, + {"silver", 15}, +}; + +// Style warnings, routed through swarn() so that they can be silenced +// while parsing declarations that a -iftf-sys- declaration in the same +// class overrides anyway. They are prefixed with the style-name of the +// class being parsed (see sty_name), so the author can tell which +// declaration each complaint is about. +static int sty_quiet; + +// Name of the style class currently being parsed; "class N" if the LOOK +// chunk does not carry a style-name: declaration for it. +static const char *sty_name = 0; + +static void swarn(const char *fmt, ...) { + va_list ap; + char msg[1024]; + if(sty_quiet) return; + va_start(ap, fmt); + vsnprintf(msg, sizeof(msg), fmt, ap); + va_end(ap); + warning(WARN_STYLE, "style class %s: %s", sty_name ? sty_name : "(unknown)", msg); +} + +// Map an rgb triplet to the nearest VIC-II color, using a perceptual +// distance (weighted RGB, a standard cheap approximation of CIE lightness). +static int rgb_to_c64(int r, int g, int b) { + int best = 0, bestdist = 0x7fffffff; + int i; + + for(i = 0; i < 16; i++) { + long dr = r - c64_rgb[i][0]; + long dg = g - c64_rgb[i][1]; + long db = b - c64_rgb[i][2]; + long dist = 2 * dr * dr + 4 * dg * dg + 3 * db * db; + if(dist < bestdist) { + bestdist = dist; + best = i; + } + } + if(bestdist > (2+3+4)*64*64) { + swarn("The color #%02x%02x%02x is not accurately represented on %s, the closest is %s (index %d, #%02x%02x%02x).", + r, g, b, sty_target->name, css2vic[best].name, best, + c64_rgb[best][0], c64_rgb[best][1], c64_rgb[best][2]); + } + return best; +} + +// Hex digit to value. +static int hex(char c) { + return c >= '0' && c <= '9'? c - '0' : (c | 0x20) - 'a' + 10; +} + +// Parse " #rgb", "#rrggbb", "rgb(r,g,b)", "rgba(r,g,b,a)", a name, +// or a specific VIC-II color index (0..15). +// Returns 1 and sets *out to a palette index; +// returns 0 if the value is not a usable color +// (inherit, initial, transparent, alpha 0, garbage). +static int parse_color(const char *v, int *out) { + int i, r = 0, g = 0, b = 0, alpha = 255, n; + + while(*v == ' ' || *v == '\t') v++; + + if(!strncmp(v, "inherit", 7) + || !strncmp(v, "initial", 7) + || !strncmp(v, "transparent", 11)) { + return 0; + } + + if(v[0] == '#') { + v++; + n = 0; + while(v[n] && ((v[n] >= '0' && v[n] <= '9') || (v[n] >= 'a' && v[n] <= 'f') || (v[n] >= 'A' && v[n] <= 'F'))) { + n++; + } + if(n == 3) { + r = hex(v[0]) * 0x11; + g = hex(v[1]) * 0x11; + b = hex(v[2]) * 0x11; + }else if(n == 6) { + r = (hex(v[0]) << 4) | hex(v[1]); + g = (hex(v[2]) << 4) | hex(v[3]); + b = (hex(v[4]) << 4) | hex(v[5]); + } else { + return 0; + } + } else if(!strncmp(v, "rgb", 3)) { + float af; + v += 3; + if(*v == 'a') v++; + if(*v != '(') return 0; + v++; + if(sscanf(v, "%d , %d , %d , %f", &r, &g, &b, &af) == 4) { + // rgba: alpha is a 0..1 float + alpha = (int)(af * 255 + 0.5); + alpha = alpha < 0? 0 : alpha > 255? 255 : alpha; + if(alpha == 0) return 0; + } else { + if(sscanf(v, "%d , %d , %d", &r, &g, &b) != 3) return 0; + } + } else if(*v >= '0' && *v <= '9') { + // Naked VIC color index: 0..15 selects the palette entry itself, + // so 3 is cyan no matter what hue CSS would call it. Reject + // trailing junk so "10px" and friends don't quietly become 10. + const char* v_old = v; + n = atoi(v); + while(*v >= '0' && *v <= '9') v++; + while(*v == ' ' || *v == '\t') v++; + if(!*v && n <= 15) { + *out = n; + return 1; + } + swarn("Invalid VIC color index \"%s\", must be in range 0..15.", v_old); + return 0; + } else { + int vic = -1; + for(i = 0; i < (int)(sizeof(css2vic) / sizeof(css2vic[0])); i++) { + if(!strcasecmp(v, css2vic[i].name)) { + vic = css2vic[i].vic; + break; + } + } + if(vic < 0) { + swarn("Unknown c64 color \"%s\".", v); + return 0; + } + *out = vic; + return 1; + } + + // Hex and rgb() paths land here with r/g/b filled in. + *out = rgb_to_c64(r, g, b); + return 1; +} + +// ---------------------------------------------------------------------------- +// CSS value parsing. +// Returns the number of conversions: +// 0 = not a length +// 1 = bare number, +// 2 = number and unit +// (*unit is "" for a bare number, "%" or "em"/"ch"/"en") +static int scan_length(const char *value, int *val, char *unit) { + float f; + int n; + + while(*value == ' ' || *value == '\t') value++; + if((*value < '0' || *value > '9') && *value != '.') return 0; + unit[0] = 0; + n = sscanf(value, "%f %15s", &f, unit); + if(n < 1) return 0; + *val = (int) f; // fractions are truncated + return n; +} + +static int isabsunit(const char *unit) { + return !strcmp(unit, "em") || !strcmp(unit, "ch") || !strcmp(unit, "en"); +} + +// Parse one null-terminated declaration, e.g. "width: 100%" or +// "-iftf-sys-c64-color: red". The line is copied so the key can be lowercased +// in place; 'key' starts at the buffer, 'value' points into it. +// +// Declarations are parsed in two passes per class (see parse_look): +// - pass 0 takes the unprefixed ones +// - pass 1 the -iftf- ones so they override +static void parse_decl(styclass *c, const char *p, int len, int pass) { + char buf[256]; + char *colon, *key, *value, *bang, *q; + int prefixed = 0; + int matched = 0; + + if(len >= (int) sizeof(buf)) len = sizeof(buf) - 1; + memcpy(buf, p, len); + buf[len] = 0; + + colon = strchr(buf, ':'); + if(!colon) return; + *colon = 0; + value = colon + 1; + while(*value == ' ' || *value == '\t') value++; + bang = strchr(value, '!'); // strip "!important" + if(bang) *bang = 0; + while(*value == ' ' || *value == '\t') value++; + + key = buf; + while(*key == ' ' || *key == '\t') key++; + for(q = key; *q; q++) { + if(*q >= 'A' && *q <= 'Z') *q |= 0x20; // make lowercase + } + while(q > key && (q[-1] == ' ' || q[-1] == '\t')) *--q = 0; + + // -iftf-sys--* properties + if(!strncmp(key, "-iftf-sys-", 10)) { + prefixed = 1; + char *prop; + key += 10; + prop = strchr(key, '-'); + if(!prop) return; + *prop = 0; + // ignore if not the target system + if(strcmp(key, sty_target->name)) { + return; + } + key = prop + 1; + } + if(prefixed != pass) return; + + char param[32] = { 0 }; + sscanf(value, "%31s", param); + + if(!strcmp(key, "-iftf-text-decoration")) { + if(!strcmp(param, "reverse")) c->styon |= AASTYLE_REVERSE; + else if(!strcmp(param, "none")) c->styoff |= AASTYLE_REVERSE; + else swarn("Invalid value for %s: %s", key, value); + return; + } + + if(!strcmp(key, "width") || !strcmp(key, "height")) { + int v; + char unit[16]; + int n = scan_length(value, &v, unit); + matched = 1; + if(n == 2 && !strcmp(unit, "%")) { + if(v > 100) { + swarn("Percent %s \"%s\" is more than 100%% and will not fit the screen.", + key, value); + } + if(*key == 'w') { + c->width = v; + c->flags |= STY_RELW; + } else { + c->height = v; + c->flags |= STY_RELH; + } + } else if(n >= 1 && (!unit[0] || isabsunit(unit))) { + // Bare number or em/ch/en: absolute. + if(*key == 'w') c->width = v; + else c->height = v; + } else { + swarn("Ignoring %s: unsupported value \"%s\".", key, value); + } + } else if(!strcmp(key, "margin-top") || !strcmp(key, "margin-bottom")) { + int v; + char unit[16]; + int n = scan_length(value, &v, unit); + matched = 1; + if(n >= 1 && (!unit[0] || isabsunit(unit))) { + // Absolute only + if(!strcmp(key, "margin-top")) c->mtop = v; + else if(!strcmp(key, "margin-bottom")) c->mbottom = v; + } else if(n == 2 && !strcmp(unit, "%")) { + swarn("Percent %s \"%s\" is not supported and was ignored.", key, value); + } else { + swarn("Ignoring %s: unsupported value \"%s\".", key, value); + } + } else if(!strcmp(key, "float")) { + matched = 1; + if(!strcmp(param, "left")) c->flo = 1; + else if(!strcmp(param, "right")) c->flo = 2; + else if(strcmp(param, "none") && strcmp(param, "inherit")) { + swarn("Invalid value for float: %s (only left, right and none are supported).", value); + } + } else if(!strcmp(key, "margin")) { + matched = 1; + if(!strcmp(param, "auto")) c->flo = 3; + else swarn("Invalid value for margin: %s (use \"margin: auto\").", value); + } else if(!strcmp(key, "font-style")) { + matched = 1; + if(!strcmp(param, "italic") || !strcmp(param, "oblique")) + c->styon |= AASTYLE_ITALIC; + else if(!strcmp(param, "normal")) c->styoff |= AASTYLE_ITALIC; + else if(strcmp(param, "inherit")) { + swarn("Invalid value for font-style: %s (only italic, oblique and normal are supported).", value); + } + } else if(!strcmp(key, "font-weight")) { + matched = 1; + if(!strcmp(param, "bold")) c->styon |= AASTYLE_BOLD; + else if(!strcmp(param, "normal")) c->styoff |= AASTYLE_BOLD; + else if(strcmp(param, "inherit")) { + // In particular: numeric weights (400, 700) are valid web + // CSS but mean nothing here. + swarn("Invalid value for font-weight: %s (only bold and normal are supported).", value); + } + } else if(!strcmp(key, "font-family")) { + matched = 1; + if(strstr(value, "monospace")) c->styon |= AASTYLE_FIXED; + else if(strcmp(param, "inherit")) c->styoff |= AASTYLE_FIXED; + } else if(!strcmp(key, "text-decoration") || !strcmp(key, "reverse-video")) { + matched = 1; + if(strstr(value, "reverse")) { + c->styon |= AASTYLE_REVERSE; + } else if(strcmp(param, "inherit")) { + c->styoff |= AASTYLE_REVERSE; + if(!strcmp(key, "text-decoration") && strcmp(param, "none")) { + swarn("Invalid value for text-decoration: %s (only reverse, none and inherit are supported).", value); + } + } + } else if(!strcmp(key, "color")) { + matched = 1; + if(sty_target->have_vic_color) { + int ci; + if(parse_color(value, &ci)) { + c->fg = ci; + } + } else { + swarn("color is not supported on %s and was ignored.", sty_target->name); + } + } else if(!strcmp(key, "display")) { + matched = 1; + if(!strcmp(param, "none")) { + swarn("display: none is not supported; the element will still be shown."); + } + } else if(!strcmp(key, "visibility")) { + matched = 1; + if(!strcmp(param, "hidden")) { + swarn("visibility: hidden is not supported; the element will still be shown."); + } + } + + // A prefix that named the target but a property the bundler does not know. + if(prefixed && !matched) { + swarn("-iftf-sys-%s-%s is not supported yet.", sty_target->name, key); + } + // Anything else is ignored, as an interpreter must per the spec. +} + +static void make_flat(uint8_t *r, const styclass *c) { + uint8_t flags = c->flags & (USTY_FL_RELW | USTY_FL_RELH); + + if(c->flo == 1) flags |= USTY_FL_FLOATL; + else if(c->flo == 2) flags |= USTY_FL_FLOATR; + + r[USTY_F_WIDTH] = c->width; + r[USTY_F_HEIGHT] = c->height; + r[USTY_F_MTOP] = c->mtop; + r[USTY_F_MBOTTOM] = c->mbottom; + r[USTY_F_STYON] = c->styon & sty_target->stymask; + r[USTY_F_STYOFF] = c->styoff & sty_target->stymask; + r[USTY_F_FLAGS] = flags; + r[USTY_F_FG] = c->fg; +} + +static uint16_t get16(const uint8_t *p) { + return (p[0] << 8) | p[1]; +} + +// Length of the null-terminated line at look[p], clamped to end. +static uint32_t line_len(const uint8_t *look, uint32_t p, uint32_t end) { + uint32_t len = 0; + while(p + len < end && look[p + len]) len++; + return len; +} + +// ---------------------------------------------------------------------------- +// LOOK parse: read the class count, then walk each class's declaration +// block (null-terminated strings, a lone null byte ends the block) + +static styclass *parse_look(int *nclassp) { + uint8_t *look; + uint32_t looksize; + uint16_t n; + styclass *cls; + int i; + int pass; + + look = find_chunk("LOOK", &looksize); + if(!look) return 0; + + n = get16(look); + if(n > 255) { + warning(WARN_STYLE, "Story has %d style classes, more than a USTY table can index (255).", n); + return 0; + } + // need at least one style, ok if it's empty + if (!n) n++; + cls = calloc(n, sizeof(styclass)); + if(!cls) return 0; + + // fg defaults to "not set", which is not the same as black. + for(i = 0; i < n; i++) { + cls[i].fg = NOCOLOR; + } + + for(i = 0; i < n; i++) { + uint32_t ptr, end, p; + int hasiftf = 0; + char pfx[32]; + char namebuf[64]; + size_t plen; + + if((uint32_t)(2 + 2 * i + 2) > looksize) break; + ptr = get16(look + 2 + 2 * i); + if(ptr >= looksize) continue; + end = looksize; + + // One lookahead pass over the class's declaration lines: pick up + // its style-name (so warnings can name it), and check whether it + // carries -iftf-sys-- declarations for this machine. If so, + // the generic (unprefixed) declarations are only a fallback that the + // prefixed pass replaces wholesale -- so pass 0 below runs with + // warnings off, and the author is not scolded twice for the same + // styling. + snprintf(namebuf, sizeof(namebuf), "class %d", i); + sty_name = namebuf; // this is on the stack so we have to unset it + snprintf(pfx, sizeof(pfx), "-iftf-sys-%s-", sty_target->name); + plen = strlen(pfx); + p = ptr; + while(p < end && look[p]) { + uint32_t linelen = line_len(look, p, end); + char line[64], nm[56]; + + if(linelen > plen && !strncasecmp((const char *) look + p, pfx, plen)) + hasiftf = 1; + // "style-name: title", always written in this exact case. + if(linelen < sizeof(line)) { + memcpy(line, look + p, linelen); + line[linelen] = 0; + if(1 == sscanf(line, " style-name : %55s", nm)) { + snprintf(namebuf, sizeof(namebuf), "@%s", nm); + sty_name = namebuf; + } + } + p += linelen + 1; + } + + // Two passes over the same block: unprefixed declarations first, + // then the -iftf-sys- ones, so that a prefixed declaration overrides + // its unprefixed counterpart no matter which comes last in the CSS. + for(pass = 0; pass < 2; pass++) { + sty_quiet = hasiftf && pass == 0; + p = ptr; + while(p < end && look[p]) { + uint32_t linelen = line_len(look, p, end); + parse_decl(&cls[i], (const char *) look + p, linelen, pass); + p += linelen + 1; + } + } + sty_quiet = 0; // re-enable warnings + sty_name = 0; // unset style class name + } + *nclassp = n; + return cls; +} + +// Build the USTY chunk (revision 13). +// +// The body array is a list of (index, datalen, data...) records, ended by +// a single $ff index byte; datalen is the per-target payload size. The +// payload is padded to an even length after the header so that +// totalwords * 2 is exactly the number of bytes the engine reads. + +static uint8_t *build_usty_flat(uint32_t *sizep) { + styclass *cls; + int nclass; + uint8_t *out; + uint32_t size, recoffs, xstyoffs, blockbytes, totalwords; + int nxsty = 0, nover = 0, maxnsty, reclen; + int i; + + cls = parse_look(&nclass); + if(!cls) return 0; + + recoffs = USTY_HDRSIZE; + xstyoffs = recoffs + nclass * USTY_RECSIZE; + reclen = 2; + maxnsty = 0; + + // Upper bound: the full body array with its terminator, which is what + // the engine's one-page scan can reach. + size = xstyoffs + 1; + + out = calloc(1, size); + if(!out) { + warning(WARN_ERROR, "Out of memory"); + exit(1); + } + + out[0] = sty_target->tag | USTY_VERSION; + out[1] = nclass; + // out[2] = nxsty, filled in below. + // out[3] stays 0: reserved (revision 12's xstysize stride byte). + out[6] = (xstyoffs - recoffs) >> 8; // from the record base, which is + out[7] = (xstyoffs - recoffs) & 0xff; // the pointer the engine holds + + for(i = 0; i < nclass; i++) { + make_flat(out + recoffs + i * USTY_RECSIZE, &cls[i]); + } + + // The array always ends in $ff, even empty: the terminator is the only + // thing that tells the engine's scan to stop. + out[xstyoffs + nxsty * reclen] = 0xff; + + if(nover) { + warning(WARN_STYLE, "Too many classes have -iftf-sys-%s- colors (max is %d) so some body styles were dropped.", + sty_target->name, maxnsty); + } + + blockbytes = nclass * USTY_RECSIZE + nxsty * reclen + 1; + totalwords = (blockbytes + 1) / 2; // word = 2 bytes + size = recoffs + totalwords * 2; // size is in bytes + out[2] = nxsty; + out[4] = totalwords >> 8; + out[5] = totalwords & 0xff; + + free(cls); + *sizep = size; + return out; +} + +// ---------------------------------------------------------------------------- +// Public API. + +// Called after rewriting USTY, exits program with an error code it didn't happen. +void gen_usty_check(void) { + if(sty_target && !sty_emitted) { + warning(WARN_ERROR, + "Could not build the %s style table.", + sty_target->name); + exit(1); + } +} + +void gen_usty_set_target(const char *target) { + sty_target = 0; + if(!strcmp(target, "c64")) { + sty_target = &sty_c64; + } else if(!strcmp(target, "apple2")) { + sty_target = &sty_apple2; + } else if(!strcmp(target, "aambox")) { + sty_target = &sty_aambox; + } + sty_payload = 0; + sty_size = 0; + sty_emitted = 0; +} + +chunk_action_t rewrite_6502_sty( + const char *id, + uint8_t *data, + uint32_t size, + char *newid, + uint8_t **newdata, + uint32_t *newsize) +{ + // drop FILE chunks + if(!strcmp(id, "FILE")) return CHUNK_DROP; + + chunk_action_t act = CHUNK_KEEP; + + if(sty_target && !sty_emitted && !strcmp(id, "LOOK")) { + if(!sty_payload) { + sty_payload = build_usty_flat(&sty_size); + } + if(sty_payload) { + memcpy(newid, "USTY", 4); + *newdata = sty_payload; + *newsize = sty_size; + sty_emitted = 1; + return CHUNK_REPLACE; + } + } + return act; +} \ No newline at end of file diff --git a/src/js/nodefrontend.js b/src/js/nodefrontend.js index 1735e48..4fe6ba8 100644 --- a/src/js/nodefrontend.js +++ b/src/js/nodefrontend.js @@ -247,7 +247,7 @@ const io = { } }); }, - + have_styles: function() { // No text styling return false; }, diff --git a/test/common.mk b/test/common.mk index 5affaef..db12212 100644 --- a/test/common.mk +++ b/test/common.mk @@ -30,6 +30,7 @@ GOLD6502 ?= $(STORY).6502.gold DIR6502 = ../../src/6502 AAMBOX = $(DIR6502)/aambox6502 AAMFRONTEND = $(DIR6502)/aambox_frontend.bin +AAMBUNDLE = ../../src/aambundle JS_ENGINE = ../../src/js/engine.js JS_FRONTEND = ../../src/js/nodefrontend.js @@ -41,6 +42,9 @@ $(AAMBOX): $(DIR6502)/aambox6502.c $(DIR6502)/fake6502.c $(AAMFRONTEND): $(DIR6502)/aambox_frontend.s $(DIR6502)/engine.s $(MAKE) -C $(DIR6502) aambox_frontend.bin +$(AAMBUNDLE): + $(MAKE) -C ../src aambundle + all: test test: test.js test.6502 @@ -56,9 +60,13 @@ $(STORY).js.out: $(STORY).aastory $(STORY).in $(JS_ENGINE) $(JS_FRONTEND) test.6502: $(STORY).6502.out $(DIFF) $(STORY).6502.out $(GOLD6502) -$(STORY).6502.out: $(STORY).aastory $(STORY).in $(AAMBOX) $(AAMFRONTEND) +$(STORY).6502.out: $(STORY).ustory $(STORY).in $(AAMBOX) $(AAMFRONTEND) $(AAMBOX) -s 1234 $(AAMFRONTEND) $< <$(STORY).in >$@ +# Convert .aastory to .ustory to add USTY chunk +$(STORY).ustory: $(STORY).aastory $(AAMBUNDLE) + $(AAMBUNDLE) -t aambox --no-warn-style -o $@ $< + clean: rm -f *.out From cda5f26dc0e85563d8d80545c00ca2dc60172469 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sun, 27 Sep 2026 11:16:59 +0200 Subject: [PATCH 2/4] added docs/aam-specification-1.1-8bit.adoc for 6502, comments refer to spec --- antora/modules/ROOT/nav.adoc | 1 + docs/aam-specification-1.1-8bit.adoc | 156 +++++++++++++++++++++++++++ src/6502/engine.s | 12 +-- src/aamshow.c | 17 +-- src/aavm.h | 7 +- src/gen_usty.c | 22 ++-- 6 files changed, 176 insertions(+), 39 deletions(-) create mode 100644 docs/aam-specification-1.1-8bit.adoc diff --git a/antora/modules/ROOT/nav.adoc b/antora/modules/ROOT/nav.adoc index 31f850e..9563e81 100644 --- a/antora/modules/ROOT/nav.adoc +++ b/antora/modules/ROOT/nav.adoc @@ -1 +1,2 @@ * xref:index.adoc[] +* xref:8bit.adoc[] diff --git a/docs/aam-specification-1.1-8bit.adoc b/docs/aam-specification-1.1-8bit.adoc new file mode 100644 index 0000000..aaf4e46 --- /dev/null +++ b/docs/aam-specification-1.1-8bit.adoc @@ -0,0 +1,156 @@ += Å-machine 8-bit Addendum + +This document describes target-specific conventions that layer on top of the +main Å-machine specification, for the 6502 engines (Commodore 64, Apple II, +and the aambox test platform). All other parts of the main specification apply +unchanged. + +The main specification is the versioned document `aam-specification-1.1.adoc`; +where this addendum and the main document disagree about a 6502 target, this +addendum wins. + +== Changes in v1.1 + +* The `USTY` chunk replaces `LOOK` on 8-bit targets. +* 8-bit interpreters no longer parse CSS at startup. The style table is + precompiled by the bundler. + +== The USTY chunk + +`USTY` is produced by `aambundle` when targeting an 8-bit platform. It is a +direct substitute for `LOOK` in the same class index space, so every +`ENTER_DIV`, `ENTER_SPAN`, `SET_BODY`, and `ENTER_STATUS` operand keeps its +meaning; nothing in the `CODE` chunk needs rewriting. + +This chunk is not intended to be emitted by the Dialog compiler. The bundler +drops `LOOK` and creates `USTY` in its place. + +A `USTY` table is target-specific: it is built for the palette and +capabilities of the machine it ships beside. + +The bundler may produce a `.ustory` file containing the `USTY` chunk for +debugging purposes. These are not valid Aa-machine stories and should not +be distributed. + +=== Tag byte + +* `BYTE`: "tag", target in the high nibble, format version in the low nibble + +[cols="1,1,4"] +|=== +| Tag | Target | Notes +| `0x00` | aambox | test platform; no styling beyond geometry +| `0x10` | Commodore 64 | per-character foreground color +| `0x20` | Apple II | reverse video only +|=== + +The USTY format is identified by integer, which is truncated to 0..15 and +put in the low nibble. + +Version 0 is described here. + +=== Header + +* `BYTE`: "nclass", number of style classes (1..255) +* `BYTE`: "nxsty", number of body records (0..255) +* `BYTE`: reserved, written as 0 +* `WORD`: "totalwords", size in words of the resident table (big-endian) +* `WORD`: "xstyoff", offset of the body array from the record base, i.e. from + byte 8 (big-endian) + +Immediately after the header come `nclass` class records, eight bytes each, +indexed directly by class number: + +---- +0 width ; columns, or percent if relw +1 height ; rows, or percent if relh +2 margin-top ; rows +3 margin-bottom ; rows +4 styon ; style bits to set +5 styoff ; style bits to reset +6 flags ; see below +7 fg ; foreground color, $80 = inherit +---- + +`styon`/`styoff` use the same bit values as the main specification: + +---- +$01 reverse +$02 bold +$04 italic +$08 fixed +---- + +A bit that the target cannot render is masked out of both bytes before the +table is written, so it can be ignored by the interpreter: + +[cols="1,1,3"] +|=== +| Target | Mask | Notes +| aambox | `$00` | no styling +| Commodore 64 | `$01 \| $02 \| $04` | fixed is not rendered +| Apple II | `$01` | reverse only +|=== + +`fg` is a target palette index, or the sentinel `$80` meaning "inherit" (no +explicit color). Colors appear only on the Commodore 64, whose palette is the +standard 16-entry VIC-II palette; the parser accepts names, `#rgb`/`#rrggbb`, +`rgb()`/`rgba()`, and a bare index 0..15. On other targets the field is +always `$80`. + +If a class specifies both a color and bold/italic styling, the color takes +precedence on the Commodore 64: the frontend uses the explicit color instead +of the palette entry it would otherwise choose for the style bits. + +`inherit` and `initial` are not distinguished in version 0; both map to the +`$80` sentinel, and an element with no color inherits the enclosing explicit +color. + +=== Flags + +---- +$01 relw ; width is a percentage of the parent width +$02 relh ; height is a percentage of the parent height +$40 floatl ; float: left +$80 floatr ; float: right +---- + +Bits 2..5 are reserved and written as 0. + +=== The body array + +The class records are followed by the body array, at `xstyoff` bytes past the +record base. Each record is + +---- +0 index ; raw class index; $ff ends the array +1 datalen ; number of data bytes that follow +2 data[datalen] +---- + +and the array is a single `$ff` index byte when empty. The array is scanned, +not indexed, by the opcode that consumes it. +The chunk is padded to an even length so that `totalwords * 2` is exactly the +size of the resident table. + +No target emits body records in +version 0; the layout is fixed here so that they can be added without moving +the class records. + +== Conditional declarations + +A style declaration whose key begins `-iftf-sys--` applies only to the +named target, where `` is `c64`, `apple2`, or `aambox`. So + +---- +-iftf-sys-c64-color: red +---- + +sets the foreground color on the Commodore 64 and is ignored elsewhere. A +prefixed declaration overrides an unprefixed declaration of the same property, +regardless of their order in the style sheet. + +Because the bundler parses `LOOK` into `USTY`, it is the bundler that decides +what a declaration means. It warns about, and otherwise ignores, unrecognized +properties and unknown targets. The 8-bit interpreter never sees the original +style sheet. diff --git a/src/6502/engine.s b/src/6502/engine.s index e680963..8b4a9ea 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -166,6 +166,7 @@ SPC_SPACE = 3 SPC_LINE = 4 SPC_PAR = 5 +; USTY class record fields; see docs/aam-specification-1.1-8bit.adoc. STY_WIDTH = 0 STY_HEIGHT = 1 STY_MTOP = 2 @@ -173,7 +174,7 @@ STY_MBOTTOM = 3 STY_STYON = 4 STY_STYOFF = 5 STY_FLAGS = 6 -STY_FG = 7 ; $80 = inherit +STY_FG = 7 STYF_RELW = $01 STYF_RELH = $02 @@ -9934,14 +9935,7 @@ done initengine4 ; Read the style table from the USTY chunk. - ; The header is: - ; 0 tag 1 nclass - ; 2 nxsty 3 reserved - ; 4 totalwords (b-e) - ; 6 xstyoff (b-e) - ; followed by: - ; records[nclass * 8] - ; padded to totalwords*2 bytes. + ; The chunk layout is specified in docs/aam-specification-1.1-8bit.adoc. lda chnklsb+CH_USTY sta virdata+2 diff --git a/src/aamshow.c b/src/aamshow.c index 274d49e..40342dc 100644 --- a/src/aamshow.c +++ b/src/aamshow.c @@ -176,20 +176,9 @@ void decode_look(struct chunk *ch) { } // USTY: bundler-generated style table for the 6502 engines. -// (USTY_VERSION). -// -// 0 tag ; high nibble target, low nibble format version -// 1 nclass -// 2 nxsty -// 3 reserved -// 4-5 totalwords ; b-e, words of heap the two arrays need -// 6-7 xstyoff ; b-e, body array offset from the record base -// class records[nclass * USTY_RECSIZE] -// xsty[] ; (index, datalen, data[datalen]) records, -// ; ended by $ff in an index byte -// pad ; 0 or 1 bytes, to totalwords * 2 -// -// Keep in step with the record layouts in gen_usty.c. +// The chunk layout is specified in docs/aam-specification-1.1-8bit.adoc. +// Keep the field offsets and record layouts below in step with that +// document and with gen_usty.c. static void put_style_bits(uint8_t bits) { int first = 1; diff --git a/src/aavm.h b/src/aavm.h index f5a77ac..5cc5dae 100644 --- a/src/aavm.h +++ b/src/aavm.h @@ -176,9 +176,10 @@ #define AA_MAX_TEMP (REG_TMP - REG_X) // 48 -// USTY chunk (precomputed style table for the 6502 targets) -// The USTY version number is internal, mainly to prevent devs from confusing -// themselves, so it should be incremented often and can wrap (0..15) +// USTY chunk (precomputed style table for the 6502 targets). +// The chunk layout is specified in docs/aam-specification-1.1-8bit.adoc. +// USTY_VERSION is an internal format version (0..15), bumped to keep a +// stale table from being misread; it may wrap. #define USTY_VERSION 0 #define USTY_HDRSIZE 8 // USTY header size diff --git a/src/gen_usty.c b/src/gen_usty.c index d038671..51ef441 100644 --- a/src/gen_usty.c +++ b/src/gen_usty.c @@ -8,10 +8,10 @@ // ============================================================================ // USTY chunk generation: precomputed style table for the 6502 engines. +// The chunk layout is specified in docs/aam-specification-1.1-8bit.adoc. // -// Parses the LOOK chunk (CSS declarations) once, in C, and emits the compact -// binary table defined in aamshow.c. USTY replaces LOOK on the 6502 -// targets which have no style sheet parser. +// Parses the LOOK chunk (CSS declarations) once, in C, and emits that table. +// USTY replaces LOOK on the 6502 targets, which have no style sheet parser. // // The parser mirrors both the dialog compiler's effective CSS subset // (~/if/dialog/src/frontend.c:2899-3000): case-insensitive keys, @@ -562,12 +562,8 @@ static styclass *parse_look(int *nclassp) { return cls; } -// Build the USTY chunk (revision 13). -// -// The body array is a list of (index, datalen, data...) records, ended by -// a single $ff index byte; datalen is the per-target payload size. The -// payload is padded to an even length after the header so that -// totalwords * 2 is exactly the number of bytes the engine reads. +// Build the USTY chunk. See docs/aam-specification-1.1-8bit.adoc for the +// layout. static uint8_t *build_usty_flat(uint32_t *sizep) { styclass *cls; @@ -597,10 +593,10 @@ static uint8_t *build_usty_flat(uint32_t *sizep) { out[0] = sty_target->tag | USTY_VERSION; out[1] = nclass; - // out[2] = nxsty, filled in below. - // out[3] stays 0: reserved (revision 12's xstysize stride byte). - out[6] = (xstyoffs - recoffs) >> 8; // from the record base, which is - out[7] = (xstyoffs - recoffs) & 0xff; // the pointer the engine holds + // out[2] = nxsty, filled in below; out[3] is reserved and stays 0. + // out[6..7] = xstyoff, relative to the record base. + out[6] = (xstyoffs - recoffs) >> 8; + out[7] = (xstyoffs - recoffs) & 0xff; for(i = 0; i < nclass; i++) { make_flat(out + recoffs + i * USTY_RECSIZE, &cls[i]); From 967c9fc0d5aac6a6ce9c4583b05a462ed71eee82 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sun, 27 Sep 2026 11:30:47 +0200 Subject: [PATCH 3/4] 6502: removed RGB color parsing --- docs/aam-specification-1.1-8bit.adoc | 32 +++++++-- src/gen_usty.c | 103 ++++++--------------------- 2 files changed, 47 insertions(+), 88 deletions(-) diff --git a/docs/aam-specification-1.1-8bit.adoc b/docs/aam-specification-1.1-8bit.adoc index aaf4e46..85e4f91 100644 --- a/docs/aam-specification-1.1-8bit.adoc +++ b/docs/aam-specification-1.1-8bit.adoc @@ -93,10 +93,30 @@ table is written, so it can be ignored by the interpreter: |=== `fg` is a target palette index, or the sentinel `$80` meaning "inherit" (no -explicit color). Colors appear only on the Commodore 64, whose palette is the -standard 16-entry VIC-II palette; the parser accepts names, `#rgb`/`#rrggbb`, -`rgb()`/`rgba()`, and a bare index 0..15. On other targets the field is -always `$80`. +explicit color). + +Colors appear only on the Commodore 64, whose palette is the +standard 16-entry VIC-II palette. The parser accepts one of the canonical +color names below, or a bare index 0..15. + +[cols="1,2,2,1,2,2"] +|=== +| Index | Name | Reference | Index | Name | Reference + +| 0 | `black` | `#000000` | 8 | `orange` | `#dd8855` +| 1 | `white` | `#ffffff` | 9 | `brown` | `#664400` +| 2 | `red` | `#880000` | 10 | `lightred` | `#ff7777` +| 3 | `cyan` | `#aaffee` | 11 | `darkgrey` | `#333333` +| 4 | `purple` | `#cc44cc` | 12 | `mediumgrey` | `#777777` +| 5 | `green` | `#00cc55` | 13 | `lightgreen` | `#aaff66` +| 6 | `blue` | `#0000aa` | 14 | `lightblue` | `#0088ff` +| 7 | `yellow` | `#eeee77` | 15 | `lightgrey` | `#bbbbbb` +|=== + +The reference RGB values are given only to help choose a color; the bundler +does not accept RGB notation in a style declaration. +Hex (`#rgb`/`#rrggbb`) and `rgb()`/`rgba()` values are rejected with a +warning. If a class specifies both a color and bold/italic styling, the color takes precedence on the Commodore 64: the frontend uses the explicit color instead @@ -109,8 +129,8 @@ color. === Flags ---- -$01 relw ; width is a percentage of the parent width -$02 relh ; height is a percentage of the parent height +$01 relw ; width is a percentage of the parent width (0-100) +$02 relh ; height is a percentage of the parent height (0-100) $40 floatl ; float: left $80 floatr ; float: right ---- diff --git a/src/gen_usty.c b/src/gen_usty.c index 51ef441..cef2ada 100644 --- a/src/gen_usty.c +++ b/src/gen_usty.c @@ -71,16 +71,11 @@ typedef struct { } styclass; // ---------------------------------------------------------------------------- -// Color mapping. The C64 has a 16-color palette; web colors are reduced to -// the nearest VIC-II color by RGB distance. "inherit", "initial" and -// "transparent" (or alpha 0) leave the field unset. - -static const uint8_t c64_rgb[16][3] = { - {0, 0, 0}, {255, 255, 255}, {136, 0, 0}, {170, 255, 238}, - {204, 68, 204}, {0, 204, 85}, {0, 0, 170}, {238, 238, 119}, - {221, 136, 85}, {102, 68, 0}, {255, 119, 119}, {51, 51, 51}, - {119, 119, 119}, {170, 255, 102}, {0, 136, 255}, {187, 187, 187} -}; +// Color mapping. The C64 has a fixed 16-color palette. Because it is so +// small, colors have to be named explicitly (or given as a bare 0..15 +// index); the bundler does not approximate an arbitrary RGB value, so the +// author always knows which palette entry they are getting. "inherit", +// "initial" and "transparent" (or alpha 0) leave the field unset. // CSS color names map by name to their obvious VIC-II counterparts static const struct { @@ -98,13 +93,13 @@ static const struct { {"yellow", 7}, {"orange", 8}, {"brown", 9}, - {"pink", 10}, + {"lightred", 10}, {"darkgrey", 11}, {"mediumgrey", 12}, {"lightgreen", 13}, {"lightblue", 14}, {"lightgrey", 15}, - // synonyms + // synonyms (undocumented) {"maroon", 2}, {"aqua", 3}, {"teal", 3}, @@ -112,9 +107,8 @@ static const struct { {"fuchsia", 4}, {"lime", 5}, {"navy", 6}, - {"lightred", 10}, - {"gray", 11}, - {"grey", 11}, + {"pink", 10}, + {"mediumgray", 12}, {"darkgray", 11}, {"lightgray", 15}, {"silver", 15}, @@ -141,42 +135,16 @@ static void swarn(const char *fmt, ...) { warning(WARN_STYLE, "style class %s: %s", sty_name ? sty_name : "(unknown)", msg); } -// Map an rgb triplet to the nearest VIC-II color, using a perceptual -// distance (weighted RGB, a standard cheap approximation of CIE lightness). -static int rgb_to_c64(int r, int g, int b) { - int best = 0, bestdist = 0x7fffffff; - int i; - - for(i = 0; i < 16; i++) { - long dr = r - c64_rgb[i][0]; - long dg = g - c64_rgb[i][1]; - long db = b - c64_rgb[i][2]; - long dist = 2 * dr * dr + 4 * dg * dg + 3 * db * db; - if(dist < bestdist) { - bestdist = dist; - best = i; - } - } - if(bestdist > (2+3+4)*64*64) { - swarn("The color #%02x%02x%02x is not accurately represented on %s, the closest is %s (index %d, #%02x%02x%02x).", - r, g, b, sty_target->name, css2vic[best].name, best, - c64_rgb[best][0], c64_rgb[best][1], c64_rgb[best][2]); - } - return best; -} - -// Hex digit to value. -static int hex(char c) { - return c >= '0' && c <= '9'? c - '0' : (c | 0x20) - 'a' + 10; -} - -// Parse " #rgb", "#rrggbb", "rgb(r,g,b)", "rgba(r,g,b,a)", a name, -// or a specific VIC-II color index (0..15). +// Parse a named VIC-II color or a bare palette index (0..15). // Returns 1 and sets *out to a palette index; // returns 0 if the value is not a usable color -// (inherit, initial, transparent, alpha 0, garbage). +// (inherit, initial, transparent, hex, rgb(), garbage). +// +// Hex (#rgb/#rrggbb) and rgb()/rgba() are deliberately rejected. The VIC-II +// palette is fixed and small, so an author has to name the color they want +// (or pick an index) instead of having the bundler approximate it. static int parse_color(const char *v, int *out) { - int i, r = 0, g = 0, b = 0, alpha = 255, n; + int i, n; while(*v == ' ' || *v == '\t') v++; @@ -187,36 +155,11 @@ static int parse_color(const char *v, int *out) { } if(v[0] == '#') { - v++; - n = 0; - while(v[n] && ((v[n] >= '0' && v[n] <= '9') || (v[n] >= 'a' && v[n] <= 'f') || (v[n] >= 'A' && v[n] <= 'F'))) { - n++; - } - if(n == 3) { - r = hex(v[0]) * 0x11; - g = hex(v[1]) * 0x11; - b = hex(v[2]) * 0x11; - }else if(n == 6) { - r = (hex(v[0]) << 4) | hex(v[1]); - g = (hex(v[2]) << 4) | hex(v[3]); - b = (hex(v[4]) << 4) | hex(v[5]); - } else { - return 0; - } - } else if(!strncmp(v, "rgb", 3)) { - float af; - v += 3; - if(*v == 'a') v++; - if(*v != '(') return 0; - v++; - if(sscanf(v, "%d , %d , %d , %f", &r, &g, &b, &af) == 4) { - // rgba: alpha is a 0..1 float - alpha = (int)(af * 255 + 0.5); - alpha = alpha < 0? 0 : alpha > 255? 255 : alpha; - if(alpha == 0) return 0; - } else { - if(sscanf(v, "%d , %d , %d", &r, &g, &b) != 3) return 0; - } + swarn("Hex colors are not supported on %s; use one of the 16 named C64 colors or an index 0..15.", sty_target->name); + return 0; + } else if(!strncmp(v, "rgb(", 4) || !strncmp(v, "rgba(", 5)) { + swarn("rgb()/rgba() colors are not supported on %s; use one of the 16 named C64 colors or an index 0..15.", sty_target->name); + return 0; } else if(*v >= '0' && *v <= '9') { // Naked VIC color index: 0..15 selects the palette entry itself, // so 3 is cyan no matter what hue CSS would call it. Reject @@ -246,10 +189,6 @@ static int parse_color(const char *v, int *out) { *out = vic; return 1; } - - // Hex and rgb() paths land here with r/g/b filled in. - *out = rgb_to_c64(r, g, b); - return 1; } // ---------------------------------------------------------------------------- From d8850e9c123ea1222fa051abaa5e2ec2c4664fca Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sun, 27 Sep 2026 11:38:28 +0200 Subject: [PATCH 4/4] 6502: support color: initial/inherit --- docs/aam-specification-1.1-8bit.adoc | 20 +++++++++++++------- src/6502/engine.s | 13 ++++++++++--- src/aamshow.c | 5 +++-- src/aavm.h | 6 ++++++ src/gen_usty.c | 21 ++++++++++++++------- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/docs/aam-specification-1.1-8bit.adoc b/docs/aam-specification-1.1-8bit.adoc index 85e4f91..9b79250 100644 --- a/docs/aam-specification-1.1-8bit.adoc +++ b/docs/aam-specification-1.1-8bit.adoc @@ -69,7 +69,7 @@ indexed directly by class number: 4 styon ; style bits to set 5 styoff ; style bits to reset 6 flags ; see below -7 fg ; foreground color, $80 = inherit +7 fg ; foreground color, $80 = inherit, $81 = initial ---- `styon`/`styoff` use the same bit values as the main specification: @@ -92,8 +92,18 @@ table is written, so it can be ignored by the interpreter: | Apple II | `$01` | reverse only |=== -`fg` is a target palette index, or the sentinel `$80` meaning "inherit" (no -explicit color). +`fg` is a target palette index, or one of two sentinels, which are +recognized by having bit 7 set: + +[cols="1,4"] +|=== +| Value | Meaning + +| `$80` | `inherit`. Keep the explicit color selected by the enclosing +elements. This is a no-op, and is also what `transparent` maps to. +| `$81` | `initial`. Discard the explicit color selected by the enclosing +elements and fall back to the color implied by the current style bits. +|=== Colors appear only on the Commodore 64, whose palette is the standard 16-entry VIC-II palette. The parser accepts one of the canonical @@ -122,10 +132,6 @@ If a class specifies both a color and bold/italic styling, the color takes precedence on the Commodore 64: the frontend uses the explicit color instead of the palette entry it would otherwise choose for the style bits. -`inherit` and `initial` are not distinguished in version 0; both map to the -`$80` sentinel, and an element with no color inherits the enclosing explicit -color. - === Flags ---- diff --git a/src/6502/engine.s b/src/6502/engine.s index 8b4a9ea..f0d132c 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -181,6 +181,9 @@ STYF_RELH = $02 STYF_FLOATL = $40 STYF_FLOATR = $80 +STYF_NOCOLOR = $80 ; fg = inherit (keep enclosing explicit color) +STYF_INITIAL = $81 ; fg = initial (drop it, use the style-derived color) + engine_firstaddr swapin @@ -544,7 +547,7 @@ unstyle bne skip #if FGCOLOR - lda #$80 + lda #STYF_NOCOLOR sta rfgcol #endif lda #0 @@ -581,7 +584,11 @@ loop #if FGCOLOR ldy #STY_FG lda (phydata),y - bmi nofgcol + bpl setfgcol + cmp #STYF_INITIAL + bne nofgcol ; $80 = inherit, leave rfgcol alone + lda #STYF_NOCOLOR ; $81 = initial, discard outer colour +setfgcol sta rfgcol nofgcol #endif @@ -708,7 +715,7 @@ clrdone restartvm .( #if FGCOLOR - lda #$80 + lda #STYF_NOCOLOR sta rfgcol #endif lda #0 diff --git a/src/aamshow.c b/src/aamshow.c index 40342dc..7ced697 100644 --- a/src/aamshow.c +++ b/src/aamshow.c @@ -223,10 +223,11 @@ static void decode_usty_records(uint8_t *d, uint8_t tag, uint32_t recoffs, printf(" off="); put_style_bits(r[USTY_F_STYOFF]); } - if(r[USTY_F_FG] != 0x80) printf(" fg=%02x", r[USTY_F_FG]); + if(r[USTY_F_FG] == USTY_FG_INITIAL) printf(" fg=initial"); + else if(r[USTY_F_FG] != USTY_FG_INHERIT) printf(" fg=%02x", r[USTY_F_FG]); // An all-default class still has a record here if(!r[0] && !r[1] && !r[2] && !r[3] - && !r[4] && !r[5] && !r[6] && r[7] == 0x80) { + && !r[4] && !r[5] && !r[6] && r[7] == USTY_FG_INHERIT) { printf(" all defaults"); } printf("\n"); diff --git a/src/aavm.h b/src/aavm.h index 5cc5dae..ea2d7e5 100644 --- a/src/aavm.h +++ b/src/aavm.h @@ -194,6 +194,12 @@ #define USTY_F_FLAGS 6 #define USTY_F_FG 7 +// USTY_F_FG holds a target palette index (0..15 on the C64) or one of these +// sentinels. Both have bit 7 set, which is how the engines tell a sentinel +// from a palette entry. +#define USTY_FG_INHERIT 0x80 // keep the enclosing explicit color (no-op) +#define USTY_FG_INITIAL 0x81 // discard it; use the style-derived color + #define USTY_FL_RELW 0x01 #define USTY_FL_RELH 0x02 #define USTY_FL_FLOATL 0x40 diff --git a/src/gen_usty.c b/src/gen_usty.c index cef2ada..cedf93d 100644 --- a/src/gen_usty.c +++ b/src/gen_usty.c @@ -28,7 +28,8 @@ #define STY_RELW 0x01 #define STY_RELH 0x02 -#define NOCOLOR 0x80 // "$80 = not set" sentinel for the sty fg field +#define NOCOLOR 0x80 // "$80 = inherit" sentinel for the sty fg field +#define INITIAL 0x81 // "$81 = initial" sentinel: reset explicitly-coloured ancestors struct sty_target { const char *name; @@ -63,7 +64,7 @@ static int sty_emitted; // Per-class record, one for every class in LOOK. typedef struct { uint8_t styon, styoff; // AASTYLE_* bits - uint8_t fg; // c64 palette index, NOCOLOR = inherit + uint8_t fg; // c64 palette index, NOCOLOR = inherit, INITIAL = initial uint8_t mtop, mbottom; // top/bottom margins in rows uint8_t width, height; // width/height in columns uint8_t flo; // 0 none, 1 left, 2 right @@ -136,9 +137,9 @@ static void swarn(const char *fmt, ...) { } // Parse a named VIC-II color or a bare palette index (0..15). -// Returns 1 and sets *out to a palette index; -// returns 0 if the value is not a usable color -// (inherit, initial, transparent, hex, rgb(), garbage). +// Returns 1 and sets *out to a palette index, NOCOLOR (inherit/transparent) +// or INITIAL; returns 0 if the value is not a usable color +// (hex, rgb(), garbage). // // Hex (#rgb/#rrggbb) and rgb()/rgba() are deliberately rejected. The VIC-II // palette is fixed and small, so an author has to name the color they want @@ -148,10 +149,16 @@ static int parse_color(const char *v, int *out) { while(*v == ' ' || *v == '\t') v++; + // transparent has no meaning for text on these targets; treat it as + // inherit, i.e. leave whatever explicit color the enclosing element set. if(!strncmp(v, "inherit", 7) - || !strncmp(v, "initial", 7) || !strncmp(v, "transparent", 11)) { - return 0; + *out = NOCOLOR; + return 1; + } + if(!strncmp(v, "initial", 7)) { + *out = INITIAL; + return 1; } if(v[0] == '#') {