From 685bf7ccc759df89112da8137c248718f18d5003 Mon Sep 17 00:00:00 2001 From: JetSetIlly Date: Sat, 3 Feb 2024 18:31:54 +0000 Subject: [PATCH] added support for stella.pro files added properties package ROM select window display property information --- cartridgeloader/loader.go | 12 +++ debugger/debugger.go | 10 +++ debugger/push.go | 9 +++ gui/sdlimgui/win_rom_select.go | 126 ++++++++++++++++++++++++++----- properties/doc.go | 18 +++++ properties/properties.go | 132 +++++++++++++++++++++++++++++++++ thumbnailer/anim.go | 12 +++ 7 files changed, 299 insertions(+), 20 deletions(-) create mode 100644 properties/doc.go create mode 100644 properties/properties.go diff --git a/cartridgeloader/loader.go b/cartridgeloader/loader.go index 87d316a7..398763b3 100644 --- a/cartridgeloader/loader.go +++ b/cartridgeloader/loader.go @@ -17,6 +17,7 @@ package cartridgeloader import ( "bytes" + "crypto/md5" "crypto/sha1" "errors" "fmt" @@ -65,6 +66,9 @@ type Loader struct { // string is empty then that check passes. Hash string + // HashMD5 is an alternative to hash for use with the properties package + HashMD5 string + // does the Data field consist of sound (PCM) data IsSoundData bool @@ -320,6 +324,7 @@ func NewLoaderFromEmbed(name string, data []byte, mapping string) (Loader, error Data: &data, embedded: true, Hash: fmt.Sprintf("%x", sha1.Sum(data)), + HashMD5: fmt.Sprintf("%x", md5.Sum(data)), NotificationHook: func(cart mapper.CartMapper, notice notifications.Notify, args ...interface{}) error { return nil }, @@ -450,5 +455,12 @@ func (cl *Loader) Load() error { } cl.Hash = hash + // generate md5 hash and check for consistency + hashmd5 := fmt.Sprintf("%x", md5.Sum(*cl.Data)) + if cl.HashMD5 != "" && cl.HashMD5 != hashmd5 { + return fmt.Errorf("cartridgeloader: unexpected hash value") + } + cl.HashMD5 = hashmd5 + return nil } diff --git a/debugger/debugger.go b/debugger/debugger.go index 8d1f0456..dcefb381 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -55,6 +55,7 @@ import ( "github.com/jetsetilly/gopher2600/patch" "github.com/jetsetilly/gopher2600/prefs" "github.com/jetsetilly/gopher2600/preview" + "github.com/jetsetilly/gopher2600/properties" "github.com/jetsetilly/gopher2600/recorder" "github.com/jetsetilly/gopher2600/reflection" "github.com/jetsetilly/gopher2600/reflection/counter" @@ -120,6 +121,9 @@ type Debugger struct { term terminal.Terminal controllers *userinput.Controllers + // stella.pro file support + pro properties.Properties + // bots coordinator bots *wrangler.Bots @@ -364,6 +368,12 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger // create bot coordinator dbg.bots = wrangler.NewBots(dbg.vcs.Input, dbg.vcs.TV) + // stella.pro support + dbg.pro, err = properties.Load() + if err != nil { + logger.Logf("debugger", err.Error()) + } + // create preview emulation dbg.preview, err = preview.NewEmulation(dbg.vcs.Env.Prefs) if err != nil { diff --git a/debugger/push.go b/debugger/push.go index 24de4163..8234962a 100644 --- a/debugger/push.go +++ b/debugger/push.go @@ -19,6 +19,7 @@ import ( "github.com/jetsetilly/gopher2600/debugger/govern" "github.com/jetsetilly/gopher2600/disassembly" "github.com/jetsetilly/gopher2600/logger" + "github.com/jetsetilly/gopher2600/properties" ) // PushFunction onto the event queue. Used to ensure that the events are @@ -72,3 +73,11 @@ func (dbg *Debugger) PushTogglePCBreak(e *disassembly.Entry) { dbg.halting.breakpoints.togglePCBreak(f) }) } + +// PushPropertyLookup looks up the supplied MD5 hash in the properties table +func (dbg *Debugger) PushPropertyLookup(hashMD5 string, result chan properties.Entry) { + dbg.PushFunctionImmediate(func() { + e, _ := dbg.pro.Lookup(hashMD5) + result <- e + }) +} diff --git a/gui/sdlimgui/win_rom_select.go b/gui/sdlimgui/win_rom_select.go index 0822100e..7b5f8fb8 100644 --- a/gui/sdlimgui/win_rom_select.go +++ b/gui/sdlimgui/win_rom_select.go @@ -28,6 +28,7 @@ import ( "github.com/jetsetilly/gopher2600/cartridgeloader" "github.com/jetsetilly/gopher2600/hardware/television/specification" "github.com/jetsetilly/gopher2600/logger" + "github.com/jetsetilly/gopher2600/properties" "github.com/jetsetilly/gopher2600/thumbnailer" ) @@ -43,7 +44,12 @@ type winSelectROM struct { entries []os.DirEntry err error - selectedFile string + selectedFile string + selectedFileBase string + loader cartridgeloader.Loader + properties properties.Entry + propertiesOpen bool + showAllFiles bool showHidden bool @@ -58,15 +64,20 @@ type winSelectROM struct { thmbImage *image.RGBA thmbDimensions image.Point + + // the return channel from the emulation goroutine for the property lookup + // for the selected cartridge + propertyResult chan properties.Entry } func newSelectROM(img *SdlImgui) (window, error) { win := &winSelectROM{ - img: img, - showAllFiles: false, - showHidden: false, - scrollToTop: true, - centreOnFile: true, + img: img, + showAllFiles: false, + showHidden: false, + scrollToTop: true, + centreOnFile: true, + propertyResult: make(chan properties.Entry, 1), } win.debuggerGeom.noFousTracking = true @@ -219,6 +230,12 @@ func (win *winSelectROM) render() { } func (win *winSelectROM) draw() { + // check for new property information + select { + case win.properties = <-win.propertyResult: + default: + } + // reset centreOnFile at end of draw defer func() { win.centreOnFile = false @@ -310,7 +327,7 @@ func (win *winSelectROM) draw() { } if fi.Mode().IsRegular() { - selected := f.Name() == filepath.Base(win.selectedFile) + selected := f.Name() == win.selectedFileBase if selected && win.centreOnFile { imgui.SetScrollHereY(0.0) @@ -337,19 +354,75 @@ func (win *winSelectROM) draw() { // control buttons. start controlHeight measurement win.controlHeight = imguiMeasureHeight(func() { - if win.thmb.IsEmulating() { - imgui.Text(win.thmb.String()) - } else { - imgui.Text("") - } + func() { + if !win.thmb.IsEmulating() { + imgui.PushItemFlag(imgui.ItemFlagsDisabled, true) + imgui.PushStyleVarFloat(imgui.StyleVarAlpha, disabledAlpha) + defer imgui.PopItemFlag() + defer imgui.PopStyleVar() + } + + imgui.SetNextItemOpen(win.propertiesOpen, imgui.ConditionAlways) + if !imgui.CollapsingHeaderV(win.selectedFileBase, imgui.TreeNodeFlagsNone) { + win.propertiesOpen = false + } else { + win.propertiesOpen = true + if win.properties.IsValid() { + if imgui.BeginTable("#properties", 2) { + imgui.TableSetupColumnV("#category", imgui.TableColumnFlagsWidthFixed, -1, 0) + imgui.TableSetupColumnV("#detail", imgui.TableColumnFlagsWidthFixed, -1, 1) + + imgui.TableNextRow() + imgui.TableNextColumn() + imgui.Text("Name") + imgui.TableNextColumn() + imgui.Text(win.properties.Name) + + if win.properties.Manufacturer != "" { + imgui.TableNextRow() + imgui.TableNextColumn() + imgui.Text("Manufacturer") + imgui.TableNextColumn() + imgui.Text(win.properties.Manufacturer) + } + if win.properties.Rarity != "" { + imgui.TableNextRow() + imgui.TableNextColumn() + imgui.Text("Rarity") + imgui.TableNextColumn() + imgui.Text(win.properties.Rarity) + } + if win.properties.Model != "" { + imgui.TableNextRow() + imgui.TableNextColumn() + imgui.Text("Model") + imgui.TableNextColumn() + imgui.Text(win.properties.Model) + } + + if win.properties.Note != "" { + imgui.TableNextRow() + imgui.TableNextColumn() + imgui.Text("Note") + imgui.TableNextColumn() + imgui.Text(win.properties.Note) + } + + imgui.EndTable() + } + } else { + imgui.Text("No information") + } + } + }() imguiSeparator() - imgui.Checkbox("Show all files", &win.showAllFiles) - imgui.SameLine() - imgui.Checkbox("Show hidden files", &win.showHidden) + // imgui.Checkbox("Show all files", &win.showAllFiles) + // imgui.SameLine() + // imgui.Checkbox("Show hidden files", &win.showHidden) - imgui.Spacing() + // imgui.Spacing() if imgui.Button("Cancel") { // close rom selected in both the debugger and playmode @@ -363,9 +436,9 @@ func (win *winSelectROM) draw() { // load or reload button if win.selectedFile == win.img.cache.VCS.Mem.Cart.Filename { - s = fmt.Sprintf("Reload %s", filepath.Base(win.selectedFile)) + s = fmt.Sprintf("Reload %s", win.selectedFileBase) } else { - s = fmt.Sprintf("Load %s", filepath.Base(win.selectedFile)) + s = fmt.Sprintf("Load %s", win.selectedFileBase) } // only show load cartridge button if the file is being @@ -419,15 +492,28 @@ func (win *winSelectROM) setSelectedFile(filename string) { // update selected file. return immediately if the filename is empty win.selectedFile = filename if filename == "" { + win.selectedFileBase = "" return } + // base filename for presentation purposes + win.selectedFileBase = filepath.Base(filename) + + var err error + // create cartridge loader and start thumbnail emulation - cartload, err := cartridgeloader.NewLoader(filename, "AUTO") + win.loader, err = cartridgeloader.NewLoader(filename, "AUTO") if err != nil { logger.Logf("ROM Select", err.Error()) return } - win.thmb.Create(cartload, thumbnailer.UndefinedNumFrames) + // push function to emulation goroutine. result will be checked for in + // draw() function + if err := win.loader.Load(); err == nil { + win.img.dbg.PushPropertyLookup(win.loader.HashMD5, win.propertyResult) + } + + // create thumbnail animation + win.thmb.Create(win.loader, thumbnailer.UndefinedNumFrames) } diff --git a/properties/doc.go b/properties/doc.go new file mode 100644 index 00000000..bfdc4c89 --- /dev/null +++ b/properties/doc.go @@ -0,0 +1,18 @@ +// 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 properties adds support for the Stella properties file, named +// stella.pro. The file should be placed in the Gopher2600 configuration folder +package properties diff --git a/properties/properties.go b/properties/properties.go new file mode 100644 index 00000000..900016f8 --- /dev/null +++ b/properties/properties.go @@ -0,0 +1,132 @@ +// 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 properties + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/jetsetilly/gopher2600/logger" + "github.com/jetsetilly/gopher2600/resources" +) + +const propertiesFile = "stella.pro" + +type Entry struct { + Hash string + Name string + Manufacturer string + Note string + Rarity string + Model string +} + +func (e Entry) IsValid() bool { + return len(e.Hash) > 0 +} + +type Properties struct { + available bool + entries map[string]Entry +} + +func Load() (Properties, error) { + pro := Properties{ + entries: make(map[string]Entry), + } + + path, err := resources.JoinPath(propertiesFile) + if err != nil { + return Properties{}, fmt.Errorf("properties: %w", err) + } + + f, err := os.Open(path) + if err != nil { + return Properties{}, fmt.Errorf("properties: %w", err) + } + defer f.Close() + + scanner := bufio.NewScanner(f) + + var entry Entry + var line int + var rejected int + + for scanner.Scan() { + line++ + + flds := strings.SplitN(scanner.Text(), " ", 2) + if len(flds) < 2 { + continue // for loop + } + + flds[0] = strings.Trim(flds[0], "\"") + flds[1] = strings.Trim(flds[1], "\"") + switch strings.ToUpper(flds[0]) { + case "CART.MD5": + // create new entry + if len(entry.Hash) == 32 { + pro.entries[entry.Hash] = entry + } + + // prepare new entry if has is valid + if len(flds[1]) != 32 { + logger.Logf("properties", "invalid hash entry at line %d", line) + rejected++ + } else { + entry = Entry{ + Hash: flds[1], + } + } + + case "CART.NAME": + entry.Name = flds[1] + + case "CART.MANUFACTURER": + entry.Manufacturer = flds[1] + + case "CART.NOTE": + entry.Note = flds[1] + + case "CART.RARITY": + entry.Rarity = flds[1] + + case "CART.MODEL": + entry.Model = flds[1] + } + } + + if err = scanner.Err(); err != nil { + return Properties{}, fmt.Errorf("pro: %w", err) + } + + logger.Logf("properties", "%d entries loaded", len(pro.entries)) + if rejected > 0 { + logger.Logf("properties", "%d entries rejected", rejected) + } + + return pro, nil +} + +// Find the property entry for the ROM with the supplied md5 hash +func (pro Properties) Lookup(md5Hash string) (Entry, bool) { + if e, ok := pro.entries[md5Hash]; ok { + return e, true + } + return Entry{}, false +} diff --git a/thumbnailer/anim.go b/thumbnailer/anim.go index b80a660c..f76bc5f8 100644 --- a/thumbnailer/anim.go +++ b/thumbnailer/anim.go @@ -55,6 +55,9 @@ type Anim struct { emulationCompleted chan bool Render chan *image.RGBA + + snapshot chan bool + snapshotVCS chan *hardware.VCS } // NewAnim is the preferred method of initialisation for the Anim type @@ -63,6 +66,8 @@ func NewAnim(prefs *preferences.Preferences) (*Anim, error) { emulationQuit: make(chan bool, 1), emulationCompleted: make(chan bool, 1), Render: make(chan *image.RGBA, 60), + snapshot: make(chan bool, 1), + snapshotVCS: make(chan *hardware.VCS, 1), } // emulation has completed, by definition, on startup @@ -211,6 +216,7 @@ func (thmb *Anim) Create(cartload cartridgeloader.Loader, numFrames int) { select { case <-thmb.emulationQuit: return govern.Ending, nil + case <-thmb.snapshot: default: } @@ -323,3 +329,9 @@ func (thmb *Anim) Reset() { func (thmb *Anim) EndRendering() error { return nil } + +// Snapshot safely returns a copy of the emulation +func (thmb *Anim) Snapshot() *hardware.VCS { + thmb.snapshot <- true + return <-thmb.snapshotVCS +}