Compare commits

...

9 Commits

Author SHA1 Message Date
Zorchenhimer 045f204a6b
[cmd] Add just-stats command
This command is meant for gathering stats across many script files.
Currently breaks due to an oversight in script parsing (if an inline
argument list goes past EOF, everything breaks).
2025-09-06 22:56:10 -04:00
Zorchenhimer db2e5b5f87
[script] Add InlineImmediate field to Instruction
Setting this field to true will skip generating auto labels for the
inline values for the instruction.
2025-09-06 22:55:06 -04:00
Zorchenhimer 4bd0027c70
[script] Add some statisticts tracking
Track the statistics of used instructions for the script.
2025-09-06 22:53:33 -04:00
Zorchenhimer b3312a8500
[rom] Tweaked packet info output
Tweaked the packet info strings to be a little more clear on what fields
are printed.
2025-09-06 22:46:26 -04:00
Zorchenhimer 1ebe698ac4
[rom] Add NoAudio option to export
Added an option to exclude the audio file from the unpacked rom data.
2025-09-06 22:45:33 -04:00
Zorchenhimer 5a001931e7
Added missing cmd/sbutil.go
Not sure how this was missing for so long.  Whoops.
2025-09-06 22:43:13 -04:00
Zorchenhimer 4b9f04874b
[script] Better auto label support
Labels are now their own object instead of just a string.  This allows
for a bit more control with them.  Labels can also now have comments.

Additionally, add the ability to load user-defined labels from a file.
The format of this file is subject to change, but for now it is just a
simple text file.  Each line of the file is a label definition.  Each
line has three fields, separated by spaces.  The first field is the
address, second is the label name, and the rest of the line is a
comment.  If a label name is just a dollar sign ($) it will not have a
name.  This is used when adding comments without a bespoke label.
2025-09-06 22:38:39 -04:00
Zorchenhimer 29e48f3cac
[script] Fix varible inline instructions; Fix off-by-one
- Fixed instructions that have -3 as the OpCount (count then count
  words).  There is not an extra word that acts as the default
  selection.  These instructions do nothing if the argument is out of
  range.
- Fixed off-by-one eating the byte following the -3 OpCount
  instructions.
- Fixed panic when a -2 op code goes beyond the end of the script.
2025-09-06 22:27:45 -04:00
Zorchenhimer 6a3a51fef7
[rom] Fix exported file modes
Export files with mode 0666 instead of 0777.
2025-09-06 22:24:34 -04:00
12 changed files with 711 additions and 171 deletions

View File

