diff --git a/.gitignore b/.gitignore index bd22531..c3a61c8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,12 @@ a2sdl.exe frontend/a2sdl/a2sdl frontend/*/*.woz frontend/*/*.dsk +frontend/*/*.DSK frontend/*/*.po frontend/*/*.2mg frontend/*/*.hdv frontend/*/*.zip +frontend/*/images frontend/a2fyne/a2fyne frontend/headless/headless frontend/*/snapshot.gif diff --git a/README.md b/README.md index e10b90d..9a9a210 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,7 @@ The available pre-configured models are: cpm: Apple ][+ with CP/M cpm3: Apple //e with CP/M 3.0 cpm65: Apple //e with CPM-65 + desktop: Apple II DeskTop dos32: Apple ][ with 13 sectors disk adapter and DOS 3.2x swyft: swyft diff --git a/apple2.go b/apple2.go index 73cef32..28c0f9f 100644 --- a/apple2.go +++ b/apple2.go @@ -23,6 +23,7 @@ type Apple2 struct { isApple2e bool hasLowerCase bool isFourColors bool // An Apple II without the 6 color mod + usesMouse bool commandChannel chan command dmaActive bool @@ -67,6 +68,11 @@ func (a *Apple2) SetMouseProvider(m MouseProvider) { a.io.setMouseProvider(m) } +// UsesMouse returns true when the emulator uses the mouse +func (a *Apple2) UsesMouse() bool { + return a.usesMouse +} + // IsPaused returns true when emulator is paused func (a *Apple2) IsPaused() bool { return a.paused diff --git a/cardMouse.go b/cardMouse.go index 533378d..64c999a 100644 --- a/cardMouse.go +++ b/cardMouse.go @@ -103,6 +103,7 @@ func (c *CardMouse) readMouse() (uint16, uint16, bool) { } func (c *CardMouse) assign(a *Apple2, slot int) { + a.usesMouse = true c.addCardSoftSwitchR(0, func() uint8 { c.checkFromFirmware() if c.iOut == 0 { diff --git a/configs/desktop.cfg b/configs/desktop.cfg new file mode 100644 index 0000000..62f39a1 --- /dev/null +++ b/configs/desktop.cfg @@ -0,0 +1,15 @@ +name: Apple II DeskTop +parent: _base +board: 2e +cpu: 65c02 +rom: /Apple2e_Enhanced.rom +charrom: /Apple IIe Video Enhanced.bin +ramworks: 8192 +nsc: main +rgb: true +s0: language +s2: vidhd +s3: fastchip +s4: mouse +s6: diskii +s7: smartport,image1=/A2DeskTop-1.4-en_800k.2mg \ No newline at end of file diff --git a/doc/usage.txt b/doc/usage.txt index 3f65e05..e480aaa 100644 --- a/doc/usage.txt +++ b/doc/usage.txt @@ -54,6 +54,7 @@ The available pre-configured models are: cpm: Apple ][+ with CP/M cpm3: Apple //e with CP/M 3.0 cpm65: Apple //e with CPM-65 + desktop: Apple II DeskTop dos32: Apple ][ with 13 sectors disk adapter and DOS 3.2x swyft: swyft diff --git a/frontend/a2sdl/main.go b/frontend/a2sdl/main.go index b49cfd8..989293a 100644 --- a/frontend/a2sdl/main.go +++ b/frontend/a2sdl/main.go @@ -51,7 +51,7 @@ func sdlRun(a *izapple2.Apple2) { s.start() a.SetSpeakerProvider(s) - j := newSDLJoysticks(true) + j := newSDLJoysticks(!a.UsesMouse()) a.SetJoysticksProvider(j) m := newSDLMouse() diff --git a/frontend/a2sdl/sdlJoysticks.go b/frontend/a2sdl/sdlJoysticks.go index 2f0065a..2092cd4 100644 --- a/frontend/a2sdl/sdlJoysticks.go +++ b/frontend/a2sdl/sdlJoysticks.go @@ -104,14 +104,16 @@ func (j *sdlJoysticks) putMouseMotionEvent(e *sdl.MouseMotionEvent, width int32, } func (j *sdlJoysticks) putMouseButtonEvent(e *sdl.MouseButtonEvent) { - pressed := e.State == sdl.PRESSED - switch e.Button { - case 1: // BUTTON_LEFT - j.mousebuttons[0] = pressed - case 3: // BUTTON_RIGHT - j.mousebuttons[1] = pressed - case 2: // BUTTON_MIDDLE - j.mousebuttons[2] = pressed + if j.useMouse { + pressed := e.State == sdl.PRESSED + switch e.Button { + case 1: // BUTTON_LEFT + j.mousebuttons[0] = pressed + case 3: // BUTTON_RIGHT + j.mousebuttons[1] = pressed + case 2: // BUTTON_MIDDLE + j.mousebuttons[2] = pressed + } } } diff --git a/resources.go b/resources.go index 08ab5bf..46d42ad 100644 --- a/resources.go +++ b/resources.go @@ -32,8 +32,7 @@ func isHTTPResource(filename string) bool { strings.HasPrefix(filename, httpsPrefix) } -// LoadResource loads in memory a file from the filesystem, http or embedded -func LoadResource(filename string) ([]uint8, bool, error) { +func normalizeFilename(filename string) string { // Remove quotes if surrounded by them if strings.HasPrefix(filename, "\"") && strings.HasSuffix(filename, "\"") { filename = filename[1 : len(filename)-1] @@ -45,7 +44,14 @@ func LoadResource(filename string) ([]uint8, bool, error) { if err == nil { filename = home + filename[1:] } + } + return filename +} + +// LoadResource loads in memory a file from the filesystem, http or embedded +func LoadResource(filename string) ([]uint8, bool, error) { + filename = normalizeFilename(filename) var writeable bool var file io.Reader @@ -132,3 +138,28 @@ func LoadDiskette(filename string) (storage.Diskette, error) { return storage.MakeDiskette(data, filename, writeable) } + +// LoadBlockDisk returns a BlockDisk +func LoadBlockDisk(filename string) (storage.BlockDisk, error) { + filename = normalizeFilename(filename) + + // Try to open as a file + readOnly := false + file, err := os.OpenFile(filename, os.O_RDWR, 0) + if os.IsPermission(err) { + // Retry in read-only mode + readOnly = true + file, _ = os.OpenFile(filename, os.O_RDONLY, 0) + } + if file != nil { + return storage.NewBlockDiskFile(file, readOnly) + } + + // Load as a resource + data, _, err := LoadResource(filename) + if err != nil { + return nil, err + } + + return storage.NewBlockDiskMemory(data) +} diff --git a/resources/A2DeskTop-1.4-en_800k.2mg b/resources/A2DeskTop-1.4-en_800k.2mg new file mode 100644 index 0000000..24e04fa Binary files /dev/null and b/resources/A2DeskTop-1.4-en_800k.2mg differ diff --git a/smartPortHardDisk.go b/smartPortHardDisk.go index 92ff02b..8cb3c34 100644 --- a/smartPortHardDisk.go +++ b/smartPortHardDisk.go @@ -20,7 +20,7 @@ type SmartPortHardDisk struct { host *CardSmartPort // For DMA filename string trace bool - disk *storage.BlockDisk + disk storage.BlockDisk } // NewSmartPortHardDisk creates a new hard disk with the smartPort interface @@ -29,7 +29,7 @@ func NewSmartPortHardDisk(host *CardSmartPort, filename string) (*SmartPortHardD d.host = host d.filename = filename - hd, err := storage.OpenBlockDisk(filename) + hd, err := LoadBlockDisk(filename) if err != nil { return nil, err } diff --git a/storage/blockDisk.go b/storage/blockDisk.go index 2cbca10..5fb371e 100644 --- a/storage/blockDisk.go +++ b/storage/blockDisk.go @@ -1,8 +1,10 @@ package storage import ( + "bytes" "errors" "fmt" + "io" "os" ) @@ -17,67 +19,93 @@ const ( ) // BlockDisk is any block device with 512 bytes blocks -type BlockDisk struct { +type BlockDisk interface { + GetSizeInBlocks() uint32 + IsReadOnly() bool + Read(block uint32) ([]uint8, error) + Write(block uint32, data []uint8) error +} + +type blockDiskBase struct { file *os.File readOnly bool dataOffset uint32 blocks uint32 } -// OpenBlockDisk creates a new block device links to a file -func OpenBlockDisk(filename string) (*BlockDisk, error) { - var bd BlockDisk +type blockDiskFile struct { + blockDiskBase + file *os.File +} - bd.readOnly = false - file, err := os.OpenFile(filename, os.O_RDWR, 0) - if os.IsPermission(err) { - // Retry in read-only mode - bd.readOnly = true - file, err = os.OpenFile(filename, os.O_RDONLY, 0) - } - if err != nil { - return nil, err - } +type blockDiskMemory struct { + blockDiskBase + data []uint8 +} + +// OpenBlockDisk creates a new block device linked to a file +func NewBlockDiskFile(file *os.File, readOnly bool) (BlockDisk, error) { + var bd blockDiskFile bd.file = file + bd.readOnly = readOnly - err2mg := parse2mg(&bd) - if err2mg == nil { - // It's a 2mg file, ready to use - return &bd, nil - } - - // Let's try to load as raw ProDOS Blocks fileInfo, err := bd.file.Stat() if err != nil { return nil, err } - if fileInfo.Size() > int64(ProDosBlockSize*proDosMaxBlocks) { - return nil, fmt.Errorf("file is too big OR %s", err2mg.Error()) - } - size := uint32(fileInfo.Size()) - if size%ProDosBlockSize != 0 { - return nil, fmt.Errorf("file size os invalid OR %s", err2mg.Error()) + bd.blocks, bd.dataOffset, err = getBlockAndOffset(bd.file, size) + if err != nil { + return nil, err } - - // It's a valid raw file - bd.blocks = size / ProDosBlockSize - bd.dataOffset = 0 return &bd, nil } +func NewBlockDiskMemory(data []uint8) (BlockDisk, error) { + var bd blockDiskMemory + bd.data = data + bd.readOnly = true + + var err error + bd.blocks, bd.dataOffset, err = getBlockAndOffset(bytes.NewReader(data), uint32(len(data))) + if err != nil { + return nil, err + } + return &bd, nil +} + +func getBlockAndOffset(reader io.Reader, size uint32) (uint32, uint32, error) { + header, err := parse2mg(reader, size) + if err == nil { + // It's a 2mg file + return header.Blocks, header.OffsetData, nil + } + + // Let's try to load as raw ProDOS Blocks + if size > ProDosBlockSize*proDosMaxBlocks { + return 0, 0, fmt.Errorf("file is too big OR %s", err.Error()) + } + + if size%ProDosBlockSize != 0 { + return 0, 0, fmt.Errorf("file size os invalid OR %s", err.Error()) + } + + // It's a valid raw file + return size / ProDosBlockSize, 0, nil +} + // GetSizeInBlocks returns the number of blocks of the device -func (bd *BlockDisk) GetSizeInBlocks() uint32 { +func (bd *blockDiskBase) GetSizeInBlocks() uint32 { return bd.blocks } // IsReadOnly returns true if the device is read only -func (bd *BlockDisk) IsReadOnly() bool { +func (bd *blockDiskBase) IsReadOnly() bool { return bd.readOnly } -func (bd *BlockDisk) Read(block uint32) ([]uint8, error) { +func (bd *blockDiskFile) Read(block uint32) ([]uint8, error) { if block >= bd.blocks { return nil, errors.New("disk block number is too big") } @@ -93,7 +121,16 @@ func (bd *BlockDisk) Read(block uint32) ([]uint8, error) { return buf, nil } -func (bd *BlockDisk) Write(block uint32, data []uint8) error { +func (bd *blockDiskMemory) Read(block uint32) ([]uint8, error) { + if block >= bd.blocks { + return nil, errors.New("disk block number is too big") + } + + offset := bd.dataOffset + block*ProDosBlockSize + return bd.data[offset : offset+ProDosBlockSize], nil +} + +func (bd *blockDiskFile) Write(block uint32, data []uint8) error { if bd.readOnly { return errors.New("can't write in a readonly disk") } @@ -109,3 +146,7 @@ func (bd *BlockDisk) Write(block uint32, data []uint8) error { return nil } + +func (bd *blockDiskMemory) Write(block uint32, data []uint8) error { + return errors.New("can't write in a readonly disk") +} diff --git a/storage/file2mg.go b/storage/file2mg.go index 6198bbf..5b62d57 100644 --- a/storage/file2mg.go +++ b/storage/file2mg.go @@ -35,31 +35,23 @@ type file2mgHeader struct { LengthCreator uint32 } -func parse2mg(bd *BlockDisk) error { - fileInfo, err := bd.file.Stat() - if err != nil { - return err - } - +func parse2mg(reader io.Reader, size uint32) (*file2mgHeader, error) { var header file2mgHeader minHeaderSize := binary.Size(&header) - if fileInfo.Size() < int64(minHeaderSize) { - return errors.New("invalid 2MG file") + if size < uint32(minHeaderSize) { + return nil, errors.New("invalid 2MG file") } - err = readHeader(bd.file, &header) + err := readHeader(reader, &header) if err != nil { - return err + return nil, err } - bd.blocks = header.Blocks - bd.dataOffset = header.OffsetData - - if fileInfo.Size() < int64(bd.dataOffset+bd.blocks*ProDosBlockSize) { - return errors.New("the 2MG file is too small") + if size < header.OffsetData+header.Blocks*ProDosBlockSize { + return nil, errors.New("the 2MG file is too small") } - return nil + return &header, nil } func readHeader(buf io.Reader, header *file2mgHeader) error {