Skip to content

Commit

Permalink
Added write modal exposing the 'restore backup' feature from PowerSav…
Browse files Browse the repository at this point in the history
…es for Amiibo software.
  • Loading branch information
malc0mn committed Dec 31, 2023
1 parent 3ff7f3e commit 79893c3
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 10 deletions.
18 changes: 13 additions & 5 deletions cmd/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type ui struct {
imageBox *imageBox
usageBox *box
logBox *box
write chan []byte
amb amiibo.Amiidump
dec bool

Expand Down Expand Up @@ -172,15 +173,24 @@ func newUi(invertImage bool) *ui {
imageBox: image,
usageBox: usage,
logBox: logs,
write: make(chan []byte),
}

// TODO: prevent overwriting modals when they're active (like reading a new amiibo while the dump modal is open)
save := newFilenameModal(s, boxOpts{title: "save dump", key: 's', xPos: -1, yPos: -1, width: 30, height: 10, minHeight: 6, minWidth: 84, needAmiibo: true}, logs.content, saveDump)
load := newFilenameModal(s, boxOpts{title: "load dump", key: 'l', xPos: -1, yPos: -1, width: 30, height: 10, minHeight: 6, minWidth: 84}, logs.content, loadDump)
// TODO: it would be cool to highlight the different data blocks in the hex dump (like ID, save data, ...)
hex := newTextModal(s, boxOpts{title: "view dump as hex", key: 'h', xPos: -1, yPos: -1, width: 84, height: 36, typ: boxTypeCharacter, needAmiibo: true, scroll: true}, logs.content)
write := newOptionsModal(
s,
boxOpts{title: "write amiibo data to token", key: 'w', xPos: -1, yPos: -1, width: 80, height: 9, typ: boxTypeCharacter, needAmiibo: true},
logs.content,
[]mopts{{'f', "write full amiibo to token", 0}, {'u', "only write userdata to token (aka 'restore backup')", 1}},
prepData,
u.write,
)

u.elements = []element{info, image, usage, logs, actions, save, load, hex}
u.elements = []element{info, image, usage, logs, actions, save, load, write, hex}

return u
}
Expand Down Expand Up @@ -222,6 +232,8 @@ func tui(conf *config) {
u.setAmiibo(am, isAmiiboDecrypted(am, conf.retailKey))
showAmiiboInfo(u.amiibo(), u.isDecrypted(), u.logBox.content, u.infoBox.content, u.usageBox.content, u.imageBox, conf.amiiboApiBaseUrl)
u.draw(false)
case data := <-u.write:
ptl.write(data[1:], data[0] == 1)
case <-conf.quit:
return
}
Expand Down Expand Up @@ -264,10 +276,6 @@ func tui(conf *config) {
case e.Rune() == 'I' || e.Rune() == 'i':
u.logBox.content <- encodeStringCell("Toggle image invert")
u.imageBox.invertImage()
case e.Rune() == 'W' || e.Rune() == 'w':
if data := prepData(u.amiibo(), u.isDecrypted(), u.logBox.content); data != nil {
ptl.write(data, false)
}
default:
u.handleElementKey(e.Rune())
}
Expand Down
85 changes: 85 additions & 0 deletions cmd/ui_box_modal_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"github.com/gdamore/tcell/v2"
"github.com/malc0mn/amiigo/amiibo"
)

// mopts describes a single option for an options modal.
// The idea of using a key to select an option and not numbers before the options, is that the user
// is forced to pay proper attention as to what is going on.
type mopts struct {
key rune // The key to select the option, should be present in text: it will be rendered underlined.
text string // The text for the option.
value int // The value passed to the submit handler for the selected option.
}

// optionsSubmitHandler defines a submithandler for an optionsModal, receiving the selected option
// value and an amiibo struct.
type optionsSubmitHandler func(value int, a amiibo.Amiidump, log chan<- []byte) []byte

// optionsModal represents a modal that will request the user to select an option. It holds a
// channel to which the data of the submit handler will be sent before the modal is closed.
type optionsModal struct {
*modal
opts []mopts
submit optionsSubmitHandler
ret chan<- []byte
}

// newOptionsModal creates a new optionsModal struct ready for use.
func newOptionsModal(s tcell.Screen, opts boxOpts, log chan<- []byte, mopts []mopts, submit optionsSubmitHandler, ret chan<- []byte) *optionsModal {
o := &optionsModal{opts: mopts, submit: submit, ret: ret}
o.modal = newModal(s, opts, o.handleInput, o.drawModalContent, nil, log)

return o
}

// handleInput will handle keyboard input for the optionsModal.
func (o *optionsModal) handleInput(e *tcell.EventKey) {
for _, opt := range o.opts {
if e.Rune() == opt.key {
o.ret <- o.submit(opt.value, o.a, o.log)
// Signal the modal is done.
o.end()
}
}
}

// drawModalContent will handle displaying of the drawModalContent content.
func (o *optionsModal) drawModalContent(x, y int) {
x++
y++
start := x
prompt := "Please select an option by pressing the key of the underlined char:"
for _, char := range prompt {
o.drawChar(x, y, char, tcell.AttrNone)
x++
}
y += 2 // Add blank line as well
start++ // Indent with one char for options
x = start // Back to start of line

for _, opt := range o.opts {
optKey := false
o.drawChar(x, y, '•', tcell.AttrBold)
x += 2
for _, char := range opt.text {
attr := tcell.AttrNone
if !optKey && char == opt.key {
attr = tcell.AttrUnderline | tcell.AttrBold
optKey = true
}
o.drawChar(x, y, char, attr)
x++
}
x = start
y += 2
}
o.s.Show()
}

// drawChar draws a single char on the given position inside the modal.
func (o *optionsModal) drawChar(x, y int, c rune, attr tcell.AttrMask) {
o.s.SetContent(x, y, c, nil, tcell.StyleDefault.Background(backColour).Foreground(fontColour).Attributes(attr))
}
15 changes: 10 additions & 5 deletions cmd/ui_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ func saveDump(filename string, a amiibo.Amiidump, log chan<- []byte) bool {
}

// prepData gets the amiibo data in the correct format for writing to the NFC portal.
func prepData(a amiibo.Amiidump, dec bool, log chan<- []byte) []byte {
if dec {
func prepData(value int, a amiibo.Amiidump, log chan<- []byte) []byte {
if isAmiiboDecrypted(a, conf.retailKey) {
log <- encodeStringCell("Refusing to write decrypted amiibo!")
return nil
}
Expand All @@ -145,14 +145,19 @@ func prepData(a amiibo.Amiidump, dec bool, log chan<- []byte) []byte {
return nil
}

var data []byte

switch a.(type) {
case *amiibo.Amiitool:
return amiibo.AmiitoolToAmiibo(a.(*amiibo.Amiitool)).Raw()
data = amiibo.AmiitoolToAmiibo(a.(*amiibo.Amiitool)).Raw()
case *amiibo.Amiibo:
return a.Raw()
data = a.Raw()
default:
panic(fmt.Sprintf("Unknown amiibo type!"))
log <- encodeStringCell("Cannot write: unknown amiibo type!")
return nil
}

return append([]byte{byte(value)}, data...)
}

// decrypt decrypts the given amiibo and returns a new amiibo.Amiidump instance.
Expand Down

0 comments on commit 79893c3

Please sign in to comment.