diff --git a/README.md b/README.md index 57a1b6f2..59398f72 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ In addition, Stella was used as reference in the following areas: * During the development of the CDF cartridge formats. These recent formats don't seem to be documented anywhere accept in the Stella source. -* Cartridge fingerprints for ParkerBros, Wickstead Design and SCABS. +* Cartridge fingerprints for ParkerBros, Wickstead Design, SCABS and UA. * As a reference for the audio implementation (the 6502.ts project was also referenced for this reason). diff --git a/cartridgeloader/extensions.go b/cartridgeloader/extensions.go index c21487ea..a79dbef1 100644 --- a/cartridgeloader/extensions.go +++ b/cartridgeloader/extensions.go @@ -18,8 +18,9 @@ package cartridgeloader // FileExtensions is the list of file extensions that are recognised by the // cartridgeloader package. var FileExtensions = [...]string{ - ".BIN", ".ROM", ".A26", ".2K", ".4K", ".F8", ".F6", - ".F4", ".2K+", ".2KSC", ".4K+", ".4KSC", ".F8+", ".F8SC", ".F6+", ".F6SC", ".F4+", ".F4SC", - ".FA", ".FE", ".E0", ".E7", ".3F", ".AR", ".DF", "3E", "3E+", "SB", - ".DPC", ".DP+", "CDF", ".WAV", ".MP3", ".MVC", + ".BIN", ".ROM", ".A26", ".2K", ".4K", ".F8", ".F6", ".F4", ".2K+", ".2KSC", + ".4K+", ".4KSC", ".F8+", ".F8SC", ".F6+", ".F6SC", ".F4+", ".F4SC", ".CV", + ".FA", ".FE", ".E0", ".E7", ".3F", ".UA", ".AR", ".DF", ".3E", ".E3P", + ".E3+", ".3E+", ".EF", ".EFSC", ".SB", ".WD", ".ACE", ".CDF0", ".CDF1", ".CDFJ", + ".CDFJ+", ".DP+", ".DPC", ".CDF", ".WAV", ".MP3", ".MVC", } diff --git a/cartridgeloader/loader.go b/cartridgeloader/loader.go index fc2562e8..ae95cb31 100644 --- a/cartridgeloader/loader.go +++ b/cartridgeloader/loader.go @@ -200,6 +200,8 @@ func NewLoader(filename string, mapping string) (Loader, error) { fallthrough case ".3F": fallthrough + case ".UA": + fallthrough case ".AR": fallthrough case ".DF": diff --git a/hardware/memory/cartridge/cartridge.go b/hardware/memory/cartridge/cartridge.go index 93600af4..d45d0b12 100644 --- a/hardware/memory/cartridge/cartridge.go +++ b/hardware/memory/cartridge/cartridge.go @@ -299,6 +299,8 @@ func (cart *Cartridge) Attach(cartload cartridgeloader.Loader) error { cart.mapper, err = newMnetwork(cart.env, *cartload.Data) case "3F": cart.mapper, err = newTigervision(cart.env, *cartload.Data) + case "UA": + cart.mapper, err = newUA(cart.env, *cartload.Data) case "AR": cart.mapper, err = supercharger.NewSupercharger(cart.env, cartload) case "DF": diff --git a/hardware/memory/cartridge/fingerprint.go b/hardware/memory/cartridge/fingerprint.go index 2d204676..252c660b 100644 --- a/hardware/memory/cartridge/fingerprint.go +++ b/hardware/memory/cartridge/fingerprint.go @@ -214,6 +214,24 @@ func fingerprintSCABS(b []byte) bool { return false } +func fingerprintUA(b []byte) bool { + // ua fingerprint taken from Stella + fingerprint := [][]byte{ + {0x8D, 0x40, 0x02}, // STA $240 (Funky Fish, Pleiades) + {0xAD, 0x40, 0x02}, // LDA $240 (???) + {0xBD, 0x1F, 0x02}, // LDA $21F,X (Gingerbread Man) + {0x2C, 0xC0, 0x02}, // BIT $2C0 (Time Pilot) + {0x8D, 0xC0, 0x02}, // STA $2C0 (Fathom, Vanguard) + {0xAD, 0xC0, 0x02}, // LDA $2C0 (Mickey) + } + for _, f := range fingerprint { + if bytes.Contains(b, f) { + return true + } + } + return false +} + func fingerprintDPCplus(b []byte) bool { if len(b) < 0x23 { return false @@ -301,6 +319,10 @@ func fingerprint8k(data []byte) func(*environment.Environment, []byte) (mapper.C return newSCABS } + if fingerprintUA(data) { + return newUA + } + return newAtari8k } diff --git a/hardware/memory/cartridge/mapper_atari.go b/hardware/memory/cartridge/mapper_atari.go index 020f1bf8..d5e59291 100644 --- a/hardware/memory/cartridge/mapper_atari.go +++ b/hardware/memory/cartridge/mapper_atari.go @@ -195,8 +195,8 @@ func (cart *atari) access(addr uint16) (uint8, uint8, bool) { return 0, mapper.CartDrivenPins, false } -// accessDriven implements the mapper.CartMapper interface. -func (cart *atari) accessDriven(addr uint16, data uint8, poke bool) error { +// accessVolatile implements the mapper.CartMapper interface. +func (cart *atari) accessVolatile(addr uint16, data uint8, poke bool) error { if cart.state.ram != nil { if addr <= 0x7f { cart.state.ram[addr] = data @@ -335,7 +335,7 @@ func (cart *atari4k) Access(addr uint16, peek bool) (uint8, uint8, error) { // AccessVolatile implements the mapper.CartMapper interface. func (cart *atari4k) AccessVolatile(addr uint16, data uint8, poke bool) error { - return cart.atari.accessDriven(addr, data, poke) + return cart.atari.accessVolatile(addr, data, poke) } // atari2k is the half-size cartridge of 2048 bytes: @@ -411,7 +411,7 @@ func (cart *atari2k) CopyBanks() []mapper.BankContent { // AccessVolatile implements the mapper.CartMapper interface. func (cart *atari2k) AccessVolatile(addr uint16, data uint8, poke bool) error { - return cart.atari.accessDriven(addr, data, poke) + return cart.atari.accessVolatile(addr, data, poke) } // atari8k (F8): @@ -482,7 +482,7 @@ func (cart *atari8k) AccessVolatile(addr uint16, data uint8, poke bool) error { } } - return cart.atari.accessDriven(addr, data, poke) + return cart.atari.accessVolatile(addr, data, poke) } // bankswitch on hotspot access. @@ -580,7 +580,7 @@ func (cart *atari16k) AccessVolatile(addr uint16, data uint8, poke bool) error { } } - return cart.atari.accessDriven(addr, data, poke) + return cart.atari.accessVolatile(addr, data, poke) } // bankswitch on hotspot access. @@ -684,7 +684,7 @@ func (cart *atari32k) AccessVolatile(addr uint16, data uint8, poke bool) error { } } - return cart.atari.accessDriven(addr, data, poke) + return cart.atari.accessVolatile(addr, data, poke) } // bankswitch on hotspot access. diff --git a/hardware/memory/cartridge/mapper_ef.go b/hardware/memory/cartridge/mapper_ef.go index 4704f1be..770b2b33 100644 --- a/hardware/memory/cartridge/mapper_ef.go +++ b/hardware/memory/cartridge/mapper_ef.go @@ -83,7 +83,7 @@ func (cart *ef) AccessVolatile(addr uint16, data uint8, poke bool) error { } } - return cart.accessDriven(addr, data, poke) + return cart.accessVolatile(addr, data, poke) } // bankswitch on hotspot access. diff --git a/hardware/memory/cartridge/mapper_tigervision.go b/hardware/memory/cartridge/mapper_tigervision.go index a2deecfe..db395993 100644 --- a/hardware/memory/cartridge/mapper_tigervision.go +++ b/hardware/memory/cartridge/mapper_tigervision.go @@ -162,13 +162,6 @@ func (cart *tigervision) Patch(offset int, data uint8) error { // AccessPassive implements the mapper.CartMapper interface. func (cart *tigervision) AccessPassive(addr uint16, data uint8) error { - // tigervision is seemingly unique in that it bank switches when an address - // outside of cartridge space is written to. for this to work, we need the - // listen() function. - // - // update: tigervision is not unique. the 3e+ mapper also uses this - // mechanism. and superbank - // although address 3F is used primarily, in actual fact writing anywhere // in TIA space is okay. from the description from Kevin Horton's document // (quoted above) whenever an address in TIA space is written to, the lower diff --git a/hardware/memory/cartridge/mapper_ua.go b/hardware/memory/cartridge/mapper_ua.go new file mode 100644 index 00000000..5ddf5604 --- /dev/null +++ b/hardware/memory/cartridge/mapper_ua.go @@ -0,0 +1,162 @@ +// This file is part of Gopher2600. +// +// Gopher2600 is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Gopher2600 is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Gopher2600. If not, see . + +package cartridge + +import ( + "crypto/sha1" + "fmt" + + "github.com/jetsetilly/gopher2600/environment" + "github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper" + "github.com/jetsetilly/gopher2600/hardware/memory/memorymap" + "github.com/jetsetilly/gopher2600/logger" +) + +type ua struct { + env *environment.Environment + + mappingID string + + // ua cartridges are 8k in size and have two banks of 4096 bytes + bankSize int + banks [][]uint8 + + // identifies the currently selected bank + bank int + + // the hotspot addresses are swapped + swappedHotspots bool +} + +func newUA(env *environment.Environment, data []byte) (mapper.CartMapper, error) { + cart := &ua{ + env: env, + mappingID: "UA", + bankSize: 4096, + swappedHotspots: false, + } + + if len(data) != cart.bankSize*cart.NumBanks() { + return nil, fmt.Errorf("UA: wrong number of bytes in the cartridge data") + } + + cart.banks = make([][]uint8, cart.NumBanks()) + + for k := 0; k < cart.NumBanks(); k++ { + cart.banks[k] = make([]uint8, cart.bankSize) + offset := k * cart.bankSize + copy(cart.banks[k], data[offset:offset+cart.bankSize]) + } + + // only one cartridge dump is known to have swapped hotspots + if fmt.Sprintf("%0x", sha1.Sum(data)) == "6d4a94c2348bbd8e9c73b73d8f3389196d42fd54" { + cart.swappedHotspots = true + logger.Logf("UA", "swapping hotspot address for this cartridge (Sorcerer's Apprentice)") + } + + return cart, nil +} + +// MappedBanks implements the mapper.CartMapper interface +func (cart *ua) MappedBanks() string { + return fmt.Sprintf("Bank: %d", cart.bank) +} + +// ID implements the mapper.CartMapper interface +func (cart *ua) ID() string { + return cart.mappingID +} + +// Snapshot implements the mapper.CartMapper interface +func (cart *ua) Snapshot() mapper.CartMapper { + n := *cart + return &n +} + +// Plumb implements the mapper.CartMapper interface +func (cart *ua) Plumb(env *environment.Environment) { + cart.env = env +} + +// Reset implements the mapper.CartMapper interface +func (cart *ua) Reset() { + cart.bank = len(cart.banks) - 1 +} + +// Access implements the mapper.CartMapper interface +func (cart *ua) Access(addr uint16, _ bool) (uint8, uint8, error) { + return cart.banks[cart.bank][addr], mapper.CartDrivenPins, nil +} + +// AccessVolatile implements the mapper.CartMapper interface +func (cart *ua) AccessVolatile(addr uint16, data uint8, poke bool) error { + if poke { + cart.banks[cart.bank][addr] = data + } + return nil +} + +// NumBanks implements the mapper.CartMapper interface +func (cart *ua) NumBanks() int { + return 2 +} + +// GetBank implements the mapper.CartMapper interface +func (cart *ua) GetBank(addr uint16) mapper.BankInfo { + // ua cartridges are like atari cartridges in that the entire address + // space points to the selected bank + return mapper.BankInfo{Number: cart.bank} +} + +// Patch implements the mapper.CartMapper interface +func (cart *ua) Patch(offset int, data uint8) error { + return fmt.Errorf("UA: patching unsupported") +} + +// AccessPassive implements the mapper.CartMapper interface +func (cart *ua) AccessPassive(addr uint16, data uint8) error { + switch addr & 0x1260 { + case 0x0220: + if cart.swappedHotspots { + cart.bank = 1 + } else { + cart.bank = 0 + } + case 0x0240: + if cart.swappedHotspots { + cart.bank = 0 + } else { + cart.bank = 1 + } + } + return nil +} + +// Step implements the mapper.CartMapper interface +func (cart *ua) Step(_ float32) { +} + +// IterateBank implements the mapper.CartMapper interface +func (cart *ua) CopyBanks() []mapper.BankContent { + c := make([]mapper.BankContent, len(cart.banks)) + for b := 0; b < len(cart.banks); b++ { + c[b] = mapper.BankContent{Number: b, + Data: cart.banks[b], + Origins: []uint16{memorymap.OriginCart}, + } + } + return c +}