Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
g026r committed Sep 16, 2024
1 parent 155ac74 commit c346560
Show file tree
Hide file tree
Showing 58 changed files with 6,234 additions and 1 deletion.
28 changes: 28 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ go.work.sum

# env file
.env
/pocket-library-editor.iml
/vendor/
/.idea/
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,50 @@
# pocket-library-editor
# pocket-library-editor

A rough-and-ready way to edit your Pocket's library.

## General Warning

This product is provided as-is. While the chance that it breaks your Pocket is extremely low, it is very much
experimental software that is doing something Analogue was not expecting users to do. Use at your own risk.

## But Why?

Because I can get remarkably anal about these things.

First off: 95% of Pocket users won't need or even want this.

This software is for the users who are annoyed that their library shows `Famicom Mini 01 - Super Mario Bros.` but also
`Famicom Mini 22: Nazo no Murasame Jou`, that it's `The Lion King` but `NewZealand Story, The`. It's for those users who
have one of the small number of carts that the Pocket misidentifies & who'd rather it appeared in their library under
the correct name.

## Limitations

The library info screen for a given cart is stored in the Pocket's internal memory. Even if your library
now shows "Sagaia" instead of "Mani 4 in 1 - Taito", clicking into it or loading the cart will still show you the
original info.

Additionally, if you have two different entries with the same cart signature, it's likely that only the playtime for the
first will get updated.

## Troubleshooting

_"My Pocket freezes or starts crash looping when trying to access the new library"_

Something's probably gone wrong with the new library if this happens. From my experience, the solution is to power down
your Pocket (hold the power button until it turns off) and then replace the list.bin and playtimes.bin files on the SD
card with the backup copies you made.

You did make backups, right? If not, just delete the two files but you'll have to rebuild
your library from scratch after.

Simply removing the SD card won't solve this problem and powering down is the only way to restore operation.

## Technical Details

Analogue has not made the file format for list.bin, playtimes.bin, or *_thumbs.bin available in their developer docs.
The following is what I've managed to decipher & reverse engineer from my copies.

* [list.bin](./docs/list.md)
* [playtimes.bin](./docs/playtimes.md)
* [*_thumbs.bin](./docs/thumbs.md)
6 changes: 6 additions & 0 deletions TODO.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- error handling
- can we do the text entry such that it checks each character & cancels if ESC is pressed? (+ Ctrl-C on Unix)
- can we allow left + right arrow keys? Or will that cause trouble with delete?
- can we update _thumbs.bin with a new image if we're adding & it doesn't exist?
- delete the record from _thumbs.bin when we remove?
- check a cart signature for Atari & PCE and see if I can generate it to check for conflicts. (Find unheadered ROMs?)
172 changes: 172 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package main

import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"

"github.com/g026r/pocket-library-editor/pkg"
"github.com/g026r/pocket-library-editor/pkg/model"
"github.com/g026r/pocket-library-editor/pkg/util"
)

//go:embed resources/*.json
var jsons embed.FS

func main() {
var err error
var arg string
switch len(os.Args) {
case 1:
if arg, err = os.Executable(); err != nil { // TODO: Would it be better to use cwd instead?
fatal(err)
}
case 2:
arg = os.Args[1]
default:
printUsage()
os.Exit(2)
}

app, err := loadPocketDir(arg)
if err != nil {
fatal(err)
}

if c, err := model.LoadConfig(); err == nil {
app.Config = c
}

if app.ShowAdd { // Only need to load these for the add UI
library, err := loadInternal()
if err != nil {
fatal(err)
}
app.Internal = library
}

if err := app.Run(); err != nil {
fatal(err)
}
}

func fatal(err error) {
fmt.Printf("\nFATAL ERROR: %v\n", err)
os.Exit(1)
}

func printUsage() {
fmt.Println("Usage: place in the root of your Pocket's SD card & run. Or run & pass it the path to the SD card root as an argument.")
fmt.Println("Outputs files in the current working directory.")
}

func loadPocketDir(d string) (pkg.Application, error) {
d, err := filepath.Abs(d)
if err != nil {
return pkg.Application{}, err
}
fi, err := os.Stat(d)
if err != nil {
return pkg.Application{}, err
} else if !fi.IsDir() {
return pkg.Application{}, fmt.Errorf("%s is not a directory", d)
}

root := os.DirFS(d)

pg, err := fs.Sub(root, "System/Played Games")
if err != nil {
return pkg.Application{}, nil
}
entries, err := model.ReadEntries(pg)
if err != nil {
return pkg.Application{}, err
}

playtimes, err := model.ReadPlayTimes(pg)
if err != nil {
return pkg.Application{}, err
}
if len(playtimes) != len(entries) {
return pkg.Application{}, fmt.Errorf("entry count mismatch between list.bin [%d] & playtimes.bin [%d]", len(entries), len(playtimes))
}

tb, err := fs.Sub(root, "System/Library/Images")
if err != nil {
return pkg.Application{}, nil
}
thumbs, err := model.LoadThumbnails(tb)
if err != nil {
return pkg.Application{}, err
}

return pkg.Application{
RootDir: root,
Entries: entries,
PlayTimes: playtimes,
Thumbs: thumbs,
Config: model.Config{
RemoveImages: true,
AdvancedEditing: false,
ShowAdd: true,
},
}, nil
}

func loadInternal() (map[util.System][]model.Entry, error) {
dir, err := jsons.ReadDir("resources")
if err != nil {
return nil, err
}

library := make(map[util.System][]model.Entry)
for _, d := range dir {
f, err := jsons.ReadFile(fmt.Sprintf("resources/%s", d.Name()))
if err != nil {
return nil, err
}
var x []jsonEntry
if err := json.Unmarshal(f, &x); err != nil {
return nil, err
}
sys, err := util.Parse(strings.TrimSuffix(d.Name(), ".json"))
if err != nil {
return nil, err
}

// Oh, for a native map function
e := make([]model.Entry, len(x))
for i := range x {
e[i] = x[i].Entry()
}

slices.SortFunc(e, model.EntrySort)
library[sys] = e
}

return library, nil
}

type jsonEntry struct {
util.System `json:"system"`
Crc32 string `json:"crc"`
Sig string `json:"sig"`
Magic string `json:"magic"` // TODO: Work out all possible mappings for this
Name string `json:"name"`
}

func (j jsonEntry) Entry() model.Entry {
e := model.Entry{
Name: j.Name,
System: j.System,
}
e.Sig, _ = util.HexStringTransform(j.Sig)
e.Magic, _ = util.HexStringTransform(j.Magic)
e.Crc32, _ = util.HexStringTransform(j.Crc32)
return e
}
Loading

0 comments on commit c346560

Please sign in to comment.