From 00e4476e86281fd7cbf2dc043eaf77d6dd081778 Mon Sep 17 00:00:00 2001 From: Ivan Izaguirre Date: Sat, 28 Sep 2019 13:37:42 +0200 Subject: [PATCH] Support the ThunderClock Plus card. Partial mulation of the microPD1990AC integrated circuit. --- README.md | 9 ++- apple2Setup.go | 7 ++ apple2main.go | 8 ++- apple2sdl/apple2.state | Bin 0 -> 65696 bytes cardBase.go | 23 +++++-- cardThunderClockPlus.go | 49 ++++++++++++++ memoryManager.go | 56 ++++++++++++---- microPD1990ac.go | 119 ++++++++++++++++++++++++++++++++++ romdumps/generate/generate.go | 2 +- romdumps/romdumps_vfsdata.go | 12 +++- 10 files changed, 261 insertions(+), 24 deletions(-) create mode 100644 apple2sdl/apple2.state create mode 100644 cardThunderClockPlus.go create mode 100644 microPD1990ac.go diff --git a/README.md b/README.md index 02d955a..c7396f7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Portable emulator of an Apple II+. Written in Go. - Emulated extension cards: - DiskII controller - 16Kb Language Card - - 256Kb Saturn RAM Card + - 256Kb Saturn RAM + - ThunderClock Plus real time clock - Graphic modes: - Text, Lores and Hires - Displays: @@ -40,7 +41,7 @@ casa@servidor:~$ ./apple2sdl ![DOS 3.3 started](doc/dos33.png) ### Play games -Download an DSK file locally or use the a link ([Asimov](https://www.apple.asimov.net/images/) is an excellent source) with the `-disk` parameter: +Download a DSK file locally or use an URL ([Asimov](https://www.apple.asimov.net/images/) is an excellent source) with the `-disk` parameter: ``` casa@servidor:~$ ./apple2sdl -disk "https://www.apple.asimov.net/images/games/action/karateka/karateka (includes intro).dsk" ``` @@ -88,7 +89,7 @@ Line: - F6: Toggle between NTSC color TV and green phosphor monochrome monitor - F7: Save current state to disk - F8: Restore state from disk -- F10: Cycle character generator codepages. Only if the character generator ROM has more than one 2Kb pages. +- F10: Cycle character generator codepages. Only if the character generator ROM has more than one 2Kb page. - F12: Save a screen snapshot to a file `snapshot.png` Only valid on SDL mode @@ -122,6 +123,8 @@ Only valid on SDL mode main rom file (default "/Apple2_Plus.rom") -saturnCardSlot int slot for the 256kb Saturn card. -1 for none (default -1) + -thunderClockCardSlot int + slot for the ThunderClock Plus card. -1 for none (default 5) -traceCpu dump to the console the CPU execution -traceSS diff --git a/apple2Setup.go b/apple2Setup.go index 832506e..342b736 100644 --- a/apple2Setup.go +++ b/apple2Setup.go @@ -87,6 +87,13 @@ func (a *Apple2) AddSaturnCard(slot int) { a.insertCard(&cardSaturn{}, slot) } +// AddThunderClockPlusCard inserts a ThunderClock Plus clock card +func (a *Apple2) AddThunderClockPlusCard(slot int, romFile string) { + var c cardThunderClockPlus + c.loadRom(romFile) + a.insertCard(&c, slot) +} + // AddCardLogger inserts a fake card that logs accesses func (a *Apple2) AddCardLogger(slot int) { a.insertCard(&cardLogger{}, slot) diff --git a/apple2main.go b/apple2main.go index 6683353..6a4c0a5 100644 --- a/apple2main.go +++ b/apple2main.go @@ -39,6 +39,10 @@ func MainApple() *Apple2 { "saturnCardSlot", -1, "slot for the 256kb Saturn card. -1 for none") + thunderClockCardSlot := flag.Int( + "thunderClockCardSlot", + 5, + "slot for the ThunderClock Plus card. -1 for none") mono := flag.Bool( "mono", false, @@ -49,7 +53,6 @@ func MainApple() *Apple2 { true, "set fast mode when the disks are spinning", ) - panicSS := flag.Bool( "panicss", false, @@ -94,6 +97,9 @@ func MainApple() *Apple2 { if *saturnCardSlot >= 0 { a.AddSaturnCard(*saturnCardSlot) } + if *thunderClockCardSlot > 0 { + a.AddThunderClockPlusCard(*thunderClockCardSlot, "/ThunderclockPlusROM.bin") + } if *disk2Slot >= 0 { a.AddDisk2(*disk2Slot, *disk2RomFile, *diskImage) } diff --git a/apple2sdl/apple2.state b/apple2sdl/apple2.state new file mode 100644 index 0000000000000000000000000000000000000000..2419393bca7b2520b21f643c99f9908b9c53ae84 GIT binary patch literal 65696 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 Hk;H!jZaWw& literal 0 HcmV?d00001 diff --git a/cardBase.go b/cardBase.go index 14d7f33..9d54279 100644 --- a/cardBase.go +++ b/cardBase.go @@ -11,11 +11,12 @@ type card interface { } type cardBase struct { - a *Apple2 - rom *memoryRange - slot int - ssr [16]softSwitchR - ssw [16]softSwitchW + a *Apple2 + rom *memoryRange + romExtra *memoryRange + slot int + ssr [16]softSwitchR + ssw [16]softSwitchW } func (c *cardBase) loadRom(filename string) { @@ -23,7 +24,12 @@ func (c *cardBase) loadRom(filename string) { panic("Rom must be loaded before inserting the card in the slot") } data := loadResource(filename) - c.rom = newMemoryRange(0, data) + if len(data) >= 0x100 { + c.rom = newMemoryRange(0, data) + } + if len(data) >= 0x800 { + c.romExtra = newMemoryRange(0, data) + } } func (c *cardBase) assign(a *Apple2, slot int) { @@ -34,6 +40,11 @@ func (c *cardBase) assign(a *Apple2, slot int) { a.mmu.setPagesRead(uint8(0xc0+slot), uint8(0xc0+slot), c.rom) } + if slot != 0 && c.romExtra != nil { + c.romExtra.base = uint16(0xc800) + a.mmu.prepareCardExtraRom(slot, c.romExtra) + } + for i := 0; i < 0x10; i++ { a.io.addSoftSwitchR(uint8(0xC80+slot*0x10+i), c.ssr[i]) a.io.addSoftSwitchW(uint8(0xC80+slot*0x10+i), c.ssw[i]) diff --git a/cardThunderClockPlus.go b/cardThunderClockPlus.go new file mode 100644 index 0000000..882ab5a --- /dev/null +++ b/cardThunderClockPlus.go @@ -0,0 +1,49 @@ +package apple2 + +/* +ThunderClock`, real time clock card. + +See: + https://ia800706.us.archive.org/22/items/ThunderClock_Plus/ThunderClock_Plus.pdf + https://prodos8.com/docs/technote/01/ + https://www.semiee.com/file/backup/NEC-D1990.pdf + + +uPD1990AC hookup: + bit 0 = data in + bit 1 = CLK + bit 2 = STB + bit 3 = C0 + bit 4 = C1 + bit 5 = C2 + bit 7 = data out +*/ + +type cardThunderClockPlus struct { + microPD1990ac + cardBase +} + +func (c *cardThunderClockPlus) assign(a *Apple2, slot int) { + c.ssr[0] = func(*ioC0Page) uint8 { + bit := c.microPD1990ac.out() + // Get the next data bit from uPD1990AC on the MSB + if bit { + return 0x80 + } + return 0 + } + + c.ssw[0] = func(_ *ioC0Page, value uint8) { + dataIn := (value & 0x01) == 1 + clock := ((value >> 1) & 0x01) == 1 + strobe := ((value >> 2) & 0x01) == 1 + command := (value >> 3) & 0x07 + /* fmt.Printf("[cardThunderClock] dataIn %v, clock %v, strobe %v, command %v.\n", + dataIn, clock, strobe, command) */ + + c.microPD1990ac.in(clock, strobe, command, dataIn) + } + + c.cardBase.assign(a, slot) +} diff --git a/memoryManager.go b/memoryManager.go index 12b525b..cc15c57 100644 --- a/memoryManager.go +++ b/memoryManager.go @@ -1,6 +1,7 @@ package apple2 import ( + "encoding/binary" "io" ) @@ -14,9 +15,13 @@ type memoryManager struct { activeMemoryWrite [256]memoryHandler // Pages prepared to be paged in and out - physicalMainRAM *memoryRange // 0x0000 to 0xbfff, Up to 48 Kb - physicalROM *memoryRange // 0xd000 to 0xffff, 12 Kb - physicalROMe *memoryRange // 0xc000 to 0xcfff, Zero or 4bk in the Apple2e + physicalMainRAM *memoryRange // 0x0000 to 0xbfff, Up to 48 Kb + physicalROM memoryHandler // 0xd000 to 0xffff, 12 Kb + physicalROMe memoryHandler // 0xc000 to 0xcfff, Zero or 4bk in the Apple2e + + // Pages prapared for optional card ROM banks + activeSlot uint8 + cardsROMExtra [8]memoryHandler // 0xc800 to 0xcfff. for each card } type memoryHandler interface { @@ -28,14 +33,28 @@ const ( ioC8Off uint16 = 0xCFFF ) -// Peek returns the data on the given address -func (mmu *memoryManager) Peek(address uint16) uint8 { +func (mmu *memoryManager) access(address uint16, activeMemory [256]memoryHandler) memoryHandler { if address == ioC8Off { mmu.resetSlotExpansionRoms() } hi := uint8(address >> 8) - mh := mmu.activeMemoryRead[hi] + if hi >= 0xC1 && hi <= 0xC7 { + slot := hi - 0xC0 + if slot != mmu.activeSlot { + mmu.activateCardRomExtra(slot) + } + } + mh := activeMemory[hi] + if mh == nil { + return nil + } + return mh +} + +// Peek returns the data on the given address +func (mmu *memoryManager) Peek(address uint16) uint8 { + mh := mmu.access(address, mmu.activeMemoryRead) if mh == nil { return 0xf4 // Or some random number } @@ -44,11 +63,7 @@ func (mmu *memoryManager) Peek(address uint16) uint8 { // Poke sets the data at the given address func (mmu *memoryManager) Poke(address uint16, value uint8) { - if address == ioC8Off { - mmu.resetSlotExpansionRoms() - } - hi := uint8(address >> 8) - mh := mmu.activeMemoryWrite[hi] + mh := mmu.access(address, mmu.activeMemoryWrite) if mh == nil { return } @@ -82,15 +97,29 @@ func (mmu *memoryManager) setPagesWrite(begin uint8, end uint8, mh memoryHandler } } +func (mmu *memoryManager) prepareCardExtraRom(slot int, mh memoryHandler) { + mmu.cardsROMExtra[slot] = mh +} + // When 0xcfff is accessed the card expansion rom is unassigned func (mmu *memoryManager) resetSlotExpansionRoms() { if mmu.apple2.io.isSoftSwitchActive(ioFlagIntCxRom) { // Ignore if the Apple2 shadow ROM is active return } + mmu.activeSlot = 0 mmu.setPagesRead(0xc8, 0xcf, nil) } +// When a card base ROM is accesed the extra rom is assigned if available +func (mmu *memoryManager) activateCardRomExtra(slot uint8) { + //fmt.Printf("Activate slot %v\n", slot) + if mmu.cardsROMExtra[slot] != nil { + mmu.setPagesRead(0xC8, 0xCF, mmu.cardsROMExtra[slot]) + } + mmu.activeSlot = slot +} + func (mmu *memoryManager) resetRomPaging() { // Assign the first 12kb of ROM from 0xd000 to 0xffff mmu.setPagesRead(0xd0, 0xff, mmu.physicalROM) @@ -114,9 +143,14 @@ func newMemoryManager(a *Apple2) *memoryManager { func (mmu *memoryManager) save(w io.Writer) { mmu.physicalMainRAM.save(w) + binary.Write(w, binary.BigEndian, mmu.activeSlot) + } func (mmu *memoryManager) load(r io.Reader) { mmu.physicalMainRAM.load(r) + binary.Read(r, binary.BigEndian, &mmu.activeSlot) + mmu.activateCardRomExtra(mmu.activeSlot) + mmu.resetBaseRamPaging() } diff --git a/microPD1990ac.go b/microPD1990ac.go new file mode 100644 index 0000000..0e9f733 --- /dev/null +++ b/microPD1990ac.go @@ -0,0 +1,119 @@ +package apple2 + +import ( + "fmt" + "time" +) + +/* + microPD1990ac Serial I/O Calendar Clock IC + See: + https://www.semiee.com/file/backup/NEC-D1990.pdf + + Used by the ThunderClock+ real time clock card. + + The 40 bit register has 5 bytes (10 nibbles): + byte 4: + month, binary from 1 to 12 + day of week, BCD 0 to 6 + byte 3: day of month, BCD 1 to 31 + byte 2: hour, BCD 0 to 23 + byte 1: minute, BCD 0 to 59 + byte 0: seconds, BCD 0 to 59 + +*/ + +type microPD1990ac struct { + clock bool // CLK state + strobe bool // STB state + command uint8 // C0, C1, C2 command. From 0 to 7 + register uint64 // 40 bit shift register +} + +const ( + mpd1990commandRegHold = 0 + mpd1990commandRegShift = 1 + mpd1990commandTimeSet = 2 + mpd1990commandTimeRead = 3 +) + +func (m *microPD1990ac) in(clock bool, strobe bool, command uint8, dataIn bool) { + // Detect signal raise + clockRaise := clock && !m.clock + strobeRaise := strobe && !m.strobe + + // Update signal status + m.clock = clock + m.strobe = strobe + + // On strobe raise, update command and execute if needed + if strobeRaise { + m.command = command + + switch m.command { + case mpd1990commandRegShift: + // Nothing to do + case mpd1990commandTimeRead: + m.loadTime() + default: + panic(fmt.Sprintf("PD1990ac command %v not implemented.", m.command)) + } + } + + // On clock raise, with shift enable, shift the register + if clockRaise && m.command == mpd1990commandRegShift { + // Rotate right the 40 bits of the shift register + lsb := m.register & 1 + m.register >>= 1 + m.register += lsb << 39 + } +} + +func (m *microPD1990ac) out() bool { + if m.command == mpd1990commandRegHold { + panic("Output on RegHold should be a 1Hz signal. Not implemented.") + } + + if m.command == mpd1990commandTimeRead { + panic("Output on RegHold should be a 512Hz signal with LSB. Not implemented.") + } + + // Return the LSB of the register shift + return (m.register & 1) == 1 +} + +func (m *microPD1990ac) loadTime() { + now := time.Now() + + var register uint64 + + register = uint64(now.Month()) + register <<= 4 + register += uint64(now.Weekday()) + + day := uint64(now.Day()) + register <<= 4 + register += day / 10 + register <<= 4 + register += day % 10 + + hour := uint64(now.Hour()) + register <<= 4 + register += hour / 10 + register <<= 4 + register += hour % 10 + + minute := uint64(now.Minute()) + register <<= 4 + register += minute / 10 + register <<= 4 + register += minute % 10 + + second := uint64(now.Second()) + register <<= 4 + register += second / 10 + register <<= 4 + register += second % 10 + + m.register = register +} diff --git a/romdumps/generate/generate.go b/romdumps/generate/generate.go index b935d84..9a98dae 100644 --- a/romdumps/generate/generate.go +++ b/romdumps/generate/generate.go @@ -1,4 +1,4 @@ -// To generate the resources put the files on a "files" subdirectory and run main +// To generate the resources put the files on a "files" subdirectory and run "go run generate.go" package main diff --git a/romdumps/romdumps_vfsdata.go b/romdumps/romdumps_vfsdata.go index 09c736a..492a9ae 100644 --- a/romdumps/romdumps_vfsdata.go +++ b/romdumps/romdumps_vfsdata.go @@ -19,7 +19,7 @@ var Assets = func() http.FileSystem { fs := vfsgen۰FS{ "/": &vfsgen۰DirInfo{ name: "/", - modTime: time.Date(2019, 6, 9, 16, 41, 30, 66545749, time.UTC), + modTime: time.Date(2019, 9, 24, 22, 1, 11, 324155917, time.UTC), }, "/Apple2_Plus.rom": &vfsgen۰CompressedFileInfo{ name: "Apple2_Plus.rom", @@ -91,6 +91,13 @@ var Assets = func() http.FileSystem { compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5a\xa4\xb0\x80\x61\x11\x73\x9b\x4d\x17\x97\x8a\xcd\x07\x01\x56\x1b\xcf\xff\x9a\x75\x1b\x38\xbc\x2e\xfc\x9e\x31\x37\x8c\xf9\xc4\x0b\x81\xa7\x0a\x11\xff\x77\xed\x65\x60\xe4\xe2\xe2\xe2\x6a\xd5\x5e\xb5\xb7\xef\xc0\xde\x9e\x03\x7b\xbb\x0e\xec\xed\x3c\xb0\x20\x60\x6f\xc3\x81\x19\x9a\xcc\x5c\xac\xda\xab\xf6\x36\x1e\x58\x19\xa6\xb0\xe2\x4f\x87\xc0\xeb\x56\xb5\x56\xdb\x56\xc7\x95\x1c\xad\xea\x12\x1c\x7b\x7b\x0e\x08\xfc\xf6\xbc\x7a\xe1\x3b\x98\x71\x72\xd5\x85\xcf\xaf\x20\xac\x69\x1f\x38\x35\x26\xdc\xf7\x5c\xfb\x41\xf5\xc2\xcd\x05\xcc\xad\x0e\x60\x51\xad\x56\x1b\x30\xad\x6a\xd3\x71\xe1\x8d\xc6\x51\xdb\x0b\xfb\x96\x3a\x1c\x75\xbc\xb0\x63\xc3\xf6\x05\x61\x2d\x36\x7b\x40\x52\x91\xd7\x98\x96\xd8\x74\xcc\x64\x60\xbe\xf0\x0e\x59\x64\xa2\xda\x89\x0b\xef\x61\xdc\x0b\xed\x0b\x18\x16\x85\x9d\x32\xf8\xbd\x51\x2d\x8e\x81\x59\x0b\x84\x41\xf2\xef\x9e\xa9\x3f\xb3\x5d\x6a\x7b\x96\x81\x63\x99\xf6\x84\xdb\x3e\x8c\x1c\x0c\x20\x00\x08\x00\x00\xff\xff\xf6\x44\x71\xce\x00\x01\x00\x00"), }, + "/ThunderclockPlusROM.bin": &vfsgen۰CompressedFileInfo{ + name: "ThunderclockPlusROM.bin", + modTime: time.Date(2019, 9, 24, 21, 55, 17, 674069636, time.UTC), + uncompressedSize: 2048, + + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x52\xc1\x6b\x23\x55\x1c\x9e\x4e\x67\x3a\xcd\xd8\x6e\x97\x76\x29\x51\x56\xfc\x95\xee\x4a\xb3\x14\x9b\x83\xae\x03\x96\xc5\xb8\x4d\x7d\x29\xd3\x74\x48\x13\xdd\x95\x25\xe4\xe0\xe1\x99\x93\x07\x0f\xb9\xa8\x39\xb8\xb0\x04\x02\x3d\x78\x48\x0a\x81\x61\x5c\xcb\xbc\x92\x66\x83\x2b\xec\xa4\x18\x8c\x20\xd8\xf7\xe6\x90\x77\xf0\x20\x82\xcb\x1e\xf6\x60\x7b\x90\x57\xb5\xec\x20\x48\x65\xd2\x52\x2f\xfe\x07\xf6\x3b\x7c\x6f\xbe\x99\xef\xf7\xfd\x3e\x86\x37\x5a\x9a\x9b\xbf\x75\xfc\xa1\x6a\xb4\x87\xa2\xde\x68\x09\x55\x50\x0d\xed\x1c\xf7\xe1\x85\x3d\x8c\x77\xab\x81\xa6\xeb\xba\x5e\x2d\x69\x18\x63\x17\x6f\x46\x14\x34\x57\x6b\x96\xb4\x56\xa0\xc5\x3e\x45\xd6\x05\xef\xcb\xeb\x5c\x79\xfa\xba\xd0\xc9\xe8\x5d\xc3\xd8\x98\x36\x2d\x4a\x9e\xbb\x7b\x9d\xe4\x1b\x9e\x42\xf4\x86\xa1\x10\xa9\x61\xa8\x98\x5e\x15\x77\xe8\xcb\xe2\x16\x5d\x14\x59\x7a\x43\x58\x74\x56\x98\x34\x2f\xde\xa3\x33\x62\x85\x5e\x13\x19\x07\xe8\xbc\xc8\x38\x73\xf4\x15\x61\x3a\x71\xba\x20\x96\x5b\x81\x86\xba\x9e\x42\x67\x44\x82\xe6\xc5\x05\x1c\x7b\x49\x37\xf6\x87\xc2\xc0\x3c\x9f\xc5\xc6\x7e\x22\x0e\x74\xfa\xe2\xe5\xed\x6e\xd5\x37\x15\xbf\x61\xa8\xe6\x02\x25\x00\x8f\x18\x19\x0d\x69\xa6\xe1\x29\xe6\x02\xad\xc0\x23\xc6\x9f\x61\x3a\x2e\xa6\x28\x88\xa5\xd8\x04\xf4\x99\x90\xc3\x62\x5a\x74\xe3\x35\xe8\xb3\x8f\x0d\x0d\x91\x48\xc3\x18\x81\x3e\x43\x8f\x8d\x11\xfe\x2c\xfc\x38\x82\xa1\xcf\x4e\xd4\xc5\x30\xe1\xab\x5d\x8c\x6b\x08\x63\x52\x45\x9b\xad\x40\x6b\x96\xb4\xae\x37\xfc\x8b\xa1\xf1\xb1\x17\x0d\x4d\x68\xf4\x4d\x3e\x5c\x2f\xf7\xb0\x8b\xb7\xf1\x5c\x01\x93\xe8\x59\x8d\xd3\x64\xaa\xc7\x65\x22\x0d\xf2\x0f\x43\x39\x3e\x90\x5d\x43\x15\x0a\x9d\xe5\x79\x5b\xc2\x54\x8f\x2b\x7b\xc6\xbe\x8e\x6a\x8e\x04\x5b\x0c\xc3\x16\x6b\x06\x5a\xc7\x50\xf9\x35\xf2\x00\x5c\x86\x6d\x05\x9f\xbe\xbf\x27\x34\xf2\x06\xb8\x8c\xff\x46\xec\xaa\x2a\x77\xfe\xf0\xc5\xf3\xe0\xb2\x3d\x7e\x44\x24\x64\xab\xf0\x80\x9d\x39\xf9\xef\xe6\x26\xc3\xdb\x58\xd7\xdd\x8a\xae\xa3\x56\xa0\x75\x0d\xd5\xb1\xe9\x62\x5b\x76\x9c\x8a\x23\x81\xcb\x3a\x5f\xf8\x7b\xe0\x32\x6a\xf3\x23\xec\x76\xbe\xfd\x57\xc0\xfd\x41\x06\x3c\x64\xf6\x70\x28\xf8\xf0\xd9\xfe\x08\xd9\x0d\x2b\x99\xb1\xd3\x8e\xf4\xaa\x50\xe8\x0d\x6e\xd9\x89\x9d\x31\x99\xda\x5c\x26\xf1\xc1\xa5\x01\x79\x67\x5c\x8e\x4d\x8c\x83\x4c\x27\xe3\xb2\x6d\x51\x89\x2b\x64\x92\x47\xe8\x54\x7c\x3a\x30\xf6\x27\x7f\x72\xc6\x10\x1c\xb3\xd8\x44\xb8\x2b\x36\x01\x5b\xcc\xb9\x04\x0f\x59\x0d\x5c\x46\x56\xc1\x65\x15\x97\x54\xc1\x65\xe6\x15\x2a\xc6\x23\xf1\x48\xb9\x21\xc9\xbf\x16\x48\x93\x1f\x11\x9b\x1f\xd6\xcb\xbd\x88\x52\x2f\xf7\xe0\x07\x96\x1a\x9c\x8c\x21\x84\x71\x01\x11\xa5\xe1\x8d\x10\xa9\xe1\xa9\x9d\x72\x4f\xff\xc4\x53\x31\x8a\x0d\x85\x6e\xb9\x5e\xee\xa5\x42\xc2\x45\xf4\xd8\x1b\xe1\x4f\x70\xd7\x53\xa3\xc5\x62\xb1\x58\x38\xf9\x35\xe1\x20\x58\x3e\x46\x60\xfa\x18\xa5\x02\x30\x7d\xc7\x86\x0f\x4e\xc8\x8e\x42\xc1\x6f\x05\x5a\x38\xf9\x33\xb6\xbf\x61\xfc\xef\x7b\xfc\x2f\x73\x81\xa2\x0d\x15\x2c\x5f\x0c\xc3\x6d\x1f\xeb\xfc\xb0\xe0\x58\xa1\x7b\x8e\x6b\x4e\x34\x7c\x58\x72\xa6\x06\xd6\xa0\xd0\x2c\x69\x04\xea\xe5\xde\xc1\xc1\x41\xea\xbf\x4e\xc6\x9f\x14\xde\x7f\x7a\xe5\xfe\xa5\xcf\xb3\x3f\x7e\xf4\xe7\xab\x5f\x2b\x9f\x2d\x7d\x3f\xbf\x78\xc7\xbc\x3c\xb6\x9e\x4b\xc3\xea\x5a\x1a\xb2\xb9\x24\xbc\x9b\x5c\x82\x2c\xca\xc1\x72\x26\x05\xeb\x89\x2c\x24\x33\x19\x58\x49\xa4\x61\x39\xf9\x16\xac\x26\x32\x90\xb0\x32\xb0\x9a\xb8\x0d\x2b\xb9\x34\xac\xe4\x4c\x48\xe4\xde\x86\xf5\xa4\x05\x6b\x37\xb3\x90\x5e\x7b\x07\x96\x92\x37\xa1\xd5\x6e\xb7\xa5\x01\xd9\xf6\x77\xc7\xe7\x38\xc7\x39\xfe\xb7\xf8\x27\x00\x00\xff\xff\xe3\xc4\x99\x1b\x00\x08\x00\x00"), + }, "/dos33.dsk": &vfsgen۰CompressedFileInfo{ name: "dos33.dsk", modTime: time.Date(2019, 6, 7, 16, 50, 59, 550488426, time.UTC), @@ -110,6 +117,7 @@ var Assets = func() http.FileSystem { fs["/BASE64A_F8.BIN"].(os.FileInfo), fs["/BASE64A_ROM7_CharGen.BIN"].(os.FileInfo), fs["/DISK2.rom"].(os.FileInfo), + fs["/ThunderclockPlusROM.bin"].(os.FileInfo), fs["/dos33.dsk"].(os.FileInfo), } @@ -134,7 +142,7 @@ func (fs vfsgen۰FS) Open(path string) (http.File, error) { } return &vfsgen۰CompressedFile{ vfsgen۰CompressedFileInfo: f, - gr: gr, + gr: gr, }, nil case *vfsgen۰DirInfo: return &vfsgen۰Dir{