added support for stella.pro files

added properties package

ROM select window display property information
This commit is contained in:
JetSetIlly 2024-02-03 18:31:54 +00:00
parent 58315e5182
commit 685bf7ccc7
7 changed files with 299 additions and 20 deletions

View file

@ -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
}

View file

@ -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 {

View file

@ -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
})
}

View file

@ -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)
}

18
properties/doc.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
// Package properties adds support for the Stella properties file, named
// stella.pro. The file should be placed in the Gopher2600 configuration folder
package properties

132
properties/properties.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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
}

View file

@ -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
}