@ -1,9 +1,12 @@
.PHONY: all
all: bin/script-decode bin/sbutil
all: bin/script-decode bin/sbutil bin/just-stats
bin/script-decode: cmd/script-decode.go script/*.go
go build -o $@ $<
bin/sbutil: cmd/sbutil.go rom/*.go
go build -o $@ $<
bin/just-stats: cmd/just-stats.go script/*.go
go build -o $@ $<

85
cmd/just-stats.go Normal file
View File

@ -0,0 +1,85 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"io/fs"
"github.com/alexflint/go-arg"
"git.zorchenhimer.com/Zorchenhimer/go-studybox/script"
)
type Arguments struct {
BaseDir string `arg:"positional,required"`
Output string `arg:"positional,required"`
}
type Walker struct {
Found []string
}
func (w *Walker) WalkFunc(path string, info fs.DirEntry, err error) error {
if info.IsDir() {
return nil
}
if strings.HasSuffix(path, "_scriptData.dat") {
w.Found = append(w.Found, path)
}
return nil
}
func run(args *Arguments) error {
w := &Walker{Found: []string{}}
err := filepath.WalkDir(args.BaseDir, w.WalkFunc)
if err != nil {
return err
}
fmt.Printf("found %d scripts\n", len(w.Found))
stats := make(script.Stats)
for _, file := range w.Found {
fmt.Println(file)
scr, err := script.ParseFile(file, 0x0000)
if err != nil {
if scr != nil {
for _, token := range scr.Tokens {
fmt.Println(token.String(scr.Labels))
}
}
return err
}
stats.Add(scr.Stats())
}
outfile, err := os.Create(args.Output)
if err != nil {
return err
}
defer outfile.Close()
_, err = stats.WriteTo(outfile)
if err != nil {
return err
}
return nil
}
func main() {
args := &Arguments{}
arg.MustParse(args)
err := run(args)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

198
cmd/sbutil.go Normal file
View File

@ -0,0 +1,198 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/alexflint/go-arg"
"git.zorchenhimer.com/Zorchenhimer/go-studybox/rom"
)
type Arguments struct {
Pack *ArgPack `arg:"subcommand:pack"`
UnPack *ArgUnPack `arg:"subcommand:unpack"`
}
type ArgPack struct {
Input string `arg:"positional,required"`
}
type ArgUnPack struct {
Input string `arg:"positional,required" help:".json metadata file"`
NoAudio bool `arg:"--no-audio" help:"Do not unpack the audio portion"`
OutDir string `arg:"--dir" help:"Base directory to unpack into (json file will be here)"`
}
func main() {
args := &Arguments{}
arg.MustParse(args)
var err error
switch {
case args.Pack != nil:
err = pack(args.Pack)
case args.UnPack != nil:
err = unpack(args.UnPack)
default:
fmt.Fprintln(os.Stderr, "Missing command")
os.Exit(1)
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
}
func pack(args *ArgPack) error {
if !strings.HasSuffix(args.Input, ".json") {
return fmt.Errorf("Pack needs a json file as input")
}
//fmt.Println("-- Processing " + args.Input)
sb, err := rom.Import(args.Input)
if err != nil {
return err
}
//outDir := filepath.Base(args.Input)
//outDir = strings.ReplaceAll(outDir, ".json", "_output")
//err = os.MkdirAll(outDir, 0777)
//if err != nil {
// return err
//}
//err = sb.Export(outDir)
//if err != nil {
// return err
//}
// TODO: put this in the json file?
outname := args.Input[:len(args.Input)-len(".json")]+".studybox"
fmt.Println(outname)
err = sb.Write(outname)
if err != nil {
return err
}
return nil
}
func unpack(args *ArgUnPack) error {
//fmt.Println("-- Processing " + file)
if !strings.HasSuffix(args.Input, ".studybox") {
return fmt.Errorf("Input needs to be a .studybox file.")
}
//outDir := filepath.Base(args.Input)
outbase := filepath.Base(args.Input[:len(args.Input)-len(".studybox")])
outdir := filepath.Dir(args.Input)
if args.OutDir != "" {
outdir = args.OutDir
}
outname := filepath.Join(outdir, outbase)
fmt.Println(outname)
//outDir = strings.ReplaceAll(outDir, ".studybox", "")
err := os.MkdirAll(outname, 0777)
if err != nil {
return err
}
sb, err := rom.ReadFile(args.Input)
if err != nil {
return err
}
err = sb.Export(outname, !args.NoAudio)
if err != nil {
return err
}
return nil
}
//func main_old() {
// if len(os.Args) < 3 {
// fmt.Println("Missing command")
// os.Exit(1)
// }
//
// matches := []string{}
// for _, glob := range os.Args[2:len(os.Args)] {
// m, err := filepath.Glob(glob)
// if err != nil {
// fmt.Println(err)
// os.Exit(1)
// }
// matches = append(matches, m...)
// }
//
// if len(matches) == 0 {
// fmt.Println("No files found!")
// }
//
// switch strings.ToLower(os.Args[1]) {
// case "unpack":
// for _, file := range matches {
// fmt.Println("-- Processing " + file)
// outDir := filepath.Base(file)
// outDir = strings.ReplaceAll(outDir, ".studybox", "")
//
// err := os.MkdirAll(outDir, 0777)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// sb, err := rom.ReadFile(file)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// err = sb.Export(outDir)
// if err != nil {
// fmt.Println(err)
// }
// }
// case "pack":
// for _, file := range matches {
// fmt.Println("-- Processing " + file)
// sb, err := rom.Import(file)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// outDir := filepath.Base(file)
// outDir = strings.ReplaceAll(outDir, ".json", "_output")
//
// err = os.MkdirAll(outDir, 0777)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// err = sb.Export(outDir)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// // TODO: put this in the json file?
//
// err = sb.Write(outDir + ".studybox")
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// }
// }
//}

View File

@ -5,8 +5,11 @@ import (
"os"
"strings"
"strconv"
"bufio"
"slices"
"github.com/alexflint/go-arg"
"git.zorchenhimer.com/Zorchenhimer/go-studybox/script"
)
@ -14,6 +17,9 @@ type Arguments struct {
Input string `arg:"positional,required"`
Output string `arg:"positional"`
StartAddr string `arg:"--start" default:"0x6000" help:"base address for the start of the script"`
StatsFile string `arg:"--stats" help:"file to write some statistics to"`
LabelFile string `arg:"--labels" help:"file containing address/label pairs"`
start int
}
@ -38,6 +44,18 @@ func run(args *Arguments) error {
return err
}
if args.LabelFile != "" {
labels, err := parseLabelFile(args.LabelFile)
if err != nil {
return err
}
for _, label := range labels {
//fmt.Printf("%#v\n", label)
scr.Labels[label.Address] = label
}
}
outfile := os.Stdout
if args.Output != "" {
outfile, err = os.Create(args.Output)
@ -61,9 +79,79 @@ func run(args *Arguments) error {
fmt.Fprintln(outfile, token.String(scr.Labels))
}
if args.StatsFile != "" {
statfile, err := os.Create(args.StatsFile)
if err != nil {
return fmt.Errorf("Unable to create stats file: %w", err)
}
defer statfile.Close()
//err = scr.WriteStats(statfile)
_, err = scr.Stats().WriteTo(statfile)
if err != nil {
return fmt.Errorf("Error writing stats: %w", err)
}
}
return nil
}
func parseLabelFile(filename string) ([]*script.Label, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
labels := []*script.Label{}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
line = strings.ReplaceAll(line, "\t", " ")
parts := strings.Split(line, " ")
parts = slices.DeleteFunc(parts, func(str string) bool {
return str == ""
})
if len(parts) < 2 {
fmt.Println("Ignoring", line)
continue
}
if strings.HasPrefix(parts[0], "$") {
parts[0] = "0x"+parts[0][1:]
}
addr, err := strconv.ParseInt(parts[0], 0, 32)
if err != nil {
fmt.Printf("Address parse error for %q: %s\n", line, err)
continue
}
lbl := &script.Label{
Name: parts[1],
Address: int(addr),
}
if lbl.Name == "$" {
lbl.Name = ""
}
if len(parts) > 2 {
lbl.Comment = strings.Join(parts[2:], " ")
}
labels = append(labels, lbl)
}
return labels, nil
}
func main() {
args := &Arguments{}
arg.MustParse(args)

View File

@ -6,7 +6,7 @@ import (
"os"
)
func (sb *StudyBox) Export(directory string) error {
func (sb *StudyBox) Export(directory string, includeAudio bool) error {
sbj := StudyBoxJson{
Version: 1,
Pages: []jsonPage{},
@ -106,7 +106,7 @@ func (sb *StudyBox) Export(directory string) error {
return fmt.Errorf("[WARN] unknown end data type: %s\n", jData.Type)
}
err = os.WriteFile(jData.File, rawData, 0777)
err = os.WriteFile(jData.File, rawData, 0666)
if err != nil {
return fmt.Errorf("Unable to write data to file [%q]: %v", jData.File, err)
}
@ -133,15 +133,17 @@ func (sb *StudyBox) Export(directory string) error {
return fmt.Errorf("Missing audio!")
}
if includeAudio {
err := sb.Audio.WriteToFile(directory + "/audio")
if err != nil {
return fmt.Errorf("Error writing audio file: %v", err)
}
}
rawJson, err := json.MarshalIndent(sbj, "", " ")
if err != nil {
return err
}
return os.WriteFile(directory+".json", rawJson, 0777)
return os.WriteFile(directory+".json", rawJson, 0666)
}

View File

@ -26,7 +26,8 @@ func (ph *packetHeader) RawBytes() []byte {
}
func (ph *packetHeader) Asm() string {
return fmt.Sprintf("header %d [Page %d] ; Checksum: %02X", ph.PageNumber, ph.PageNumber+1, ph.Checksum)
return fmt.Sprintf("header %d [Page %d] ; Checksum: %02X",
ph.PageNumber, ph.PageNumber+1, ph.Checksum)
}
func (ph *packetHeader) Address() int {
@ -127,7 +128,8 @@ func (p *packetBulkData) Asm() string {
// data = append(data, fmt.Sprintf("$%02X", b))
//}
//return fmt.Sprintf("[%08X] data %s ; Length %d Checksum: %02X", p.address, strings.Join(data, ", "), len(p.Data), p.checksum)
return fmt.Sprintf("data $%02X, [...], $%02X ; Length %d Checksum: %02X", p.Data[0], p.Data[len(p.Data)-1], len(p.Data), p.checksum)
return fmt.Sprintf("data $%02X, [...], $%02X ; Length:%d Checksum:%02X",
p.Data[0], p.Data[len(p.Data)-1], len(p.Data), p.checksum)
}
func (p *packetBulkData) RawBytes() []byte {
@ -178,7 +180,7 @@ func (p *packetMarkDataStart) dataType() string {
}
func (p *packetMarkDataStart) Asm() string {
return fmt.Sprintf("mark_datatype_start %s $%02X $%02X ; Checksum: %02X",
return fmt.Sprintf("mark_datatype_start Type:%s ArgA:$%02X ArgB:$%02X ; Checksum:%02X",
p.dataType(), p.ArgA, p.ArgB, p.checksum)
}
@ -229,30 +231,31 @@ func (p *packetMarkDataEnd) RawBytes() []byte {
}
func (p *packetMarkDataEnd) Asm() string {
var tstr string
var typeStr string
switch p.Type & 0x0F {
case 2:
tstr = "script"
typeStr = "script"
case 3:
tstr = "nametable"
typeStr = "nametable"
case 4:
tstr = "pattern"
typeStr = "pattern"
case 5:
tstr = "delay"
typeStr = "delay"
default:
tstr = fmt.Sprintf("unknown $%02X", p.Type)
typeStr = fmt.Sprintf("unknown $%02X", p.Type)
}
if p.Reset {
tstr += " reset_state"
typeStr += " reset_state"
}
s := []string{}
for _, b := range p.RawBytes() {
s = append(s, fmt.Sprintf("%02X", b))
}
return fmt.Sprintf("mark_datatype_end %s ; %s Checksum: %02X",
tstr, strings.Join(s, " "), p.checksum)
return fmt.Sprintf("mark_datatype_end %s ; Raw:[%s] Checksum:%02X",
typeStr, strings.Join(s, " "), p.checksum)
}
func (p *packetMarkDataEnd) Address() int {

View File

@ -123,7 +123,7 @@ func (ta TapeAudio) String() string {
func (ta *TapeAudio) WriteToFile(basename string) error {
ext := "." + strings.ToLower(string(ta.Format))
return os.WriteFile(basename+ext, ta.Data, 0777)
return os.WriteFile(basename+ext, ta.Data, 0666)
}
func (ta *TapeAudio) ext() string {

View File

@ -1,6 +1,7 @@
package script
import (
"fmt"
)
var InstrMap map[byte]*Instruction
@ -13,186 +14,206 @@ func init() {
}
var Instructions []*Instruction = []*Instruction{
&Instruction{ 0x80, 0, 0, 0, "play_beep"},
&Instruction{ 0x81, 0, 0, 0, "halt"},
&Instruction{ 0x82, 0, 0, 0, "tape_nmi_shenanigans"},
&Instruction{ 0x83, 0, 0, 0, "tape_wait"},
&Instruction{ 0x80, 0, 0, 0, false, "play_beep"},
&Instruction{ 0x81, 0, 0, 0, false, "halt"},
&Instruction{ 0x82, 0, 0, 0, false, "tape_nmi_shenanigans"},
&Instruction{ 0x83, 0, 0, 0, false, "tape_wait"},
// Jump to the inline word, in the VM
&Instruction{ 0x84, 0, 2, 0, "jump_abs"},
&Instruction{ 0x84, 0, 2, 0, false, "jump_abs"},
// Call a routine at the inline word, in the VM
&Instruction{ 0x85, 0, 2, 0, "call_abs"},
&Instruction{ 0x85, 0, 2, 0, false, "call_abs"},
// Return from a previous call
&Instruction{ 0x86, 0, 0, 0, "return"},
&Instruction{ 0x87, 0, 0, 0, "loop"},
&Instruction{ 0x88, 0, 0, 0, "play_sound"},
&Instruction{ 0x89, 3, 0, 0, ""},
&Instruction{ 0x8A, 0, 2, 0, "pop_string_to_addr"},
&Instruction{ 0x8B, 1, 0, 0, ""},
&Instruction{ 0x8C, 0, 0, 1, "string_length"},
&Instruction{ 0x8D, 0, 0, 1, "string_to_int"},
&Instruction{ 0x8E, 0, 0, 16, "string_concat"},
&Instruction{ 0x8F, 0, 0, 1, "strings_equal"},
&Instruction{ 0x86, 0, 0, 0, false, "return"},
&Instruction{ 0x90, 0, 0, 1, "strings_not_equal"},
&Instruction{ 0x91, 0, 0, 1, "string_less_than"},
&Instruction{ 0x92, 0, 0, 1, "string_less_than_equal"},
&Instruction{ 0x93, 0, 0, 1, "string_greater_than_equal"},
&Instruction{ 0x94, 0, 0, 1, "string_greater_than"},
&Instruction{ 0x87, 0, 0, 0, false, "loop"},
&Instruction{ 0x88, 0, 0, 0, false, "play_sound"},
&Instruction{ 0x89, 3, 0, 0, false, ""},
&Instruction{ 0x8A, 0, 2, 0, false, "pop_string_to_addr"},
&Instruction{ 0x8B, 1, 0, 0, false, ""},
&Instruction{ 0x8C, 0, 0, 1, false, "string_length"},
&Instruction{ 0x8D, 0, 0, 1, false, "string_to_int"},
&Instruction{ 0x8E, 0, 0, 16, false, "string_concat"},
&Instruction{ 0x8F, 0, 0, 1, false, "strings_equal"},
&Instruction{ 0x90, 0, 0, 1, false, "strings_not_equal"},
&Instruction{ 0x91, 0, 0, 1, false, "string_less_than"},
&Instruction{ 0x92, 0, 0, 1, false, "string_less_than_equal"},
&Instruction{ 0x93, 0, 0, 1, false, "string_greater_than_equal"},
&Instruction{ 0x94, 0, 0, 1, false, "string_greater_than"},
// Sets some tape NMI stuff if the byte at $0740 is not zero.
&Instruction{ 0x95, 1, 0, 0, "tape_nmi_shenigans_set"},
&Instruction{ 0x96, 0, 2, 0, "set_word_4E"},
&Instruction{ 0x97, 2, 0, 0, ""},
&Instruction{ 0x98, 1, 0, 0, ""},
&Instruction{ 0x99, 1, 0, 0, ""},
&Instruction{ 0x9A, 0, 0, 0, ""},
&Instruction{ 0x9B, 0, 0, 0, "halt"},
&Instruction{ 0x9C, 0, 0, 0, "toggle_44FE"},
&Instruction{ 0x9D, 2, 0, 0, "something_tape"},
// Will call 0x82 tape_nmi_shenanigans if $0740 != 0
&Instruction{ 0x95, 1, 0, 0, false, "tape_nmi_shenigans_set"},
// Calls 0xEB draw_overlay. Seems to draw a whole screen.
&Instruction{ 0x9E, 2, 0, 0, ""},
&Instruction{ 0x9F, 6, 0, 0, ""},
&Instruction{ 0x96, 0, 2, 0, true, "set_word_4E"},
&Instruction{ 0x97, 2, 0, 0, false, ""},
&Instruction{ 0x98, 1, 0, 0, false, ""},
&Instruction{ 0x99, 1, 0, 0, false, ""},
&Instruction{ 0x9A, 0, 0, 0, false, ""},
&Instruction{ 0x9B, 0, 0, 0, false, "halt"},
&Instruction{ 0x9C, 0, 0, 0, false, "toggle_44FE"},
&Instruction{ 0x9D, 2, 0, 0, false, "something_tape"},
&Instruction{ 0xA0, 2, 0, 1, ""},
&Instruction{ 0xA1, 1, 0, 0, ""},
&Instruction{ 0xA2, 1, 0, 0, "buffer_palette"},
// Calls 0xEB draw_overlay. Draws the whole screen from data previously
// loaded from the tape.
&Instruction{ 0x9E, 2, 0, 0, false, "draw_and_show_screen"},
&Instruction{ 0x9F, 6, 0, 0, false, ""},
&Instruction{ 0xA0, 2, 0, 1, false, ""},
&Instruction{ 0xA1, 1, 0, 0, false, ""},
&Instruction{ 0xA2, 1, 0, 0, false, "buffer_palette"},
// Possibly a sprite setup routine. loads up some CHR data and some palette
// data.
&Instruction{ 0xA3, 1, 0, 0, "sprite_setup"},
&Instruction{ 0xA4, 3, 0, 0, ""},
&Instruction{ 0xA5, 1, 0, 0, "set_470A"},
&Instruction{ 0xA6, 1, 0, 0, "set_470B"},
&Instruction{ 0xA3, 1, 0, 0, false, "sprite_setup"},
&Instruction{ 0xA4, 3, 0, 0, false, ""},
&Instruction{ 0xA5, 1, 0, 0, false, "set_470A"},
&Instruction{ 0xA6, 1, 0, 0, false, "set_470B"},
// jump to the inline address, in assembly, not in the VM
// (built-in ACE, lmao)
&Instruction{ 0xA7, 0, 0, 0, "call_asm"},
&Instruction{ 0xA8, 5, 0, 0, ""},
&Instruction{ 0xA9, 1, 0, 0, ""},
&Instruction{ 0xAA, 1, 0, 0, ""},
&Instruction{ 0xAB, 1, 0, 0, "long_call"},
&Instruction{ 0xAC, 0, 0, 0, "long_return"},
&Instruction{ 0xAD, 1, 0, 1, "absolute"},
&Instruction{ 0xAE, 1, 0, 1, "compare"},
&Instruction{ 0xAF, 0, 0, 1, ""},
// Will not jump to anything at or above $8000 or below $5000.
// Addresses in $5000-$5FFF use $470A as the bank ID
// Addresses in $6000-$7FFF use $470B as the bank ID
&Instruction{ 0xA7, 0, 0, 0, false, "call_asm"},
&Instruction{ 0xB0, 1, 0, 16, ""},
&Instruction{ 0xB1, 1, 0, 16, "to_hex_string"},
&Instruction{ 0xB2, 0, 0, 1, ""},
&Instruction{ 0xB3, 7, 0, 0, ""}, // possible 16-bit inline?
&Instruction{ 0xB4, 0, 0, 0, ""},
&Instruction{ 0xB5, 0, 0, 0, ""},
&Instruction{ 0xB6, 0, 0, 0, ""},
&Instruction{ 0xA8, 5, 0, 0, false, ""},
&Instruction{ 0xA9, 1, 0, 0, false, ""},
&Instruction{ 0xAA, 1, 0, 0, false, ""},
&Instruction{ 0xAB, 1, 0, 0, false, "long_call"},
&Instruction{ 0xAC, 0, 0, 0, false, "long_return"},
&Instruction{ 0xAD, 1, 0, 1, false, "absolute"},
&Instruction{ 0xAE, 1, 0, 1, false, "compare"},
&Instruction{ 0xAF, 0, 0, 1, false, ""},
&Instruction{ 0xB0, 1, 0, 16, false, ""},
&Instruction{ 0xB1, 1, 0, 16, false, "to_hex_string"},
&Instruction{ 0xB2, 0, 0, 1, false, ""},
&Instruction{ 0xB3, 7, 0, 0, false, ""}, // possible 16-bit inline?
&Instruction{ 0xB4, 0, 0, 0, false, ""},
&Instruction{ 0xB5, 0, 0, 0, false, ""},
&Instruction{ 0xB6, 0, 0, 0, false, ""},
// Uses the inline word as a pointer, and pushes the byte value at that
// address to the stack.
&Instruction{ 0xB7, 0, 2, 0, "deref_ptr_inline"},
&Instruction{ 0xB7, 0, 2, 0, false, "deref_ptr_inline"},
// Pushes the inline word to the stack
&Instruction{ 0xB8, 0, 2, 0, "push_word"},
&Instruction{ 0xB9, 0, 2, 0, "push_word_indexed"},
&Instruction{ 0xBA, 0, 2, 0, "push"},
&Instruction{ 0xBB, 0, -1, 0, "push_data"},
&Instruction{ 0xBC, 0, 2, 0, "push_string_from_table"},
&Instruction{ 0xB8, 0, 2, 0, true, "push_word"},
&Instruction{ 0xB9, 0, 2, 0, false, "push_word_indexed"},
&Instruction{ 0xBA, 0, 2, 0, false, "push"},
&Instruction{ 0xBB, 0, -1, 0, false, "push_data"},
&Instruction{ 0xBC, 0, 2, 0, false, "push_string_from_table"},
// Pops a byte off the stack and stores it at the inline address.
&Instruction{ 0xBD, 0, 2, 0, "pop"},
&Instruction{ 0xBE, 0, 2, 0, "write_to_table"},
&Instruction{ 0xBF, 0, 2, 0, "jump_not_zero"},
&Instruction{ 0xBD, 0, 2, 0, false, "pop_into"},
&Instruction{ 0xBE, 0, 2, 0, false, "write_to_table"},
&Instruction{ 0xBF, 0, 2, 0, false, "jump_not_zero"},
// One byte off stack; jumps to inline if byte is zero
&Instruction{ 0xC0, 1, 2, 0, "jump_zero"},
&Instruction{ 0xC1, 1, -2, 0, "jump_switch"},
&Instruction{ 0xC2, 1, 0, 1, "equals_zero"},
&Instruction{ 0xC3, 2, 0, 1, "and_a_b"},
&Instruction{ 0xC4, 2, 0, 1, "or_a_b"},
&Instruction{ 0xC5, 2, 0, 1, "equal"},
&Instruction{ 0xC0, 1, 2, 0, false, "jump_zero"},
&Instruction{ 0xC1, 1, -2, 0, false, "jump_switch"},
&Instruction{ 0xC2, 1, 0, 1, false, "equals_zero"},
&Instruction{ 0xC3, 2, 0, 1, false, "and_a_b"},
&Instruction{ 0xC4, 2, 0, 1, false, "or_a_b"},
&Instruction{ 0xC5, 2, 0, 1, false, "equal"},
// Two bytes off stack; result pushed back; 1 if A == B, 0 if A != B
&Instruction{ 0xC6, 2, 0, 1, "not_equal"},
&Instruction{ 0xC7, 2, 0, 1, "less_than"},
&Instruction{ 0xC8, 2, 0, 1, "less_than_equal"},
&Instruction{ 0xC9, 2, 0, 1, "greater_than"},
&Instruction{ 0xCA, 2, 0, 1, "greater_than_equal"},
&Instruction{ 0xCB, 2, 0, 1, "sum"},
&Instruction{ 0xCC, 2, 0, 1, "subtract"},
&Instruction{ 0xCD, 2, 0, 1, "multiply"},
&Instruction{ 0xCE, 2, 0, 1, "signed_divide"},
&Instruction{ 0xCF, 1, 0, 1, "negate"},
&Instruction{ 0xC6, 2, 0, 1, false, "not_equal"},
&Instruction{ 0xD0, 1, 0, 1, "modulus"},
&Instruction{ 0xD1, 2, 0, 1, "expansion_controller"},
&Instruction{ 0xD2, 2, 0, 1, ""},
&Instruction{ 0xD3, 2, 0, 16, ""},
&Instruction{ 0xD4, 3, 0, 0, ""},
&Instruction{ 0xD5, 1, 0, 0, "wait_for_tape"},
&Instruction{ 0xD6, 1, 0, 16, "truncate_string"},
&Instruction{ 0xD7, 1, 0, 16, "trim_string"},
&Instruction{ 0xD8, 1, 0, 16, "trim_string_start"},
&Instruction{ 0xD9, 2, 0, 16, "trim_string_start"},
&Instruction{ 0xDA, 1, 0, 16, "to_int_string"},
&Instruction{ 0xDB, 3, 0, 0, ""},
&Instruction{ 0xDC, 5, 0, 0, ""},
&Instruction{ 0xC7, 2, 0, 1, false, "less_than"},
&Instruction{ 0xC8, 2, 0, 1, false, "less_than_equal"},
&Instruction{ 0xC9, 2, 0, 1, false, "greater_than"},
&Instruction{ 0xCA, 2, 0, 1, false, "greater_than_equal"},
&Instruction{ 0xCB, 2, 0, 1, false, "sum"},
&Instruction{ 0xCC, 2, 0, 1, false, "subtract"},
&Instruction{ 0xCD, 2, 0, 1, false, "multiply"},
&Instruction{ 0xCE, 2, 0, 1, false, "signed_divide"},
&Instruction{ 0xCF, 1, 0, 1, false, "negate"},
&Instruction{ 0xD0, 1, 0, 1, false, "modulus"},
&Instruction{ 0xD1, 2, 0, 1, false, "expansion_controller"},
&Instruction{ 0xD2, 2, 0, 1, false, ""},
&Instruction{ 0xD3, 2, 0, 16, false, ""},
&Instruction{ 0xD4, 3, 0, 0, false, "set_cursor_location"},
// Wait for ArgA itterations. "itterations" is undefined as of now. (data from tape?)
&Instruction{ 0xD5, 1, 0, 0, false, "wait_for_tape"},
&Instruction{ 0xD6, 1, 0, 16, false, "truncate_string"},
&Instruction{ 0xD7, 1, 0, 16, false, "trim_string"},
&Instruction{ 0xD8, 1, 0, 16, false, "trim_string_start"},
&Instruction{ 0xD9, 2, 0, 16, false, "trim_string_start"},
&Instruction{ 0xDA, 1, 0, 16, false, "to_int_string"},
&Instruction{ 0xDB, 3, 0, 0, false, ""},
&Instruction{ 0xDC, 5, 0, 0, false, ""},
// ArgA, ArgB: X,Y of corner A
// ArgC, ArgD: X,Y of corner B
// ArgE: fill value. This is an index into
// the table at $B451.
// Fills a box with a tile
&Instruction{ 0xDD, 5, 0, 0, "fill_box"},
&Instruction{ 0xDD, 5, 0, 0, false, "fill_box"},
&Instruction{ 0xDE, 3, 0, 0, ""},
&Instruction{ 0xDF, 3, 0, 0, ""},
&Instruction{ 0xDE, 3, 0, 0, false, ""},
&Instruction{ 0xDF, 3, 0, 0, false, ""},
&Instruction{ 0xE0, 2, 0, 1, "signed_divide"},
&Instruction{ 0xE1, 4, 0, 0, ""},
&Instruction{ 0xE2, 7, 0, 0, "setup_sprite"},
// Divide and return remainder
&Instruction{ 0xE0, 2, 0, 1, false, "modulo"},
&Instruction{ 0xE1, 4, 0, 0, false, ""},
&Instruction{ 0xE2, 7, 0, 0, false, "setup_sprite"},
// Pops a word off the stack, uses it as a pointer, and pushes the byte
// value at that address to the stack.
&Instruction{ 0xE3, 1, 0, 1, "deref_ptr_stack"},
&Instruction{ 0xE4, 2, 0, 0, "swap_ram_bank"},
&Instruction{ 0xE5, 1, 0, 0, "disable_sprite"},
&Instruction{ 0xE6, 1, 0, 0, "tape_nmi_setup"},
&Instruction{ 0xE7, 7, 0, 0, ""},
&Instruction{ 0xE8, 1, 0, 0, "setup_tape_nmi"},
&Instruction{ 0xE9, 0, 1, 0, "setup_loop"},
&Instruction{ 0xEA, 0, 0, 0, "string_write_to_table"},
&Instruction{ 0xE3, 1, 0, 1, false, "deref_ptr_stack"},
&Instruction{ 0xE4, 2, 0, 0, false, "swap_ram_bank"},
&Instruction{ 0xE5, 1, 0, 0, false, "disable_sprite"},
// Will call 0x82 tape_nmi_shenanigans if $0740 != 0
&Instruction{ 0xE6, 1, 0, 0, false, "tape_nmi_setup"},
&Instruction{ 0xE7, 7, 0, 0, false, "draw_metasprite"},
&Instruction{ 0xE8, 1, 0, 0, false, "setup_tape_nmi"},
&Instruction{ 0xE9, 0, 1, 0, false, "setup_loop"},
&Instruction{ 0xEA, 0, 0, 0, false, "string_write_to_table"},
// Reads and saves tiles from the PPU, then draws over them.
// This is used to draw dialog boxes, so saving what it overwrites
// so it can re-draw them later makes sense.
// Not sure what the arguments actually mean.
// ArgB and ArgC are probably coordinates.
&Instruction{ 0xEB, 4, 0, 0, "draw_overlay"},
&Instruction{ 0xEB, 4, 0, 0, false, "draw_overlay"},
&Instruction{ 0xEC, 2, 0, 0, "scroll"},
&Instruction{ 0xED, 1, 0, 0, "disable_sprites"},
&Instruction{ 0xEE, 1, -3, 0, "call_switch"},
&Instruction{ 0xEF, 6, 0, 0, ""},
&Instruction{ 0xEC, 2, 0, 0, false, "scroll"},
&Instruction{ 0xED, 1, 0, 0, false, "disable_sprites"},
&Instruction{ 0xF0, 0, 0, 0, "disable_sprites"},
&Instruction{ 0xF1, 4, 0, 0, ""},
&Instruction{ 0xF2, 0, 0, 0, "halt"},
&Instruction{ 0xF3, 0, 0, 0, "halt"},
&Instruction{ 0xF4, 0, 0, 16, "halt"},
&Instruction{ 0xF5, 1, 0, 1, "halt"},
&Instruction{ 0xF6, 1, 0, 0, "halt"},
&Instruction{ 0xF7, 0, 0, 0, "halt"},
&Instruction{ 0xF8, 2, 0, 0, "halt"},
&Instruction{ 0xF9, 0, 0, 1, ""},
&Instruction{ 0xFA, 0, 0, 1, ""},
&Instruction{ 0xFB, 1, 0, 0, "jump_arg_a"},
&Instruction{ 0xFC, 2, 0, 1, ""},
&Instruction{ 0xFD, 0, 0, 16, "halt"},
&Instruction{ 0xFE, 4, 0, 0, ""},
&Instruction{ 0xEE, 1, -3, 0, false, "call_switch"},
&Instruction{ 0xEF, 6, 0, 0, false, ""},
&Instruction{ 0xF0, 0, 0, 0, false, "disable_sprites"},
&Instruction{ 0xF1, 4, 0, 0, false, ""},
&Instruction{ 0xF2, 0, 0, 0, false, "halt"},
&Instruction{ 0xF3, 0, 0, 0, false, "halt"},
&Instruction{ 0xF4, 0, 0, 16, false, "halt"},
&Instruction{ 0xF5, 1, 0, 1, false, "halt"},
&Instruction{ 0xF6, 1, 0, 0, false, "halt"},
&Instruction{ 0xF7, 0, 0, 0, false, "halt"},
&Instruction{ 0xF8, 2, 0, 0, false, "halt"},
&Instruction{ 0xF9, 0, 0, 1, false, ""},
&Instruction{ 0xFA, 0, 0, 1, false, ""},
&Instruction{ 0xFB, 1, 0, 0, false, "jump_arg_a"},
&Instruction{ 0xFC, 2, 0, 1, false, ""},
&Instruction{ 0xFD, 0, 0, 16, false, "halt"},
&Instruction{ 0xFE, 4, 2, 0, false, "draw_rom_char"},
// code handler is $FFFF
&Instruction{ 0xFF, 0, 0, 0, "break_engine"},
&Instruction{ 0xFF, 0, 0, 0, false, "break_engine"},
}
type Instruction struct {
@ -201,8 +222,9 @@ type Instruction struct {
OpCount int // inline operands. length in bytes.
// -1: nul-terminated
// -2: first byte is count, followed by that number of words
// -3: like -2, but with one additional word
// -3: like -2, but with no default. code continues after list on OOB
RetCount int // return count
InlineImmediate bool // don't turn the inline value into a variable
Name string
}
@ -212,7 +234,7 @@ func (i Instruction) String() string {
return i.Name
}
//return fmt.Sprintf("$%02X_unknown", i.Opcode)
return "unknown"
return fmt.Sprintf("unknown_0x%02X", i.Opcode)
//return "unknown"
}

View File

@ -5,6 +5,42 @@ import (
"os"
)
type Label struct {
Address int
Name string
Comment string
FarLabel bool
}
func AutoLabel(address int) *Label {
return &Label{
Address: address,
Name: fmt.Sprintf("L%04X", address),
}
}
func AutoLabelVar(address int) *Label {
return &Label{
Address: address,
Name: fmt.Sprintf("Var_%04X", address),
}
}
func AutoLabelFar(address int) *Label {
return &Label{
Address: address,
Name: fmt.Sprintf("F%04X", address),
FarLabel: true,
}
}
func NewLabel(address int, name string) *Label {
return &Label{
Address: address,
Name: name,
}
}
func ParseFile(filename string, startAddr int) (*Script, error) {
rawfile, err := os.ReadFile(filename)
if err != nil {
@ -24,7 +60,7 @@ func Parse(rawinput []byte, startAddr int) (*Script, error) {
Warnings: []string{},
StackAddress: (int(rawinput[1])<<8) | int(rawinput[0]),
StartAddress: startAddr,
Labels: make(map[int]string), // map[location]name
Labels: make(map[int]*Label), // map[location]name
}
tokenMap := make(map[int]*Token)
@ -45,7 +81,7 @@ func Parse(rawinput []byte, startAddr int) (*Script, error) {
op, ok := InstrMap[raw]
if !ok {
return nil, fmt.Errorf("OP %02X not in instruction map", raw)
return nil, fmt.Errorf("OP 0x%02X not in instruction map", raw)
}
token.Instruction = op
@ -66,19 +102,25 @@ func Parse(rawinput []byte, startAddr int) (*Script, error) {
args = append(args, ByteVal(l))
i++
for c := 0; c < l; c++ {
if len(rawinput) <= i+1 {
return script, fmt.Errorf("OP early end at offset 0x%X (%d) {%d} %#v", i, i, l, op)
}
args = append(args, WordVal([2]byte{rawinput[i], rawinput[i+1]}))
i+=2
}
i--
case -3: // count then count+1 words (extra is default case)
case -3: // count then count words. "default" is no call (skip Code_Pointer to after args)
i++
l := int(rawinput[i])
args = append(args, ByteVal(l))
i++
for c := 0; c < l+1; c++ {
for c := 0; c < l; c++ {
args = append(args, WordVal([2]byte{rawinput[i], rawinput[i+1]}))
i+=2
}
i--
case 2:
args = append(args, WordVal([2]byte{rawinput[i+1], rawinput[i+2]}))
@ -92,6 +134,7 @@ func Parse(rawinput []byte, startAddr int) (*Script, error) {
token.Inline = args
}
// Find and mark labels for a few instructions
for _, t := range script.Tokens {
switch t.Raw {
case 0x84, 0x85, 0xBF, 0xC0: // jmp/call
@ -105,7 +148,7 @@ func Parse(rawinput []byte, startAddr int) (*Script, error) {
if tok.Offset == addr {
tok.IsTarget = true
found = true
script.Labels[addr] = fmt.Sprintf("L%04X", addr)
script.Labels[addr] = AutoLabel(addr) //fmt.Sprintf("L%04X", addr)
break
}
}
@ -126,7 +169,7 @@ func Parse(rawinput []byte, startAddr int) (*Script, error) {
if tok.Offset == addr {
tok.IsTarget = true
found = true
script.Labels[addr] = fmt.Sprintf("L%04X", addr)
script.Labels[addr] = AutoLabel(addr) //fmt.Sprintf("L%04X", addr)
break
}
}
@ -141,11 +184,12 @@ func Parse(rawinput []byte, startAddr int) (*Script, error) {
if t.Instruction == nil {
continue
}
if t.Instruction.OpCount == 2 {
if t.Instruction.OpCount == 2 && !t.Instruction.InlineImmediate {
addr := t.Inline[0].Int()
if tok, ok := tokenMap[addr]; ok {
tok.IsVariable = true
script.Labels[addr] = fmt.Sprintf("Var_%04X", addr)
script.Labels[addr] = AutoLabelVar(addr) //fmt.Sprintf("Var_%04X", addr)
}
}
}

View File

@ -1,6 +1,7 @@
package script
import (
"fmt"
)
type Script struct {
@ -10,5 +11,35 @@ type Script struct {
StartAddress int
StackAddress int
Labels map[int]string
Labels map[int]*Label
}
type InstrStat struct {
Instr *Instruction
Count int
}
func (is InstrStat) String() string {
return fmt.Sprintf("0x%02X %3d %s", is.Instr.Opcode, is.Count, is.Instr.String())
}
func (s *Script) Stats() Stats {
st := make(Stats)
for _, t := range s.Tokens {
if t.Instruction == nil {
continue
}
op := t.Instruction.Opcode
if _, ok := st[op]; !ok {
st[op] = &InstrStat{
Instr: t.Instruction,
Count: 0,
}
}
st[op].Count++
}
return st
}

56
script/stats.go Normal file
View File

@ -0,0 +1,56 @@
package script
import (
"io"
"slices"
"maps"
"fmt"
)
type Stats map[byte]*InstrStat
func (this Stats) Add(that Stats) {
for _, st := range that {
op := st.Instr.Opcode
if _, ok := this[op]; !ok {
this[op] = st
} else {
this[op].Count += that[op].Count
}
}
}
func (s Stats) WriteTo(w io.Writer) (int64, error) {
count := int64(0)
keys := slices.Sorted(maps.Keys(s))
unknownInstr := 0
unknownUses := 0
for _, key := range keys {
n, err := fmt.Fprintln(w, s[key])
count += int64(n)
if err != nil {
return count, err
}
if s[key].Instr.Name == "" {
unknownInstr++
unknownUses += s[key].Count
}
}
n, err := fmt.Fprintln(w, "\nUnknown uses:", unknownUses)
count += int64(n)
if err != nil {
return count, err
}
n, err = fmt.Fprintln(w, "Unknown instructions:", unknownInstr)
count += int64(n)
if err != nil {
return count, err
}
return count, nil
}

View File

@ -15,7 +15,7 @@ type Token struct {
Instruction *Instruction
}
func (t Token) String(labels map[int]string) string {
func (t Token) String(labels map[int]*Label) string {
suffix := ""
switch t.Raw {
case 0x86:
@ -25,10 +25,18 @@ func (t Token) String(labels map[int]string) string {
prefix := ""
if t.IsTarget || t.IsVariable {
if lbl, ok := labels[t.Offset]; ok {
prefix = "\n"+lbl+":\n"
comment := ""
if lbl.Comment != "" {
comment = "; "+lbl.Comment+"\n"
}
prefix = "\n"+comment+lbl.Name+":\n"
} else {
prefix = fmt.Sprintf("\nL%04X:\n", t.Offset)
}
} else {
if lbl, ok := labels[t.Offset]; ok && lbl.Comment != "" {
suffix = " ; "+lbl.Comment+suffix
}
}
if t.Raw < 0x80 {
@ -56,7 +64,7 @@ func (t Token) String(labels map[int]string) string {
argstr := []string{}
for _, a := range t.Inline {
if lbl, ok := labels[a.Int()]; ok {
argstr = append(argstr, lbl)
argstr = append(argstr, lbl.Name)
} else {
argstr = append(argstr, a.HexString())
}