mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-01 19:48:01 -04:00
added archivefs package and support in cartridgeloader and ROM select window
archivefs allows opening of files inside a zip archive. support for other archive file type will be added in the future when possible
This commit is contained in:
parent
8edee622cc
commit
e3f4a743b6
32
archivefs/archivefs.go
Normal file
32
archivefs/archivefs.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
package archivefs
|
||||
|
||||
import "io"
|
||||
|
||||
// Open and return an io.ReadSeeker for the specified filename. Filename can be
|
||||
// inside an archive supported by archivefs
|
||||
//
|
||||
// Returns the io.ReadSeeker, the size of the data behind the ReadSeeker and any
|
||||
// errors.
|
||||
func Open(filename string) (io.ReadSeeker, int, error) {
|
||||
var afs Path
|
||||
err := afs.Set(filename)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer afs.Close()
|
||||
return afs.Open()
|
||||
}
|
126
archivefs/archivefs_test.go
Normal file
126
archivefs/archivefs_test.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
package archivefs_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/archivefs"
|
||||
"github.com/jetsetilly/gopher2600/test"
|
||||
)
|
||||
|
||||
func TestArchivefsPath(t *testing.T) {
|
||||
var afs archivefs.Path
|
||||
var path string
|
||||
var entries []archivefs.Node
|
||||
var err error
|
||||
|
||||
// non-existant file
|
||||
path = "foo"
|
||||
err = afs.Set(path)
|
||||
test.ExpectFailure(t, err)
|
||||
test.ExpectEquality(t, afs.String(), "")
|
||||
|
||||
// a real directory
|
||||
path = "testdir"
|
||||
err = afs.Set(path)
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, afs.String(), path)
|
||||
test.ExpectSuccess(t, afs.IsDir())
|
||||
test.ExpectSuccess(t, !afs.InArchive())
|
||||
|
||||
// entries in a directory
|
||||
entries, err = afs.List()
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, len(entries), 2)
|
||||
test.ExpectEquality(t, fmt.Sprintf("%s", entries), "[testarchive.zip testfile]")
|
||||
|
||||
// non-existant file in directory
|
||||
path = filepath.Join("testdir", "foo")
|
||||
err = afs.Set(path)
|
||||
test.ExpectFailure(t, err)
|
||||
test.ExpectEquality(t, afs.String(), "")
|
||||
|
||||
// a real file in directory
|
||||
path = filepath.Join("testdir", "testfile")
|
||||
err = afs.Set(path)
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, afs.String(), path)
|
||||
test.ExpectSuccess(t, !afs.IsDir())
|
||||
test.ExpectSuccess(t, !afs.InArchive())
|
||||
|
||||
// calling List() when path is set to a file type (ie not a direcotry) the
|
||||
// list returned should be of the containing directory
|
||||
entries, err = afs.List()
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, len(entries), 2)
|
||||
test.ExpectEquality(t, fmt.Sprintf("%s", entries), "[testarchive.zip testfile]")
|
||||
|
||||
// a real archive
|
||||
path = filepath.Join("testdir", "testarchive.zip")
|
||||
err = afs.Set(path)
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, afs.String(), path)
|
||||
test.ExpectSuccess(t, afs.IsDir())
|
||||
test.ExpectSuccess(t, afs.InArchive())
|
||||
|
||||
// entries in an archive
|
||||
entries, err = afs.List()
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, len(entries), 3)
|
||||
test.ExpectEquality(t, fmt.Sprintf("%s", entries), "[archivedir archivefile1 archivefile2]")
|
||||
|
||||
// file in a real archive
|
||||
path = filepath.Join("testdir", "testarchive.zip", "archivefile1")
|
||||
err = afs.Set(path)
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, afs.String(), path)
|
||||
test.ExpectSuccess(t, !afs.IsDir())
|
||||
test.ExpectSuccess(t, afs.InArchive())
|
||||
|
||||
// directory a real archive
|
||||
path = filepath.Join("testdir", "testarchive.zip", "archivedir")
|
||||
err = afs.Set(path)
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, afs.String(), path)
|
||||
test.ExpectSuccess(t, afs.IsDir())
|
||||
test.ExpectSuccess(t, afs.InArchive())
|
||||
|
||||
// entries in an archive
|
||||
entries, err = afs.List()
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, len(entries), 2)
|
||||
test.ExpectEquality(t, fmt.Sprintf("%s", entries), "[archivedir2 archivefile3]")
|
||||
|
||||
// file in a real archive
|
||||
path = filepath.Join("testdir", "testarchive.zip", "archivedir", "archivefile3")
|
||||
err = afs.Set(path)
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, afs.String(), path)
|
||||
test.ExpectSuccess(t, !afs.IsDir())
|
||||
test.ExpectSuccess(t, afs.InArchive())
|
||||
}
|
||||
|
||||
func TestArchivefsOpen(t *testing.T) {
|
||||
r, sz, err := archivefs.Open(filepath.Join("testdir", "testarchive.zip", "archivefile1"))
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, sz, 22)
|
||||
d, err := io.ReadAll(r)
|
||||
test.ExpectSuccess(t, err)
|
||||
test.ExpectEquality(t, string(d), "archivefile1 contents\n")
|
||||
}
|
20
archivefs/doc.go
Normal file
20
archivefs/doc.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package archivefs allows the traversal of a filetree and treating archive
|
||||
// files transparently.
|
||||
//
|
||||
// Supported archive formats is currently limited to zip.
|
||||
package archivefs
|
51
archivefs/extensions.go
Normal file
51
archivefs/extensions.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
package archivefs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// list of file extensions for the supported archive types
|
||||
var ArchiveExtensions = [...]string{".ZIP"}
|
||||
|
||||
// RemoveArchiveExt removes the file extension of any supported/recognised
|
||||
// archive type from within the string. Only the first instance of the extension
|
||||
// is removed
|
||||
func RemoveArchiveExt(s string) string {
|
||||
t := strings.ToUpper(s)
|
||||
for _, ext := range ArchiveExtensions {
|
||||
i := strings.Index(t, ext)
|
||||
if i >= 0 {
|
||||
return fmt.Sprintf("%s%s", s[:i], s[i+len(ext):])
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// TrimArchiveExt removes the file extension of any supported/recognised archive
|
||||
// type from the end of the string
|
||||
func TrimArchiveExt(s string) string {
|
||||
sext := strings.ToUpper(filepath.Ext(s))
|
||||
for _, ext := range ArchiveExtensions {
|
||||
if sext == ext {
|
||||
return strings.TrimSuffix(s, filepath.Ext(s))
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
289
archivefs/path.go
Normal file
289
archivefs/path.go
Normal file
|
@ -0,0 +1,289 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
package archivefs
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Node represents a single part of a full path
|
||||
type Node struct {
|
||||
Name string
|
||||
|
||||
// a directory has the the field of IsDir set to true
|
||||
IsDir bool
|
||||
|
||||
// a recognised archive file has InArchive set to true. note that an archive
|
||||
// file is also considered to be directory
|
||||
IsArchive bool
|
||||
}
|
||||
|
||||
func (e Node) String() string {
|
||||
return e.Name
|
||||
}
|
||||
|
||||
// Path represents a single destination in the file system
|
||||
type Path struct {
|
||||
current string
|
||||
isDir bool
|
||||
|
||||
zf *zip.ReadCloser
|
||||
|
||||
// if the path is inside a zip file, we split the in-zip path into the path
|
||||
// to a file and the file itself
|
||||
inZipPath string
|
||||
inZipFile string
|
||||
}
|
||||
|
||||
// String returns the current path
|
||||
func (afs Path) String() string {
|
||||
return afs.current
|
||||
}
|
||||
|
||||
// Base returns the last element of the current path
|
||||
func (afs Path) Base() string {
|
||||
return filepath.Base(afs.current)
|
||||
}
|
||||
|
||||
// Dir returns all but the last element of path
|
||||
func (afs Path) Dir() string {
|
||||
if afs.isDir {
|
||||
return afs.current
|
||||
}
|
||||
return filepath.Dir(afs.current)
|
||||
}
|
||||
|
||||
// IsDir returns true if Path is currently set to a directory. For the purposes
|
||||
// of archivefs, the root of an archive is treated as a directory
|
||||
func (afs Path) IsDir() bool {
|
||||
return afs.isDir
|
||||
}
|
||||
|
||||
// InArchive returns true if path is currently inside an archive
|
||||
func (afs Path) InArchive() bool {
|
||||
return afs.zf != nil
|
||||
}
|
||||
|
||||
// Open and return an io.ReadSeeker for the filename previously set by the Set()
|
||||
// function.
|
||||
//
|
||||
// Returns the io.ReadSeeker, the size of the data behind the ReadSeeker and any
|
||||
// errors.
|
||||
func (afs Path) Open() (io.ReadSeeker, int, error) {
|
||||
if afs.zf != nil {
|
||||
f, err := afs.zf.Open(filepath.Join(afs.inZipPath, afs.inZipFile))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return bytes.NewReader(b), len(b), nil
|
||||
}
|
||||
|
||||
f, err := os.Open(afs.current)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return f, int(info.Size()), nil
|
||||
}
|
||||
|
||||
// Close any open zip files and reset path
|
||||
func (afs *Path) Close() {
|
||||
afs.current = ""
|
||||
afs.isDir = false
|
||||
afs.inZipPath = ""
|
||||
afs.inZipFile = ""
|
||||
if afs.zf != nil {
|
||||
afs.zf.Close()
|
||||
afs.zf = nil
|
||||
}
|
||||
}
|
||||
|
||||
// List returns the child entries for the current path location. If the current
|
||||
// path is a file then the list will be the contents of the containing directory
|
||||
// of that file
|
||||
func (afs *Path) List() ([]Node, error) {
|
||||
var ent []Node
|
||||
|
||||
if afs.zf != nil {
|
||||
for _, f := range afs.zf.File {
|
||||
// split file name into parts. the list is joined together again
|
||||
// below to create the path to the file. this is better than
|
||||
// filepath.Dir() because that will add path components that make it
|
||||
// awkward to compare with afs.inZipPath
|
||||
flst := strings.Split(filepath.Clean(f.Name), string(filepath.Separator))
|
||||
fdir := filepath.Join(flst[:len(flst)-1]...)
|
||||
|
||||
// if path to the file is not the same as inZipPath then continue
|
||||
// with the next file
|
||||
if fdir != afs.inZipPath {
|
||||
continue
|
||||
}
|
||||
|
||||
fi := f.FileInfo()
|
||||
if fi.IsDir() {
|
||||
ent = append(ent, Node{
|
||||
Name: fi.Name(),
|
||||
IsDir: true,
|
||||
})
|
||||
} else {
|
||||
ent = append(ent, Node{
|
||||
Name: fi.Name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
path := afs.current
|
||||
if !afs.isDir {
|
||||
path = filepath.Dir(path)
|
||||
}
|
||||
|
||||
dir, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return []Node{}, fmt.Errorf("archivefs: entries: %w", err)
|
||||
}
|
||||
|
||||
for _, d := range dir {
|
||||
// using os.Stat() to get file information otherwise links to
|
||||
// directories do not have the IsDir() property
|
||||
fi, err := os.Stat(filepath.Join(path, d.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
ent = append(ent, Node{
|
||||
Name: d.Name(),
|
||||
IsDir: true,
|
||||
})
|
||||
} else {
|
||||
p := filepath.Join(path, d.Name())
|
||||
_, err := zip.OpenReader(p)
|
||||
if err == nil {
|
||||
ent = append(ent, Node{
|
||||
Name: d.Name(),
|
||||
IsDir: true,
|
||||
IsArchive: true,
|
||||
})
|
||||
} else {
|
||||
ent = append(ent, Node{
|
||||
Name: d.Name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort so that directories are at the start of the list
|
||||
sort.Slice(ent, func(i int, j int) bool {
|
||||
return ent[i].IsDir
|
||||
})
|
||||
|
||||
// sort alphabetically (case insensitive)
|
||||
sort.SliceStable(ent, func(i int, j int) bool {
|
||||
return strings.ToLower(ent[i].Name) < strings.ToLower(ent[j].Name)
|
||||
})
|
||||
|
||||
return ent, nil
|
||||
}
|
||||
|
||||
func (afs *Path) Set(path string) error {
|
||||
afs.Close()
|
||||
|
||||
// clean path and split into parts
|
||||
path = filepath.Clean(path)
|
||||
lst := strings.Split(path, string(filepath.Separator))
|
||||
|
||||
// strings.Split will remove a leading filepath.Separator. we need to add
|
||||
// one back so that filepath.Join() works as expected
|
||||
if lst[0] == "" {
|
||||
lst[0] = string(filepath.Separator)
|
||||
}
|
||||
|
||||
// reuse path string
|
||||
path = ""
|
||||
|
||||
for _, l := range lst {
|
||||
path = filepath.Join(path, l)
|
||||
|
||||
if afs.zf != nil {
|
||||
p := filepath.Join(afs.inZipPath, l)
|
||||
|
||||
zf, err := afs.zf.Open(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("archivefs: set: %v", err)
|
||||
}
|
||||
|
||||
zfi, err := zf.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("archivefs: set: %v", err)
|
||||
}
|
||||
|
||||
afs.isDir = zfi.IsDir()
|
||||
if afs.isDir {
|
||||
afs.inZipPath = p
|
||||
afs.inZipFile = ""
|
||||
} else {
|
||||
afs.inZipFile = l
|
||||
}
|
||||
|
||||
} else {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("archivefs: set: %v", err)
|
||||
}
|
||||
|
||||
afs.isDir = fi.IsDir()
|
||||
if afs.isDir {
|
||||
continue
|
||||
}
|
||||
|
||||
afs.zf, err = zip.OpenReader(path)
|
||||
if err == nil {
|
||||
// the root of an archive file is considered to be a directory
|
||||
afs.isDir = true
|
||||
continue
|
||||
}
|
||||
|
||||
if !errors.Is(err, zip.ErrFormat) {
|
||||
return fmt.Errorf("archivefs: set: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make sure path is clean
|
||||
afs.current = filepath.Clean(path)
|
||||
|
||||
return nil
|
||||
}
|
BIN
archivefs/testdir/testarchive.zip
Normal file
BIN
archivefs/testdir/testarchive.zip
Normal file
Binary file not shown.
1
archivefs/testdir/testfile
Normal file
1
archivefs/testdir/testfile
Normal file
|
@ -0,0 +1 @@
|
|||
contents of testfile
|
|
@ -24,12 +24,20 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/archivefs"
|
||||
)
|
||||
|
||||
// the maximum amount of data to load into the peep slice
|
||||
const maxPeepLength = 1048576
|
||||
|
||||
func peepData(data []byte) []byte {
|
||||
return data[:min(len(data), maxPeepLength)]
|
||||
}
|
||||
|
||||
// Loader abstracts all the ways data can be loaded into the emulation.
|
||||
type Loader struct {
|
||||
io.ReadSeeker
|
||||
|
@ -51,16 +59,24 @@ type Loader struct {
|
|||
// empty string or "AUTO" indicates automatic fingerprinting
|
||||
Mapping string
|
||||
|
||||
// hashes of data. will be empty if data is being streamed
|
||||
// hashes of data
|
||||
HashSHA1 string
|
||||
HashMD5 string
|
||||
|
||||
// does the Data field consist of sound (PCM) data
|
||||
IsSoundData bool
|
||||
|
||||
// cartridge data
|
||||
data []byte
|
||||
dataReader io.ReadSeeker
|
||||
// data and size of
|
||||
data io.ReadSeeker
|
||||
size int
|
||||
|
||||
// peep is the data at the beginning of the cartridge data. it is used to
|
||||
// help fingerprinting and for creating the SHA1 and MD5 hashes
|
||||
//
|
||||
// in reality, most cartridges are small enough to fit entirely inside the
|
||||
// peep slice. currently it is only moviecart data and maybe supercharger
|
||||
// sound files that will be larger than the maxPeepLength
|
||||
peep []byte
|
||||
|
||||
// data was supplied through NewLoaderFromData()
|
||||
embedded bool
|
||||
|
@ -129,17 +145,14 @@ func NewLoaderFromFilename(filename string, mapping string) (Loader, error) {
|
|||
// we want to do this now so we can initialise the stream
|
||||
if ld.Mapping == "AUTO" {
|
||||
ok, err := miniFingerprintMovieCart(filename)
|
||||
if err != nil {
|
||||
return Loader{}, fmt.Errorf("loader: %w", err)
|
||||
}
|
||||
if ok {
|
||||
if err == nil && ok {
|
||||
ld.Mapping = "MVC"
|
||||
}
|
||||
}
|
||||
|
||||
err = ld.open()
|
||||
if err != nil {
|
||||
return Loader{}, fmt.Errorf("loader: %w", err)
|
||||
return Loader{}, err
|
||||
}
|
||||
|
||||
// decide on the name for this cartridge
|
||||
|
@ -173,13 +186,14 @@ func NewLoaderFromData(name string, data []byte, mapping string) (Loader, error)
|
|||
}
|
||||
|
||||
ld := Loader{
|
||||
Filename: name,
|
||||
Mapping: mapping,
|
||||
data: data,
|
||||
dataReader: bytes.NewReader(data),
|
||||
HashSHA1: fmt.Sprintf("%x", sha1.Sum(data)),
|
||||
HashMD5: fmt.Sprintf("%x", md5.Sum(data)),
|
||||
embedded: true,
|
||||
Filename: name,
|
||||
Mapping: mapping,
|
||||
peep: peepData(data),
|
||||
data: bytes.NewReader(data),
|
||||
HashSHA1: fmt.Sprintf("%x", sha1.Sum(data)),
|
||||
HashMD5: fmt.Sprintf("%x", md5.Sum(data)),
|
||||
size: len(data),
|
||||
embedded: true,
|
||||
}
|
||||
|
||||
// decide on the name for this cartridge
|
||||
|
@ -188,6 +202,14 @@ func NewLoaderFromData(name string, data []byte, mapping string) (Loader, error)
|
|||
return ld, nil
|
||||
}
|
||||
|
||||
func (ld *Loader) Reload() error {
|
||||
err := ld.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ld.open()
|
||||
}
|
||||
|
||||
// Reset prepares the loader for fresh reading. Useful to call after data has
|
||||
// been Read() or if you need to make absolutely sure subsequent calls to Read()
|
||||
// start from the beginning of the data stream
|
||||
|
@ -200,54 +222,55 @@ func (ld *Loader) Reset() error {
|
|||
//
|
||||
// Should be called before disposing of a Loader instance.
|
||||
func (ld *Loader) Close() error {
|
||||
ld.data = nil
|
||||
|
||||
if closer, ok := ld.dataReader.(io.Closer); ok {
|
||||
if closer, ok := ld.data.(io.Closer); ok {
|
||||
err := closer.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loader: %w", err)
|
||||
}
|
||||
}
|
||||
ld.data = nil
|
||||
ld.size = 0
|
||||
ld.peep = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Implements the io.Reader interface.
|
||||
func (ld Loader) Read(p []byte) (int, error) {
|
||||
if ld.dataReader != nil {
|
||||
return ld.dataReader.Read(p)
|
||||
if ld.data != nil {
|
||||
return ld.data.Read(p)
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// Implements the io.Seeker interface.
|
||||
func (ld Loader) Seek(offset int64, whence int) (int64, error) {
|
||||
if ld.dataReader != nil {
|
||||
return ld.dataReader.Seek(offset, whence)
|
||||
if ld.data != nil {
|
||||
return ld.data.Seek(offset, whence)
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// Size returns the size of the cartridge data in bytes
|
||||
func (ld Loader) Size() int {
|
||||
return len(ld.data)
|
||||
return ld.size
|
||||
}
|
||||
|
||||
// Contains returns true if subslice appears anywhere in the data
|
||||
func (ld Loader) Contains(subslice []byte) bool {
|
||||
return bytes.Contains(ld.data, subslice)
|
||||
return bytes.Contains(ld.peep, subslice)
|
||||
}
|
||||
|
||||
// ContainsLimit returns true if subslice appears in the data at an offset between
|
||||
// zero and limit
|
||||
func (ld Loader) ContainsLimit(limit int, subslice []byte) bool {
|
||||
limit = min(limit, ld.Size())
|
||||
return bytes.Contains(ld.data[:limit], subslice)
|
||||
return bytes.Contains(ld.peep[:limit], subslice)
|
||||
}
|
||||
|
||||
// Count returns the number of non-overlapping instances of subslice in the data
|
||||
func (ld Loader) Count(subslice []byte) int {
|
||||
return bytes.Count(ld.data, subslice)
|
||||
return bytes.Count(ld.peep, subslice)
|
||||
}
|
||||
|
||||
// open the cartridge data. filenames with a valid schema will use that method
|
||||
|
@ -271,41 +294,40 @@ func (ld *Loader) open() error {
|
|||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
ld.data, err = io.ReadAll(resp.Body)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loader: %w", err)
|
||||
}
|
||||
|
||||
ld.data = bytes.NewReader(data)
|
||||
ld.size = len(data)
|
||||
ld.peep = peepData(data)
|
||||
|
||||
case "file":
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
f, err := os.Open(ld.Filename)
|
||||
r, sz, err := archivefs.Open(ld.Filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loader: %w", err)
|
||||
}
|
||||
|
||||
fs, err := f.Stat()
|
||||
// peep at data
|
||||
ld.peep, err = io.ReadAll(io.LimitReader(r, maxPeepLength))
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("loader: %w", err)
|
||||
}
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return fmt.Errorf("loader: %w", err)
|
||||
}
|
||||
|
||||
if fs.Size() < 1048576 {
|
||||
defer f.Close()
|
||||
ld.data, err = io.ReadAll(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loader: %w", err)
|
||||
}
|
||||
ld.dataReader = bytes.NewReader(ld.data)
|
||||
} else {
|
||||
ld.dataReader = f
|
||||
}
|
||||
ld.data = r
|
||||
ld.size = sz
|
||||
}
|
||||
|
||||
// generate hashes
|
||||
ld.HashSHA1 = fmt.Sprintf("%x", sha1.Sum(ld.data))
|
||||
ld.HashMD5 = fmt.Sprintf("%x", md5.Sum(ld.data))
|
||||
ld.HashSHA1 = fmt.Sprintf("%x", sha1.Sum(ld.peep))
|
||||
ld.HashMD5 = fmt.Sprintf("%x", md5.Sum(ld.peep))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -32,9 +32,9 @@ func miniFingerprintMovieCart(filename string) (bool, error) {
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer f.Close()
|
||||
b := make([]byte, 4)
|
||||
f.Read(b)
|
||||
f.Close()
|
||||
if bytes.Compare(b, []byte{'M', 'V', 'C', 0x00}) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -963,7 +963,10 @@ func (dbg *Debugger) run() error {
|
|||
return fmt.Errorf("debugger: %w", err)
|
||||
}
|
||||
} else if errors.Is(err, terminal.UserReload) {
|
||||
dbg.reloadCartridge()
|
||||
err = dbg.reloadCartridge()
|
||||
if err != nil {
|
||||
logger.Logf("debugger", err.Error())
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("debugger: %w", err)
|
||||
}
|
||||
|
@ -1450,25 +1453,16 @@ func (dbg *Debugger) Plugged(port plugging.PortID, peripheral plugging.Periphera
|
|||
}
|
||||
|
||||
func (dbg *Debugger) reloadCartridge() error {
|
||||
dbg.setState(govern.Initialising, govern.Normal)
|
||||
spec := dbg.vcs.TV.GetFrameInfo().Spec.ID
|
||||
|
||||
err := dbg.insertCartridge("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("debugger: %w", err)
|
||||
}
|
||||
|
||||
// set spec to what it was before the cartridge insertion
|
||||
err = dbg.vcs.TV.SetSpec(spec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debugger: %w", err)
|
||||
if dbg.loader == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// reset macro to beginning
|
||||
if dbg.macro != nil {
|
||||
dbg.macro.Reset()
|
||||
}
|
||||
|
||||
return nil
|
||||
return dbg.insertCartridge(dbg.loader.Filename)
|
||||
}
|
||||
|
||||
// ReloadCartridge inserts the current cartridge and states the emulation over.
|
||||
|
|
|
@ -91,6 +91,8 @@ const (
|
|||
TimelineComparison = '\uf02e'
|
||||
TimelineComparisonLock = '\uf023'
|
||||
Developer = '\uf0c3'
|
||||
Paperclip = '\uf0c6'
|
||||
Directory = '\uf07c'
|
||||
)
|
||||
|
||||
// The first and last unicode points used in the application. We use this to
|
||||
|
|
|
@ -266,7 +266,7 @@ func newColors() *imguiColors {
|
|||
PlayWindowBorder: imgui.Vec4{0.0, 0.0, 0.0, 1.0},
|
||||
|
||||
// ROM selector
|
||||
ROMSelectDir: imgui.Vec4{1.0, 0.5, 0.5, 1.0},
|
||||
ROMSelectDir: imgui.Vec4{0.5, 0.5, 1.0, 1.0},
|
||||
ROMSelectFile: imgui.Vec4{1.0, 1.0, 1.0, 1.0},
|
||||
|
||||
// deferring CapturedScreenTitle & CapturedScreenBorder
|
||||
|
@ -381,7 +381,7 @@ func newColors() *imguiColors {
|
|||
// tia
|
||||
TIApointer: imgui.Vec4{0.8, 0.8, 0.8, 1.0},
|
||||
|
||||
// deffering collision window CollisionBit
|
||||
// deferring collision window CollisionBit
|
||||
|
||||
// deferring chip registers window RegisterBit
|
||||
|
||||
|
@ -438,6 +438,7 @@ func newColors() *imguiColors {
|
|||
cols.SaveKeyBit = imgui.CurrentStyle().Color(imgui.StyleColorButton)
|
||||
cols.TimelineWSYNC = cols.reflectionColors[reflection.WSYNC]
|
||||
cols.TimelineCoProc = cols.reflectionColors[reflection.CoProcActive]
|
||||
cols.CollisionBit = imgui.CurrentStyle().Color(imgui.StyleColorButton)
|
||||
|
||||
// colors that are used in context where an imgui.PackedColor is required
|
||||
cols.windowBg = imgui.PackedColorFromVec4(cols.WindowBg)
|
||||
|
|
|
@ -27,7 +27,9 @@ import (
|
|||
"golang.org/x/image/draw"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v4"
|
||||
"github.com/jetsetilly/gopher2600/archivefs"
|
||||
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
||||
"github.com/jetsetilly/gopher2600/gui/fonts"
|
||||
"github.com/jetsetilly/gopher2600/hardware/peripherals"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/specification"
|
||||
"github.com/jetsetilly/gopher2600/logger"
|
||||
|
@ -45,16 +47,15 @@ type winSelectROM struct {
|
|||
|
||||
img *SdlImgui
|
||||
|
||||
currPath string
|
||||
entries []os.DirEntry
|
||||
|
||||
selectedFile string
|
||||
selectedFileBase string
|
||||
selectedFileProperties properties.Entry
|
||||
path archivefs.Path
|
||||
pathEntries []archivefs.Node
|
||||
|
||||
// selectedName is the name of the ROM in a normalised form
|
||||
selectedName string
|
||||
|
||||
// properties of selected
|
||||
selectedProperties properties.Entry
|
||||
|
||||
showAllFiles bool
|
||||
showHidden bool
|
||||
|
||||
|
@ -142,30 +143,17 @@ func (win winSelectROM) id() string {
|
|||
}
|
||||
|
||||
func (win *winSelectROM) setOpen(open bool) {
|
||||
if open {
|
||||
var err error
|
||||
var p string
|
||||
|
||||
// open at the most recently selected ROM
|
||||
f := win.img.dbg.Prefs.RecentROM.String()
|
||||
if f == "" {
|
||||
p, err = os.Getwd()
|
||||
if err != nil {
|
||||
logger.Logf("sdlimgui", err.Error())
|
||||
}
|
||||
} else {
|
||||
p = filepath.Dir(f)
|
||||
}
|
||||
|
||||
// set path and selected file
|
||||
err = win.setPath(p)
|
||||
if err != nil {
|
||||
logger.Logf("sdlimgui", "error setting path (%s)", p)
|
||||
}
|
||||
win.setSelectedFile(f)
|
||||
|
||||
if !open {
|
||||
win.path.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// open at the most recently selected ROM
|
||||
recent := win.img.dbg.Prefs.RecentROM.String()
|
||||
err := win.setPath(recent)
|
||||
if err != nil {
|
||||
logger.Logf("sdlimgui", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (win *winSelectROM) playmodeSetOpen(open bool) {
|
||||
|
@ -266,10 +254,10 @@ func (win *winSelectROM) render() {
|
|||
func (win *winSelectROM) draw() {
|
||||
// check for new property information
|
||||
select {
|
||||
case win.selectedFileProperties = <-win.propertyResult:
|
||||
win.selectedName = win.selectedFileProperties.Name
|
||||
case win.selectedProperties = <-win.propertyResult:
|
||||
win.selectedName = win.selectedProperties.Name
|
||||
if win.selectedName == "" {
|
||||
win.selectedName = win.selectedFileBase
|
||||
win.selectedName = win.path.Base()
|
||||
win.selectedName = strings.TrimSuffix(win.selectedName, filepath.Ext(win.selectedName))
|
||||
}
|
||||
|
||||
|
@ -291,7 +279,7 @@ func (win *winSelectROM) draw() {
|
|||
}()
|
||||
|
||||
if imgui.Button("Parent") {
|
||||
d := filepath.Dir(win.currPath)
|
||||
d := filepath.Dir(win.path.Dir())
|
||||
err := win.setPath(d)
|
||||
if err != nil {
|
||||
logger.Logf("sdlimgui", "error setting path (%s)", d)
|
||||
|
@ -300,7 +288,7 @@ func (win *winSelectROM) draw() {
|
|||
}
|
||||
|
||||
imgui.SameLine()
|
||||
imgui.Text(win.currPath)
|
||||
imgui.Text(archivefs.RemoveArchiveExt(win.path.Dir()))
|
||||
|
||||
if imgui.BeginTable("romSelector", 2) {
|
||||
imgui.TableSetupColumnV("filelist", imgui.TableColumnFlagsWidthStretch, -1, 0)
|
||||
|
@ -319,25 +307,27 @@ func (win *winSelectROM) draw() {
|
|||
|
||||
// list directories
|
||||
imgui.PushStyleColor(imgui.StyleColorText, win.img.cols.ROMSelectDir)
|
||||
for _, f := range win.entries {
|
||||
for _, e := range win.pathEntries {
|
||||
// ignore dot files
|
||||
if !win.showHidden && f.Name()[0] == '.' {
|
||||
if !win.showHidden && e.Name[0] == '.' {
|
||||
continue
|
||||
}
|
||||
|
||||
fi, err := os.Stat(filepath.Join(win.currPath, f.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if fi.Mode().IsDir() {
|
||||
if e.IsDir {
|
||||
s := strings.Builder{}
|
||||
s.WriteString(f.Name())
|
||||
s.WriteString(" [dir]")
|
||||
if e.IsArchive {
|
||||
s.WriteString(string(fonts.Paperclip))
|
||||
s.WriteString(" ")
|
||||
s.WriteString(archivefs.TrimArchiveExt(e.Name))
|
||||
} else {
|
||||
s.WriteString(string(fonts.Directory))
|
||||
s.WriteString(" ")
|
||||
s.WriteString(e.Name)
|
||||
}
|
||||
|
||||
if imgui.Selectable(s.String()) {
|
||||
d := filepath.Join(win.currPath, f.Name())
|
||||
err = win.setPath(d)
|
||||
d := filepath.Join(win.path.Dir(), e.Name)
|
||||
err := win.setPath(d)
|
||||
if err != nil {
|
||||
logger.Logf("sdlimgui", "error setting path (%s)", d)
|
||||
}
|
||||
|
@ -349,19 +339,14 @@ func (win *winSelectROM) draw() {
|
|||
|
||||
// list files
|
||||
imgui.PushStyleColor(imgui.StyleColorText, win.img.cols.ROMSelectFile)
|
||||
for _, f := range win.entries {
|
||||
for _, e := range win.pathEntries {
|
||||
// ignore dot files
|
||||
if !win.showHidden && f.Name()[0] == '.' {
|
||||
continue
|
||||
}
|
||||
|
||||
fi, err := os.Stat(filepath.Join(win.currPath, f.Name()))
|
||||
if err != nil {
|
||||
if !win.showHidden && e.Name[0] == '.' {
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore invalid file extensions unless showAllFiles flags is set
|
||||
ext := strings.ToUpper(filepath.Ext(fi.Name()))
|
||||
ext := strings.ToUpper(filepath.Ext(e.Name))
|
||||
if !win.showAllFiles {
|
||||
hasExt := false
|
||||
for _, e := range cartridgeloader.FileExtensions {
|
||||
|
@ -370,20 +355,28 @@ func (win *winSelectROM) draw() {
|
|||
break
|
||||
}
|
||||
}
|
||||
if !hasExt {
|
||||
for _, e := range archivefs.ArchiveExtensions {
|
||||
if e == ext {
|
||||
hasExt = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasExt {
|
||||
continue // to next file
|
||||
}
|
||||
}
|
||||
|
||||
if fi.Mode().IsRegular() {
|
||||
selected := f.Name() == win.selectedFileBase
|
||||
if !e.IsDir {
|
||||
selected := e.Name == win.path.Base()
|
||||
|
||||
if selected && win.centreOnFile {
|
||||
imgui.SetScrollHereY(0.0)
|
||||
}
|
||||
|
||||
if imgui.SelectableV(f.Name(), selected, 0, imgui.Vec2{0, 0}) {
|
||||
win.setSelectedFile(filepath.Join(win.currPath, f.Name()))
|
||||
if imgui.SelectableV(e.Name, selected, 0, imgui.Vec2{0, 0}) {
|
||||
win.setPath(filepath.Join(win.path.Dir(), e.Name))
|
||||
}
|
||||
if imgui.IsItemHovered() && imgui.IsMouseDoubleClicked(0) {
|
||||
win.insertCartridge()
|
||||
|
@ -438,8 +431,8 @@ func (win *winSelectROM) draw() {
|
|||
imgui.TableNextColumn()
|
||||
imgui.Text("Name")
|
||||
imgui.TableNextColumn()
|
||||
if win.selectedFileProperties.IsValid() {
|
||||
imgui.Text(win.selectedFileProperties.Name)
|
||||
if win.selectedProperties.IsValid() {
|
||||
imgui.Text(win.selectedProperties.Name)
|
||||
} else {
|
||||
imgui.Text(win.selectedName)
|
||||
}
|
||||
|
@ -507,38 +500,38 @@ func (win *winSelectROM) draw() {
|
|||
imgui.PopStyleVar()
|
||||
imgui.PopItemFlag()
|
||||
|
||||
if win.selectedFileProperties.Manufacturer != "" {
|
||||
if win.selectedProperties.Manufacturer != "" {
|
||||
imgui.TableNextRow()
|
||||
imgui.TableNextColumn()
|
||||
imgui.AlignTextToFramePadding()
|
||||
imgui.Text("Manufacturer")
|
||||
imgui.TableNextColumn()
|
||||
imgui.Text(win.selectedFileProperties.Manufacturer)
|
||||
imgui.Text(win.selectedProperties.Manufacturer)
|
||||
}
|
||||
if win.selectedFileProperties.Rarity != "" {
|
||||
if win.selectedProperties.Rarity != "" {
|
||||
imgui.TableNextRow()
|
||||
imgui.TableNextColumn()
|
||||
imgui.AlignTextToFramePadding()
|
||||
imgui.Text("Rarity")
|
||||
imgui.TableNextColumn()
|
||||
imgui.Text(win.selectedFileProperties.Rarity)
|
||||
imgui.Text(win.selectedProperties.Rarity)
|
||||
}
|
||||
if win.selectedFileProperties.Model != "" {
|
||||
if win.selectedProperties.Model != "" {
|
||||
imgui.TableNextRow()
|
||||
imgui.TableNextColumn()
|
||||
imgui.AlignTextToFramePadding()
|
||||
imgui.Text("Model")
|
||||
imgui.TableNextColumn()
|
||||
imgui.Text(win.selectedFileProperties.Model)
|
||||
imgui.Text(win.selectedProperties.Model)
|
||||
}
|
||||
|
||||
if win.selectedFileProperties.Note != "" {
|
||||
if win.selectedProperties.Note != "" {
|
||||
imgui.TableNextRow()
|
||||
imgui.TableNextColumn()
|
||||
imgui.AlignTextToFramePadding()
|
||||
imgui.Text("Note")
|
||||
imgui.TableNextColumn()
|
||||
imgui.Text(win.selectedFileProperties.Note)
|
||||
imgui.Text(win.selectedProperties.Note)
|
||||
}
|
||||
|
||||
imgui.EndTable()
|
||||
|
@ -590,11 +583,11 @@ func (win *winSelectROM) draw() {
|
|||
win.playmodeSetOpen(false)
|
||||
}
|
||||
|
||||
if win.selectedFile != "" {
|
||||
if win.selectedName != "" {
|
||||
var s string
|
||||
|
||||
// load or reload button
|
||||
if win.selectedFile == win.img.cache.VCS.Mem.Cart.Filename {
|
||||
if win.path.String() == win.img.cache.VCS.Mem.Cart.Filename {
|
||||
s = fmt.Sprintf("Reload %s", win.selectedName)
|
||||
} else {
|
||||
s = fmt.Sprintf("Load %s", win.selectedName)
|
||||
|
@ -621,7 +614,7 @@ func (win *winSelectROM) insertCartridge() {
|
|||
return
|
||||
}
|
||||
|
||||
win.img.dbg.InsertCartridge(win.selectedFile)
|
||||
win.img.dbg.InsertCartridge(win.path.String())
|
||||
|
||||
// close rom selected in both the debugger and playmode
|
||||
win.debuggerSetOpen(false)
|
||||
|
@ -630,33 +623,28 @@ func (win *winSelectROM) insertCartridge() {
|
|||
|
||||
func (win *winSelectROM) setPath(path string) error {
|
||||
var err error
|
||||
win.currPath = filepath.Clean(path)
|
||||
win.entries, err = os.ReadDir(win.currPath)
|
||||
win.path.Set(path)
|
||||
win.pathEntries, err = win.path.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
win.setSelectedFile("")
|
||||
if win.path.IsDir() {
|
||||
win.setSelectedFile("")
|
||||
} else {
|
||||
win.setSelectedFile(win.path.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (win *winSelectROM) setSelectedFile(filename string) {
|
||||
// do nothing if this file has already been selected
|
||||
if win.selectedFile == filename {
|
||||
return
|
||||
}
|
||||
|
||||
// update selected file. return immediately if the filename is empty
|
||||
win.selectedFile = filename
|
||||
if filename == "" {
|
||||
win.selectedFileBase = ""
|
||||
win.selectedName = ""
|
||||
return
|
||||
}
|
||||
|
||||
// base filename for presentation purposes
|
||||
win.selectedFileBase = filepath.Base(filename)
|
||||
// selected name will be
|
||||
win.selectedName = ""
|
||||
|
||||
// return immediately if the filename is empty
|
||||
if filename == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// create cartridge loader and start thumbnail emulation
|
||||
loader, err := cartridgeloader.NewLoaderFromFilename(filename, "AUTO")
|
||||
if err != nil {
|
||||
|
@ -679,7 +667,7 @@ func (win *winSelectROM) findBoxart() error {
|
|||
win.boxartUse = false
|
||||
|
||||
// fuzzy find a candidate image
|
||||
n, _, _ := strings.Cut(win.selectedFileProperties.Name, "(")
|
||||
n, _, _ := strings.Cut(win.selectedProperties.Name, "(")
|
||||
n = strings.TrimSpace(n)
|
||||
m := fuzzy.Find(n, win.boxart)
|
||||
if len(m) == 0 {
|
||||
|
|
|
@ -345,6 +345,7 @@ func (cart *Moviecart) Plumb(env *environment.Environment) {
|
|||
|
||||
// Reset implements the mapper.CartMapper interface.
|
||||
func (cart *Moviecart) Reset() {
|
||||
cart.state.initialise()
|
||||
}
|
||||
|
||||
// Access implements the mapper.CartMapper interface.
|
||||
|
|
Loading…
Reference in a new issue