[build-script] Start adding a new build script format
This format is meant to replace the current .json file when describing .studybox ROM files, as well as handle compiling/assembling new ROMs from scratch. In theory, this will handle both ROMs that were decoded from tape as well as instructions to build a new ROM from various files. This will include options to compile scripts, encode images, and handle some audio timing by allowing individual audio files for each page.
This commit is contained in:
parent
b1d8d8335a
commit
ac5f29a696
|
|
@ -0,0 +1,180 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bufio"
|
||||
"os"
|
||||
"io"
|
||||
"testing"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type parseFunc func(key, values string) (Token, error)
|
||||
|
||||
var parseFuncs = map[string]parseFunc{
|
||||
"rom": parseStrValue,
|
||||
"fullaudio": parseStrValue,
|
||||
"audiooffsets": parseAudioOffsets,
|
||||
|
||||
"page": parseNumValue,
|
||||
"padding": parseNumValue,
|
||||
"version": parseNumValue,
|
||||
|
||||
"delay": parseDelay,
|
||||
|
||||
"script": parseData,
|
||||
"tiles": parseData,
|
||||
"pattern": parseData,
|
||||
}
|
||||
|
||||
func ParseFile(filename string) ([]Token, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return Parse(file)
|
||||
}
|
||||
|
||||
func Parse(r io.Reader) ([]Token, error) {
|
||||
items := []Token{}
|
||||
scanner := bufio.NewScanner(r)
|
||||
prev := ""
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// blanks and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
splitln := strings.SplitN(line, " ", 2)
|
||||
if len(splitln) != 2 {
|
||||
return nil, fmt.Errorf("invalid line: %q", line)
|
||||
}
|
||||
|
||||
var itm Token
|
||||
var err error
|
||||
|
||||
// TODO: Some of these will need to be unique in the file
|
||||
// (rom, version, fullaudio), and probably exclusive/ordered.
|
||||
// IE, rom, version, & fullaudio must come before the first
|
||||
// page ##, and must only appear once. if a page has an
|
||||
// audio line, all pages must have one and there can be no
|
||||
// fullaudio.
|
||||
|
||||
if fn, ok := parseFuncs[splitln[0]]; ok {
|
||||
itm, err = fn(splitln[0], splitln[1])
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown line: %s\n", splitln[0])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !itm.ValidAfter(prev) {
|
||||
if prev == "" {
|
||||
prev = "[empty]"
|
||||
}
|
||||
return nil, fmt.Errorf("%s not valid after %s", itm.Type(), prev)
|
||||
}
|
||||
prev = itm.Type()
|
||||
|
||||
items = append(items, itm)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
var t *testing.T
|
||||
|
||||
func pKeyVals(values string) (map[string]string, error) {
|
||||
m := map[string]string{}
|
||||
|
||||
start := 0
|
||||
runes := []rune(values)
|
||||
currentKey := ""
|
||||
quote := false
|
||||
tlog("[pKeyVals] start")
|
||||
var i int
|
||||
|
||||
for i = 0; i < len(runes); i++ {
|
||||
tlogf("[pKeyVals] rune: %c\n", runes[i])
|
||||
if !quote && runes[i] == '"' {
|
||||
quote = true
|
||||
tlog("[pKeyVals] start quote")
|
||||
continue
|
||||
}
|
||||
|
||||
if !quote && unicode.IsSpace(runes[i]) {
|
||||
tlog("[pKeyVals] !quote && IsSpace()")
|
||||
if currentKey == "" {
|
||||
tlog("[pKeyVals] currentKey empty")
|
||||
start = i+1
|
||||
} else {
|
||||
tlog("[pKeyVals] currentKey not empty")
|
||||
m[currentKey] = string(runes[start:i])
|
||||
currentKey = ""
|
||||
start = i+1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if quote && runes[i] == '"' {
|
||||
tlog("[pKeyVals] quote && rune == \"")
|
||||
quote = false
|
||||
|
||||
if currentKey == "" {
|
||||
currentKey = string(runes[start+1:i])
|
||||
i++
|
||||
start = i+1
|
||||
tlogf("[pKeyVals] currentKey empty; set to %s\n", currentKey)
|
||||
|
||||
} else {
|
||||
m[currentKey] = string(runes[start+1:i])
|
||||
currentKey = ""
|
||||
tlog("[pKeyVals] currentKey not empty")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if runes[i] == ':' {
|
||||
tlog("[pKeyVals] rune == :")
|
||||
if i == start {
|
||||
return nil, fmt.Errorf("missing key")
|
||||
}
|
||||
|
||||
currentKey = string(runes[start:i])
|
||||
start = i+1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if quote {
|
||||
return nil, fmt.Errorf("missmatched quote")
|
||||
}
|
||||
|
||||
if currentKey != "" {
|
||||
//return nil, fmt.Errorf("missing value for %q", currentKey)
|
||||
tlogf("[pKeyVals] outside loop assign m[%s] = %s\n", currentKey, string(runes[start:i]))
|
||||
m[currentKey] = string(runes[start:i])
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func tlog(args ...any) {
|
||||
if t != nil {
|
||||
t.Log(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func tlogf(format string, args ...any) {
|
||||
if t != nil {
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"strconv"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Token interface {
|
||||
Type() string
|
||||
String() string
|
||||
ValidAfter(t string) bool
|
||||
Text() string
|
||||
}
|
||||
|
||||
type TokenDelay struct {
|
||||
Value int
|
||||
Reset bool
|
||||
}
|
||||
|
||||
func (itm *TokenDelay) Type() string { return "delay" }
|
||||
func (itm *TokenDelay) String() string { return fmt.Sprintf("{TokenDelay Value:%d Reset:%t}", itm.Value, itm.Reset) }
|
||||
|
||||
func (itm *TokenDelay) ValidAfter(t string) bool {
|
||||
switch t {
|
||||
case "audiooffsets", "pattern", "delay", "page", "script", "tiles":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (itm *TokenDelay) Text() string {
|
||||
return fmt.Sprintf("delay %d reset:%t", itm.Value, itm.Reset)
|
||||
}
|
||||
|
||||
func parseDelay(key, line string) (Token, error) {
|
||||
vals := strings.Split(line, " ")
|
||||
itm := &TokenDelay{}
|
||||
|
||||
for _, val := range vals {
|
||||
if strings.Contains(val, ":") {
|
||||
keyval := strings.SplitN(val, ":", 2)
|
||||
if len(keyval) != 2 {
|
||||
return nil, fmt.Errorf("Invalid key/value for delay: %s", val)
|
||||
}
|
||||
|
||||
if keyval[0] != "reset" {
|
||||
return nil, fmt.Errorf("Invalid key/value for delay: %s", val)
|
||||
}
|
||||
|
||||
switch strings.ToLower(keyval[1]) {
|
||||
case "true", "yes", "1":
|
||||
itm.Reset = true
|
||||
case "false", "no", "0":
|
||||
itm.Reset = false
|
||||
default:
|
||||
return nil, fmt.Errorf("Invalid reset value: %s", keyval[1])
|
||||
}
|
||||
} else {
|
||||
num, err := strconv.ParseUint(val, 0, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid delay vaule: %s: %w", val, err)
|
||||
}
|
||||
|
||||
itm.Value = int(num)
|
||||
}
|
||||
}
|
||||
|
||||
return itm, nil
|
||||
}
|
||||
|
||||
type TokenNumValue struct {
|
||||
ValType string // delay, padding, page
|
||||
Value int
|
||||
}
|
||||
|
||||
func (itm *TokenNumValue) Type() string { return itm.ValType }
|
||||
func (itm *TokenNumValue) String() string { return fmt.Sprintf("{TokenNumValue ValType:%s Value:%d}", itm.ValType, itm.Value) }
|
||||
|
||||
func (itm *TokenNumValue) ValidAfter(t string) bool {
|
||||
switch itm.ValType {
|
||||
case "page":
|
||||
if t == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
case "padding":
|
||||
switch t {
|
||||
case "delay", "pattern", "tiles", "script", "page":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
case "version":
|
||||
switch t {
|
||||
case "", "rom", "fullaudio":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (itm *TokenNumValue) Text() string {
|
||||
return fmt.Sprintf("%s %d", itm.ValType, itm.Value)
|
||||
}
|
||||
|
||||
func parseNumValue(key, val string) (Token, error) {
|
||||
v, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid %s value: %q", key, val)
|
||||
}
|
||||
|
||||
return &TokenNumValue{
|
||||
ValType: key,
|
||||
Value: int(v),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type TokenStrValue struct {
|
||||
ValType string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (itm *TokenStrValue) Type() string { return itm.ValType }
|
||||
func (itm *TokenStrValue) String() string { return fmt.Sprintf("{TokenStrVal ValType:%s Value:%q}", itm.ValType, itm.Value) }
|
||||
|
||||
func (itm *TokenStrValue) ValidAfter(t string) bool {
|
||||
switch t {
|
||||
case "", "rom", "fullaudio", "version":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (itm *TokenStrValue) Text() string {
|
||||
return itm.ValType+" "+itm.Value
|
||||
}
|
||||
|
||||
func parseStrValue(key, value string) (Token, error) {
|
||||
return &TokenStrValue{
|
||||
ValType: key,
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type TokenAudioOffsets struct {
|
||||
LeadIn uint64
|
||||
Data uint64
|
||||
}
|
||||
|
||||
func (itm *TokenAudioOffsets) Type() string { return "audiooffsets" }
|
||||
func (itm *TokenAudioOffsets) String() string { return fmt.Sprintf("{ItemAudioOffsets LeadIn:%d Data:%d}", itm.LeadIn, itm.Data) }
|
||||
|
||||
func (itm *TokenAudioOffsets) ValidAfter(t string) bool {
|
||||
switch t {
|
||||
case "page", "delay", "script", "tiles", "pattern":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (itm *TokenAudioOffsets) Text() string {
|
||||
return fmt.Sprintf("audiooffsets leadin:%d data:%d", itm.LeadIn, itm.Data)
|
||||
}
|
||||
|
||||
func parseAudioOffsets(key, line string) (Token, error) {
|
||||
vals := strings.Split(line, " ")
|
||||
itm := &TokenAudioOffsets{}
|
||||
for _, keyval := range vals {
|
||||
pair := strings.Split(keyval, ":")
|
||||
if len(pair) != 2 {
|
||||
return nil, fmt.Errorf("invalid syntax: %q", keyval)
|
||||
}
|
||||
|
||||
num, err := strconv.ParseUint(pair[1], 0, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch pair[0] {
|
||||
case "leadin":
|
||||
itm.LeadIn = num
|
||||
case "data":
|
||||
itm.Data = num
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown key: %s", pair[0])
|
||||
}
|
||||
}
|
||||
|
||||
return itm, nil
|
||||
}
|
||||
|
||||
type TokenData struct {
|
||||
ValType string
|
||||
Bank int
|
||||
Addr int
|
||||
File string
|
||||
}
|
||||
|
||||
func (itm *TokenData) Type() string { return itm.ValType }
|
||||
func (itm *TokenData) String() string {
|
||||
return fmt.Sprintf("{TokenData ValType:%s Bank:0x%02X Addr:0x%02X File:%q}",
|
||||
itm.ValType,
|
||||
itm.Bank,
|
||||
itm.Addr,
|
||||
itm.File,
|
||||
)
|
||||
}
|
||||
|
||||
func (itm *TokenData) ValidAfter(t string) bool {
|
||||
switch t {
|
||||
case "page", "delay", "script", "tiles", "pattern":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (itm *TokenData) Text() string {
|
||||
return fmt.Sprintf("%s bank:0x%02X addr:0x%02X file:%q",
|
||||
itm.ValType,
|
||||
itm.Bank,
|
||||
itm.Addr,
|
||||
itm.File,
|
||||
)
|
||||
}
|
||||
|
||||
func parseData(tokType, vals string) (Token, error) {
|
||||
args, err := pKeyVals(vals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
itm := &TokenData{ValType: tokType}
|
||||
for key, value := range args {
|
||||
switch key {
|
||||
case "bank":
|
||||
val, err := strconv.ParseUint(value, 0, 8)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s bank value error: %w", key, err)
|
||||
}
|
||||
itm.Bank = int(val)
|
||||
|
||||
case "addr":
|
||||
val, err := strconv.ParseUint(value, 0, 8)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s addr value error: %w", key, err)
|
||||
}
|
||||
itm.Addr = int(val)
|
||||
|
||||
case "file":
|
||||
itm.File = value
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%s unknown key: %q", tokType, key)
|
||||
}
|
||||
}
|
||||
|
||||
return itm, nil
|
||||
}
|
||||
|
|
@ -4,6 +4,9 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.zorchenhimer.com/Zorchenhimer/go-studybox/build-script"
|
||||
)
|
||||
|
||||
func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
||||
|
|
@ -13,6 +16,18 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
|||
Audio: directory + "/audio" + sb.Audio.ext(),
|
||||
}
|
||||
|
||||
bscript := []build.Token{
|
||||
&build.TokenStrValue{ValType: "rom", Value: filepath.Base(directory+".studybox")},
|
||||
&build.TokenNumValue{ValType: "version", Value: 1},
|
||||
&build.TokenStrValue{
|
||||
ValType: "fullaudio",
|
||||
Value: "audio"+sb.Audio.ext(),
|
||||
},
|
||||
}
|
||||
|
||||
// for delay resets and data file names
|
||||
var prevTok build.Token
|
||||
|
||||
// A "Page" here does not correspond to the entered "Page" number on the
|
||||
// title screen. These are really segments. The "Page" that is entered on
|
||||
// the title screen is stored in the header of a segment. Multiple
|
||||
|
|
@ -44,15 +59,31 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
|||
jp.Data = append(jp.Data, jData)
|
||||
jData = jsonData{}
|
||||
|
||||
bscript = append(bscript, &build.TokenNumValue{ValType: "page", Value: int(p.PageNumber)})
|
||||
bscript = append(bscript, &build.TokenAudioOffsets{
|
||||
LeadIn: uint64(page.AudioOffsetLeadIn),
|
||||
Data: uint64(page.AudioOffsetData),
|
||||
})
|
||||
prevTok = nil
|
||||
|
||||
case *packetDelay:
|
||||
jData.Type = "delay"
|
||||
jData.Values = []int{p.Length}
|
||||
|
||||
prevTok = &build.TokenDelay{Value: int(p.Length)}
|
||||
bscript = append(bscript, prevTok)
|
||||
|
||||
case *packetWorkRamLoad:
|
||||
jData.Type = "script"
|
||||
jData.Values = []int{int(p.bankId), int(p.loadAddressHigh)}
|
||||
dataStartId = i
|
||||
|
||||
prevTok = &build.TokenData{
|
||||
Bank: int(p.bankId),
|
||||
Addr: int(p.loadAddressHigh),
|
||||
}
|
||||
bscript = append(bscript, prevTok)
|
||||
|
||||
case *packetPadding:
|
||||
jData.Type = "padding"
|
||||
jData.Values = []int{p.Length}
|
||||
|
|
@ -61,11 +92,23 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
|||
jp.Data = append(jp.Data, jData)
|
||||
jData = jsonData{}
|
||||
|
||||
prevTok = nil
|
||||
bscript = append(bscript, &build.TokenNumValue{
|
||||
ValType: "padding",
|
||||
Value: int(p.Length),
|
||||
})
|
||||
|
||||
case *packetMarkDataStart:
|
||||
jData.Values = []int{int(p.ArgA), int(p.ArgB)}
|
||||
jData.Type = p.dataType()
|
||||
dataStartId = i
|
||||
|
||||
prevTok = &build.TokenData{
|
||||
Bank: int(p.ArgA),
|
||||
Addr: int(p.ArgB),
|
||||
}
|
||||
bscript = append(bscript, prevTok)
|
||||
|
||||
case *packetMarkDataEnd:
|
||||
jData.Reset = p.Reset
|
||||
|
||||
|
|
@ -79,12 +122,18 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
|||
switch jData.Type {
|
||||
case "pattern":
|
||||
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_chrData.chr", directory, pidx, dataStartId)
|
||||
d := prevTok.(*build.TokenData)
|
||||
d.ValType = "pattern"
|
||||
|
||||
case "nametable":
|
||||
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_ntData.dat", directory, pidx, dataStartId)
|
||||
d := prevTok.(*build.TokenData)
|
||||
d.ValType = "tiles"
|
||||
|
||||
case "script":
|
||||
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_scriptData.dat", directory, pidx, dataStartId)
|
||||
d := prevTok.(*build.TokenData)
|
||||
d.ValType = "script"
|
||||
|
||||
//script, err := DissassembleScript(scriptData)
|
||||
//if err != nil {
|
||||
|
|
@ -106,6 +155,16 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
|||
return fmt.Errorf("[WARN] unknown end data type: %s\n", jData.Type)
|
||||
}
|
||||
|
||||
if prevTok != nil {
|
||||
if jData.Type == "delay" {
|
||||
d := prevTok.(*build.TokenDelay)
|
||||
d.Reset = p.Reset
|
||||
} else {
|
||||
d := prevTok.(*build.TokenData)
|
||||
d.File = filepath.Base(jData.File)
|
||||
}
|
||||
}
|
||||
|
||||
err = os.WriteFile(jData.File, rawData, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to write data to file [%q]: %v", jData.File, err)
|
||||
|
|
@ -145,5 +204,29 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(directory+".json", rawJson, 0666)
|
||||
err = os.WriteFile(directory+".json", rawJson, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bfile, err := os.Create(directory+".sbb")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer bfile.Close()
|
||||
|
||||
for _, tok := range bscript {
|
||||
if tok.Type() == "page" {
|
||||
_, err = fmt.Fprintln(bfile, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing bscript file: %w", err)
|
||||
}
|
||||
}
|
||||
_, err = fmt.Fprintln(bfile, tok.Text())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing bscript file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue