package apple2 import ( "fmt" "image" "image/color" "strings" "time" ) const ( charWidth = 7 charHeight = 8 textColumns = 40 textLines = 24 textLinesMix = 4 textPage1Address = uint16(0x0400) textPage2Address = uint16(0x0800) textPageSize = uint16(0x0400) ) func getTextCharOffset(col int, line int) uint16 { // See "Understanding the Apple II", page 5-10 // http://www.applelogic.org/files/UNDERSTANDINGTHEAII.pdf section := line / 8 // Top, middle and bottom eigth := line % 8 return uint16(section*40 + eigth*0x80 + col) } func snapshotTextMode(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool, light color.Color) *image.RGBA { text, columns, lines := getActiveText(a, is80Columns, isSecondPage, isMixMode) return renderTextMode(a, text, columns, lines, light) } func getActiveText(a *Apple2, is80Columns bool, isSecondPage bool, isMixMode bool) ([]uint8, int, int) { lines := textLines if isMixMode { lines = textLinesMix } if !is80Columns { text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage, isMixMode) return text40Columns, textColumns, lines } text40Columns := getTextFromMemory(a.mmu.physicalMainRAM, isSecondPage, isMixMode) text40ColumnsAlt := getTextFromMemory(a.mmu.physicalMainRAMAlt, isSecondPage, isMixMode) // Merge the two 40 cols to return 80 cols text80Columns := make([]uint8, 2*len(text40Columns)) for i := 0; i < len(text40Columns); i++ { text80Columns[2*i] = text40ColumnsAlt[i] text80Columns[2*i+1] = text40Columns[i] } return text80Columns, textColumns * 2, lines } func getTextFromMemory(mem *memoryRange, isSecondPage bool, isMixMode bool) []uint8 { lineStart := 0 if isMixMode { lineStart = textLines - textLinesMix } addressStart := textPage1Address if isSecondPage { addressStart = textPage2Address } addressEnd := addressStart + textPageSize data := mem.subRange(addressStart, addressEnd) lines := textLines - lineStart text := make([]uint8, lines*textColumns) for l := 0; l < lines; l++ { for c := 0; c < textColumns; c++ { char := data[getTextCharOffset(c, l+lineStart)] text[textColumns*l+c] = char } } return text } func renderTextMode(a *Apple2, text []uint8, columns int, lines int, light color.Color) *image.RGBA { // Flash mode is 2Hz isFlashedFrame := time.Now().Nanosecond() > (1 * 1000 * 1000 * 1000 / 2) width := columns * charWidth height := lines * charHeight size := image.Rect(0, 0, width, height) img := image.NewRGBA(size) for x := 0; x < width; x++ { for y := 0; y < height; y++ { line := y / charHeight col := x / charWidth rowInChar := y % charHeight colInChar := x % charWidth char := text[line*columns+col] var pixel bool if a.isApple2e { isAltText := a.io.isSoftSwitchActive(ioFlagAltChar) vid6 := (char & 0x40) != 0 vid7 := (char & 0x80) != 0 char := char & 0x3f if vid6 && (vid7 || isAltText) { char += 0x40 } if vid7 || (vid6 && isFlashedFrame && !isAltText) { char += 0x80 } pixel = !a.cg.getPixel(char, rowInChar, colInChar) } else { pixel = a.cg.getPixel(char, rowInChar, colInChar) topBits := char >> 6 isInverse := topBits == 0 isFlash := topBits == 1 pixel = pixel != (isInverse || (isFlash && isFlashedFrame)) } var colour color.Color if pixel { colour = light } else { colour = color.Black } img.Set(x, y, colour) } } return img } // DumpTextModeAnsi returns the text mode contents using ANSI escape codes // for reverse and flash func DumpTextModeAnsi(a *Apple2) string { is80Columns := a.io.isSoftSwitchActive(ioFlag80Col) isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active text, columns, lines := getActiveText(a, is80Columns, isSecondPage, false /*isMixedMode*/) content := "\n" content += fmt.Sprintln(strings.Repeat("#", columns+4)) isAltText := a.isApple2e && a.io.isSoftSwitchActive(ioFlagAltChar) for l := 0; l < lines; l++ { line := "" for c := 0; c < columns; c++ { char := text[l*columns+c] line += textMemoryByteToString(char, isAltText) } content += fmt.Sprintf("# %v #\n", line) } content += fmt.Sprintln(strings.Repeat("#", columns+4)) return content } func textMemoryByteToString(value uint8, isAltCharSet bool) string { // See https://en.wikipedia.org/wiki/Apple_II_character_set // Supports the new lowercase characters in the Apple2e // Only ascii from 0x20 to 0x5F is visible topBits := value >> 6 isInverse := topBits == 0 isFlash := topBits == 1 if isFlash && isAltCharSet { // On the Apple2e with lowercase chars there is not flash mode. isFlash = false isInverse = true } if isAltCharSet { value = value & 0x7F } else { value = value & 0x3F } if value < 0x20 { value += 0x40 } if value == 0x7f { // DEL is full box value = '_' } if isFlash { if value == ' ' { // Flashing space in Apple is the full box. It can't be done with ANSI codes value = '_' } return fmt.Sprintf("\033[5m%v\033[0m", string(value)) } else if isInverse { return fmt.Sprintf("\033[7m%v\033[0m", string(value)) } else { return string(value) } }