From 110720c8154b658ee58ca05d8f169a617c6a7f57 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Wed, 8 Oct 2025 00:44:52 -0400 Subject: [PATCH 01/52] chore: Ignore vscode directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9badca4..40b1473 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ frontend/package-lock* frontend/package.json.md5 cover.out .DS_Store +.vscode/* From 37527fd5095bacddc9eb121aacc0a7a18a8bada9 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 17 Oct 2025 18:57:24 -0400 Subject: [PATCH 02/52] - merged upstream; ported NWA and QUSB2SNES autosplitters - to do: file format for splitter language, add usage to main project, test functionality --- autosplitters/NWA/NWA usage.go | 174 ++ autosplitters/NWA/nwa_client.go | 243 +++ autosplitters/NWA/nwa_splitter.go | 122 ++ autosplitters/QUSB2SNES/QUSB2SNES Usage.go | 344 ++++ autosplitters/QUSB2SNES/qusb2snes_client.go | 392 +++++ autosplitters/QUSB2SNES/qusb2snes_splitter.go | 1399 +++++++++++++++++ 6 files changed, 2674 insertions(+) create mode 100644 autosplitters/NWA/NWA usage.go create mode 100644 autosplitters/NWA/nwa_client.go create mode 100644 autosplitters/NWA/nwa_splitter.go create mode 100644 autosplitters/QUSB2SNES/QUSB2SNES Usage.go create mode 100644 autosplitters/QUSB2SNES/qusb2snes_client.go create mode 100644 autosplitters/QUSB2SNES/qusb2snes_splitter.go diff --git a/autosplitters/NWA/NWA usage.go b/autosplitters/NWA/NWA usage.go new file mode 100644 index 0000000..df90fca --- /dev/null +++ b/autosplitters/NWA/NWA usage.go @@ -0,0 +1,174 @@ +type SupermetroidAutoSplitter struct { + PriorState uint8 + State uint8 + PriorRoomID uint16 + RoomID uint16 + ResetTimerOnGameReset bool + Client NWASyncClient +} + +type BattletoadsAutoSplitter struct { + PriorLevel uint8 + Level uint8 + ResetTimerOnGameReset bool + Client NWASyncClient +} + +type Splitter interface { + ClientID() + EmuInfo() + EmuGameInfo() + EmuStatus() + CoreInfo() + CoreMemories() + Update() (NWASummary, error) + Start() bool + Reset() bool + Split() bool +} + +type Game int + +const ( + Battletoads Game = iota + SuperMetroid +) + +func nwaobject(game Game, appConfig *sync.RWMutex, ip string, port uint32) Splitter { + appConfig.RLock() + defer appConfig.RUnlock() + + // Assuming appConfig is a struct pointer with ResetTimerOnGameReset field + // This is a placeholder for actual config reading logic + resetTimer := YesOrNo(0) + // You need to implement actual reading from appConfig here + + switch game { + case Battletoads: + client, _ := (&NWASyncClient{}).Connect(ip, port) + return &BattletoadsAutoSplitter{ + PriorLevel: 0, + Level: 0, + ResetTimerOnGameReset: resetTimer, + Client: *client, + } + case SuperMetroid: + client, _ := (&NWASyncClient{}).Connect(ip, port) + return &SupermetroidAutoSplitter{ + PriorState: 0, + State: 0, + PriorRoomID: 0, + RoomID: 0, + ResetTimerOnGameReset: resetTimer, + Client: *client, + } + default: + return nil + } +} + +import ( + "sync" + "time" + + "github.com/pkg/errors" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/eventlog" +) + +func appInit( + app *LiveSplitCoreRenderer, + syncReceiver <-chan ThreadEvent, + cc *eframeCreationContext, + appConfig *sync.RWMutex +) { + context := cc.eguiCtx.Clone() + context.SetVisuals(eguiVisualsDark()) + + if app.appConfig.Read().GlobalHotkeys == YesOrNoYes { + messageboxOnError(func() error { + return app.enableGlobalHotkeys() + }) + } + + frameRate := app.appConfig.Read().FrameRate + if frameRate == 0 { + frameRate = DefaultFrameRate + } + pollingRate := app.appConfig.Read().PollingRate + if pollingRate == 0 { + pollingRate = DefaultPollingRate + } + + go func() { + for { + context.Clone().RequestRepaint() + time.Sleep(time.Duration(1000.0/frameRate) * time.Millisecond) + } + }() + + timer := app.timer.Clone() + appConfig := app.appConfig.Clone() + + if appConfig.Read().UseAutosplitter == YesOrNoYes { + if appConfig.Read().AutosplitterType == autosplittersATypeNWA { + game := app.game + address := app.address.Read() + port := *app.port.Read() + + go func() { + for { + client := nwaobject(game, appConfig, &address, port) + err := printOnError(func() error { + if err := client.emuInfo(); err != nil { + return err + } + if err := client.emuGameInfo(); err != nil { + return err + } + if err := client.emuStatus(); err != nil { + return err + } + if err := client.clientID(); err != nil { + return err + } + if err := client.coreInfo(); err != nil { + return err + } + if err := client.coreMemories(); err != nil { + return err + } + + for { + autoSplitStatus, err := client.update() + if err != nil { + return err + } + if autoSplitStatus.Start { + if err := timer.WriteLock().Start(); err != nil { + return errors.Wrap(err, "failed to start timer") + } + } + if autoSplitStatus.Reset { + if err := timer.WriteLock().Reset(true); err != nil { + return errors.Wrap(err, "failed to reset timer") + } + } + if autoSplitStatus.Split { + if err := timer.WriteLock().Split(); err != nil { + return errors.Wrap(err, "failed to split timer") + } + } + + time.Sleep(time.Duration(1000.0/pollingRate) * time.Millisecond) + } + }) + if err != nil { + // handle error if needed + } + time.Sleep(1 * time.Second) + } + }() + } + } +} \ No newline at end of file diff --git a/autosplitters/NWA/nwa_client.go b/autosplitters/NWA/nwa_client.go new file mode 100644 index 0000000..9d390a3 --- /dev/null +++ b/autosplitters/NWA/nwa_client.go @@ -0,0 +1,243 @@ +package nwa + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "strings" + "time" +) + +type ErrorKind int + +const ( + InvalidError ErrorKind = iota + InvalidCommand + InvalidArgument + NotAllowed + ProtocolError +) + +type NWAError struct { + Kind ErrorKind + Reason string +} + +type AsciiReply interface{} + +type AsciiOk struct{} + +type AsciiHash map[string]string + +type AsciiListHash []map[string]string + +type Ok struct{} + +type Hash map[string]string + +type ListHash []map[string]string + +type EmulatorReply interface{} + +type Ascii struct { + Reply AsciiReply +} + +type Error struct { + Err NWAError +} + +type Binary struct { + Data []byte +} + +type NWASyncClient struct { + Connection net.Conn + Port uint32 + Addr net.Addr +} + +func Connect(ip string, port uint32) (*NWASyncClient, error) { + address := fmt.Sprintf("%s:%d", ip, port) + tcpAddr, err := net.ResolveTCPAddr("tcp", address) + if err != nil { + return nil, fmt.Errorf("can't resolve address: %w", err) + } + + conn, err := net.DialTimeout("tcp", tcpAddr.String(), time.Millisecond*1000) + if err != nil { + return nil, err + } + + return &NWASyncClient{ + Connection: conn, + Port: port, + Addr: tcpAddr, + }, nil +} + +func (c *NWASyncClient) GetReply() (EmulatorReply, error) { + readStream := bufio.NewReader(c.Connection) + firstByte, err := readStream.ReadByte() + if err != nil { + if err == io.EOF { + return nil, errors.New("connection aborted") + } + return nil, err + } + + if firstByte == '\n' { + mapResult := make(map[string]string) + for { + line, err := readStream.ReadBytes('\n') + if err != nil { + return nil, err + } + if len(line) == 0 { + break + } + if line[0] == '\n' && len(mapResult) == 0 { + return AsciiOk{}, nil + } + if line[0] == '\n' { + break + } + colonIndex := bytes.IndexByte(line, ':') + if colonIndex == -1 { + return nil, errors.New("malformed line, missing ':'") + } + key := strings.TrimSpace(string(line[:colonIndex])) + value := strings.TrimSpace(string(line[colonIndex+1 : len(line)-1])) // remove trailing \n + mapResult[key] = value + } + if _, ok := mapResult["error"]; ok { + reason, hasReason := mapResult["reason"] + errorStr, hasError := mapResult["error"] + if hasReason && hasError { + var mkind ErrorKind + switch errorStr { + case "protocol_error": + mkind = ProtocolError + case "invalid_command": + mkind = InvalidCommand + case "invalid_argument": + mkind = InvalidArgument + case "not_allowed": + mkind = NotAllowed + default: + mkind = InvalidError + } + return NWAError{ + Kind: mkind, + Reason: reason, + }, nil + } else { + return NWAError{ + Kind: InvalidError, + Reason: "Invalid reason", + }, nil + } + } + return Hash(mapResult), nil + } + + if firstByte == 0 { + // Binary reply + header := make([]byte, 4) + n, err := io.ReadFull(readStream, header) + if err != nil || n != 4 { + return nil, errors.New("failed to read header") + } + size := binary.BigEndian.Uint32(header) + data := make([]byte, size) + _, err = io.ReadFull(readStream, data) + if err != nil { + return nil, err + } + return data, nil + } + + return nil, errors.New("invalid reply") +} + +func (c *NWASyncClient) ExecuteCommand(cmd string, argString *string) (EmulatorReply, error) { + var command string + if argString == nil { + command = fmt.Sprintf("%s\n", cmd) + } else { + command = fmt.Sprintf("%s %s\n", cmd, *argString) + } + + _, err := io.WriteString(c.Connection, command) + if err != nil { + return nil, err + } + + return c.GetReply() +} + +func (c *NWASyncClient) ExecuteRawCommand(cmd string, argString *string) { + var command string + if argString == nil { + command = fmt.Sprintf("%s\n", cmd) + } else { + command = fmt.Sprintf("%s %s\n", cmd, *argString) + } + + // ignoring error as per TODO in Rust code + _, _ = io.WriteString(c.Connection, command) +} + +func (c *NWASyncClient) sendData(data []byte) { + buf := make([]byte, 5) + size := len(data) + buf[0] = 0 + buf[1] = byte((size >> 24) & 0xFF) + buf[2] = byte((size >> 16) & 0xFF) + buf[3] = byte((size >> 8) & 0xFF) + buf[4] = byte(size & 0xFF) + // TODO: handle the error + c.Connection.Write(buf) + // TODO: handle the error + c.Connection.Write(data) +} + +func (c *NWASyncClient) isConnected() bool { + // net.Conn in Go does not have a Peek method. + // We can try to set a read deadline and read with a zero-length buffer to check connection. + // But zero-length read returns immediately, so we try to read 1 byte with deadline. + buf := make([]byte, 1) + c.Connection.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) + n, err := c.Connection.Read(buf) + if err != nil { + // If timeout or no data, consider connected + netErr, ok := err.(net.Error) + if ok && netErr.Timeout() { + return true + } + return false + } + if n > 0 { + // Data was read, connection is alive + return true + } + return false +} + +func (c *NWASyncClient) close() { + // TODO: handle the error + c.Connection.Close() +} + +func (c *NWASyncClient) reconnected() (bool, error) { + conn, err := net.DialTimeout("tcp", c.Addr.String(), time.Second) + if err != nil { + return false, err + } + c.Connection = conn + return true, nil +} diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go new file mode 100644 index 0000000..52e0126 --- /dev/null +++ b/autosplitters/NWA/nwa_splitter.go @@ -0,0 +1,122 @@ +package nwa + +import ( + "fmt" +) + +type NWASummary struct { + Start bool + Reset bool + Split bool +} + +type NWASplitter struct { + priorLevel uint8 + level uint8 + resetTimerOnGameReset bool + client NWASyncClient +} + +func (b *NWASplitter) ClientID() { + cmd := "MY_NAME_IS" + args := "Annelid" + summary, err := b.client.ExecuteCommand(cmd, &args) + if err != nil { + panic(err) + } + fmt.Printf("%#v\n", summary) +} + +func (b *NWASplitter) EmuInfo() { + cmd := "EMULATOR_INFO" + args := "0" + summary, err := b.client.ExecuteCommand(cmd, &args) + if err != nil { + panic(err) + } + fmt.Printf("%#v\n", summary) +} + +func (b *NWASplitter) EmuGameInfo() { + cmd := "GAME_INFO" + summary, err := b.client.ExecuteCommand(cmd, nil) + if err != nil { + panic(err) + } + fmt.Printf("%#v\n", summary) +} + +func (b *NWASplitter) EmuStatus() { + cmd := "EMULATION_STATUS" + summary, err := b.client.ExecuteCommand(cmd, nil) + if err != nil { + panic(err) + } + fmt.Printf("%#v\n", summary) +} + +func (b *NWASplitter) CoreInfo() { + cmd := "CORE_CURRENT_INFO" + summary, err := b.client.ExecuteCommand(cmd, nil) + if err != nil { + panic(err) + } + fmt.Printf("%#v\n", summary) +} + +func (b *NWASplitter) CoreMemories() { + cmd := "CORE_MEMORIES" + summary, err := b.client.ExecuteCommand(cmd, nil) + if err != nil { + panic(err) + } + fmt.Printf("%#v\n", summary) +} + +func (b *NWASplitter) Update() (NWASummary, error) { + b.priorLevel = b.level + cmd := "CORE_READ" + args := "RAM;$0010;1" + summary, err := b.client.ExecuteCommand(cmd, &args) + if err != nil { + return NWASummary{}, err + } + fmt.Printf("%#v\n", summary) + + switch v := summary.(type) { + case []byte: + if len(v) > 0 { + b.level = v[0] + } + case NWAError: + fmt.Printf("%#v\n", v) + default: + fmt.Printf("%#v\n", v) + } + + fmt.Printf("%#v\n", b.level) + + start := b.Start() + reset := b.Reset() + split := b.Split() + + return NWASummary{ + Start: start, + Reset: reset, + Split: split, + }, nil +} + +func (b *NWASplitter) Start() bool { + return b.level == 1 && b.priorLevel == 0 +} + +func (b *NWASplitter) Reset() bool { + return b.level == 0 && + b.priorLevel != 0 && + b.resetTimerOnGameReset +} + +func (b *NWASplitter) Split() bool { + return b.level > b.priorLevel && b.priorLevel < 100 +} diff --git a/autosplitters/QUSB2SNES/QUSB2SNES Usage.go b/autosplitters/QUSB2SNES/QUSB2SNES Usage.go new file mode 100644 index 0000000..77efe35 --- /dev/null +++ b/autosplitters/QUSB2SNES/QUSB2SNES Usage.go @@ -0,0 +1,344 @@ +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/pkg/errors" +) + +type LiveSplitCoreRenderer struct { + appConfig *AppConfig + timer *Timer + settings *Settings +} + +type AppConfig struct { + mu sync.RWMutex + GlobalHotkeys *YesOrNo + FrameRate *float64 + PollingRate *float64 + UseAutosplitter *YesOrNo + AutosplitterType *AType + ResetTimerOnGameReset *YesOrNo + ResetGameOnTimerReset *YesOrNo +} + +type YesOrNo int + +const ( + No YesOrNo = iota + Yes +) + +type AType int + +const ( + QUSB2SNES AType = iota +) + +type ThreadEvent int + +const ( + TimerReset ThreadEvent = iota +) + +type Timer struct { + mu sync.RWMutex + // timer state fields here +} + +func (t *Timer) Start() error { + // start timer logic + return nil +} + +func (t *Timer) Reset(force bool) error { + // reset timer logic + return nil +} + +func (t *Timer) SetGameTime(tSec float64) error { + // set game time logic + return nil +} + +func (t *Timer) Split() error { + // split timer logic + return nil +} + +type Settings struct{} + +type AutoSplitter interface { + Update(client *SyncClient) (*Summary, error) + ResetGameTracking() + GameTimeToSeconds() *float64 +} + +type SuperMetroidAutoSplitter struct { + settings *Settings +} + +func NewSuperMetroidAutoSplitter(settings *Settings) *SuperMetroidAutoSplitter { + return &SuperMetroidAutoSplitter{settings: settings} +} + +func (a *SuperMetroidAutoSplitter) Update(client *SyncClient) (*Summary, error) { + // update logic + return &Summary{}, nil +} + +func (a *SuperMetroidAutoSplitter) ResetGameTracking() { + // reset tracking logic +} + +func (a *SuperMetroidAutoSplitter) GameTimeToSeconds() *float64 { + // return game time in seconds + return nil +} + +type Summary struct { + Start bool + Reset bool + Split bool + LatencyAverage float64 + LatencyStddev float64 +} + +type SyncClient struct{} + +func ConnectSyncClient() (*SyncClient, error) { + // connect logic + return &SyncClient{}, nil +} + +func (c *SyncClient) SetName(name string) error { + return nil +} + +func (c *SyncClient) AppVersion() (string, error) { + return "version", nil +} + +func (c *SyncClient) ListDevice() ([]string, error) { + return []string{"device1"}, nil +} + +func (c *SyncClient) Attach(device string) error { + return nil +} + +func (c *SyncClient) Info() (interface{}, error) { + return nil, nil +} + +func (c *SyncClient) Reset() error { + return nil +} + +func messageBoxOnError(f func() error) { + if err := f(); err != nil { + fmt.Println("Error:", err) + } +} + +func (app *LiveSplitCoreRenderer) EnableGlobalHotkeys() error { + // enable global hotkeys logic + return nil +} + +func appInit( + app *LiveSplitCoreRenderer, + syncReceiver <-chan ThreadEvent, + cc *CreationContext, +) { + context := cc.EguiCtx.Clone() + context.SetVisuals(DarkVisuals()) + + app.appConfig.mu.RLock() + globalHotkeys := app.appConfig.GlobalHotkeys + app.appConfig.mu.RUnlock() + + if globalHotkeys != nil && *globalHotkeys == Yes { + messageBoxOnError(func() error { + return app.EnableGlobalHotkeys() + }) + } + + app.appConfig.mu.RLock() + frameRate := DEFAULT_FRAME_RATE + if app.appConfig.FrameRate != nil { + frameRate = *app.appConfig.FrameRate + } + pollingRate := DEFAULT_POLLING_RATE + if app.appConfig.PollingRate != nil { + pollingRate = *app.appConfig.PollingRate + } + app.appConfig.mu.RUnlock() + + // Frame Rate Thread + go func() { + ticker := time.NewTicker(time.Duration(float64(time.Second) / frameRate)) + defer ticker.Stop() + for { + select { + case <-ticker.C: + context.Clone().RequestRepaint() + } + } + }() + + timer := app.timer + appConfig := app.appConfig + + appConfig.mu.RLock() + useAutosplitter := appConfig.UseAutosplitter + appConfig.mu.RUnlock() + + if useAutosplitter != nil && *useAutosplitter == Yes { + appConfig.mu.RLock() + autosplitterType := appConfig.AutosplitterType + appConfig.mu.RUnlock() + + if autosplitterType != nil && *autosplitterType == QUSB2SNES { + settings := app.settings + + go func() { + for { + latency := struct { + mu sync.RWMutex + value [2]float64 + }{} + + err := func() error { + client, err := ConnectSyncClient() + if err != nil { + return errors.Wrap(err, "creating usb2snes connection") + } + if err := client.SetName("annelid"); err != nil { + return err + } + version, err := client.AppVersion() + if err != nil { + return err + } + fmt.Printf("Server version is %v\n", version) + + devices, err := client.ListDevice() + if err != nil { + return err + } + if len(devices) != 1 { + if len(devices) == 0 { + return errors.New("no devices present") + } + return errors.Errorf("unexpected devices: %#v", devices) + } + device := devices[0] + fmt.Printf("Using device %v\n", device) + + if err := client.Attach(device); err != nil { + return err + } + fmt.Println("Connected.") + info, err := client.Info() + if err != nil { + return err + } + fmt.Printf("%#v\n", info) + + var autosplitter AutoSplitter = NewSuperMetroidAutoSplitter(settings) + + for { + summary, err := autosplitter.Update(client) + if err != nil { + return err + } + if summary.Start { + if err := timer.Start(); err != nil { + return errors.Wrap(err, "start timer") + } + } + if summary.Reset { + appConfig.mu.RLock() + resetTimerOnGameReset := appConfig.ResetTimerOnGameReset + appConfig.mu.RUnlock() + if resetTimerOnGameReset != nil && *resetTimerOnGameReset == Yes { + if err := timer.Reset(true); err != nil { + return errors.Wrap(err, "reset timer") + } + } + } + if summary.Split { + if t := autosplitter.GameTimeToSeconds(); t != nil { + if err := timer.SetGameTime(*t); err != nil { + return errors.Wrap(err, "set game time") + } + } + if err := timer.Split(); err != nil { + return errors.Wrap(err, "split timer") + } + } + + latency.mu.Lock() + latency.value[0] = summary.LatencyAverage + latency.value[1] = summary.LatencyStddev + latency.mu.Unlock() + + select { + case ev := <-syncReceiver: + if ev == TimerReset { + autosplitter.ResetGameTracking() + appConfig.mu.RLock() + resetGameOnTimerReset := appConfig.ResetGameOnTimerReset + appConfig.mu.RUnlock() + if resetGameOnTimerReset != nil && *resetGameOnTimerReset == Yes { + if err := client.Reset(); err != nil { + return err + } + } + } + default: + } + + time.Sleep(time.Duration(float64(time.Second) / pollingRate)) + } + }() + if err != nil { + fmt.Println("Error:", err) + } + } + }() + + time.Sleep(time.Second) + } + } +} + +// Dummy types and functions to make the above compile + +type CreationContext struct { + EguiCtx *EguiContext +} + +type EguiContext struct{} + +func (e *EguiContext) Clone() *EguiContext { + return &EguiContext{} +} + +func (e *EguiContext) SetVisuals(v Visuals) {} + +func (e *EguiContext) RequestRepaint() {} + +type Visuals struct{} + +func DarkVisuals() Visuals { + return Visuals{} +} + +const ( + DEFAULT_FRAME_RATE = 60.0 + DEFAULT_POLLING_RATE = 30.0 +) \ No newline at end of file diff --git a/autosplitters/QUSB2SNES/qusb2snes_client.go b/autosplitters/QUSB2SNES/qusb2snes_client.go new file mode 100644 index 0000000..a185758 --- /dev/null +++ b/autosplitters/QUSB2SNES/qusb2snes_client.go @@ -0,0 +1,392 @@ +package qusb2snes + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + + "github.com/gorilla/websocket" +) + +type Command int + +const ( + AppVersion Command = iota + Name + DeviceList + Attach + Info + Boot + Reset + Menu + + List + PutFile + GetFile + Rename + Remove + + GetAddress +) + +func (c Command) String() string { + return [...]string{ + "AppVersion", + "Name", + "DeviceList", + "Attach", + "Info", + "Boot", + "Reset", + "Menu", + "List", + "PutFile", + "GetFile", + "Rename", + "Remove", + "GetAddress", + }[c] +} + +type Space int + +const ( + None Space = iota + SNES + CMD +) + +func (s Space) String() string { + return [...]string{ + "None", + "SNES", + "CMD", + }[s] +} + +type Infos struct { + Version string + DevType string + Game string + Flags []string +} + +type USB2SnesQuery struct { + Opcode string `json:"Opcode"` + Space string `json:"Space,omitempty"` + Flags []string `json:"Flags"` + Operands []string `json:"Operands"` +} + +type USB2SnesResult struct { + Results []string `json:"Results"` +} + +type USB2SnesFileType int + +const ( + File USB2SnesFileType = iota + Dir +) + +type USB2SnesFileInfo struct { + Name string + FileType USB2SnesFileType +} + +type SyncClient struct { + client *websocket.Conn + devel bool +} + +func Connect() (*SyncClient, error) { + return connect(false) +} + +func ConnectWithDevel() (*SyncClient, error) { + return connect(true) +} + +func connect(devel bool) (*SyncClient, error) { + u := url.URL{Scheme: "ws", Host: "localhost:23074", Path: "/"} + conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + return nil, err + } + return &SyncClient{ + client: conn, + devel: devel, + }, nil +} + +func (sc *SyncClient) sendCommand(command Command, args []string) error { + return sc.sendCommandWithSpace(command, None, args) +} + +func (sc *SyncClient) sendCommandWithSpace(command Command, space Space, args []string) error { + if sc.devel { + fmt.Printf("Send command : %s\n", command.String()) + } + // var nspace *string + // if space != nil { + // s := space.String() + // nspace = &s + // } + query := USB2SnesQuery{ + Opcode: command.String(), + Space: space.String(), + Flags: []string{}, + Operands: args, + } + jsonData, err := json.Marshal(query) + if err != nil { + return err + } + if sc.devel { + prettyJSON, err := json.MarshalIndent(query, "", " ") + if err == nil { + fmt.Println(string(prettyJSON)) + } + } + err = sc.client.WriteMessage(websocket.TextMessage, jsonData) + return err +} + +func (sc *SyncClient) getReply() (*USB2SnesResult, error) { + _, message, err := sc.client.ReadMessage() + if err != nil { + return nil, err + } + if sc.devel { + fmt.Println("Reply:") + fmt.Println(string(message)) + } + var result USB2SnesResult + err = json.Unmarshal(message, &result) + if err != nil { + return nil, err + } + return &result, nil +} + +func (sc *SyncClient) SetName(name string) error { + return sc.sendCommand(Name, []string{name}) +} + +func (sc *SyncClient) AppVersion() (string, error) { + err := sc.sendCommand(AppVersion, []string{}) + if err != nil { + return "", err + } + reply, err := sc.getReply() + if err != nil { + return "", err + } + if len(reply.Results) == 0 { + return "", fmt.Errorf("no results in reply") + } + return reply.Results[0], nil +} + +func (sc *SyncClient) ListDevice() ([]string, error) { + err := sc.sendCommand(DeviceList, []string{}) + if err != nil { + return nil, err + } + reply, err := sc.getReply() + if err != nil { + return nil, err + } + return reply.Results, nil +} + +func (sc *SyncClient) Attach(device string) error { + return sc.sendCommand(Attach, []string{device}) +} + +func (sc *SyncClient) Info() (*Infos, error) { + err := sc.sendCommand(Info, []string{}) + if err != nil { + return nil, err + } + usbreply, err := sc.getReply() + if err != nil { + return nil, err + } + info := usbreply.Results + if len(info) < 3 { + return nil, fmt.Errorf("unexpected reply length") + } + flags := []string{} + if len(info) > 3 { + flags = info[3:] + } + return &Infos{ + Version: info[0], + DevType: info[1], + Game: info[2], + Flags: flags, + }, nil +} + +func (sc *SyncClient) Reset() error { + return sc.sendCommand(Reset, []string{}) +} + +func (sc *SyncClient) Menu() error { + return sc.sendCommand(Menu, []string{}) +} + +func (sc *SyncClient) Boot(toboot string) error { + return sc.sendCommand(Boot, []string{toboot}) +} + +func (sc *SyncClient) Ls(path string) ([]USB2SnesFileInfo, error) { + err := sc.sendCommand(List, []string{path}) + if err != nil { + return nil, err + } + usbreply, err := sc.getReply() + if err != nil { + return nil, err + } + vecInfo := usbreply.Results + var toret []USB2SnesFileInfo + for i := 0; i < len(vecInfo); i += 2 { + if i+1 >= len(vecInfo) { + break + } + fileType := Dir + if vecInfo[i] == "1" { + fileType = File + } + info := USB2SnesFileInfo{ + FileType: fileType, + Name: vecInfo[i+1], + } + toret = append(toret, info) + } + return toret, nil +} + +func (sc *SyncClient) SendFile(path string, data []byte) error { + err := sc.sendCommand(PutFile, []string{path, fmt.Sprintf("%x", len(data))}) + if err != nil { + return err + } + chunkSize := 1024 + for start := 0; start < len(data); start += chunkSize { + stop := start + chunkSize + if stop > len(data) { + stop = len(data) + } + err = sc.client.WriteMessage(websocket.BinaryMessage, data[start:stop]) + if err != nil { + return err + } + } + return nil +} + +func (sc *SyncClient) getFile(path string) ([]byte, error) { + err := sc.sendCommand(GetFile, []string{path}) + if err != nil { + return nil, err + } + reply, err := sc.getReply() + if err != nil { + return nil, err + } + if len(reply.Results) == 0 { + return nil, errors.New("no results in reply") + } + stringHex := reply.Results[0] + size, err := strconv.ParseUint(stringHex, 16, 0) + if err != nil { + return nil, err + } + data := make([]byte, 0, size) + for { + _, msgData, err := sc.client.ReadMessage() + if err != nil { + return nil, err + } + // In Rust code, it expects binary message + // Here, msgData is []byte already + data = append(data, msgData...) + if len(data) == int(size) { + break + } + } + return data, nil +} + +func (sc *SyncClient) removePath(path string) error { + return sc.sendCommand(Remove, []string{path}) +} + +func (sc *SyncClient) getAddress(address uint32, size int) ([]byte, error) { + err := sc.sendCommandWithSpace(GetAddress, SNES, []string{ + fmt.Sprintf("%x", address), + fmt.Sprintf("%x", size), + }) + if err != nil { + return nil, err + } + data := make([]byte, 0, size) + for { + _, msgData, err := sc.client.ReadMessage() + if err != nil { + return nil, err + } + data = append(data, msgData...) + if len(data) == size { + break + } + } + return data, nil +} + +func (sc *SyncClient) getAddresses(pairs [][2]int) ([][]byte, error) { + args := make([]string, 0, len(pairs)*2) + totalSize := 0 + for _, pair := range pairs { + address := pair[0] + size := pair[1] + args = append(args, fmt.Sprintf("%x", address)) + args = append(args, fmt.Sprintf("%x", size)) + totalSize += size + } + + err := sc.sendCommandWithSpace(GetAddress, SNES, args) + if err != nil { + return nil, err + } + + data := make([]byte, 0, totalSize) + ret := make([][]byte, 0, len(pairs)) + + for { + _, msgData, err := sc.client.ReadMessage() + if err != nil { + return nil, err + } + + data = append(data, msgData...) + + if len(data) == totalSize { + break + } + } + + consumed := 0 + for _, pair := range pairs { + size := pair[1] + ret = append(ret, data[consumed:consumed+size]) + consumed += size + } + + return ret, nil +} diff --git a/autosplitters/QUSB2SNES/qusb2snes_splitter.go b/autosplitters/QUSB2SNES/qusb2snes_splitter.go new file mode 100644 index 0000000..5b2faa4 --- /dev/null +++ b/autosplitters/QUSB2SNES/qusb2snes_splitter.go @@ -0,0 +1,1399 @@ +package qusb2snes + +import ( + "fmt" + "math" + "sync" + "time" +) + +var ( + roomIDEnum = map[string]uint32{ + "landingSite": 0x91F8, + "crateriaPowerBombRoom": 0x93AA, + "westOcean": 0x93FE, + "elevatorToMaridia": 0x94CC, + "crateriaMoat": 0x95FF, + "elevatorToCaterpillar": 0x962A, + "gauntletETankRoom": 0x965B, + "climb": 0x96BA, + "pitRoom": 0x975C, + "elevatorToMorphBall": 0x97B5, + "bombTorizo": 0x9804, + "terminator": 0x990D, + "elevatorToGreenBrinstar": 0x9938, + "greenPirateShaft": 0x99BD, + "crateriaSupersRoom": 0x99F9, + "theFinalMissile": 0x9A90, + "greenBrinstarMainShaft": 0x9AD9, + "sporeSpawnSuper": 0x9B5B, + "earlySupers": 0x9BC8, + "brinstarReserveRoom": 0x9C07, + "bigPink": 0x9D19, + "sporeSpawnKeyhunter": 0x9D9C, + "sporeSpawn": 0x9DC7, + "pinkBrinstarPowerBombRoom": 0x9E11, + "greenHills": 0x9E52, + "noobBridge": 0x9FBA, + "morphBall": 0x9E9F, + "blueBrinstarETankRoom": 0x9F64, + "etecoonETankRoom": 0xA011, + "etecoonSuperRoom": 0xA051, + "waterway": 0xA0D2, + "alphaMissileRoom": 0xA107, + "hopperETankRoom": 0xA15B, + "billyMays": 0xA1D8, + "redTower": 0xA253, + "xRay": 0xA2CE, + "caterpillar": 0xA322, + "betaPowerBombRoom": 0xA37C, + "alphaPowerBombsRoom": 0xA3AE, + "bat": 0xA3DD, + "spazer": 0xA447, + "warehouseETankRoom": 0xA4B1, + "warehouseZeela": 0xA471, + "warehouseKiHunters": 0xA4DA, + "kraidEyeDoor": 0xA56B, + "kraid": 0xA59F, + "statuesHallway": 0xA5ED, + "statues": 0xA66A, + "warehouseEntrance": 0xA6A1, + "varia": 0xA6E2, + "cathedral": 0xA788, + "businessCenter": 0xA7DE, + "iceBeam": 0xA890, + "crumbleShaft": 0xA8F8, + "crocomireSpeedway": 0xA923, + "crocomire": 0xA98D, + "hiJump": 0xA9E5, + "crocomireEscape": 0xAA0E, + "hiJumpShaft": 0xAA41, + "postCrocomirePowerBombRoom": 0xAADE, + "cosineRoom": 0xAB3B, + "preGrapple": 0xAB8F, + "grapple": 0xAC2B, + "norfairReserveRoom": 0xAC5A, + "greenBubblesRoom": 0xAC83, + "bubbleMountain": 0xACB3, + "speedBoostHall": 0xACF0, + "speedBooster": 0xAD1B, + "singleChamber": 0xAD5E, // Exit room from Lower Norfair, also on the path to Wave + "doubleChamber": 0xADAD, + "waveBeam": 0xADDE, + "volcano": 0xAE32, + "kronicBoost": 0xAE74, + "magdolliteTunnel": 0xAEB4, + "lowerNorfairElevator": 0xAF3F, + "risingTide": 0xAFA3, + "spikyAcidSnakes": 0xAFFB, + "acidStatue": 0xB1E5, + "mainHall": 0xB236, // First room in Lower Norfair + "goldenTorizo": 0xB283, + "ridley": 0xB32E, + "lowerNorfairFarming": 0xB37A, + "mickeyMouse": 0xB40A, + "pillars": 0xB457, + "writg": 0xB4AD, + "amphitheatre": 0xB4E5, + "lowerNorfairSpringMaze": 0xB510, + "lowerNorfairEscapePowerBombRoom": 0xB55A, + "redKiShaft": 0xB585, + "wasteland": 0xB5D5, + "metalPirates": 0xB62B, + "threeMusketeers": 0xB656, + "ridleyETankRoom": 0xB698, + "screwAttack": 0xB6C1, + "lowerNorfairFireflea": 0xB6EE, + "bowling": 0xC98E, + "wreckedShipEntrance": 0xCA08, + "attic": 0xCA52, + "atticWorkerRobotRoom": 0xCAAE, + "wreckedShipMainShaft": 0xCAF6, + "wreckedShipETankRoom": 0xCC27, + "basement": 0xCC6F, // Basement of Wrecked Ship + "phantoon": 0xCD13, + "wreckedShipLeftSuperRoom": 0xCDA8, + "wreckedShipRightSuperRoom": 0xCDF1, + "gravity": 0xCE40, + "glassTunnel": 0xCEFB, + "mainStreet": 0xCFC9, + "mamaTurtle": 0xD055, + "wateringHole": 0xD13B, + "beach": 0xD1DD, + "plasmaBeam": 0xD2AA, + "maridiaElevator": 0xD30B, + "plasmaSpark": 0xD340, + "toiletBowl": 0xD408, + "oasis": 0xD48E, + "leftSandPit": 0xD4EF, + "rightSandPit": 0xD51E, + "aqueduct": 0xD5A7, + "butterflyRoom": 0xD5EC, + "botwoonHallway": 0xD617, + "springBall": 0xD6D0, + "precious": 0xD78F, + "botwoonETankRoom": 0xD7E4, + "botwoon": 0xD95E, + "spaceJump": 0xD9AA, + "westCactusAlley": 0xD9FE, + "draygon": 0xDA60, + "tourianElevator": 0xDAAE, + "metroidOne": 0xDAE1, + "metroidTwo": 0xDB31, + "metroidThree": 0xDB7D, + "metroidFour": 0xDBCD, + "dustTorizo": 0xDC65, + "tourianHopper": 0xDC19, + "tourianEyeDoor": 0xDDC4, + "bigBoy": 0xDCB1, + "motherBrain": 0xDD58, + "rinkaShaft": 0xDDF3, + "tourianEscape4": 0xDEDE, + "ceresElevator": 0xDF45, + "flatRoom": 0xE06B, // Placeholder name for the flat room in Ceres Station + "ceresRidley": 0xE0B5, + } + mapInUseEnum = map[string]uint32{ + "crateria": 0x0, + "brinstar": 0x1, + "norfair": 0x2, + "wreckedShip": 0x3, + "maridia": 0x4, + "tourian": 0x5, + "ceres": 0x6, + } + gameStateEnum = map[string]uint32{ + "normalGameplay": 0x8, + "doorTransition": 0xB, + "startOfCeresCutscene": 0x20, + "preEndCutscene": 0x26, // briefly at this value during the black screen transition after the ship fades out + "endCutscene": 0x27, + } + unlockFlagEnum = map[string]uint32{ + // First item byte + "variaSuit": 0x1, + "springBall": 0x2, + "morphBall": 0x4, + "screwAttack": 0x8, + "gravSuit": 0x20, + // Second item byte + "hiJump": 0x1, + "spaceJump": 0x2, + "bomb": 0x10, + "speedBooster": 0x20, + "grapple": 0x40, + "xray": 0x80, + // Beams + "wave": 0x1, + "ice": 0x2, + "spazer": 0x4, + "plasma": 0x8, + // Charge + "chargeBeam": 0x10, + } + motherBrainMaxHPEnum = map[string]uint32{ + "phase1": 0xBB8, // 3000 + "phase2": 0x4650, // 18000 + "phase3": 0x8CA0, // 36000 + } + eventFlagEnum = map[string]uint32{ + "zebesAblaze": 0x40, + "tubeBroken": 0x8, + } + bossFlagEnum = map[string]uint32{ + // Crateria + "bombTorizo": 0x4, + // Brinstar + "sporeSpawn": 0x2, + "kraid": 0x1, + // Norfair + "ridley": 0x1, + "crocomire": 0x2, + "goldenTorizo": 0x4, + // Wrecked Ship + "phantoon": 0x1, + // Maridia + "draygon": 0x1, + "botwoon": 0x2, + // Tourian + "motherBrain": 0x2, + // Ceres + "ceresRidley": 0x1, + } +) + +type Settings struct { + data map[string]struct { + value bool + parent *string + } + modifiedAfterCreation bool + mu sync.RWMutex +} + +func NewSettings() *Settings { + s := &Settings{ + data: make(map[string]struct { + value bool + parent *string + }), + modifiedAfterCreation: false, + } + // Split on Missiles, Super Missiles, and Power Bombs + s.Insert("ammoPickups", true) + // Split on the first Missile pickup + s.InsertWithParent("firstMissile", false, "ammoPickups") + // Split on each Missile upgrade + s.InsertWithParent("allMissiles", false, "ammoPickups") + // Split on specific Missile Pack locations + s.InsertWithParent("specificMissiles", false, "ammoPickups") + // Split on Crateria Missile Pack locations + s.InsertWithParent("crateriaMissiles", false, "specificMissiles") + // Split on picking up the Missile Pack located at the bottom left of the West Ocean + s.InsertWithParent("oceanBottomMissiles", false, "crateriaMissiles") + // Split on picking up the Missile Pack located in the ceiling tile in West Ocean + s.InsertWithParent("oceanTopMissiles", false, "crateriaMissiles") + // Split on picking up the Missile Pack located in the Morphball maze section of West Ocean + s.InsertWithParent("oceanMiddleMissiles", false, "crateriaMissiles") + // Split on picking up the Missile Pack in The Moat, also known as The Lake + s.InsertWithParent("moatMissiles", false, "crateriaMissiles") + // Split on picking up the Missile Pack in the Pit Room + s.InsertWithParent("oldTourianMissiles", false, "crateriaMissiles") + // Split on picking up the right side Missile Pack at the end of Gauntlet(Green Pirates Shaft) + s.InsertWithParent("gauntletRightMissiles", false, "crateriaMissiles") + // Split on picking up the left side Missile Pack at the end of Gauntlet(Green Pirates Shaft) + s.InsertWithParent("gauntletLeftMissiles", false, "crateriaMissiles") + // Split on picking up the Missile Pack located in The Final Missile + s.InsertWithParent("dentalPlan", false, "crateriaMissiles") + // Split on Brinstar Missile Pack locations + s.InsertWithParent("brinstarMissiles", false, "specificMissiles") + // Split on picking up the Missile Pack located below the crumble bridge in the Early Supers Room + s.InsertWithParent("earlySuperBridgeMissiles", false, "brinstarMissiles") + // Split on picking up the first Missile Pack behind the Brinstar Reserve Tank + s.InsertWithParent("greenBrinstarReserveMissiles", false, "brinstarMissiles") + // Split on picking up the second Missile Pack behind the Brinstar Reserve Tank Room + s.InsertWithParent("greenBrinstarExtraReserveMissiles", false, "brinstarMissiles") + // Split on picking up the Missile Pack located left of center in Big Pink + s.InsertWithParent("bigPinkTopMissiles", false, "brinstarMissiles") + // Split on picking up the Missile Pack located at the bottom left of Big Pink + s.InsertWithParent("chargeMissiles", false, "brinstarMissiles") + // Split on picking up the Missile Pack in Green Hill Zone + s.InsertWithParent("greenHillsMissiles", false, "brinstarMissiles") + // Split on picking up the Missile Pack in the Blue Brinstar Energy Tank Room + s.InsertWithParent("blueBrinstarETankMissiles", false, "brinstarMissiles") + // Split on picking up the first Missile Pack of the game(First Missile Room) + s.InsertWithParent("alphaMissiles", false, "brinstarMissiles") + // Split on picking up the Missile Pack located on the pedestal in Billy Mays' Room + s.InsertWithParent("billyMaysMissiles", false, "brinstarMissiles") + // Split on picking up the Missile Pack located in the floor of Billy Mays' Room + s.InsertWithParent("butWaitTheresMoreMissiles", false, "brinstarMissiles") + // Split on picking up the Missile Pack in the Alpha Power Bombs Room + s.InsertWithParent("redBrinstarMissiles", false, "brinstarMissiles") + // Split on picking up the Missile Pack in the Warehouse Kihunter Room + s.InsertWithParent("warehouseMissiles", false, "brinstarMissiles") + // Split on Norfair Missile Pack locations + s.InsertWithParent("norfairMissiles", false, "specificMissiles") + // Split on picking up the Missile Pack in Cathedral + s.InsertWithParent("cathedralMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in Crumble Shaft + s.InsertWithParent("crumbleShaftMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in Crocomire Escape + s.InsertWithParent("crocomireEscapeMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in the Hi Jump Energy Tank Room + s.InsertWithParent("hiJumpMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in the Post Crocomire Missile Room, also known as Cosine Room + s.InsertWithParent("postCrocomireMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in the Post Crocomire Jump Room + s.InsertWithParent("grappleMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in the Norfair Reserve Tank Room + s.InsertWithParent("norfairReserveMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in the Green Bubbles Missile Room + s.InsertWithParent("greenBubblesMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in Bubble Mountain + s.InsertWithParent("bubbleMountainMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in Speed Booster Hall + s.InsertWithParent("speedBoostMissiles", false, "norfairMissiles") + // Split on picking up the Wave Missile Pack in Double Chamber + s.InsertWithParent("waveMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in the Golden Torizo's Room + s.InsertWithParent("goldTorizoMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in the Mickey Mouse Room + s.InsertWithParent("mickeyMouseMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in the Lower Norfair Springball Maze Room + s.InsertWithParent("lowerNorfairSpringMazeMissiles", false, "norfairMissiles") + // Split on picking up the Missile Pack in the The Musketeers' Room + s.InsertWithParent("threeMusketeersMissiles", false, "norfairMissiles") + // Split on Wrecked Ship Missile Pack locations + s.InsertWithParent("wreckedShipMissiles", false, "specificMissiles") + // Split on picking up the Missile Pack in Wrecked Ship Main Shaft + s.InsertWithParent("wreckedShipMainShaftMissiles", false, "wreckedShipMissiles") + // Split on picking up the Missile Pack in Bowling Alley + s.InsertWithParent("bowlingMissiles", false, "wreckedShipMissiles") + // Split on picking up the Missile Pack in the Wrecked Ship East Missile Room + s.InsertWithParent("atticMissiles", false, "wreckedShipMissiles") + // Split on Maridia Missile Pack locations + s.InsertWithParent("maridiaMissiles", false, "specificMissiles") + // Split on picking up the Missile Pack in Main Street + s.InsertWithParent("mainStreetMissiles", false, "maridiaMissiles") + // Split on picking up the Missile Pack in the Mama Turtle Room + s.InsertWithParent("mamaTurtleMissiles", false, "maridiaMissiles") + // Split on picking up the Missile Pack in Watering Hole + s.InsertWithParent("wateringHoleMissiles", false, "maridiaMissiles") + // Split on picking up the Missile Pack in the Pseudo Plasma Spark Room + s.InsertWithParent("beachMissiles", false, "maridiaMissiles") + // Split on picking up the Missile Pack in West Sand Hole + s.InsertWithParent("leftSandPitMissiles", false, "maridiaMissiles") + // Split on picking up the Missile Pack in East Sand Hole + s.InsertWithParent("rightSandPitMissiles", false, "maridiaMissiles") + // Split on picking up the Missile Pack in Aqueduct + s.InsertWithParent("aqueductMissiles", false, "maridiaMissiles") + // Split on picking up the Missile Pack in The Precious Room + s.InsertWithParent("preDraygonMissiles", false, "maridiaMissiles") + // Split on the first Super Missile pickup + s.InsertWithParent("firstSuper", false, "ammoPickups") + // Split on each Super Missile upgrade + s.InsertWithParent("allSupers", false, "ammoPickups") + // Split on specific Super Missile Pack locations + s.InsertWithParent("specificSupers", false, "ammoPickups") + // Split on picking up the Super Missile Pack in the Crateria Super Room + s.InsertWithParent("climbSupers", false, "specificSupers") + // Split on picking up the Super Missile Pack in the Spore Spawn Super Room (NOTE: SSTRA splits when the dialogue box disappears, not on touch. Use Spore Spawn RTA Finish for SSTRA runs.) + s.InsertWithParent("sporeSpawnSupers", false, "specificSupers") + // Split on picking up the Super Missile Pack in the Early Supers Room + s.InsertWithParent("earlySupers", false, "specificSupers") + // Split on picking up the Super Missile Pack in the Etecoon Super Room + s.InsertWithParent("etecoonSupers", false, "specificSupers") + // Split on picking up the Super Missile Pack in the Golden Torizo's Room + s.InsertWithParent("goldTorizoSupers", false, "specificSupers") + // Split on picking up the Super Missile Pack in the Wrecked Ship West Super Room + s.InsertWithParent("wreckedShipLeftSupers", false, "specificSupers") + // Split on picking up the Super Missile Pack in the Wrecked Ship East Super Room + s.InsertWithParent("wreckedShipRightSupers", false, "specificSupers") + // Split on picking up the Super Missile Pack in Main Street + s.InsertWithParent("crabSupers", false, "specificSupers") + // Split on picking up the Super Missile Pack in Watering Hole + s.InsertWithParent("wateringHoleSupers", false, "specificSupers") + // Split on picking up the Super Missile Pack in Aqueduct + s.InsertWithParent("aqueductSupers", false, "specificSupers") + // Split on the first Power Bomb pickup + s.InsertWithParent("firstPowerBomb", true, "ammoPickups") + // Split on each Power Bomb upgrade + s.InsertWithParent("allPowerBombs", false, "ammoPickups") + // Split on specific Power Bomb Pack locations + s.InsertWithParent("specificBombs", false, "ammoPickups") + // Split on picking up the Power Bomb Pack in the Crateria Power Bomb Room + s.InsertWithParent("landingSiteBombs", false, "specificBombs") + // Split on picking up the Power Bomb Pack in the Etecoon Room section of Green Brinstar Main Shaft + s.InsertWithParent("etecoonBombs", false, "specificBombs") + // Split on picking up the Power Bomb Pack in the Pink Brinstar Power Bomb Room + s.InsertWithParent("pinkBrinstarBombs", false, "specificBombs") + // Split on picking up the Power Bomb Pack in the Morph Ball Room + s.InsertWithParent("blueBrinstarBombs", false, "specificBombs") + // Split on picking up the Power Bomb Pack in the Alpha Power Bomb Room + s.InsertWithParent("alphaBombs", false, "specificBombs") + // Split on picking up the Power Bomb Pack in the Beta Power Bomb Room + s.InsertWithParent("betaBombs", false, "specificBombs") + // Split on picking up the Power Bomb Pack in the Post Crocomire Power Bomb Room + s.InsertWithParent("crocomireBombs", false, "specificBombs") + // Split on picking up the Power Bomb Pack in the Lower Norfair Escape Power Bomb Room + s.InsertWithParent("lowerNorfairEscapeBombs", false, "specificBombs") + // Split on picking up the Power Bomb Pack in Wasteland + s.InsertWithParent("shameBombs", false, "specificBombs") + // Split on picking up the Power Bomb Pack in East Sand Hall + s.InsertWithParent("rightSandPitBombs", false, "specificBombs") + + // Split on Varia and Gravity pickups + s.Insert("suitUpgrades", true) + // Split on picking up the Varia Suit + s.InsertWithParent("variaSuit", true, "suitUpgrades") + // Split on picking up the Gravity Suit + s.InsertWithParent("gravSuit", true, "suitUpgrades") + + // Split on beam upgrades + s.Insert("beamUpgrades", true) + // Split on picking up the Charge Beam + s.InsertWithParent("chargeBeam", false, "beamUpgrades") + // Split on picking up the Spazer + s.InsertWithParent("spazer", false, "beamUpgrades") + // Split on picking up the Wave Beam + s.InsertWithParent("wave", true, "beamUpgrades") + // Split on picking up the Ice Beam + s.InsertWithParent("ice", false, "beamUpgrades") + // Split on picking up the Plasma Beam + s.InsertWithParent("plasma", false, "beamUpgrades") + + // Split on boot upgrades + s.Insert("bootUpgrades", false) + // Split on picking up the Hi-Jump Boots + s.InsertWithParent("hiJump", false, "bootUpgrades") + // Split on picking up Space Jump + s.InsertWithParent("spaceJump", false, "bootUpgrades") + // Split on picking up the Speed Booster + s.InsertWithParent("speedBooster", false, "bootUpgrades") + + // Split on Energy Tanks and Reserve Tanks + s.Insert("energyUpgrades", false) + // Split on picking up the first Energy Tank + s.InsertWithParent("firstETank", false, "energyUpgrades") + // Split on picking up each Energy Tank + s.InsertWithParent("allETanks", false, "energyUpgrades") + // Split on specific Energy Tank locations + s.InsertWithParent("specificETanks", false, "energyUpgrades") + // Split on picking up the Energy Tank in the Gauntlet Energy Tank Room + s.InsertWithParent("gauntletETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Terminator Room + s.InsertWithParent("terminatorETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Blue Brinstar Energy Tank Room + s.InsertWithParent("ceilingETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Etecoon Energy Tank Room + s.InsertWithParent("etecoonsETank", false, "specificETanks") + // Split on picking up the Energy Tank in Waterway + s.InsertWithParent("waterwayETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Hopper Energy Tank Room + s.InsertWithParent("waveGateETank", false, "specificETanks") + // Split on picking up the Kraid Energy Tank in the Warehouse Energy Tank Room + s.InsertWithParent("kraidETank", false, "specificETanks") + // Split on picking up the Energy Tank in Crocomire's Room + s.InsertWithParent("crocomireETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Hi Jump Energy Tank Room + s.InsertWithParent("hiJumpETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Ridley Tank Room + s.InsertWithParent("ridleyETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Lower Norfair Fireflea Room + s.InsertWithParent("firefleaETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Wrecked Ship Energy Tank Room + s.InsertWithParent("wreckedShipETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Mama Turtle Room + s.InsertWithParent("tatoriETank", false, "specificETanks") + // Split on picking up the Energy Tank in the Botwoon Energy Tank Room + s.InsertWithParent("botwoonETank", false, "specificETanks") + // Split on picking up each Reserve Tank + s.InsertWithParent("reserveTanks", false, "energyUpgrades") + // Split on specific Reserve Tank locations + s.InsertWithParent("specificRTanks", false, "energyUpgrades") + // Split on picking up the Reserve Tank in the Brinstar Reserve Tank Room + s.InsertWithParent("brinstarReserve", false, "specificRTanks") + // Split on picking up the Reserve Tank in the Norfair Reserve Tank Room + s.InsertWithParent("norfairReserve", false, "specificRTanks") + // Split on picking up the Reserve Tank in Bowling Alley + s.InsertWithParent("wreckedShipReserve", false, "specificRTanks") + // Split on picking up the Reserve Tank in West Sand Hole + s.InsertWithParent("maridiaReserve", false, "specificRTanks") + + // Split on the miscellaneous upgrades + s.Insert("miscUpgrades", false) + // Split on picking up the Morphing Ball + s.InsertWithParent("morphBall", false, "miscUpgrades") + // Split on picking up the Bomb + s.InsertWithParent("bomb", false, "miscUpgrades") + // Split on picking up the Spring Ball + s.InsertWithParent("springBall", false, "miscUpgrades") + // Split on picking up the Screw Attack + s.InsertWithParent("screwAttack", false, "miscUpgrades") + // Split on picking up the Grapple Beam + s.InsertWithParent("grapple", false, "miscUpgrades") + // Split on picking up the X-Ray Scope + s.InsertWithParent("xray", false, "miscUpgrades") + + // Split on transitions between areas + s.Insert("areaTransitions", true) + // Split on entering miniboss rooms (except Bomb Torizo) + s.InsertWithParent("miniBossRooms", false, "areaTransitions") + // Split on entering major boss rooms + s.InsertWithParent("bossRooms", false, "areaTransitions") + // Split on elevator transitions between areas (except Statue Room to Tourian) + s.InsertWithParent("elevatorTransitions", false, "areaTransitions") + // Split on leaving Ceres Station + s.InsertWithParent("ceresEscape", false, "areaTransitions") + // Split on entering the Wrecked Ship Entrance from the lower door of West Ocean + s.InsertWithParent("wreckedShipEntrance", false, "areaTransitions") + // Split on entering Red Tower from Noob Bridge + s.InsertWithParent("redTowerMiddleEntrance", false, "areaTransitions") + // Split on entering Red Tower from Skree Boost room + s.InsertWithParent("redTowerBottomEntrance", false, "areaTransitions") + // Split on entering Kraid's Lair + s.InsertWithParent("kraidsLair", false, "areaTransitions") + // Split on entering Rising Tide from Cathedral + s.InsertWithParent("risingTideEntrance", false, "areaTransitions") + // Split on exiting Attic + s.InsertWithParent("atticExit", false, "areaTransitions") + // Split on blowing up the tube to enter Maridia + s.InsertWithParent("tubeBroken", false, "areaTransitions") + // Split on exiting West Cacattack Alley + s.InsertWithParent("cacExit", false, "areaTransitions") + // Split on entering Toilet Bowl from either direction + s.InsertWithParent("toilet", false, "areaTransitions") + // Split on entering Kronic Boost room + s.InsertWithParent("kronicBoost", false, "areaTransitions") + // Split on the elevator down to Lower Norfair + s.InsertWithParent("lowerNorfairEntrance", false, "areaTransitions") + // Split on entering Worst Room in the Game + s.InsertWithParent("writg", false, "areaTransitions") + // Split on entering Red Kihunter Shaft from either Amphitheatre or Wastelands (NOTE: will split twice) + s.InsertWithParent("redKiShaft", false, "areaTransitions") + // Split on entering Metal Pirates Room from Wasteland + s.InsertWithParent("metalPirates", false, "areaTransitions") + // Split on entering Lower Norfair Springball Maze Room + s.InsertWithParent("lowerNorfairSpringMaze", false, "areaTransitions") + // Split on moving from the Three Musketeers' Room to the Single Chamber + s.InsertWithParent("lowerNorfairExit", false, "areaTransitions") + // Split on entering the Statues Room with all four major bosses defeated + s.InsertWithParent("goldenFour", true, "areaTransitions") + // Split on the elevator down to Tourian + s.InsertWithParent("tourianEntrance", false, "areaTransitions") + // Split on exiting each of the Metroid rooms in Tourian + s.InsertWithParent("metroids", false, "areaTransitions") + // Split on moving from the Dust Torizo Room to the Big Boy Room + s.InsertWithParent("babyMetroidRoom", false, "areaTransitions") + // Split on moving from Tourian Escape Room 4 to The Climb + s.InsertWithParent("escapeClimb", false, "areaTransitions") + + // Split on defeating minibosses + s.Insert("miniBosses", false) + // Split on starting the Ceres Escape + s.InsertWithParent("ceresRidley", false, "miniBosses") + // Split on Bomb Torizo's drops appearing + s.InsertWithParent("bombTorizo", false, "miniBosses") + // Split on the last hit to Spore Spawn + s.InsertWithParent("sporeSpawn", false, "miniBosses") + // Split on Crocomire's drops appearing + s.InsertWithParent("crocomire", false, "miniBosses") + // Split on Botwoon's vertical column being fully destroyed + s.InsertWithParent("botwoon", false, "miniBosses") + // Split on Golden Torizo's drops appearing + s.InsertWithParent("goldenTorizo", false, "miniBosses") + + // Split on defeating major bosses + s.Insert("bosses", true) + // Split shortly after Kraid's drops appear + s.InsertWithParent("kraid", false, "bosses") + // Split on Phantoon's drops appearing + s.InsertWithParent("phantoon", false, "bosses") + // Split on Draygon's drops appearing + s.InsertWithParent("draygon", false, "bosses") + // Split on Ridley's drops appearing + s.InsertWithParent("ridley", true, "bosses") + // Split on Mother Brain's head hitting the ground at the end of the first phase + s.InsertWithParent("mb1", false, "bosses") + // Split on the Baby Metroid detaching from Mother Brain's head + s.InsertWithParent("mb2", true, "bosses") + // Split on the start of the Zebes Escape + s.InsertWithParent("mb3", false, "bosses") + + // Split on facing forward at the end of Zebes Escape + s.Insert("rtaFinish", true) + // Split on In-Game Time finalizing, when the end cutscene starts + s.Insert("igtFinish", false) + // Split on the end of a Spore Spawn RTA run, when the text box clears after collecting the Super Missiles + s.Insert("sporeSpawnRTAFinish", false) + // Split on the end of a 100 Missile RTA run, when the text box clears after collecting the hundredth missile + s.Insert("hundredMissileRTAFinish", false) + s.modifiedAfterCreation = false + return s +} + +func (s *Settings) Insert(name string, value bool) { + s.mu.Lock() + defer s.mu.Unlock() + s.modifiedAfterCreation = true + s.data[name] = struct { + value bool + parent *string + }{value: value, parent: nil} +} + +func (s *Settings) InsertWithParent(name string, value bool, parent string) { + s.mu.Lock() + defer s.mu.Unlock() + s.modifiedAfterCreation = true + p := parent + s.data[name] = struct { + value bool + parent *string + }{value: value, parent: &p} +} + +func (s *Settings) Contains(varName string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + _, ok := s.data[varName] + return ok +} + +func (s *Settings) Get(varName string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.getRecursive(varName) +} + +func (s *Settings) getRecursive(varName string) bool { + entry, ok := s.data[varName] + if !ok { + return false + } + if entry.parent == nil { + return entry.value + } + return entry.value && s.getRecursive(*entry.parent) +} + +func (s *Settings) Set(varName string, value bool) { + s.mu.Lock() + defer s.mu.Unlock() + entry, ok := s.data[varName] + if !ok { + s.data[varName] = struct { + value bool + parent *string + }{value: value, parent: nil} + } else { + s.data[varName] = struct { + value bool + parent *string + }{value: value, parent: entry.parent} + } + s.modifiedAfterCreation = true +} + +func (s *Settings) Roots() []string { + s.mu.RLock() + defer s.mu.RUnlock() + var roots []string + for k, v := range s.data { + if v.parent == nil { + roots = append(roots, k) + } + } + return roots +} + +func (s *Settings) Children(key string) []string { + s.mu.RLock() + defer s.mu.RUnlock() + var children []string + for k, v := range s.data { + if v.parent != nil && *v.parent == key { + children = append(children, k) + } + } + return children +} + +func (s *Settings) Lookup(varName string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + entry, ok := s.data[varName] + if !ok { + panic("variable not found") + } + return entry.value +} + +func (s *Settings) LookupMut(varName string) *bool { + s.mu.Lock() + defer s.mu.Unlock() + entry, ok := s.data[varName] + if !ok { + panic("variable not found") + } + s.modifiedAfterCreation = true + // To mutate the value, we need to update the map entry. + // Return a pointer to the value inside the map by re-assigning. + // Since Go does not allow direct pointer to map values, we simulate with a helper struct. + val := entry.value + // parent := entry.parent + // Create a wrapper struct to hold pointer to value + type boolWrapper struct { + val *bool + } + bw := boolWrapper{val: &val} + // Return pointer to val, but user must call Set to update map. + return bw.val +} + +func (s *Settings) HasBeenModified() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.modifiedAfterCreation +} + +func (s *Settings) SplitOnMiscUpgrades() { + s.Set("miscUpgrades", true) + s.Set("morphBall", true) + s.Set("bomb", true) + s.Set("springBall", true) + s.Set("screwAttack", true) + s.Set("grapple", true) + s.Set("xray", true) +} + +func (s *Settings) SplitOnHundo() { + s.Set("ammoPickups", true) + s.Set("allMissiles", true) + s.Set("allSupers", true) + s.Set("allPowerBombs", true) + s.Set("beamUpgrades", true) + s.Set("chargeBeam", true) + s.Set("spazer", true) + s.Set("wave", true) + s.Set("ice", true) + s.Set("plasma", true) + s.Set("bootUpgrades", true) + s.Set("hiJump", true) + s.Set("spaceJump", true) + s.Set("speedBooster", true) + s.Set("energyUpgrades", true) + s.Set("allETanks", true) + s.Set("reserveTanks", true) + s.SplitOnMiscUpgrades() + s.Set("areaTransitions", true) // should already be true + s.Set("tubeBroken", true) + s.Set("ceresEscape", true) + s.Set("bosses", true) // should already be true + s.Set("kraid", true) + s.Set("phantoon", true) + s.Set("draygon", true) + s.Set("ridley", true) + s.Set("mb1", true) + s.Set("mb2", true) + s.Set("mb3", true) + s.Set("miniBosses", true) + s.Set("ceresRidley", true) + s.Set("bombTorizo", true) + s.Set("crocomire", true) + s.Set("botwoon", true) + s.Set("goldenTorizo", true) + s.Set("babyMetroidRoom", true) +} + +func (s *Settings) SplitOnAnyPercent() { + s.Set("ammoPickups", true) + s.Set("specificMissiles", true) + s.Set("specificSupers", true) + s.Set("wreckedShipLeftSupers", true) + s.Set("specificPowerBombs", true) + s.Set("firstMissile", true) + s.Set("firstSuper", true) + s.Set("firstPowerBomb", true) + s.Set("brinstarMissiles", true) + s.Set("norfairMissiles", true) + s.Set("chargeMissiles", true) + s.Set("waveMissiles", true) + s.Set("beamUpgrades", true) + s.Set("chargeBeam", true) + s.Set("wave", true) + s.Set("ice", true) + s.Set("plasma", true) + s.Set("bootUpgrades", true) + s.Set("hiJump", true) + s.Set("speedBooster", true) + s.Set("specificETanks", true) + s.Set("energyUpgrades", true) + s.Set("terminatorETank", true) + s.Set("hiJumpETank", true) + s.Set("botwoonETank", true) + s.Set("miscUpgrades", true) + s.Set("morphBall", true) + s.Set("spaceJump", true) + s.Set("bomb", true) + s.Set("areaTransitions", true) // should already be true + s.Set("tubeBroken", true) + s.Set("ceresEscape", true) + s.Set("bosses", true) // should already be true + s.Set("kraid", true) + s.Set("phantoon", true) + s.Set("draygon", true) + s.Set("ridley", true) + s.Set("mb1", true) + s.Set("mb2", true) + s.Set("mb3", true) + s.Set("miniBosses", true) + s.Set("ceresRidley", true) + s.Set("bombTorizo", true) + s.Set("botwoon", true) + s.Set("goldenTorizo", true) + s.Set("babyMetroidRoom", true) +} + +// Width enum equivalent +type Width int + +const ( + Byte Width = iota + Word +) + +type MemoryWatcher struct { + address uint32 + current uint32 + old uint32 + width Width +} + +func NewMemoryWatcher(address uint32, width Width) *MemoryWatcher { + return &MemoryWatcher{ + address: address, + current: 0, + old: 0, + width: width, + } +} + +func (mw *MemoryWatcher) UpdateValue(memory []byte) { + mw.old = mw.current + switch mw.width { + case Byte: + mw.current = uint32(memory[mw.address]) + case Word: + addr := mw.address + mw.current = uint32(memory[addr]) | uint32(memory[addr+1])<<8 + } +} + +func split(settings *Settings, snes *SNESState) bool { + firstMissile := settings.Get("firstMissile") && snes.vars["maxMissiles"].old == 0 && snes.vars["maxMissiles"].current == 5 + allMissiles := settings.Get("allMissiles") && (snes.vars["maxMissiles"].old+5) == snes.vars["maxMissiles"].current + oceanBottomMissiles := settings.Get("oceanBottomMissiles") && snes.vars["roomID"].current == roomIDEnum["westOcean"] && (snes.vars["crateriaItems"].old+2) == (snes.vars["crateriaItems"].current) + oceanTopMissiles := settings.Get("oceanTopMissiles") && snes.vars["roomID"].current == roomIDEnum["westOcean"] && (snes.vars["crateriaItems"].old+4) == (snes.vars["crateriaItems"].current) + oceanMiddleMissiles := settings.Get("oceanMiddleMissiles") && snes.vars["roomID"].current == roomIDEnum["westOcean"] && (snes.vars["crateriaItems"].old+8) == (snes.vars["crateriaItems"].current) + moatMissiles := settings.Get("moatMissiles") && snes.vars["roomID"].current == roomIDEnum["crateriaMoat"] && (snes.vars["crateriaItems"].old+16) == (snes.vars["crateriaItems"].current) + oldTourianMissiles := settings.Get("oldTourianMissiles") && snes.vars["roomID"].current == roomIDEnum["pitRoom"] && (snes.vars["crateriaItems"].old+64) == (snes.vars["crateriaItems"].current) + gauntletRightMissiles := settings.Get("gauntletRightMissiles") && snes.vars["roomID"].current == roomIDEnum["greenPirateShaft"] && (snes.vars["brinteriaItems"].old+2) == (snes.vars["brinteriaItems"].current) + gauntletLeftMissiles := settings.Get("gauntletLeftMissiles") && snes.vars["roomID"].current == roomIDEnum["greenPirateShaft"] && (snes.vars["brinteriaItems"].old+4) == (snes.vars["brinteriaItems"].current) + dentalPlan := settings.Get("dentalPlan") && snes.vars["roomID"].current == roomIDEnum["theFinalMissile"] && (snes.vars["brinteriaItems"].old+16) == (snes.vars["brinteriaItems"].current) + earlySuperBridgeMissiles := settings.Get("earlySuperBridgeMissiles") && snes.vars["roomID"].current == roomIDEnum["earlySupers"] && (snes.vars["brinteriaItems"].old+128) == (snes.vars["brinteriaItems"].current) + greenBrinstarReserveMissiles := settings.Get("greenBrinstarReserveMissiles") && snes.vars["roomID"].current == roomIDEnum["brinstarReserveRoom"] && (snes.vars["brinstarItems2"].old+8) == (snes.vars["brinstarItems2"].current) + greenBrinstarExtraReserveMissiles := settings.Get("greenBrinstarExtraReserveMissiles") && snes.vars["roomID"].current == roomIDEnum["brinstarReserveRoom"] && (snes.vars["brinstarItems2"].old+4) == (snes.vars["brinstarItems2"].current) + bigPinkTopMissiles := settings.Get("bigPinkTopMissiles") && snes.vars["roomID"].current == roomIDEnum["bigPink"] && (snes.vars["brinstarItems2"].old+32) == (snes.vars["brinstarItems2"].current) + chargeMissiles := settings.Get("chargeMissiles") && snes.vars["roomID"].current == roomIDEnum["bigPink"] && (snes.vars["brinstarItems2"].old+64) == (snes.vars["brinstarItems2"].current) + greenHillsMissiles := settings.Get("greenHillsMissiles") && snes.vars["roomID"].current == roomIDEnum["greenHills"] && (snes.vars["brinstarItems3"].old+2) == (snes.vars["brinstarItems3"].current) + blueBrinstarETankMissiles := settings.Get("blueBrinstarETankMissiles") && snes.vars["roomID"].current == roomIDEnum["blueBrinstarETankRoom"] && (snes.vars["brinstarItems3"].old+16) == (snes.vars["brinstarItems3"].current) + alphaMissiles := settings.Get("alphaMissiles") && snes.vars["roomID"].current == roomIDEnum["alphaMissileRoom"] && (snes.vars["brinstarItems4"].old+4) == (snes.vars["brinstarItems4"].current) + billyMaysMissiles := settings.Get("billyMaysMissiles") && snes.vars["roomID"].current == roomIDEnum["billyMays"] && (snes.vars["brinstarItems4"].old+16) == (snes.vars["brinstarItems4"].current) + butWaitTheresMoreMissiles := settings.Get("butWaitTheresMoreMissiles") && snes.vars["roomID"].current == roomIDEnum["billyMays"] && (snes.vars["brinstarItems4"].old+32) == (snes.vars["brinstarItems4"].current) + redBrinstarMissiles := settings.Get("redBrinstarMissiles") && snes.vars["roomID"].current == roomIDEnum["alphaPowerBombsRoom"] && (snes.vars["brinstarItems5"].old+2) == (snes.vars["brinstarItems5"].current) + warehouseMissiles := settings.Get("warehouseMissiles") && snes.vars["roomID"].current == roomIDEnum["warehouseKiHunters"] && (snes.vars["brinstarItems5"].old+16) == (snes.vars["brinstarItems5"].current) + cathedralMissiles := settings.Get("cathedralMissiles") && snes.vars["roomID"].current == roomIDEnum["cathedral"] && (snes.vars["norfairItems1"].old+2) == (snes.vars["norfairItems1"].current) + crumbleShaftMissiles := settings.Get("crumbleShaftMissiles") && snes.vars["roomID"].current == roomIDEnum["crumbleShaft"] && (snes.vars["norfairItems1"].old+8) == (snes.vars["norfairItems1"].current) + crocomireEscapeMissiles := settings.Get("crocomireEscapeMissiles") && snes.vars["roomID"].current == roomIDEnum["crocomireEscape"] && (snes.vars["norfairItems1"].old+64) == (snes.vars["norfairItems1"].current) + hiJumpMissiles := settings.Get("hiJumpMissiles") && snes.vars["roomID"].current == roomIDEnum["hiJumpShaft"] && (snes.vars["norfairItems1"].old+128) == (snes.vars["norfairItems1"].current) + postCrocomireMissiles := settings.Get("postCrocomireMissiles") && snes.vars["roomID"].current == roomIDEnum["cosineRoom"] && (snes.vars["norfairItems2"].old+4) == (snes.vars["norfairItems2"].current) + grappleMissiles := settings.Get("grappleMissiles") && snes.vars["roomID"].current == roomIDEnum["preGrapple"] && (snes.vars["norfairItems2"].old+8) == (snes.vars["norfairItems2"].current) + norfairReserveMissiles := settings.Get("norfairReserveMissiles") && snes.vars["roomID"].current == roomIDEnum["norfairReserveRoom"] && (snes.vars["norfairItems2"].old+64) == (snes.vars["norfairItems2"].current) + greenBubblesMissiles := settings.Get("greenBubblesMissiles") && snes.vars["roomID"].current == roomIDEnum["greenBubblesRoom"] && (snes.vars["norfairItems2"].old+128) == (snes.vars["norfairItems2"].current) + bubbleMountainMissiles := settings.Get("bubbleMountainMissiles") && snes.vars["roomID"].current == roomIDEnum["bubbleMountain"] && (snes.vars["norfairItems3"].old+1) == (snes.vars["norfairItems3"].current) + speedBoostMissiles := settings.Get("speedBoostMissiles") && snes.vars["roomID"].current == roomIDEnum["speedBoostHall"] && (snes.vars["norfairItems3"].old+2) == (snes.vars["norfairItems3"].current) + waveMissiles := settings.Get("waveMissiles") && snes.vars["roomID"].current == roomIDEnum["doubleChamber"] && (snes.vars["norfairItems3"].old+8) == (snes.vars["norfairItems3"].current) + goldTorizoMissiles := settings.Get("goldTorizoMissiles") && snes.vars["roomID"].current == roomIDEnum["goldenTorizo"] && (snes.vars["norfairItems3"].old+64) == (snes.vars["norfairItems3"].current) + mickeyMouseMissiles := settings.Get("mickeyMouseMissiles") && snes.vars["roomID"].current == roomIDEnum["mickeyMouse"] && (snes.vars["norfairItems4"].old+2) == (snes.vars["norfairItems4"].current) + lowerNorfairSpringMazeMissiles := settings.Get("lowerNorfairSpringMazeMissiles") && snes.vars["roomID"].current == roomIDEnum["lowerNorfairSpringMaze"] && (snes.vars["norfairItems4"].old+4) == (snes.vars["norfairItems4"].current) + threeMusketeersMissiles := settings.Get("threeMusketeersMissiles") && snes.vars["roomID"].current == roomIDEnum["threeMusketeers"] && (snes.vars["norfairItems4"].old+32) == (snes.vars["norfairItems4"].current) + wreckedShipMainShaftMissiles := settings.Get("wreckedShipMainShaftMissiles") && snes.vars["roomID"].current == roomIDEnum["wreckedShipMainShaft"] && (snes.vars["wreckedShipItems"].old+1) == (snes.vars["wreckedShipItems"].current) + bowlingMissiles := settings.Get("bowlingMissiles") && snes.vars["roomID"].current == roomIDEnum["bowling"] && (snes.vars["wreckedShipItems"].old+4) == (snes.vars["wreckedShipItems"].current) + atticMissiles := settings.Get("atticMissiles") && snes.vars["roomID"].current == roomIDEnum["atticWorkerRobotRoom"] && (snes.vars["wreckedShipItems"].old+8) == (snes.vars["wreckedShipItems"].current) + mainStreetMissiles := settings.Get("mainStreetMissiles") && snes.vars["roomID"].current == roomIDEnum["mainStreet"] && (snes.vars["maridiaItems1"].old+1) == (snes.vars["maridiaItems1"].current) + mamaTurtleMissiles := settings.Get("mamaTurtleMissiles") && snes.vars["roomID"].current == roomIDEnum["mamaTurtle"] && (snes.vars["maridiaItems1"].old+8) == (snes.vars["maridiaItems1"].current) + wateringHoleMissiles := settings.Get("wateringHoleMissiles") && snes.vars["roomID"].current == roomIDEnum["wateringHole"] && (snes.vars["maridiaItems1"].old+32) == (snes.vars["maridiaItems1"].current) + beachMissiles := settings.Get("beachMissiles") && snes.vars["roomID"].current == roomIDEnum["beach"] && (snes.vars["maridiaItems1"].old+64) == (snes.vars["maridiaItems1"].current) + leftSandPitMissiles := settings.Get("leftSandPitMissiles") && snes.vars["roomID"].current == roomIDEnum["leftSandPit"] && (snes.vars["maridiaItems2"].old+1) == (snes.vars["maridiaItems2"].current) + rightSandPitMissiles := settings.Get("rightSandPitMissiles") && snes.vars["roomID"].current == roomIDEnum["rightSandPit"] && (snes.vars["maridiaItems2"].old+4) == (snes.vars["maridiaItems2"].current) + aqueductMissiles := settings.Get("aqueductMissiles") && snes.vars["roomID"].current == roomIDEnum["aqueduct"] && (snes.vars["maridiaItems2"].old+16) == (snes.vars["maridiaItems2"].current) + preDraygonMissiles := settings.Get("preDraygonMissiles") && snes.vars["roomID"].current == roomIDEnum["precious"] && (snes.vars["maridiaItems2"].old+128) == (snes.vars["maridiaItems2"].current) + firstSuper := settings.Get("firstSuper") && snes.vars["maxSupers"].old == 0 && snes.vars["maxSupers"].current == 5 + allSupers := settings.Get("allSupers") && (snes.vars["maxSupers"].old+5) == (snes.vars["maxSupers"].current) + climbSupers := settings.Get("climbSupers") && snes.vars["roomID"].current == roomIDEnum["crateriaSupersRoom"] && (snes.vars["brinteriaItems"].old+8) == (snes.vars["brinteriaItems"].current) + sporeSpawnSupers := settings.Get("sporeSpawnSupers") && snes.vars["roomID"].current == roomIDEnum["sporeSpawnSuper"] && (snes.vars["brinteriaItems"].old+64) == (snes.vars["brinteriaItems"].current) + earlySupers := settings.Get("earlySupers") && snes.vars["roomID"].current == roomIDEnum["earlySupers"] && (snes.vars["brinstarItems2"].old+1) == (snes.vars["brinstarItems2"].current) + etecoonSupers := (settings.Get("etecoonSupers") || settings.Get("etacoonSupers")) && snes.vars["roomID"].current == roomIDEnum["etecoonSuperRoom"] && (snes.vars["brinstarItems3"].old+128) == (snes.vars["brinstarItems3"].current) + goldTorizoSupers := settings.Get("goldTorizoSupers") && snes.vars["roomID"].current == roomIDEnum["goldenTorizo"] && (snes.vars["norfairItems3"].old+128) == (snes.vars["norfairItems3"].current) + wreckedShipLeftSupers := settings.Get("wreckedShipLeftSupers") && snes.vars["roomID"].current == roomIDEnum["wreckedShipLeftSuperRoom"] && (snes.vars["wreckedShipItems"].old+32) == (snes.vars["wreckedShipItems"].current) + wreckedShipRightSupers := settings.Get("wreckedShipRightSupers") && snes.vars["roomID"].current == roomIDEnum["wreckedShipRightSuperRoom"] && (snes.vars["wreckedShipItems"].old+64) == (snes.vars["wreckedShipItems"].current) + crabSupers := settings.Get("crabSupers") && snes.vars["roomID"].current == roomIDEnum["mainStreet"] && (snes.vars["maridiaItems1"].old+2) == (snes.vars["maridiaItems1"].current) + wateringHoleSupers := settings.Get("wateringHoleSupers") && snes.vars["roomID"].current == roomIDEnum["wateringHole"] && (snes.vars["maridiaItems1"].old+16) == (snes.vars["maridiaItems1"].current) + aqueductSupers := settings.Get("aqueductSupers") && snes.vars["roomID"].current == roomIDEnum["aqueduct"] && (snes.vars["maridiaItems2"].old+32) == (snes.vars["maridiaItems2"].current) + firstPowerBomb := settings.Get("firstPowerBomb") && snes.vars["maxPowerBombs"].old == 0 && snes.vars["maxPowerBombs"].current == 5 + allPowerBombs := settings.Get("allPowerBombs") && (snes.vars["maxPowerBombs"].old+5) == (snes.vars["maxPowerBombs"].current) + landingSiteBombs := settings.Get("landingSiteBombs") && snes.vars["roomID"].current == roomIDEnum["crateriaPowerBombRoom"] && (snes.vars["crateriaItems"].old+1) == (snes.vars["crateriaItems"].current) + etecoonBombs := (settings.Get("etecoonBombs") || settings.Get("etacoonBombs")) && snes.vars["roomID"].current == roomIDEnum["greenBrinstarMainShaft"] && (snes.vars["brinteriaItems"].old+32) == (snes.vars["brinteriaItems"].current) + pinkBrinstarBombs := settings.Get("pinkBrinstarBombs") && snes.vars["roomID"].current == roomIDEnum["pinkBrinstarPowerBombRoom"] && (snes.vars["brinstarItems3"].old+1) == (snes.vars["brinstarItems3"].current) + blueBrinstarBombs := settings.Get("blueBrinstarBombs") && snes.vars["roomID"].current == roomIDEnum["morphBall"] && (snes.vars["brinstarItems3"].old+8) == (snes.vars["brinstarItems3"].current) + alphaBombs := settings.Get("alphaBombs") && snes.vars["roomID"].current == roomIDEnum["alphaPowerBombsRoom"] && (snes.vars["brinstarItems5"].old+1) == (snes.vars["brinstarItems5"].current) + betaBombs := settings.Get("betaBombs") && snes.vars["roomID"].current == roomIDEnum["betaPowerBombRoom"] && (snes.vars["brinstarItems4"].old+128) == (snes.vars["brinstarItems4"].current) + crocomireBombs := settings.Get("crocomireBombs") && snes.vars["roomID"].current == roomIDEnum["postCrocomirePowerBombRoom"] && (snes.vars["norfairItems2"].old+2) == (snes.vars["norfairItems2"].current) + lowerNorfairEscapeBombs := settings.Get("lowerNorfairEscapeBombs") && snes.vars["roomID"].current == roomIDEnum["lowerNorfairEscapePowerBombRoom"] && (snes.vars["norfairItems4"].old+8) == (snes.vars["norfairItems4"].current) + shameBombs := settings.Get("shameBombs") && snes.vars["roomID"].current == roomIDEnum["wasteland"] && (snes.vars["norfairItems4"].old+16) == (snes.vars["norfairItems4"].current) + rightSandPitBombs := settings.Get("rightSandPitBombs") && snes.vars["roomID"].current == roomIDEnum["rightSandPit"] && (snes.vars["maridiaItems2"].old+8) == (snes.vars["maridiaItems2"].current) + pickup := firstMissile || allMissiles || oceanBottomMissiles || oceanTopMissiles || oceanMiddleMissiles || moatMissiles || oldTourianMissiles || gauntletRightMissiles || gauntletLeftMissiles || dentalPlan || earlySuperBridgeMissiles || greenBrinstarReserveMissiles || greenBrinstarExtraReserveMissiles || bigPinkTopMissiles || chargeMissiles || greenHillsMissiles || blueBrinstarETankMissiles || alphaMissiles || billyMaysMissiles || butWaitTheresMoreMissiles || redBrinstarMissiles || warehouseMissiles || cathedralMissiles || crumbleShaftMissiles || crocomireEscapeMissiles || hiJumpMissiles || postCrocomireMissiles || grappleMissiles || norfairReserveMissiles || greenBubblesMissiles || bubbleMountainMissiles || speedBoostMissiles || waveMissiles || goldTorizoMissiles || mickeyMouseMissiles || lowerNorfairSpringMazeMissiles || threeMusketeersMissiles || wreckedShipMainShaftMissiles || bowlingMissiles || atticMissiles || mainStreetMissiles || mamaTurtleMissiles || wateringHoleMissiles || beachMissiles || leftSandPitMissiles || rightSandPitMissiles || aqueductMissiles || preDraygonMissiles || firstSuper || allSupers || climbSupers || sporeSpawnSupers || earlySupers || etecoonSupers || goldTorizoSupers || wreckedShipLeftSupers || wreckedShipRightSupers || crabSupers || wateringHoleSupers || aqueductSupers || firstPowerBomb || allPowerBombs || landingSiteBombs || etecoonBombs || pinkBrinstarBombs || blueBrinstarBombs || alphaBombs || betaBombs || crocomireBombs || lowerNorfairEscapeBombs || shameBombs || rightSandPitBombs + + // Item unlock section + varia := settings.Get("variaSuit") && snes.vars["roomID"].current == roomIDEnum["varia"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["variaSuit"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["variaSuit"]) > 0 + springBall := settings.Get("springBall") && snes.vars["roomID"].current == roomIDEnum["springBall"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["springBall"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["springBall"]) > 0 + morphBall := settings.Get("morphBall") && snes.vars["roomID"].current == roomIDEnum["morphBall"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["morphBall"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["morphBall"]) > 0 + screwAttack := settings.Get("screwAttack") && snes.vars["roomID"].current == roomIDEnum["screwAttack"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["screwAttack"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["screwAttack"]) > 0 + gravSuit := settings.Get("gravSuit") && snes.vars["roomID"].current == roomIDEnum["gravity"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["gravSuit"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["gravSuit"]) > 0 + hiJump := settings.Get("hiJump") && snes.vars["roomID"].current == roomIDEnum["hiJump"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["hiJump"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["hiJump"]) > 0 + spaceJump := settings.Get("spaceJump") && snes.vars["roomID"].current == roomIDEnum["spaceJump"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["spaceJump"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["spaceJump"]) > 0 + bomb := settings.Get("bomb") && snes.vars["roomID"].current == roomIDEnum["bombTorizo"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["bomb"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["bomb"]) > 0 + speedBooster := settings.Get("speedBooster") && snes.vars["roomID"].current == roomIDEnum["speedBooster"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["speedBooster"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["speedBooster"]) > 0 + grapple := settings.Get("grapple") && snes.vars["roomID"].current == roomIDEnum["grapple"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["grapple"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["grapple"]) > 0 + xray := settings.Get("xray") && snes.vars["roomID"].current == roomIDEnum["xRay"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["xray"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["xray"]) > 0 + unlock := varia || springBall || morphBall || screwAttack || gravSuit || hiJump || spaceJump || bomb || speedBooster || grapple || xray + + // Beam unlock section + wave := settings.Get("wave") && snes.vars["roomID"].current == roomIDEnum["waveBeam"] && (snes.vars["unlockedBeams"].old&unlockFlagEnum["wave"]) == 0 && (snes.vars["unlockedBeams"].current&unlockFlagEnum["wave"]) > 0 + ice := settings.Get("ice") && snes.vars["roomID"].current == roomIDEnum["iceBeam"] && (snes.vars["unlockedBeams"].old&unlockFlagEnum["ice"]) == 0 && (snes.vars["unlockedBeams"].current&unlockFlagEnum["ice"]) > 0 + spazer := settings.Get("spazer") && snes.vars["roomID"].current == roomIDEnum["spazer"] && (snes.vars["unlockedBeams"].old&unlockFlagEnum["spazer"]) == 0 && (snes.vars["unlockedBeams"].current&unlockFlagEnum["spazer"]) > 0 + plasma := settings.Get("plasma") && snes.vars["roomID"].current == roomIDEnum["plasmaBeam"] && (snes.vars["unlockedBeams"].old&unlockFlagEnum["plasma"]) == 0 && (snes.vars["unlockedBeams"].current&unlockFlagEnum["plasma"]) > 0 + chargeBeam := settings.Get("chargeBeam") && snes.vars["roomID"].current == roomIDEnum["bigPink"] && (snes.vars["unlockedCharge"].old&unlockFlagEnum["chargeBeam"]) == 0 && (snes.vars["unlockedCharge"].current&unlockFlagEnum["chargeBeam"]) > 0 + beam := wave || ice || spazer || plasma || chargeBeam + + // E-tanks and reserve tanks + firstETank := settings.Get("firstETank") && snes.vars["maxEnergy"].old == 99 && snes.vars["maxEnergy"].current == 199 + allETanks := settings.Get("allETanks") && (snes.vars["maxEnergy"].old+100) == (snes.vars["maxEnergy"].current) + gauntletETank := settings.Get("gauntletETank") && snes.vars["roomID"].current == roomIDEnum["gauntletETankRoom"] && (snes.vars["crateriaItems"].old+32) == (snes.vars["crateriaItems"].current) + terminatorETank := settings.Get("terminatorETank") && snes.vars["roomID"].current == roomIDEnum["terminator"] && (snes.vars["brinteriaItems"].old+1) == (snes.vars["brinteriaItems"].current) + ceilingETank := settings.Get("ceilingETank") && snes.vars["roomID"].current == roomIDEnum["blueBrinstarETankRoom"] && (snes.vars["brinstarItems3"].old+32) == (snes.vars["brinstarItems3"].current) + etecoonsETank := (settings.Get("etecoonsETank") || settings.Get("etacoonsETank")) && snes.vars["roomID"].current == roomIDEnum["etecoonETankRoom"] && (snes.vars["brinstarItems3"].old+64) == (snes.vars["brinstarItems3"].current) + waterwayETank := settings.Get("waterwayETank") && snes.vars["roomID"].current == roomIDEnum["waterway"] && (snes.vars["brinstarItems4"].old+2) == (snes.vars["brinstarItems4"].current) + waveGateETank := settings.Get("waveGateETank") && snes.vars["roomID"].current == roomIDEnum["hopperETankRoom"] && (snes.vars["brinstarItems4"].old+8) == (snes.vars["brinstarItems4"].current) + kraidETank := settings.Get("kraidETank") && snes.vars["roomID"].current == roomIDEnum["warehouseETankRoom"] && (snes.vars["brinstarItems5"].old+8) == (snes.vars["brinstarItems5"].current) + crocomireETank := settings.Get("crocomireETank") && snes.vars["roomID"].current == roomIDEnum["crocomire"] && (snes.vars["norfairItems1"].old+16) == (snes.vars["norfairItems1"].current) + hiJumpETank := settings.Get("hiJumpETank") && snes.vars["roomID"].current == roomIDEnum["hiJumpShaft"] && (snes.vars["norfairItems2"].old+1) == (snes.vars["norfairItems2"].current) + ridleyETank := settings.Get("ridleyETank") && snes.vars["roomID"].current == roomIDEnum["ridleyETankRoom"] && (snes.vars["norfairItems4"].old+64) == (snes.vars["norfairItems4"].current) + firefleaETank := settings.Get("firefleaETank") && snes.vars["roomID"].current == roomIDEnum["lowerNorfairFireflea"] && (snes.vars["norfairItems5"].old+1) == (snes.vars["norfairItems5"].current) + wreckedShipETank := settings.Get("wreckedShipETank") && snes.vars["roomID"].current == roomIDEnum["wreckedShipETankRoom"] && (snes.vars["wreckedShipItems"].old+16) == (snes.vars["wreckedShipItems"].current) + tatoriETank := settings.Get("tatoriETank") && snes.vars["roomID"].current == roomIDEnum["mamaTurtle"] && (snes.vars["maridiaItems1"].old+4) == (snes.vars["maridiaItems1"].current) + botwoonETank := settings.Get("botwoonETank") && snes.vars["roomID"].current == roomIDEnum["botwoonETankRoom"] && (snes.vars["maridiaItems3"].old+1) == (snes.vars["maridiaItems3"].current) + reserveTanks := settings.Get("reserveTanks") && (snes.vars["maxReserve"].old+100) == (snes.vars["maxReserve"].current) + brinstarReserve := settings.Get("brinstarReserve") && snes.vars["roomID"].current == roomIDEnum["brinstarReserveRoom"] && (snes.vars["brinstarItems2"].old+2) == (snes.vars["brinstarItems2"].current) + norfairReserve := settings.Get("norfairReserve") && snes.vars["roomID"].current == roomIDEnum["norfairReserveRoom"] && (snes.vars["norfairItems2"].old+32) == (snes.vars["norfairItems2"].current) + wreckedShipReserve := settings.Get("wreckedShipReserve") && snes.vars["roomID"].current == roomIDEnum["bowling"] && (snes.vars["wreckedShipItems"].old+2) == (snes.vars["wreckedShipItems"].current) + maridiaReserve := settings.Get("maridiaReserve") && snes.vars["roomID"].current == roomIDEnum["leftSandPit"] && (snes.vars["maridiaItems2"].old+2) == (snes.vars["maridiaItems2"].current) + energyUpgrade := firstETank || allETanks || gauntletETank || terminatorETank || ceilingETank || etecoonsETank || waterwayETank || waveGateETank || kraidETank || crocomireETank || hiJumpETank || ridleyETank || firefleaETank || wreckedShipETank || tatoriETank || botwoonETank || reserveTanks || brinstarReserve || norfairReserve || wreckedShipReserve || maridiaReserve + + // Miniboss room transitions + miniBossRooms := false + if settings.Get("miniBossRooms") { + ceresRidleyRoom := snes.vars["roomID"].old == roomIDEnum["flatRoom"] && snes.vars["roomID"].current == roomIDEnum["ceresRidley"] + sporeSpawnRoom := snes.vars["roomID"].old == roomIDEnum["sporeSpawnKeyhunter"] && snes.vars["roomID"].current == roomIDEnum["sporeSpawn"] + crocomireRoom := snes.vars["roomID"].old == roomIDEnum["crocomireSpeedway"] && snes.vars["roomID"].current == roomIDEnum["crocomire"] + botwoonRoom := snes.vars["roomID"].old == roomIDEnum["botwoonHallway"] && snes.vars["roomID"].current == roomIDEnum["botwoon"] + // Allow either vanilla or GGG entry + goldenTorizoRoom := (snes.vars["roomID"].old == roomIDEnum["acidStatue"] || snes.vars["roomID"].old == roomIDEnum["screwAttack"]) && snes.vars["roomID"].current == roomIDEnum["goldenTorizo"] + miniBossRooms = ceresRidleyRoom || sporeSpawnRoom || crocomireRoom || botwoonRoom || goldenTorizoRoom + } + + // Boss room transitions + bossRooms := false + if settings.Get("bossRooms") { + kraidRoom := snes.vars["roomID"].old == roomIDEnum["kraidEyeDoor"] && snes.vars["roomID"].current == roomIDEnum["kraid"] + phantoonRoom := snes.vars["roomID"].old == roomIDEnum["basement"] && snes.vars["roomID"].current == roomIDEnum["phantoon"] + draygonRoom := snes.vars["roomID"].old == roomIDEnum["precious"] && snes.vars["roomID"].current == roomIDEnum["draygon"] + ridleyRoom := snes.vars["roomID"].old == roomIDEnum["lowerNorfairFarming"] && snes.vars["roomID"].current == roomIDEnum["ridley"] + motherBrainRoom := snes.vars["roomID"].old == roomIDEnum["rinkaShaft"] && snes.vars["roomID"].current == roomIDEnum["motherBrain"] + bossRooms = kraidRoom || phantoonRoom || draygonRoom || ridleyRoom || motherBrainRoom + } + + // Elevator transitions between areas + elevatorTransitions := false + if settings.Get("elevatorTransitions") { + blueBrinstar := (snes.vars["roomID"].old == roomIDEnum["elevatorToMorphBall"] && snes.vars["roomID"].current == roomIDEnum["morphBall"]) || (snes.vars["roomID"].old == roomIDEnum["morphBall"] && snes.vars["roomID"].current == roomIDEnum["elevatorToMorphBall"]) + greenBrinstar := (snes.vars["roomID"].old == roomIDEnum["elevatorToGreenBrinstar"] && snes.vars["roomID"].current == roomIDEnum["greenBrinstarMainShaft"]) || (snes.vars["roomID"].old == roomIDEnum["greenBrinstarMainShaft"] && snes.vars["roomID"].current == roomIDEnum["elevatorToGreenBrinstar"]) + businessCenter := (snes.vars["roomID"].old == roomIDEnum["warehouseEntrance"] && snes.vars["roomID"].current == roomIDEnum["businessCenter"]) || (snes.vars["roomID"].old == roomIDEnum["businessCenter"] && snes.vars["roomID"].current == roomIDEnum["warehouseEntrance"]) + caterpillar := (snes.vars["roomID"].old == roomIDEnum["elevatorToCaterpillar"] && snes.vars["roomID"].current == roomIDEnum["caterpillar"]) || (snes.vars["roomID"].old == roomIDEnum["caterpillar"] && snes.vars["roomID"].current == roomIDEnum["elevatorToCaterpillar"]) + maridiaElevator := (snes.vars["roomID"].old == roomIDEnum["elevatorToMaridia"] && snes.vars["roomID"].current == roomIDEnum["maridiaElevator"]) || (snes.vars["roomID"].old == roomIDEnum["maridiaElevator"] && snes.vars["roomID"].current == roomIDEnum["elevatorToMaridia"]) + elevatorTransitions = blueBrinstar || greenBrinstar || businessCenter || caterpillar || maridiaElevator + } + + // Room transitions + ceresEscape := settings.Get("ceresEscape") && snes.vars["roomID"].current == roomIDEnum["ceresElevator"] && snes.vars["gameState"].old == gameStateEnum["normalGameplay"] && snes.vars["gameState"].current == gameStateEnum["startOfCeresCutscene"] + wreckedShipEntrance := settings.Get("wreckedShipEntrance") && snes.vars["roomID"].old == roomIDEnum["westOcean"] && snes.vars["roomID"].current == roomIDEnum["wreckedShipEntrance"] + redTowerMiddleEntrance := settings.Get("redTowerMiddleEntrance") && snes.vars["roomID"].old == roomIDEnum["noobBridge"] && snes.vars["roomID"].current == roomIDEnum["redTower"] + redTowerBottomEntrance := settings.Get("redTowerBottomEntrance") && snes.vars["roomID"].old == roomIDEnum["bat"] && snes.vars["roomID"].current == roomIDEnum["redTower"] + kraidsLair := settings.Get("kraidsLair") && snes.vars["roomID"].old == roomIDEnum["warehouseEntrance"] && snes.vars["roomID"].current == roomIDEnum["warehouseZeela"] + risingTideEntrance := settings.Get("risingTideEntrance") && snes.vars["roomID"].old == roomIDEnum["cathedral"] && snes.vars["roomID"].current == roomIDEnum["risingTide"] + atticExit := settings.Get("atticExit") && snes.vars["roomID"].old == roomIDEnum["attic"] && snes.vars["roomID"].current == roomIDEnum["westOcean"] + tubeBroken := settings.Get("tubeBroken") && snes.vars["roomID"].current == roomIDEnum["glassTunnel"] && (snes.vars["eventFlags"].old&eventFlagEnum["tubeBroken"]) == 0 && (snes.vars["eventFlags"].current&eventFlagEnum["tubeBroken"]) > 0 + cacExit := settings.Get("cacExit") && snes.vars["roomID"].old == roomIDEnum["westCactusAlley"] && snes.vars["roomID"].current == roomIDEnum["butterflyRoom"] + toilet := settings.Get("toilet") && (snes.vars["roomID"].old == roomIDEnum["plasmaSpark"] && snes.vars["roomID"].current == roomIDEnum["toiletBowl"] || snes.vars["roomID"].old == roomIDEnum["oasis"] && snes.vars["roomID"].current == roomIDEnum["toiletBowl"]) + kronicBoost := settings.Get("kronicBoost") && (snes.vars["roomID"].old == roomIDEnum["magdolliteTunnel"] && snes.vars["roomID"].current == roomIDEnum["kronicBoost"] || snes.vars["roomID"].old == roomIDEnum["spikyAcidSnakes"] && snes.vars["roomID"].current == roomIDEnum["kronicBoost"] || snes.vars["roomID"].old == roomIDEnum["volcano"] && snes.vars["roomID"].current == roomIDEnum["kronicBoost"]) + lowerNorfairEntrance := settings.Get("lowerNorfairEntrance") && snes.vars["roomID"].old == roomIDEnum["lowerNorfairElevator"] && snes.vars["roomID"].current == roomIDEnum["mainHall"] + writg := settings.Get("writg") && snes.vars["roomID"].old == roomIDEnum["pillars"] && snes.vars["roomID"].current == roomIDEnum["writg"] + redKiShaft := settings.Get("redKiShaft") && (snes.vars["roomID"].old == roomIDEnum["amphitheatre"] && snes.vars["roomID"].current == roomIDEnum["redKiShaft"] || snes.vars["roomID"].old == roomIDEnum["wasteland"] && snes.vars["roomID"].current == roomIDEnum["redKiShaft"]) + metalPirates := settings.Get("metalPirates") && snes.vars["roomID"].old == roomIDEnum["wasteland"] && snes.vars["roomID"].current == roomIDEnum["metalPirates"] + lowerNorfairSpringMaze := settings.Get("lowerNorfairSpringMaze") && snes.vars["roomID"].old == roomIDEnum["lowerNorfairFireflea"] && snes.vars["roomID"].current == roomIDEnum["lowerNorfairSpringMaze"] + lowerNorfairExit := settings.Get("lowerNorfairExit") && snes.vars["roomID"].old == roomIDEnum["threeMusketeers"] && snes.vars["roomID"].current == roomIDEnum["singleChamber"] + allBossesFinished := (snes.vars["brinstarBosses"].current&bossFlagEnum["kraid"]) > 0 && (snes.vars["wreckedShipBosses"].current&bossFlagEnum["phantoon"]) > 0 && (snes.vars["maridiaBosses"].current&bossFlagEnum["draygon"]) > 0 && (snes.vars["norfairBosses"].current&bossFlagEnum["ridley"]) > 0 + goldenFour := settings.Get("goldenFour") && snes.vars["roomID"].old == roomIDEnum["statuesHallway"] && snes.vars["roomID"].current == roomIDEnum["statues"] && allBossesFinished + tourianEntrance := settings.Get("tourianEntrance") && snes.vars["roomID"].old == roomIDEnum["statues"] && snes.vars["roomID"].current == roomIDEnum["tourianElevator"] + metroids := settings.Get("metroids") && (snes.vars["roomID"].old == roomIDEnum["metroidOne"] && snes.vars["roomID"].current == roomIDEnum["metroidTwo"] || snes.vars["roomID"].old == roomIDEnum["metroidTwo"] && snes.vars["roomID"].current == roomIDEnum["metroidThree"] || snes.vars["roomID"].old == roomIDEnum["metroidThree"] && snes.vars["roomID"].current == roomIDEnum["metroidFour"] || snes.vars["roomID"].old == roomIDEnum["metroidFour"] && snes.vars["roomID"].current == roomIDEnum["tourianHopper"]) + babyMetroidRoom := settings.Get("babyMetroidRoom") && snes.vars["roomID"].old == roomIDEnum["dustTorizo"] && snes.vars["roomID"].current == roomIDEnum["bigBoy"] + escapeClimb := settings.Get("escapeClimb") && snes.vars["roomID"].old == roomIDEnum["tourianEscape4"] && snes.vars["roomID"].current == roomIDEnum["climb"] + roomTransitions := miniBossRooms || bossRooms || elevatorTransitions || ceresEscape || wreckedShipEntrance || redTowerMiddleEntrance || redTowerBottomEntrance || kraidsLair || risingTideEntrance || atticExit || tubeBroken || cacExit || toilet || kronicBoost || lowerNorfairEntrance || writg || redKiShaft || metalPirates || lowerNorfairSpringMaze || lowerNorfairExit || tourianEntrance || goldenFour || metroids || babyMetroidRoom || escapeClimb + + // Minibosses + ceresRidley := settings.Get("ceresRidley") && (snes.vars["ceresBosses"].old&bossFlagEnum["ceresRidley"]) == 0 && (snes.vars["ceresBosses"].current&bossFlagEnum["ceresRidley"]) > 0 && snes.vars["roomID"].current == roomIDEnum["ceresRidley"] + bombTorizo := settings.Get("bombTorizo") && (snes.vars["crateriaBosses"].old&bossFlagEnum["bombTorizo"]) == 0 && (snes.vars["crateriaBosses"].current&bossFlagEnum["bombTorizo"]) > 0 && snes.vars["roomID"].current == roomIDEnum["bombTorizo"] + sporeSpawn := settings.Get("sporeSpawn") && (snes.vars["brinstarBosses"].old&bossFlagEnum["sporeSpawn"]) == 0 && (snes.vars["brinstarBosses"].current&bossFlagEnum["sporeSpawn"]) > 0 && snes.vars["roomID"].current == roomIDEnum["sporeSpawn"] + crocomire := settings.Get("crocomire") && (snes.vars["norfairBosses"].old&bossFlagEnum["crocomire"]) == 0 && (snes.vars["norfairBosses"].current&bossFlagEnum["crocomire"]) > 0 && snes.vars["roomID"].current == roomIDEnum["crocomire"] + botwoon := settings.Get("botwoon") && (snes.vars["maridiaBosses"].old&bossFlagEnum["botwoon"]) == 0 && (snes.vars["maridiaBosses"].current&bossFlagEnum["botwoon"]) > 0 && snes.vars["roomID"].current == roomIDEnum["botwoon"] + goldenTorizo := settings.Get("goldenTorizo") && (snes.vars["norfairBosses"].old&bossFlagEnum["goldenTorizo"]) == 0 && (snes.vars["norfairBosses"].current&bossFlagEnum["goldenTorizo"]) > 0 && snes.vars["roomID"].current == roomIDEnum["goldenTorizo"] + minibossDefeat := ceresRidley || bombTorizo || sporeSpawn || crocomire || botwoon || goldenTorizo + + // Bosses + kraid := settings.Get("kraid") && (snes.vars["brinstarBosses"].old&bossFlagEnum["kraid"]) == 0 && (snes.vars["brinstarBosses"].current&bossFlagEnum["kraid"]) > 0 && snes.vars["roomID"].current == roomIDEnum["kraid"] + if kraid { + fmt.Println("Split due to kraid defeat") + } + phantoon := settings.Get("phantoon") && (snes.vars["wreckedShipBosses"].old&bossFlagEnum["phantoon"]) == 0 && (snes.vars["wreckedShipBosses"].current&bossFlagEnum["phantoon"]) > 0 && snes.vars["roomID"].current == roomIDEnum["phantoon"] + if phantoon { + fmt.Println("Split due to phantoon defeat") + } + draygon := settings.Get("draygon") && (snes.vars["maridiaBosses"].old&bossFlagEnum["draygon"]) == 0 && (snes.vars["maridiaBosses"].current&bossFlagEnum["draygon"]) > 0 && snes.vars["roomID"].current == roomIDEnum["draygon"] + if draygon { + fmt.Println("Split due to draygon defeat") + } + ridley := settings.Get("ridley") && (snes.vars["norfairBosses"].old&bossFlagEnum["ridley"]) == 0 && (snes.vars["norfairBosses"].current&bossFlagEnum["ridley"]) > 0 && snes.vars["roomID"].current == roomIDEnum["ridley"] + if ridley { + fmt.Println("Split due to ridley defeat") + } + // Mother Brain phases + inMotherBrainRoom := snes.vars["roomID"].current == roomIDEnum["motherBrain"] + mb1 := settings.Get("mb1") && inMotherBrainRoom && snes.vars["gameState"].current == gameStateEnum["normalGameplay"] && snes.vars["motherBrainHP"].old == 0 && snes.vars["motherBrainHP"].current == (motherBrainMaxHPEnum["phase2"]) + if mb1 { + fmt.Println("Split due to mb1 defeat") + } + mb2 := settings.Get("mb2") && inMotherBrainRoom && snes.vars["gameState"].current == gameStateEnum["normalGameplay"] && snes.vars["motherBrainHP"].old == 0 && snes.vars["motherBrainHP"].current == (motherBrainMaxHPEnum["phase3"]) + if mb2 { + fmt.Println("Split due to mb2 defeat") + } + mb3 := settings.Get("mb3") && inMotherBrainRoom && (snes.vars["tourianBosses"].old&bossFlagEnum["motherBrain"]) == 0 && (snes.vars["tourianBosses"].current&bossFlagEnum["motherBrain"]) > 0 + if mb3 { + fmt.Println("Split due to mb3 defeat") + } + bossDefeat := kraid || phantoon || draygon || ridley || mb1 || mb2 || mb3 + + // Run-ending splits + escape := settings.Get("rtaFinish") && (snes.vars["eventFlags"].current&eventFlagEnum["zebesAblaze"]) > 0 && snes.vars["shipAI"].old != 0xaa4f && snes.vars["shipAI"].current == 0xaa4f + + takeoff := settings.Get("igtFinish") && snes.vars["roomID"].current == roomIDEnum["landingSite"] && snes.vars["gameState"].old == gameStateEnum["preEndCutscene"] && snes.vars["gameState"].current == gameStateEnum["endCutscene"] + + sporeSpawnRTAFinish := false + if settings.Get("sporeSpawnRTAFinish") { + if snes.pickedUpSporeSpawnSuper { + if snes.vars["igtFrames"].old != snes.vars["igtFrames"].current { + sporeSpawnRTAFinish = true + snes.pickedUpSporeSpawnSuper = false + } + } else { + snes.pickedUpSporeSpawnSuper = snes.vars["roomID"].current == roomIDEnum["sporeSpawnSuper"] && (snes.vars["maxSupers"].old+5) == (snes.vars["maxSupers"].current) && (snes.vars["brinstarBosses"].current&bossFlagEnum["sporeSpawn"]) > 0 + } + } + + hundredMissileRTAFinish := false + if settings.Get("hundredMissileRTAFinish") { + if snes.pickedUpHundredthMissile { + if snes.vars["igtFrames"].old != snes.vars["igtFrames"].current { + hundredMissileRTAFinish = true + snes.pickedUpHundredthMissile = false + } + } else { + snes.pickedUpHundredthMissile = snes.vars["maxMissiles"].old == 95 && snes.vars["maxMissiles"].current == 100 + } + } + + nonStandardCategoryFinish := sporeSpawnRTAFinish || hundredMissileRTAFinish + + if pickup { + fmt.Println("Split due to pickup") + } + + if unlock { + fmt.Println("Split due to unlock") + } + + if beam { + fmt.Println("Split due to beam upgrade") + } + + if energyUpgrade { + fmt.Println("Split due to energy upgrade") + } + + if roomTransitions { + fmt.Println("Split due to room transition") + } + + if minibossDefeat { + fmt.Println("Split due to miniboss defeat") + } + + // individual boss defeat conditions already covered above + if escape { + fmt.Println("Split due to escape") + } + + if takeoff { + fmt.Println("Split due to takeoff") + } + + if nonStandardCategoryFinish { + fmt.Println("Split due to non standard category finish") + } + + return pickup || unlock || beam || energyUpgrade || roomTransitions || minibossDefeat || bossDefeat || escape || takeoff || nonStandardCategoryFinish +} + +const NUM_LATENCY_SAMPLES = 10 + +type SNESState struct { + vars map[string]*MemoryWatcher + pickedUpHundredthMissile bool + pickedUpSporeSpawnSuper bool + latencySamples []uint128 + data []byte + doExtraUpdate bool + mu sync.Mutex +} + +type uint128 struct { + hi uint64 + lo uint64 +} + +func (a uint128) Add(b uint128) uint128 { + lo := a.lo + b.lo + hi := a.hi + b.hi + if lo < a.lo { + hi++ + } + return uint128{hi: hi, lo: lo} +} + +func (a uint128) Sub(b uint128) uint128 { + lo := a.lo - b.lo + hi := a.hi - b.hi + if a.lo < b.lo { + hi-- + } + return uint128{hi: hi, lo: lo} +} + +func (a uint128) ToFloat64() float64 { + return float64(a.hi)*math.Pow(2, 64) + float64(a.lo) +} + +func NewSNESState() *SNESState { + data := make([]byte, 0x10000) + vars := map[string]*MemoryWatcher{ + // Word + "controller": NewMemoryWatcher(0x008B, Word), + "roomID": NewMemoryWatcher(0x079B, Word), + "enemyHP": NewMemoryWatcher(0x0F8C, Word), + "shipAI": NewMemoryWatcher(0x0FB2, Word), + "motherBrainHP": NewMemoryWatcher(0x0FCC, Word), + // Byte + "mapInUse": NewMemoryWatcher(0x079F, Byte), + "gameState": NewMemoryWatcher(0x0998, Byte), + "unlockedEquips2": NewMemoryWatcher(0x09A4, Byte), + "unlockedEquips": NewMemoryWatcher(0x09A5, Byte), + "unlockedBeams": NewMemoryWatcher(0x09A8, Byte), + "unlockedCharge": NewMemoryWatcher(0x09A9, Byte), + "maxEnergy": NewMemoryWatcher(0x09C4, Word), + "maxMissiles": NewMemoryWatcher(0x09C8, Byte), + "maxSupers": NewMemoryWatcher(0x09CC, Byte), + "maxPowerBombs": NewMemoryWatcher(0x09D0, Byte), + "maxReserve": NewMemoryWatcher(0x09D4, Word), + "igtFrames": NewMemoryWatcher(0x09DA, Byte), + "igtSeconds": NewMemoryWatcher(0x09DC, Byte), + "igtMinutes": NewMemoryWatcher(0x09DE, Byte), + "igtHours": NewMemoryWatcher(0x09E0, Byte), + "playerState": NewMemoryWatcher(0x0A28, Byte), + "eventFlags": NewMemoryWatcher(0xD821, Byte), + "crateriaBosses": NewMemoryWatcher(0xD828, Byte), + "brinstarBosses": NewMemoryWatcher(0xD829, Byte), + "norfairBosses": NewMemoryWatcher(0xD82A, Byte), + "wreckedShipBosses": NewMemoryWatcher(0xD82B, Byte), + "maridiaBosses": NewMemoryWatcher(0xD82C, Byte), + "tourianBosses": NewMemoryWatcher(0xD82D, Byte), + "ceresBosses": NewMemoryWatcher(0xD82E, Byte), + "crateriaItems": NewMemoryWatcher(0xD870, Byte), + "brinteriaItems": NewMemoryWatcher(0xD871, Byte), + "brinstarItems2": NewMemoryWatcher(0xD872, Byte), + "brinstarItems3": NewMemoryWatcher(0xD873, Byte), + "brinstarItems4": NewMemoryWatcher(0xD874, Byte), + "brinstarItems5": NewMemoryWatcher(0xD875, Byte), + "norfairItems1": NewMemoryWatcher(0xD876, Byte), + "norfairItems2": NewMemoryWatcher(0xD877, Byte), + "norfairItems3": NewMemoryWatcher(0xD878, Byte), + "norfairItems4": NewMemoryWatcher(0xD879, Byte), + "norfairItems5": NewMemoryWatcher(0xD87A, Byte), + "wreckedShipItems": NewMemoryWatcher(0xD880, Byte), + "maridiaItems1": NewMemoryWatcher(0xD881, Byte), + "maridiaItems2": NewMemoryWatcher(0xD882, Byte), + "maridiaItems3": NewMemoryWatcher(0xD883, Byte), + } + return &SNESState{ + doExtraUpdate: true, + data: data, + latencySamples: make([]uint128, 0), + pickedUpHundredthMissile: false, + pickedUpSporeSpawnSuper: false, + vars: vars, + } +} + +func (mw MemoryWatcher) ptr() *MemoryWatcher { + return &mw +} + +func (s *SNESState) update() { + s.mu.Lock() + defer s.mu.Unlock() + for _, watcher := range s.vars { + if s.doExtraUpdate { + watcher.UpdateValue(s.data) + s.doExtraUpdate = false + } + watcher.UpdateValue(s.data) + } +} + +type SNESSummary struct { + LatencyAverage float64 + LatencyStddev float64 + Start interface{} + Reset interface{} + Split interface{} +} + +func (s *SNESState) FetchAll(client SyncClient, settings *Settings) (*SNESSummary, error) { + startTime := time.Now() + addresses := [][2]int{ + {0xF5008B, 2}, // Controller 1 Input + {0xF5079B, 3}, // ROOM ID + ROOM # for region + Region Number + {0xF50998, 1}, // GAME STATE + {0xF509A4, 61}, // ITEMS + {0xF50A28, 1}, + {0xF50F8C, 66}, + {0xF5D821, 14}, + {0xF5D870, 20}, + } + snesData, err := client.getAddresses(addresses) + if err != nil { + return nil, err + } + + copy(s.data[0x008B:0x008B+2], snesData[0]) + copy(s.data[0x079B:0x079B+3], snesData[1]) + s.data[0x0998] = snesData[2][0] + copy(s.data[0x09A4:0x09A4+61], snesData[3]) + s.data[0x0A28] = snesData[4][0] + copy(s.data[0x0F8C:0x0F8C+66], snesData[5]) + copy(s.data[0xD821:0xD821+14], snesData[6]) + copy(s.data[0xD870:0xD870+20], snesData[7]) + + s.update() + + start := s.start() + reset := s.reset() + split := split(settings, s) + + elapsed := time.Since(startTime).Milliseconds() + + if len(s.latencySamples) == NUM_LATENCY_SAMPLES { + s.latencySamples = s.latencySamples[1:] + } + s.latencySamples = append(s.latencySamples, uint128FromInt(elapsed)) + + averageLatency := averageUint128Slice(s.latencySamples) + + var sdevSum float64 + for _, x := range s.latencySamples { + diff := x.ToFloat64() - averageLatency + sdevSum += diff * diff + } + stddev := math.Sqrt(sdevSum / float64(len(s.latencySamples)-1)) + + return &SNESSummary{ + LatencyAverage: averageLatency, + LatencyStddev: stddev, + Start: start, + Reset: reset, + Split: split, + }, nil +} + +func uint128FromInt(i int64) uint128 { + if i < 0 { + return uint128{hi: math.MaxUint64, lo: uint64(i)} + } + return uint128{hi: 0, lo: uint64(i)} +} + +func averageUint128Slice(arr []uint128) float64 { + var sum uint128 + for _, v := range arr { + sum = sum.Add(v) + } + return sum.ToFloat64() / float64(len(arr)) +} + +func (s *SNESState) start() bool { + normalStart := s.vars["gameState"].old == 2 && s.vars["gameState"].current == 0x1f + cutsceneEnded := s.vars["gameState"].old == 0x1E && s.vars["gameState"].current == 0x1F + zebesStart := s.vars["gameState"].old == 5 && s.vars["gameState"].current == 6 + return normalStart || cutsceneEnded || zebesStart +} + +func (s *SNESState) reset() bool { + return s.vars["roomID"].old != 0 && s.vars["roomID"].current == 0 +} + +type TimeSpan struct { + seconds float64 +} + +func (t TimeSpan) Seconds() float64 { + return t.seconds +} + +func TimeSpanFromSeconds(seconds float64) TimeSpan { + return TimeSpan{seconds: seconds} +} + +func (s *SNESState) gametimeToSeconds() TimeSpan { + hours := float64(s.vars["igtHours"].current) + minutes := float64(s.vars["igtMinutes"].current) + seconds := float64(s.vars["igtSeconds"].current) + + totalSeconds := hours*3600 + minutes*60 + seconds + return TimeSpanFromSeconds(totalSeconds) +} + +type SuperMetroidAutoSplitter struct { + snes *SNESState + settings *sync.RWMutex + settingsData *Settings +} + +func NewSuperMetroidAutoSplitter(settings *sync.RWMutex, settingsData *Settings) *SuperMetroidAutoSplitter { + return &SuperMetroidAutoSplitter{ + snes: NewSNESState(), + settings: settings, + settingsData: settingsData, + } +} + +func (a *SuperMetroidAutoSplitter) Update(client SyncClient) (*SNESSummary, error) { + return a.snes.FetchAll(client, a.settingsData) +} + +func (a *SuperMetroidAutoSplitter) GametimeToSeconds() *TimeSpan { + t := a.snes.gametimeToSeconds() + return &t +} + +func (a *SuperMetroidAutoSplitter) ResetGameTracking() { + a.snes = NewSNESState() +} From 6472ef3c95b356681b69a87241e71e463c125e7c Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 20 Oct 2025 02:51:10 -0400 Subject: [PATCH 03/52] removed example files --- autosplitters/NWA/NWA usage.go | 174 ----------- autosplitters/QUSB2SNES/QUSB2SNES Usage.go | 344 --------------------- 2 files changed, 518 deletions(-) delete mode 100644 autosplitters/NWA/NWA usage.go delete mode 100644 autosplitters/QUSB2SNES/QUSB2SNES Usage.go diff --git a/autosplitters/NWA/NWA usage.go b/autosplitters/NWA/NWA usage.go deleted file mode 100644 index df90fca..0000000 --- a/autosplitters/NWA/NWA usage.go +++ /dev/null @@ -1,174 +0,0 @@ -type SupermetroidAutoSplitter struct { - PriorState uint8 - State uint8 - PriorRoomID uint16 - RoomID uint16 - ResetTimerOnGameReset bool - Client NWASyncClient -} - -type BattletoadsAutoSplitter struct { - PriorLevel uint8 - Level uint8 - ResetTimerOnGameReset bool - Client NWASyncClient -} - -type Splitter interface { - ClientID() - EmuInfo() - EmuGameInfo() - EmuStatus() - CoreInfo() - CoreMemories() - Update() (NWASummary, error) - Start() bool - Reset() bool - Split() bool -} - -type Game int - -const ( - Battletoads Game = iota - SuperMetroid -) - -func nwaobject(game Game, appConfig *sync.RWMutex, ip string, port uint32) Splitter { - appConfig.RLock() - defer appConfig.RUnlock() - - // Assuming appConfig is a struct pointer with ResetTimerOnGameReset field - // This is a placeholder for actual config reading logic - resetTimer := YesOrNo(0) - // You need to implement actual reading from appConfig here - - switch game { - case Battletoads: - client, _ := (&NWASyncClient{}).Connect(ip, port) - return &BattletoadsAutoSplitter{ - PriorLevel: 0, - Level: 0, - ResetTimerOnGameReset: resetTimer, - Client: *client, - } - case SuperMetroid: - client, _ := (&NWASyncClient{}).Connect(ip, port) - return &SupermetroidAutoSplitter{ - PriorState: 0, - State: 0, - PriorRoomID: 0, - RoomID: 0, - ResetTimerOnGameReset: resetTimer, - Client: *client, - } - default: - return nil - } -} - -import ( - "sync" - "time" - - "github.com/pkg/errors" - "golang.org/x/sys/windows" - "golang.org/x/sys/windows/svc/eventlog" -) - -func appInit( - app *LiveSplitCoreRenderer, - syncReceiver <-chan ThreadEvent, - cc *eframeCreationContext, - appConfig *sync.RWMutex -) { - context := cc.eguiCtx.Clone() - context.SetVisuals(eguiVisualsDark()) - - if app.appConfig.Read().GlobalHotkeys == YesOrNoYes { - messageboxOnError(func() error { - return app.enableGlobalHotkeys() - }) - } - - frameRate := app.appConfig.Read().FrameRate - if frameRate == 0 { - frameRate = DefaultFrameRate - } - pollingRate := app.appConfig.Read().PollingRate - if pollingRate == 0 { - pollingRate = DefaultPollingRate - } - - go func() { - for { - context.Clone().RequestRepaint() - time.Sleep(time.Duration(1000.0/frameRate) * time.Millisecond) - } - }() - - timer := app.timer.Clone() - appConfig := app.appConfig.Clone() - - if appConfig.Read().UseAutosplitter == YesOrNoYes { - if appConfig.Read().AutosplitterType == autosplittersATypeNWA { - game := app.game - address := app.address.Read() - port := *app.port.Read() - - go func() { - for { - client := nwaobject(game, appConfig, &address, port) - err := printOnError(func() error { - if err := client.emuInfo(); err != nil { - return err - } - if err := client.emuGameInfo(); err != nil { - return err - } - if err := client.emuStatus(); err != nil { - return err - } - if err := client.clientID(); err != nil { - return err - } - if err := client.coreInfo(); err != nil { - return err - } - if err := client.coreMemories(); err != nil { - return err - } - - for { - autoSplitStatus, err := client.update() - if err != nil { - return err - } - if autoSplitStatus.Start { - if err := timer.WriteLock().Start(); err != nil { - return errors.Wrap(err, "failed to start timer") - } - } - if autoSplitStatus.Reset { - if err := timer.WriteLock().Reset(true); err != nil { - return errors.Wrap(err, "failed to reset timer") - } - } - if autoSplitStatus.Split { - if err := timer.WriteLock().Split(); err != nil { - return errors.Wrap(err, "failed to split timer") - } - } - - time.Sleep(time.Duration(1000.0/pollingRate) * time.Millisecond) - } - }) - if err != nil { - // handle error if needed - } - time.Sleep(1 * time.Second) - } - }() - } - } -} \ No newline at end of file diff --git a/autosplitters/QUSB2SNES/QUSB2SNES Usage.go b/autosplitters/QUSB2SNES/QUSB2SNES Usage.go deleted file mode 100644 index 77efe35..0000000 --- a/autosplitters/QUSB2SNES/QUSB2SNES Usage.go +++ /dev/null @@ -1,344 +0,0 @@ -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/pkg/errors" -) - -type LiveSplitCoreRenderer struct { - appConfig *AppConfig - timer *Timer - settings *Settings -} - -type AppConfig struct { - mu sync.RWMutex - GlobalHotkeys *YesOrNo - FrameRate *float64 - PollingRate *float64 - UseAutosplitter *YesOrNo - AutosplitterType *AType - ResetTimerOnGameReset *YesOrNo - ResetGameOnTimerReset *YesOrNo -} - -type YesOrNo int - -const ( - No YesOrNo = iota - Yes -) - -type AType int - -const ( - QUSB2SNES AType = iota -) - -type ThreadEvent int - -const ( - TimerReset ThreadEvent = iota -) - -type Timer struct { - mu sync.RWMutex - // timer state fields here -} - -func (t *Timer) Start() error { - // start timer logic - return nil -} - -func (t *Timer) Reset(force bool) error { - // reset timer logic - return nil -} - -func (t *Timer) SetGameTime(tSec float64) error { - // set game time logic - return nil -} - -func (t *Timer) Split() error { - // split timer logic - return nil -} - -type Settings struct{} - -type AutoSplitter interface { - Update(client *SyncClient) (*Summary, error) - ResetGameTracking() - GameTimeToSeconds() *float64 -} - -type SuperMetroidAutoSplitter struct { - settings *Settings -} - -func NewSuperMetroidAutoSplitter(settings *Settings) *SuperMetroidAutoSplitter { - return &SuperMetroidAutoSplitter{settings: settings} -} - -func (a *SuperMetroidAutoSplitter) Update(client *SyncClient) (*Summary, error) { - // update logic - return &Summary{}, nil -} - -func (a *SuperMetroidAutoSplitter) ResetGameTracking() { - // reset tracking logic -} - -func (a *SuperMetroidAutoSplitter) GameTimeToSeconds() *float64 { - // return game time in seconds - return nil -} - -type Summary struct { - Start bool - Reset bool - Split bool - LatencyAverage float64 - LatencyStddev float64 -} - -type SyncClient struct{} - -func ConnectSyncClient() (*SyncClient, error) { - // connect logic - return &SyncClient{}, nil -} - -func (c *SyncClient) SetName(name string) error { - return nil -} - -func (c *SyncClient) AppVersion() (string, error) { - return "version", nil -} - -func (c *SyncClient) ListDevice() ([]string, error) { - return []string{"device1"}, nil -} - -func (c *SyncClient) Attach(device string) error { - return nil -} - -func (c *SyncClient) Info() (interface{}, error) { - return nil, nil -} - -func (c *SyncClient) Reset() error { - return nil -} - -func messageBoxOnError(f func() error) { - if err := f(); err != nil { - fmt.Println("Error:", err) - } -} - -func (app *LiveSplitCoreRenderer) EnableGlobalHotkeys() error { - // enable global hotkeys logic - return nil -} - -func appInit( - app *LiveSplitCoreRenderer, - syncReceiver <-chan ThreadEvent, - cc *CreationContext, -) { - context := cc.EguiCtx.Clone() - context.SetVisuals(DarkVisuals()) - - app.appConfig.mu.RLock() - globalHotkeys := app.appConfig.GlobalHotkeys - app.appConfig.mu.RUnlock() - - if globalHotkeys != nil && *globalHotkeys == Yes { - messageBoxOnError(func() error { - return app.EnableGlobalHotkeys() - }) - } - - app.appConfig.mu.RLock() - frameRate := DEFAULT_FRAME_RATE - if app.appConfig.FrameRate != nil { - frameRate = *app.appConfig.FrameRate - } - pollingRate := DEFAULT_POLLING_RATE - if app.appConfig.PollingRate != nil { - pollingRate = *app.appConfig.PollingRate - } - app.appConfig.mu.RUnlock() - - // Frame Rate Thread - go func() { - ticker := time.NewTicker(time.Duration(float64(time.Second) / frameRate)) - defer ticker.Stop() - for { - select { - case <-ticker.C: - context.Clone().RequestRepaint() - } - } - }() - - timer := app.timer - appConfig := app.appConfig - - appConfig.mu.RLock() - useAutosplitter := appConfig.UseAutosplitter - appConfig.mu.RUnlock() - - if useAutosplitter != nil && *useAutosplitter == Yes { - appConfig.mu.RLock() - autosplitterType := appConfig.AutosplitterType - appConfig.mu.RUnlock() - - if autosplitterType != nil && *autosplitterType == QUSB2SNES { - settings := app.settings - - go func() { - for { - latency := struct { - mu sync.RWMutex - value [2]float64 - }{} - - err := func() error { - client, err := ConnectSyncClient() - if err != nil { - return errors.Wrap(err, "creating usb2snes connection") - } - if err := client.SetName("annelid"); err != nil { - return err - } - version, err := client.AppVersion() - if err != nil { - return err - } - fmt.Printf("Server version is %v\n", version) - - devices, err := client.ListDevice() - if err != nil { - return err - } - if len(devices) != 1 { - if len(devices) == 0 { - return errors.New("no devices present") - } - return errors.Errorf("unexpected devices: %#v", devices) - } - device := devices[0] - fmt.Printf("Using device %v\n", device) - - if err := client.Attach(device); err != nil { - return err - } - fmt.Println("Connected.") - info, err := client.Info() - if err != nil { - return err - } - fmt.Printf("%#v\n", info) - - var autosplitter AutoSplitter = NewSuperMetroidAutoSplitter(settings) - - for { - summary, err := autosplitter.Update(client) - if err != nil { - return err - } - if summary.Start { - if err := timer.Start(); err != nil { - return errors.Wrap(err, "start timer") - } - } - if summary.Reset { - appConfig.mu.RLock() - resetTimerOnGameReset := appConfig.ResetTimerOnGameReset - appConfig.mu.RUnlock() - if resetTimerOnGameReset != nil && *resetTimerOnGameReset == Yes { - if err := timer.Reset(true); err != nil { - return errors.Wrap(err, "reset timer") - } - } - } - if summary.Split { - if t := autosplitter.GameTimeToSeconds(); t != nil { - if err := timer.SetGameTime(*t); err != nil { - return errors.Wrap(err, "set game time") - } - } - if err := timer.Split(); err != nil { - return errors.Wrap(err, "split timer") - } - } - - latency.mu.Lock() - latency.value[0] = summary.LatencyAverage - latency.value[1] = summary.LatencyStddev - latency.mu.Unlock() - - select { - case ev := <-syncReceiver: - if ev == TimerReset { - autosplitter.ResetGameTracking() - appConfig.mu.RLock() - resetGameOnTimerReset := appConfig.ResetGameOnTimerReset - appConfig.mu.RUnlock() - if resetGameOnTimerReset != nil && *resetGameOnTimerReset == Yes { - if err := client.Reset(); err != nil { - return err - } - } - } - default: - } - - time.Sleep(time.Duration(float64(time.Second) / pollingRate)) - } - }() - if err != nil { - fmt.Println("Error:", err) - } - } - }() - - time.Sleep(time.Second) - } - } -} - -// Dummy types and functions to make the above compile - -type CreationContext struct { - EguiCtx *EguiContext -} - -type EguiContext struct{} - -func (e *EguiContext) Clone() *EguiContext { - return &EguiContext{} -} - -func (e *EguiContext) SetVisuals(v Visuals) {} - -func (e *EguiContext) RequestRepaint() {} - -type Visuals struct{} - -func DarkVisuals() Visuals { - return Visuals{} -} - -const ( - DEFAULT_FRAME_RATE = 60.0 - DEFAULT_POLLING_RATE = 30.0 -) \ No newline at end of file From 09e357582626a93a50d22ea1b3cf847456f102f6 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 20 Oct 2025 02:53:27 -0400 Subject: [PATCH 04/52] changed SNESSummary struct members to bools --- autosplitters/QUSB2SNES/qusb2snes_splitter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autosplitters/QUSB2SNES/qusb2snes_splitter.go b/autosplitters/QUSB2SNES/qusb2snes_splitter.go index 5b2faa4..9594f10 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_splitter.go +++ b/autosplitters/QUSB2SNES/qusb2snes_splitter.go @@ -1262,9 +1262,9 @@ func (s *SNESState) update() { type SNESSummary struct { LatencyAverage float64 LatencyStddev float64 - Start interface{} - Reset interface{} - Split interface{} + Start bool + Reset bool + Split bool } func (s *SNESState) FetchAll(client SyncClient, settings *Settings) (*SNESSummary, error) { From 7c098291a5641d300d888b0b4ac24d22920209c8 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Tue, 21 Oct 2025 20:46:59 -0400 Subject: [PATCH 05/52] Service file for autosplitters Currently only supports NWA --- autosplitters/service.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 autosplitters/service.go diff --git a/autosplitters/service.go b/autosplitters/service.go new file mode 100644 index 0000000..3de3af3 --- /dev/null +++ b/autosplitters/service.go @@ -0,0 +1,35 @@ +package autosplitters + +import ( + nwa "github.com/zellydev-games/opensplit/autosplitters/NWA" +) + +type AutosplitterType int + +const ( + NWA AutosplitterType = iota + QUSB2SNES +) + +// type Splitter interface {} + +// need a return type that can handle any type we give it +func NewService(UseAutosplitter bool, ResetTimerOnGameReset bool, Port uint32, Addr string /*game Game,*/, Type AutosplitterType) *nwa.NWASplitter { + if UseAutosplitter { + if Type == NWA { + client, _ := nwa.Connect(Addr, Port) + return &nwa.NWASplitter{ + ResetTimerOnGameReset: ResetTimerOnGameReset, + Client: *client, + } + } + // if Type == QUSB2SNES { + // client, err := qusb2snes.Connect() + // if err != nil { + // return err + // } + // return &client + // } + } + return nil +} From 40e17e97b532af7493a19373c1dfe45f18c36ff6 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Tue, 21 Oct 2025 20:50:51 -0400 Subject: [PATCH 06/52] - removed unneeded types, structs, and interfaces - moved public stuff to top of file and private stuff to bottom - added comments --- autosplitters/NWA/nwa_client.go | 197 +++++++++++++++----------------- 1 file changed, 89 insertions(+), 108 deletions(-) diff --git a/autosplitters/NWA/nwa_client.go b/autosplitters/NWA/nwa_client.go index 9d390a3..7d0b3a2 100644 --- a/autosplitters/NWA/nwa_client.go +++ b/autosplitters/NWA/nwa_client.go @@ -12,49 +12,12 @@ import ( "time" ) -type ErrorKind int - -const ( - InvalidError ErrorKind = iota - InvalidCommand - InvalidArgument - NotAllowed - ProtocolError -) - +// public type NWAError struct { - Kind ErrorKind + Kind errorKind Reason string } -type AsciiReply interface{} - -type AsciiOk struct{} - -type AsciiHash map[string]string - -type AsciiListHash []map[string]string - -type Ok struct{} - -type Hash map[string]string - -type ListHash []map[string]string - -type EmulatorReply interface{} - -type Ascii struct { - Reply AsciiReply -} - -type Error struct { - Err NWAError -} - -type Binary struct { - Data []byte -} - type NWASyncClient struct { Connection net.Conn Port uint32 @@ -80,7 +43,86 @@ func Connect(ip string, port uint32) (*NWASyncClient, error) { }, nil } -func (c *NWASyncClient) GetReply() (EmulatorReply, error) { +func (c *NWASyncClient) ExecuteCommand(cmd string, argString *string) (emulatorReply, error) { + var command string + if argString == nil { + command = fmt.Sprintf("%s\n", cmd) + } else { + command = fmt.Sprintf("%s %s\n", cmd, *argString) + } + + _, err := io.WriteString(c.Connection, command) + if err != nil { + return nil, err + } + + return c.getReply() +} + +func (c *NWASyncClient) ExecuteRawCommand(cmd string, argString *string) { + var command string + if argString == nil { + command = fmt.Sprintf("%s\n", cmd) + } else { + command = fmt.Sprintf("%s %s\n", cmd, *argString) + } + + // ignoring error as per TODO in Rust code + _, _ = io.WriteString(c.Connection, command) +} + +func (c *NWASyncClient) IsConnected() bool { + // net.Conn in Go does not have a Peek method. + // We can try to set a read deadline and read with a zero-length buffer to check connection. + // But zero-length read returns immediately, so we try to read 1 byte with deadline. + buf := make([]byte, 1) + c.Connection.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) + n, err := c.Connection.Read(buf) + if err != nil { + // If timeout or no data, consider connected + netErr, ok := err.(net.Error) + if ok && netErr.Timeout() { + return true + } + return false + } + if n > 0 { + // Data was read, connection is alive + return true + } + return false +} + +func (c *NWASyncClient) Close() { + // TODO: handle the error + c.Connection.Close() +} + +func (c *NWASyncClient) Reconnected() (bool, error) { + conn, err := net.DialTimeout("tcp", c.Addr.String(), time.Second) + if err != nil { + return false, err + } + c.Connection = conn + return true, nil +} + +// private +type errorKind int + +const ( + InvalidError errorKind = iota + InvalidCommand + InvalidArgument + NotAllowed + ProtocolError +) + +type hash map[string]string + +type emulatorReply interface{} + +func (c *NWASyncClient) getReply() (emulatorReply, error) { readStream := bufio.NewReader(c.Connection) firstByte, err := readStream.ReadByte() if err != nil { @@ -90,6 +132,8 @@ func (c *NWASyncClient) GetReply() (EmulatorReply, error) { return nil, err } + // Ascii + // stops reading when the only result is a new line if firstByte == '\n' { mapResult := make(map[string]string) for { @@ -101,7 +145,7 @@ func (c *NWASyncClient) GetReply() (EmulatorReply, error) { break } if line[0] == '\n' && len(mapResult) == 0 { - return AsciiOk{}, nil + return nil, nil } if line[0] == '\n' { break @@ -118,7 +162,7 @@ func (c *NWASyncClient) GetReply() (EmulatorReply, error) { reason, hasReason := mapResult["reason"] errorStr, hasError := mapResult["error"] if hasReason && hasError { - var mkind ErrorKind + var mkind errorKind switch errorStr { case "protocol_error": mkind = ProtocolError @@ -142,11 +186,11 @@ func (c *NWASyncClient) GetReply() (EmulatorReply, error) { }, nil } } - return Hash(mapResult), nil + return hash(mapResult), nil } + // Binary if firstByte == 0 { - // Binary reply header := make([]byte, 4) n, err := io.ReadFull(readStream, header) if err != nil || n != 4 { @@ -164,34 +208,7 @@ func (c *NWASyncClient) GetReply() (EmulatorReply, error) { return nil, errors.New("invalid reply") } -func (c *NWASyncClient) ExecuteCommand(cmd string, argString *string) (EmulatorReply, error) { - var command string - if argString == nil { - command = fmt.Sprintf("%s\n", cmd) - } else { - command = fmt.Sprintf("%s %s\n", cmd, *argString) - } - - _, err := io.WriteString(c.Connection, command) - if err != nil { - return nil, err - } - - return c.GetReply() -} - -func (c *NWASyncClient) ExecuteRawCommand(cmd string, argString *string) { - var command string - if argString == nil { - command = fmt.Sprintf("%s\n", cmd) - } else { - command = fmt.Sprintf("%s %s\n", cmd, *argString) - } - - // ignoring error as per TODO in Rust code - _, _ = io.WriteString(c.Connection, command) -} - +// I think this would be used if I actually sent data func (c *NWASyncClient) sendData(data []byte) { buf := make([]byte, 5) size := len(data) @@ -205,39 +222,3 @@ func (c *NWASyncClient) sendData(data []byte) { // TODO: handle the error c.Connection.Write(data) } - -func (c *NWASyncClient) isConnected() bool { - // net.Conn in Go does not have a Peek method. - // We can try to set a read deadline and read with a zero-length buffer to check connection. - // But zero-length read returns immediately, so we try to read 1 byte with deadline. - buf := make([]byte, 1) - c.Connection.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) - n, err := c.Connection.Read(buf) - if err != nil { - // If timeout or no data, consider connected - netErr, ok := err.(net.Error) - if ok && netErr.Timeout() { - return true - } - return false - } - if n > 0 { - // Data was read, connection is alive - return true - } - return false -} - -func (c *NWASyncClient) close() { - // TODO: handle the error - c.Connection.Close() -} - -func (c *NWASyncClient) reconnected() (bool, error) { - conn, err := net.DialTimeout("tcp", c.Addr.String(), time.Second) - if err != nil { - return false, err - } - c.Connection = conn - return true, nil -} From 856496f5db05574ff32e052aab2a8c549d9e6280 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Tue, 21 Oct 2025 21:03:29 -0400 Subject: [PATCH 07/52] - imports updated - moved public stuff to the top of the file and private to the bottom - changed NWASplitter stuct to make it generic - added compare_type enum (might remove this) - added Element and MemoryEntry structs to make generic interface - added setup function for initializing memory and conditions - made client public - updated Update function for generic interface, construct arguments, build multi-byte values from little endian data - start, reset, and split helper functions updated for generic usability - addedfindInSlice, compareTypeConverter, and compare functions for generic usability --- autosplitters/NWA/nwa_splitter.go | 452 +++++++++++++++++++++++++++--- 1 file changed, 407 insertions(+), 45 deletions(-) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index 52e0126..8d2015a 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -1,26 +1,205 @@ package nwa import ( + "bytes" + "encoding/binary" "fmt" + "log" + "strconv" + "strings" ) -type NWASummary struct { - Start bool - Reset bool - Split bool +// public +type NWASplitter struct { + ResetTimerOnGameReset bool + Client NWASyncClient + nwaMemory []MemoryEntry + startConditions [][]Element + resetConditions [][]Element + splitConditions [][]Element } -type NWASplitter struct { - priorLevel uint8 - level uint8 - resetTimerOnGameReset bool - client NWASyncClient +type compare_type int + +const ( + ceqp compare_type = iota // current value equal to prior value + ceqe // current value equal to expected value + cnep // current value not equal to prior value + cnee // current value not equal to expected value + cgtp // current value greater than prior value + cgte // current value greater than expected value + cltp // current value less than than prior value + clte // current value less than than expected value + eeqc // expected value equal to current value + eeqp // expected value equal to prior value + enec // expected value not equal to current value + enep // expected value not equal to prior value + egtc // expected value greater than current value + egtp // expected value greater than prior value + eltc // expected value less than than current value + eltp // expected value less than than prior value + peqc // prior value equal to current value + peqe // prior value equal to expected value + pnec // prior value not equal to current value + pnee // prior value not equal to expected value + pgtc // prior value greater than current value + pgte // prior value greater than expected value + pltc // prior value less than than current value + plte // prior value less than than expected value + cter // compare type error +) + +type Element struct { + // name string + memoryEntryName string + expectedValue *int + compareType compare_type +} + +type MemoryEntry struct { + name string + memoryBank string + memory string + size string + currentValue *int + priorValue *int +} + +func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImport []string, resetConditionImport []string, splitConditionImport []string) { + // Populate Start Condition List + for _, p := range memData { + // create memory entry + memName := strings.Split(p, ",") + // integer, err := strconv.Atoi(memName[2]) // Atoi returns an int and an error + + // if err != nil { + // log.Fatalf("Failed to convert string to integer: %v", err) + // } + + entry := MemoryEntry{ + name: memName[0], + memoryBank: memName[1], + memory: memName[2], + size: memName[3]} + // add memory map entries to nwaMemory list + b.nwaMemory = append(b.nwaMemory, entry) + } + + // Populate Start Condition List + for _, p := range startConditionImport { + var condition []Element + // create elements + // add elements to condition list + startCon := strings.Split(p, ",") + if len(startCon) != 2 || len(startCon) != 3 { + // Error. Too many or too few elements + } else { + // convert string compare type to enum + cT := compareTypeConverter(startCon[2]) + if cT == cter { + // return an error + } + + if len(startCon) == 3 { + integer, err := strconv.Atoi(startCon[1]) // Atoi returns an int and an error + if err != nil { + log.Fatalf("Failed to convert string to integer: %v", err) + } + intPtr := new(int) + *intPtr = integer + + condition = append(condition, Element{ + memoryEntryName: startCon[0], + expectedValue: intPtr, + compareType: cT}) + } else if len(startCon) == 2 { + condition = append(condition, Element{ + memoryEntryName: startCon[0], + compareType: cT}) + } + // add condition lists to StartConditions list + b.startConditions = append(b.startConditions, condition) + } + } + // Populate Reset Condition List + for _, p := range resetConditionImport { + var condition []Element + // create elements + // add elements to condition list + resetCon := strings.Split(p, ",") + if len(resetCon) != 2 || len(resetCon) != 3 { + // Error. Too many or too few elements + } else { + // convert string compare type to enum + cT := compareTypeConverter(resetCon[2]) + if cT == cter { + // return an error + } + + if len(resetCon) == 3 { + integer, err := strconv.Atoi(resetCon[1]) // Atoi returns an int and an error + if err != nil { + log.Fatalf("Failed to convert string to integer: %v", err) + } + intPtr := new(int) + *intPtr = integer + + condition = append(condition, Element{ + memoryEntryName: resetCon[0], + expectedValue: intPtr, + compareType: cT}) + } else if len(resetCon) == 2 { + condition = append(condition, Element{ + memoryEntryName: resetCon[0], + compareType: cT}) + } + // add condition lists to StartConditions list + b.resetConditions = append(b.resetConditions, condition) + } + } + + // Populate Split Condition List + for _, p := range splitConditionImport { + var condition []Element + // create elements + // add elements to condition list + splitCon := strings.Split(p, ",") + if len(splitCon) != 2 || len(splitCon) != 3 { + // Error. Too many or too few elements + } else { + // convert string compare type to enum + cT := compareTypeConverter(splitCon[2]) + if cT == cter { + // return an error + } + + if len(splitCon) == 3 { + integer, err := strconv.Atoi(splitCon[1]) // Atoi returns an int and an error + if err != nil { + log.Fatalf("Failed to convert string to integer: %v", err) + } + intPtr := new(int) + *intPtr = integer + + condition = append(condition, Element{ + memoryEntryName: splitCon[0], + expectedValue: intPtr, + compareType: cT}) + } else if len(splitCon) == 2 { + condition = append(condition, Element{ + memoryEntryName: splitCon[0], + compareType: cT}) + } + // add condition lists to StartConditions list + b.splitConditions = append(b.splitConditions, condition) + } + } } func (b *NWASplitter) ClientID() { cmd := "MY_NAME_IS" args := "Annelid" - summary, err := b.client.ExecuteCommand(cmd, &args) + summary, err := b.Client.ExecuteCommand(cmd, &args) if err != nil { panic(err) } @@ -30,7 +209,7 @@ func (b *NWASplitter) ClientID() { func (b *NWASplitter) EmuInfo() { cmd := "EMULATOR_INFO" args := "0" - summary, err := b.client.ExecuteCommand(cmd, &args) + summary, err := b.Client.ExecuteCommand(cmd, &args) if err != nil { panic(err) } @@ -39,7 +218,7 @@ func (b *NWASplitter) EmuInfo() { func (b *NWASplitter) EmuGameInfo() { cmd := "GAME_INFO" - summary, err := b.client.ExecuteCommand(cmd, nil) + summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { panic(err) } @@ -48,7 +227,7 @@ func (b *NWASplitter) EmuGameInfo() { func (b *NWASplitter) EmuStatus() { cmd := "EMULATION_STATUS" - summary, err := b.client.ExecuteCommand(cmd, nil) + summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { panic(err) } @@ -57,7 +236,7 @@ func (b *NWASplitter) EmuStatus() { func (b *NWASplitter) CoreInfo() { cmd := "CORE_CURRENT_INFO" - summary, err := b.client.ExecuteCommand(cmd, nil) + summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { panic(err) } @@ -66,57 +245,240 @@ func (b *NWASplitter) CoreInfo() { func (b *NWASplitter) CoreMemories() { cmd := "CORE_MEMORIES" - summary, err := b.client.ExecuteCommand(cmd, nil) + summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { panic(err) } fmt.Printf("%#v\n", summary) } -func (b *NWASplitter) Update() (NWASummary, error) { - b.priorLevel = b.level +func (b *NWASplitter) Update() (nwaSummary, error) { cmd := "CORE_READ" - args := "RAM;$0010;1" - summary, err := b.client.ExecuteCommand(cmd, &args) - if err != nil { - return NWASummary{}, err - } - fmt.Printf("%#v\n", summary) + for _, p := range b.nwaMemory { + args := p.memoryBank + ";" + p.memory + ";" + p.size + summary, err := b.Client.ExecuteCommand(cmd, &args) + if err != nil { + return nwaSummary{}, err + } + fmt.Printf("%#v\n", summary) - switch v := summary.(type) { - case []byte: - if len(v) > 0 { - b.level = v[0] + switch v := summary.(type) { + case []byte: + if len(v) == 1 { + *p.currentValue = int(v[0]) + } else if len(v) > 1 { + var i int + buf := bytes.NewReader(v) + err := binary.Read(buf, binary.LittleEndian, &i) + if err != nil { + fmt.Println("Error reading binary data:", err) + } + *p.currentValue = i + } + case NWAError: + fmt.Printf("%#v\n", v) + default: + fmt.Printf("%#v\n", v) } - case NWAError: - fmt.Printf("%#v\n", v) - default: - fmt.Printf("%#v\n", v) } - fmt.Printf("%#v\n", b.level) + start := b.start() + reset := b.reset() + split := b.split() - start := b.Start() - reset := b.Reset() - split := b.Split() - - return NWASummary{ + return nwaSummary{ Start: start, Reset: reset, Split: split, }, nil } -func (b *NWASplitter) Start() bool { - return b.level == 1 && b.priorLevel == 0 +// private +type nwaSummary struct { + Start bool + Reset bool + Split bool +} + +func (b *NWASplitter) start() bool { + startState := true + for _, p := range b.startConditions { + var tempstate bool + for _, q := range p { + index, found := b.findInSlice(b.nwaMemory, q.memoryEntryName) + if found { + tempstate = compare(q.compareType, b.nwaMemory[index].currentValue, b.nwaMemory[index].priorValue, q.expectedValue) + } else { + // throw error + } + startState = startState && tempstate + } + if startState { + return true + } + } + return false +} + +func (b *NWASplitter) reset() bool { + if b.ResetTimerOnGameReset { + resetState := true + for _, p := range b.resetConditions { + var tempstate bool + for _, q := range p { + index, found := b.findInSlice(b.nwaMemory, q.memoryEntryName) + if found { + tempstate = compare(q.compareType, b.nwaMemory[index].currentValue, b.nwaMemory[index].priorValue, q.expectedValue) + } else { + // throw error + } + resetState = resetState && tempstate + } + if resetState { + return true + } + } + return false + } else { + return false + } } -func (b *NWASplitter) Reset() bool { - return b.level == 0 && - b.priorLevel != 0 && - b.resetTimerOnGameReset +func (b *NWASplitter) split() bool { + splitState := true + for _, p := range b.splitConditions { + var tempstate bool + for _, q := range p { + index, found := b.findInSlice(b.nwaMemory, q.memoryEntryName) + if found { + tempstate = compare(q.compareType, b.nwaMemory[index].currentValue, b.nwaMemory[index].priorValue, q.expectedValue) + } else { + // throw error + } + splitState = splitState && tempstate + } + if splitState { + return true + } + } + return false } -func (b *NWASplitter) Split() bool { - return b.level > b.priorLevel && b.priorLevel < 100 +func (b *NWASplitter) findInSlice(slice []MemoryEntry, target string) (int, bool) { + for i, v := range slice { + if v.name == target { + return i, true // Return index and true if found + } + } + return -1, false // Return -1 and false if not found +} + +func compareTypeConverter(input string) compare_type { + switch input { + case "ceqp": + return ceqp + case "ceqe": + return ceqe + case "cnep": + return cnep + case "cnee": + return cnee + case "cgtp": + return cgtp + case "cgte": + return cgte + case "cltp": + return cltp + case "clte": + return clte + case "eeqc": + return eeqc + case "eeqp": + return eeqp + case "enec": + return enec + case "enep": + return enep + case "egtc": + return egtc + case "egtp": + return egtp + case "eltc": + return eltc + case "eltp": + return eltp + case "peqc": + return peqc + case "peqe": + return peqe + case "pnec": + return pnec + case "pnee": + return pnee + case "pgtc": + return pgtc + case "pgte": + return pgte + case "pltc": + return pltc + case "plte": + return plte + default: + return cter + } +} + +func compare(input compare_type, current *int, prior *int, expected *int) bool { + switch input { + case ceqp: + return *current == *prior + case ceqe: + return *current == *expected + case cnep: + return *current != *prior + case cnee: + return *current != *expected + case cgtp: + return *current > *prior + case cgte: + return *current > *expected + case cltp: + return *current < *prior + case clte: + return *current < *expected + case eeqc: + return *expected == *current + case eeqp: + return *expected == *prior + case enec: + return *expected != *current + case enep: + return *expected != *prior + case egtc: + return *expected > *current + case egtp: + return *expected > *prior + case eltc: + return *expected < *current + case eltp: + return *expected < *prior + case peqc: + return *prior == *current + case peqe: + return *prior == *expected + case pnec: + return *prior != *current + case pnee: + return *prior != *expected + case pgtc: + return *prior > *current + case pgte: + return *prior > *expected + case pltc: + return *prior < *current + case plte: + return *prior < *expected + default: + return false + } } From ff8dcfdb71e3ba997868aca932bfbb04efc16196 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Tue, 21 Oct 2025 21:04:36 -0400 Subject: [PATCH 08/52] - autosplitter import added - added example usage --- main.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/main.go b/main.go index 70a5dd4..86cd32f 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "time" "github.com/wailsapp/wails/v2/pkg/runtime" + "github.com/zellydev-games/opensplit/autosplitters" "github.com/zellydev-games/opensplit/bridge" "github.com/zellydev-games/opensplit/config" "github.com/zellydev-games/opensplit/dispatcher" @@ -60,6 +61,93 @@ func main() { sessionUIBridge := bridge.NewSession(sessionUpdateChannel, runtimeProvider) configUIBridge := bridge.NewConfig(configUpdateChannel, runtimeProvider) + // NWA example + // // This should try to re/connect if used + // // { + autosplitterService := autosplitters.NewService(true, true, 48879, "0.0.0.0", autosplitters.NWA) + // // time.Sleep(1 * time.Second) + // // } + + autosplitterService.EmuInfo() + autosplitterService.EmuGameInfo() + autosplitterService.EmuStatus() + autosplitterService.ClientID() + autosplitterService.CoreInfo() + autosplitterService.CoreMemories() + + // //this is the core loop of autosplitting + // //queries the device (emu, hardware, application) at the rate specified in ms + // { + autoState, err2 := autosplitterService.Update() + if err2 != nil { + return + } + if autoState.Start { + //start run + } + if autoState.Reset { + //restart run + } + if autoState.Split { + //split run + } + // time.Sleep(time.Duration(1000.0/pollingRate) * time.Millisecond) + // } + + // //QUSB2SNES example + // client, err := ConnectSyncClient() + // client.SetName("annelid") + + // version, err := client.AppVersion() + // fmt.Printf("Server version is %v\n", version) + + // devices, err := client.ListDevice() + + // if len(devices) != 1 { + // if len(devices) == 0 { + // return errors.New("no devices present") + // } + // return errors.Errorf("unexpected devices: %#v", devices) + // } + // device := devices[0] + // fmt.Printf("Using device %v\n", device) + + // client.Attach(device) + // fmt.Println("Connected.") + + // info, err := client.Info() + // fmt.Printf("%#v\n", info) + + // var autosplitter AutoSplitter = NewSuperMetroidAutoSplitter(settings) + + // for { + // summary, err := autosplitter.Update(client) + // if summary.Start { + // timer.Start() + // } + // if summary.Reset { + // if resetTimerOnGameReset == true { + // timer.Reset(true) + // } + // } + // if summary.Split { + // // IGT + // timer.SetGameTime(*t) + // // RTA + // timer.Split() + // } + + // if ev == TimerReset { + // // creates a new SNES state + // autosplitter.ResetGameTracking() + // if resetGameOnTimerReset == true { + // client.Reset() + // } + // } + + // time.Sleep(time.Duration(float64(time.Second) / pollingRate)) + // } + // Build dispatcher that can receive commands from frontend or backend and dispatch them to the state machine commandDispatcher := dispatcher.NewService(machine) From 2f7533a83fff299376657e5dd50071ef0c1a22b3 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Tue, 21 Oct 2025 21:32:27 -0400 Subject: [PATCH 09/52] updated the client name --- autosplitters/NWA/nwa_splitter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index 8d2015a..9930d2c 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -198,7 +198,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo func (b *NWASplitter) ClientID() { cmd := "MY_NAME_IS" - args := "Annelid" + args := "OpenSplit" summary, err := b.Client.ExecuteCommand(cmd, &args) if err != nil { panic(err) From 11e40a428a2e2484e506a46ab057734868947c8e Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Tue, 21 Oct 2025 21:41:52 -0400 Subject: [PATCH 10/52] added comments --- main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 86cd32f..b767e23 100644 --- a/main.go +++ b/main.go @@ -68,12 +68,12 @@ func main() { // // time.Sleep(1 * time.Second) // // } - autosplitterService.EmuInfo() - autosplitterService.EmuGameInfo() - autosplitterService.EmuStatus() - autosplitterService.ClientID() - autosplitterService.CoreInfo() - autosplitterService.CoreMemories() + autosplitterService.EmuInfo() // Gets info about the emu; name, version, nwa_version, id, supported commands + autosplitterService.EmuGameInfo() // Gets info about the loaded game + autosplitterService.EmuStatus() // Gets the status of the emu + autosplitterService.ClientID() // Provides the client name to the NWA interface + autosplitterService.CoreInfo() // Might be useful to display the platform & core names + autosplitterService.CoreMemories() // Get info about the memory banks available // //this is the core loop of autosplitting // //queries the device (emu, hardware, application) at the rate specified in ms From 6f058eba34b88c372369bc5557eec8f6aed87ec4 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Tue, 21 Oct 2025 21:54:33 -0400 Subject: [PATCH 11/52] added file format example --- autosplitters/NWA/nwa_splitter.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index 9930d2c..0a3c88d 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -9,6 +9,25 @@ import ( "strings" ) +// File format +// +// Type +// IP +// Port +// +// #memory +// MemName,address,size +// +// #start (some games might have multiple start conditions) (expectedValue is optional) +// start:MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType +// +// #reset (some games might have multiple reset conditions) (expectedValue is optional) +// reset:MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType +// +// #split (some games might have multiple start conditions) (expectedValue is optional) +// level:MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType +// state:MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType + // public type NWASplitter struct { ResetTimerOnGameReset bool From 8e5d349103c061c0ea1c618642fc02a095c03191 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 24 Nov 2025 01:16:09 -0500 Subject: [PATCH 12/52] removed autosplitter test code added autosplitter service initialization and activation --- main.go | 102 +++++++++----------------------------------------------- 1 file changed, 15 insertions(+), 87 deletions(-) diff --git a/main.go b/main.go index b767e23..cee1b32 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,8 @@ import ( "github.com/wailsapp/wails/v2/pkg/runtime" "github.com/zellydev-games/opensplit/autosplitters" + nwa "github.com/zellydev-games/opensplit/autosplitters/NWA" + qusb2snes "github.com/zellydev-games/opensplit/autosplitters/QUSB2SNES" "github.com/zellydev-games/opensplit/bridge" "github.com/zellydev-games/opensplit/config" "github.com/zellydev-games/opensplit/dispatcher" @@ -61,96 +63,19 @@ func main() { sessionUIBridge := bridge.NewSession(sessionUpdateChannel, runtimeProvider) configUIBridge := bridge.NewConfig(configUpdateChannel, runtimeProvider) - // NWA example - // // This should try to re/connect if used - // // { - autosplitterService := autosplitters.NewService(true, true, 48879, "0.0.0.0", autosplitters.NWA) - // // time.Sleep(1 * time.Second) - // // } - - autosplitterService.EmuInfo() // Gets info about the emu; name, version, nwa_version, id, supported commands - autosplitterService.EmuGameInfo() // Gets info about the loaded game - autosplitterService.EmuStatus() // Gets the status of the emu - autosplitterService.ClientID() // Provides the client name to the NWA interface - autosplitterService.CoreInfo() // Might be useful to display the platform & core names - autosplitterService.CoreMemories() // Get info about the memory banks available - - // //this is the core loop of autosplitting - // //queries the device (emu, hardware, application) at the rate specified in ms - // { - autoState, err2 := autosplitterService.Update() - if err2 != nil { - return - } - if autoState.Start { - //start run - } - if autoState.Reset { - //restart run - } - if autoState.Split { - //split run - } - // time.Sleep(time.Duration(1000.0/pollingRate) * time.Millisecond) - // } - - // //QUSB2SNES example - // client, err := ConnectSyncClient() - // client.SetName("annelid") - - // version, err := client.AppVersion() - // fmt.Printf("Server version is %v\n", version) - - // devices, err := client.ListDevice() - - // if len(devices) != 1 { - // if len(devices) == 0 { - // return errors.New("no devices present") - // } - // return errors.Errorf("unexpected devices: %#v", devices) - // } - // device := devices[0] - // fmt.Printf("Using device %v\n", device) - - // client.Attach(device) - // fmt.Println("Connected.") - - // info, err := client.Info() - // fmt.Printf("%#v\n", info) - - // var autosplitter AutoSplitter = NewSuperMetroidAutoSplitter(settings) - - // for { - // summary, err := autosplitter.Update(client) - // if summary.Start { - // timer.Start() - // } - // if summary.Reset { - // if resetTimerOnGameReset == true { - // timer.Reset(true) - // } - // } - // if summary.Split { - // // IGT - // timer.SetGameTime(*t) - // // RTA - // timer.Split() - // } - - // if ev == TimerReset { - // // creates a new SNES state - // autosplitter.ResetGameTracking() - // if resetGameOnTimerReset == true { - // client.Reset() - // } - // } - - // time.Sleep(time.Duration(float64(time.Second) / pollingRate)) - // } - // Build dispatcher that can receive commands from frontend or backend and dispatch them to the state machine commandDispatcher := dispatcher.NewService(machine) + // All the config should come from either the config file or the autosplitter service thread + AutoSplitterService := autosplitters.Splitters{ + NWAAutoSplitter: new(nwa.NWASplitter), + QUSB2SNESAutoSplitter: new(qusb2snes.SyncClient), + UseAutosplitter: true, + ResetTimerOnGameReset: true, + Addr: "0.0.0.0", + Port: 48879, + Type: autosplitters.NWA} + var hotkeyProvider statemachine.HotkeyProvider err := wails.Run(&options.App{ @@ -181,6 +106,9 @@ func main() { timerUIBridge.StartUIPump() configUIBridge.StartUIPump() + //Start autosplitter + AutoSplitterService.Run(commandDispatcher) + startInterruptListener(ctx, hotkeyProvider) runtime.WindowSetAlwaysOnTop(ctx, true) logger.Info("application startup complete") From d1540025dbefb81b952a0440999410b99d736b5b Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 24 Nov 2025 01:19:09 -0500 Subject: [PATCH 13/52] added todo list added service struct added service thread to connect, setup NWA splitter, control splitter --- autosplitters/service.go | 276 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 259 insertions(+), 17 deletions(-) diff --git a/autosplitters/service.go b/autosplitters/service.go index 3de3af3..6cbc341 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -1,9 +1,30 @@ package autosplitters +// TODO: +// check status of splits file +// update object variables +// qusb2snes usage +// load mem and condition data + import ( + "fmt" + "time" + nwa "github.com/zellydev-games/opensplit/autosplitters/NWA" + qusb2snes "github.com/zellydev-games/opensplit/autosplitters/QUSB2SNES" + "github.com/zellydev-games/opensplit/dispatcher" ) +type Splitters struct { + NWAAutoSplitter *nwa.NWASplitter + QUSB2SNESAutoSplitter *qusb2snes.SyncClient + UseAutosplitter bool + ResetTimerOnGameReset bool + Addr string + Port uint32 + Type AutosplitterType +} + type AutosplitterType int const ( @@ -11,25 +32,246 @@ const ( QUSB2SNES ) -// type Splitter interface {} +// I don't think this should be here not sure why it's not updating the object +// func (s Splitters) Load() { +// useAuto := make(chan bool) +// resetTimer := make(chan bool) +// addr := make(chan string) +// port := make(chan uint32) +// aType := make(chan AutosplitterType) +// s.UseAutosplitter = <-useAuto +// s.ResetTimerOnGameReset = <-resetTimer +// s.Addr = <-addr +// s.Port = <-port +// s.Type = <-aType +// } + +func (s Splitters) Run(commandDispatcher *dispatcher.Service) { + go func() { + // loop trying to connect + for { + mil := 2 * time.Millisecond + + //check for split file loaded + // if !splitsFile.loaded { + // continue + // } + + connectStart := time.Now() + + s.NWAAutoSplitter, s.QUSB2SNESAutoSplitter = s.newClient( /*s.UseAutosplitter, s.ResetTimerOnGameReset, s.Addr, s.Port, s.Type*/ ) + + if s.NWAAutoSplitter != nil || s.QUSB2SNESAutoSplitter != nil { + + if s.NWAAutoSplitter.Client.IsConnected() { + s.processNWA(commandDispatcher) + } + // if s.QUSB2SNESAutoSplitter != nil { + // s.processQUSB2SNES(commandDispatcher) + // } + } + connectElapsed := time.Since(connectStart) + // fmt.Println(mil - connectElapsed) + time.Sleep(min(mil, max(0, mil-connectElapsed))) + } + }() +} + +func (s Splitters) newClient( /*UseAutosplitter bool, ResetTimerOnGameReset bool, Addr string, Port uint32, Type AutosplitterType*/ ) (*nwa.NWASplitter, *qusb2snes.SyncClient) { + fmt.Printf("Creating AutoSplitter Service\n") -// need a return type that can handle any type we give it -func NewService(UseAutosplitter bool, ResetTimerOnGameReset bool, Port uint32, Addr string /*game Game,*/, Type AutosplitterType) *nwa.NWASplitter { - if UseAutosplitter { - if Type == NWA { - client, _ := nwa.Connect(Addr, Port) - return &nwa.NWASplitter{ - ResetTimerOnGameReset: ResetTimerOnGameReset, - Client: *client, + if s.UseAutosplitter { + if s.Type == NWA { + // fmt.Printf("Creating NWA AutoSplitter\n") + client, connectError := nwa.Connect(s.Addr, s.Port) + if connectError == nil { + return &nwa.NWASplitter{ + ResetTimerOnGameReset: s.ResetTimerOnGameReset, + Client: *client, + }, nil + } else { + return nil, nil } } - // if Type == QUSB2SNES { - // client, err := qusb2snes.Connect() - // if err != nil { - // return err - // } - // return &client - // } + if s.Type == QUSB2SNES { + fmt.Printf("Creating QUSB2SNES AutoSplitter\n") + client, connectError := qusb2snes.Connect() + if connectError == nil { + return nil, client + } else { + return nil, nil + } + } + } + return nil, nil +} + +// Memory should be moved out of here and received from the config file and sent to the splitter +func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { + mil := 2 * time.Millisecond + + // // Battletoads test data + // memData := []string{ + // ("level,RAM,$0010,1")} + // // startConditionImport := []string{} + // resetConditionImport := []string{ + // ("reset:level,0,eeqc level,0,pnee")} + // splitConditionImport := []string{ + // ("start:level,0,peqe level,1,ceqe"), + // ("level:level,255,peqe level,2,ceqe"), + // ("level:level,255,peqe level,3,ceqe"), + // ("level:level,255,peqe level,4,ceqe"), + // ("level:level,255,peqe level,5,ceqe"), + // ("level:level,255,peqe level,6,ceqe"), + // ("level:level,255,peqe level,7,ceqe"), + // ("level:level,255,peqe level,8,ceqe"), + // ("level:level,255,peqe level,9,ceqe"), + // ("level:level,255,peqe level,10,ceqe"), + // ("level:level,255,peqe level,11,ceqe"), + // ("level:level,255,peqe level,12,ceqe"), + // ("level:level,255,peqe level,13,ceqe")} + + // Home Improvment test data + memData := []string{ + ("crates,WRAM,$001A8A,1"), + ("scene,WRAM,$00161F,1"), + ("W2P2HP,WRAM,$001499,1"), + ("W2P1HP,WRAM,$001493,1"), + ("BossHP,WRAM,$001491,1"), + ("state,WRAM,$001400,1"), + ("gameplay,WRAM,$000AE5,1"), + ("substage,WRAM,$000AE3,1"), + ("stage,WRAM,$000AE1,1"), + ("scene2,WRAM,$000886,1"), + ("play_state,WRAM,$0003B1,1"), + ("power_up,WRAM,$0003AF,1"), + ("weapon,WRAM,$0003CD,1"), + ("invul,WRAM,$001C05,1"), + ("FBossHP,WRAM,$00149D,1"), + } + + resetConditionImport := []string{ + ("cutscene_reset:state,0,eeqc state,D0,peqe gameplay,11,peqe gameplay,0,eeqc"), + ("tool_reset:gameplay,11,peqe gameplay,0,eeqc scene,4,peqe scene,0,eeqc, scene2,3,peqe scene2,0,eeqc"), + ("level_reset:gameplay,13,peqe gameplay,0,eeqc crates,0,ceqe substage,0,ceqe stage,0,ceqe scene2,0,ceqe"), + } + + splitConditionImport := []string{ + ("start:state,C0,peqe state,0,ceqe stage,0,ceqe substage,0,ceqe gameplay,11,ceqe play_state,0,ceqe"), + ("start2:state,D0,peqe state,0,ceqe stage,0,ceqe substage,0,ceqe gameplay,11,ceqe play_state,0,ceqe"), + ("1-1:state,C8,peqe state,0,ceqe stage,0,ceqe substage,1,ceqe gameplay,13,ceqe"), + ("1-2:state,C8,peqe state,0,ceqe stage,0,ceqe substage,2,ceqe gameplay,13,ceqe"), + ("1-3:state,C8,peqe state,0,ceqe stage,0,ceqe substage,3,ceqe gameplay,13,ceqe"), + ("1-4:state,C8,peqe state,0,ceqe stage,0,ceqe substage,4,ceqe gameplay,13,ceqe"), + ("1-5:state,C8,peqe state,0,ceqe stage,0,ceqe substage,4,ceqe gameplay,13,ceqe BossHP,0,ceqe"), + ("2-1:state,C8,peqe state,0,ceqe stage,1,ceqe substage,1,ceqe gameplay,13,ceqe"), + ("2-2:state,C8,peqe state,0,ceqe stage,1,ceqe substage,2,ceqe gameplay,13,ceqe"), + ("2-3:state,C8,peqe state,0,ceqe stage,1,ceqe substage,3,ceqe gameplay,13,ceqe"), + ("2-4:state,C8,peqe state,0,ceqe stage,1,ceqe substage,4,ceqe gameplay,13,ceqe"), + ("2-5:state,C8,peqe state,0,ceqe stage,1,ceqe substage,4,ceqe gameplay,13,ceqe W2P2HP,1,ceqe W2P1HP,0,ceqe BossHP,0,ceqe"), + ("3-1:state,C8,peqe state,0,ceqe stage,2,ceqe substage,1,ceqe gameplay,13,ceqe"), + ("3-2:state,C8,peqe state,0,ceqe stage,2,ceqe substage,2,ceqe gameplay,13,ceqe"), + ("3-3:state,C8,peqe state,0,ceqe stage,2,ceqe substage,3,ceqe gameplay,13,ceqe"), + ("3-4:state,C8,peqe state,0,ceqe stage,2,ceqe substage,4,ceqe gameplay,13,ceqe"), + ("3-5:state,C8,peqe state,0,ceqe stage,2,ceqe substage,4,ceqe gameplay,13,ceqe BossHP,0,ceqe"), + ("4-1:state,C8,peqe state,0,ceqe stage,3,ceqe substage,1,ceqe gameplay,13,ceqe"), + ("4-2:state,C8,peqe state,0,ceqe stage,3,ceqe substage,2,ceqe gameplay,13,ceqe"), + ("4-3:state,C8,peqe state,0,ceqe stage,3,ceqe substage,3,ceqe gameplay,13,ceqe"), + ("4-4:state,C8,peqe state,0,ceqe stage,3,ceqe substage,4,ceqe gameplay,13,ceqe"), + ("4-5:state,C8,peqe state,0,ceqe stage,3,ceqe substage,4,ceqe gameplay,13,ceqe FBossHP,FF,ceqe"), + } + + // receive setup data...probably through a channel + //Setup Memory + s.NWAAutoSplitter.MemAndConditionsSetup(memData /*startConditionImport,*/, resetConditionImport, splitConditionImport) + + s.NWAAutoSplitter.EmuInfo() // Gets info about the emu; name, version, nwa_version, id, supported commands + s.NWAAutoSplitter.EmuGameInfo() // Gets info about the loaded game + s.NWAAutoSplitter.EmuStatus() // Gets the status of the emu + s.NWAAutoSplitter.ClientID() // Provides the client name to the NWA interface + s.NWAAutoSplitter.CoreInfo() // Might be useful to display the platform & core names + s.NWAAutoSplitter.CoreMemories() // Get info about the memory banks available + + // this is the core loop of autosplitting + // queries the device (emu, hardware, application) at the rate specified in ms + for { + processStart := time.Now() + + fmt.Printf("Checking for autosplitting updates.\n") + autoState, err2 := s.NWAAutoSplitter.Update() + if err2 != nil { + return + } + if autoState.Reset { + //restart run + commandDispatcher.Dispatch(dispatcher.RESET, nil) + } + if autoState.Split { + //split run + commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + } + // TODO: Close the connection after closing the splits file or receiving a disconnect signal + // s.NWAAutoSplitter.Client.Close() + processElapsed := time.Since(processStart) + // fmt.Println(processStart) + // fmt.Println(processElapsed) + time.Sleep(min(mil, max(0, mil-processElapsed))) } - return nil +} + +func (s Splitters) processQUSB2SNES(commandDispatcher *dispatcher.Service) { + // // //QUSB2SNES example + // if QUSB2SNESAutoSplitterService != nil { + // // client.SetName("annelid") + + // // version, err := client.AppVersion() + // // fmt.Printf("Server version is %v\n", version) + + // // devices, err := client.ListDevice() + + // // if len(devices) != 1 { + // // if len(devices) == 0 { + // // return errors.New("no devices present") + // // } + // // return errors.Errorf("unexpected devices: %#v", devices) + // // } + // // device := devices[0] + // // fmt.Printf("Using device %v\n", device) + + // // client.Attach(device) + // // fmt.Println("Connected.") + + // // info, err := client.Info() + // // fmt.Printf("%#v\n", info) + + // // var autosplitter AutoSplitter = NewSuperMetroidAutoSplitter(settings) + + // // for { + // // summary, err := autosplitter.Update(client) + // // if summary.Start { + // // timer.Start() + // // } + // // if summary.Reset { + // // if resetTimerOnGameReset == true { + // // timer.Reset(true) + // // } + // // } + // // if summary.Split { + // // // IGT + // // timer.SetGameTime(*t) + // // // RTA + // // timer.Split() + // // } + + // // if ev == TimerReset { + // // // creates a new SNES state + // // autosplitter.ResetGameTracking() + // // if resetGameOnTimerReset == true { + // // client.Reset() + // // } + // // } + + // // time.Sleep(time.Duration(float64(time.Second) / pollingRate)) + // // } + // } } From 20f78c904e5c17c1732a0f5d94543c9384b92ea4 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 24 Nov 2025 01:20:05 -0500 Subject: [PATCH 14/52] commented out sendData function added read deadline --- autosplitters/NWA/nwa_client.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/autosplitters/NWA/nwa_client.go b/autosplitters/NWA/nwa_client.go index 7d0b3a2..e665bcb 100644 --- a/autosplitters/NWA/nwa_client.go +++ b/autosplitters/NWA/nwa_client.go @@ -45,6 +45,7 @@ func Connect(ip string, port uint32) (*NWASyncClient, error) { func (c *NWASyncClient) ExecuteCommand(cmd string, argString *string) (emulatorReply, error) { var command string + c.Connection.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) if argString == nil { command = fmt.Sprintf("%s\n", cmd) } else { @@ -61,6 +62,7 @@ func (c *NWASyncClient) ExecuteCommand(cmd string, argString *string) (emulatorR func (c *NWASyncClient) ExecuteRawCommand(cmd string, argString *string) { var command string + c.Connection.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) if argString == nil { command = fmt.Sprintf("%s\n", cmd) } else { @@ -209,16 +211,16 @@ func (c *NWASyncClient) getReply() (emulatorReply, error) { } // I think this would be used if I actually sent data -func (c *NWASyncClient) sendData(data []byte) { - buf := make([]byte, 5) - size := len(data) - buf[0] = 0 - buf[1] = byte((size >> 24) & 0xFF) - buf[2] = byte((size >> 16) & 0xFF) - buf[3] = byte((size >> 8) & 0xFF) - buf[4] = byte(size & 0xFF) - // TODO: handle the error - c.Connection.Write(buf) - // TODO: handle the error - c.Connection.Write(data) -} +// func (c *NWASyncClient) sendData(data []byte) { +// buf := make([]byte, 5) +// size := len(data) +// buf[0] = 0 +// buf[1] = byte((size >> 24) & 0xFF) +// buf[2] = byte((size >> 16) & 0xFF) +// buf[3] = byte((size >> 8) & 0xFF) +// buf[4] = byte(size & 0xFF) +// // TODO: handle the error +// c.Connection.Write(buf) +// // TODO: handle the error +// c.Connection.Write(data) +// } From b10556d3d1fa182fcea24a4a54d5510c10365dc4 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 24 Nov 2025 01:24:35 -0500 Subject: [PATCH 15/52] removed file format example removed unneeded enum and converter function made MemoryEntry struct variables public fixed hex to int conversion and byte to int conversion added output for invalid condition lengths --- autosplitters/NWA/nwa_splitter.go | 463 +++++++++++++----------------- 1 file changed, 197 insertions(+), 266 deletions(-) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index 0a3c88d..ae77553 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -1,5 +1,7 @@ package nwa +// TODO: handle errors correctly + import ( "bytes" "encoding/binary" @@ -9,154 +11,90 @@ import ( "strings" ) -// File format -// -// Type -// IP -// Port -// -// #memory -// MemName,address,size -// -// #start (some games might have multiple start conditions) (expectedValue is optional) -// start:MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType -// -// #reset (some games might have multiple reset conditions) (expectedValue is optional) -// reset:MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType -// -// #split (some games might have multiple start conditions) (expectedValue is optional) -// level:MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType -// state:MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType MemName,expectedValue,compareType - // public type NWASplitter struct { ResetTimerOnGameReset bool Client NWASyncClient nwaMemory []MemoryEntry - startConditions [][]Element resetConditions [][]Element splitConditions [][]Element } -type compare_type int - -const ( - ceqp compare_type = iota // current value equal to prior value - ceqe // current value equal to expected value - cnep // current value not equal to prior value - cnee // current value not equal to expected value - cgtp // current value greater than prior value - cgte // current value greater than expected value - cltp // current value less than than prior value - clte // current value less than than expected value - eeqc // expected value equal to current value - eeqp // expected value equal to prior value - enec // expected value not equal to current value - enep // expected value not equal to prior value - egtc // expected value greater than current value - egtp // expected value greater than prior value - eltc // expected value less than than current value - eltp // expected value less than than prior value - peqc // prior value equal to current value - peqe // prior value equal to expected value - pnec // prior value not equal to current value - pnee // prior value not equal to expected value - pgtc // prior value greater than current value - pgte // prior value greater than expected value - pltc // prior value less than than current value - plte // prior value less than than expected value - cter // compare type error -) +// type compare_type int + +// const ( +// ceqp compare_type = iota // current value equal to prior value +// ceqe // current value equal to expected value +// cnep // current value not equal to prior value +// cnee // current value not equal to expected value +// cgtp // current value greater than prior value +// cgte // current value greater than expected value +// cltp // current value less than than prior value +// clte // current value less than than expected value +// eeqc // expected value equal to current value +// eeqp // expected value equal to prior value +// enec // expected value not equal to current value +// enep // expected value not equal to prior value +// egtc // expected value greater than current value +// egtp // expected value greater than prior value +// eltc // expected value less than than current value +// eltp // expected value less than than prior value +// peqc // prior value equal to current value +// peqe // prior value equal to expected value +// pnec // prior value not equal to current value +// pnee // prior value not equal to expected value +// pgtc // prior value greater than current value +// pgte // prior value greater than expected value +// pltc // prior value less than than current value +// plte // prior value less than than expected value +// ) type Element struct { - // name string memoryEntryName string expectedValue *int - compareType compare_type + compareType string } type MemoryEntry struct { - name string - memoryBank string - memory string - size string + Name string + MemoryBank string + Address string + Size string currentValue *int priorValue *int } -func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImport []string, resetConditionImport []string, splitConditionImport []string) { +// Setup the memory map being read by the NWA splitter and the maps for the reset and split conditions +func (b *NWASplitter) MemAndConditionsSetup(memData []string, resetConditionImport []string, splitConditionImport []string) { // Populate Start Condition List for _, p := range memData { // create memory entry memName := strings.Split(p, ",") - // integer, err := strconv.Atoi(memName[2]) // Atoi returns an int and an error - - // if err != nil { - // log.Fatalf("Failed to convert string to integer: %v", err) - // } entry := MemoryEntry{ - name: memName[0], - memoryBank: memName[1], - memory: memName[2], - size: memName[3]} + Name: memName[0], + MemoryBank: memName[1], + Address: memName[2], + Size: memName[3]} // add memory map entries to nwaMemory list b.nwaMemory = append(b.nwaMemory, entry) } - // Populate Start Condition List - for _, p := range startConditionImport { - var condition []Element - // create elements - // add elements to condition list - startCon := strings.Split(p, ",") - if len(startCon) != 2 || len(startCon) != 3 { - // Error. Too many or too few elements - } else { - // convert string compare type to enum - cT := compareTypeConverter(startCon[2]) - if cT == cter { - // return an error - } - - if len(startCon) == 3 { - integer, err := strconv.Atoi(startCon[1]) // Atoi returns an int and an error - if err != nil { - log.Fatalf("Failed to convert string to integer: %v", err) - } - intPtr := new(int) - *intPtr = integer - - condition = append(condition, Element{ - memoryEntryName: startCon[0], - expectedValue: intPtr, - compareType: cT}) - } else if len(startCon) == 2 { - condition = append(condition, Element{ - memoryEntryName: startCon[0], - compareType: cT}) - } - // add condition lists to StartConditions list - b.startConditions = append(b.startConditions, condition) - } - } // Populate Reset Condition List for _, p := range resetConditionImport { var condition []Element // create elements // add elements to condition list - resetCon := strings.Split(p, ",") - if len(resetCon) != 2 || len(resetCon) != 3 { - // Error. Too many or too few elements - } else { - // convert string compare type to enum - cT := compareTypeConverter(resetCon[2]) - if cT == cter { - // return an error - } + resetCon := strings.Split(strings.Split(p, ":")[1], " ") + for _, q := range resetCon { + elements := strings.Split(q, ",") - if len(resetCon) == 3 { - integer, err := strconv.Atoi(resetCon[1]) // Atoi returns an int and an error + if len(elements) == 3 { + cT := elements[2] + + // convert hex string to int + num, err := strconv.ParseUint(elements[1], 16, 64) + integer := int(num) if err != nil { log.Fatalf("Failed to convert string to integer: %v", err) } @@ -164,17 +102,21 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo *intPtr = integer condition = append(condition, Element{ - memoryEntryName: resetCon[0], + memoryEntryName: elements[0], expectedValue: intPtr, compareType: cT}) - } else if len(resetCon) == 2 { + } else if len(elements) == 2 { + cT := elements[1] + condition = append(condition, Element{ - memoryEntryName: resetCon[0], + memoryEntryName: elements[0], compareType: cT}) + } else { + fmt.Printf("Too many or too few conditions given: %#v\n", q) } - // add condition lists to StartConditions list - b.resetConditions = append(b.resetConditions, condition) } + // add condition lists to Reset Conditions list + b.resetConditions = append(b.resetConditions, condition) } // Populate Split Condition List @@ -182,18 +124,15 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo var condition []Element // create elements // add elements to condition list - splitCon := strings.Split(p, ",") - if len(splitCon) != 2 || len(splitCon) != 3 { - // Error. Too many or too few elements - } else { - // convert string compare type to enum - cT := compareTypeConverter(splitCon[2]) - if cT == cter { - // return an error - } + splitCon := strings.Split(strings.Split(p, ":")[1], " ") + for _, q := range splitCon { + elements := strings.Split(q, ",") + + if len(elements) == 3 { + cT := elements[2] - if len(splitCon) == 3 { - integer, err := strconv.Atoi(splitCon[1]) // Atoi returns an int and an error + num, err := strconv.ParseUint(elements[1], 16, 64) + integer := int(num) if err != nil { log.Fatalf("Failed to convert string to integer: %v", err) } @@ -201,17 +140,21 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo *intPtr = integer condition = append(condition, Element{ - memoryEntryName: splitCon[0], + memoryEntryName: elements[0], expectedValue: intPtr, compareType: cT}) - } else if len(splitCon) == 2 { + } else if len(elements) == 2 { + cT := elements[1] + condition = append(condition, Element{ - memoryEntryName: splitCon[0], + memoryEntryName: elements[0], compareType: cT}) + } else { + fmt.Printf("Too many or too few conditions given: %#v\n", q) } - // add condition lists to StartConditions list - b.splitConditions = append(b.splitConditions, condition) } + // add condition lists to Split Conditions list + b.splitConditions = append(b.splitConditions, condition) } } @@ -271,29 +214,41 @@ func (b *NWASplitter) CoreMemories() { fmt.Printf("%#v\n", summary) } +// currently only supports 1 byte reads func (b *NWASplitter) Update() (nwaSummary, error) { cmd := "CORE_READ" - for _, p := range b.nwaMemory { - args := p.memoryBank + ";" + p.memory + ";" + p.size + for i, p := range b.nwaMemory { + args := p.MemoryBank + ";" + p.Address + ";" + p.Size summary, err := b.Client.ExecuteCommand(cmd, &args) if err != nil { return nwaSummary{}, err } fmt.Printf("%#v\n", summary) + b.nwaMemory[i].priorValue = b.nwaMemory[i].currentValue switch v := summary.(type) { case []byte: - if len(v) == 1 { - *p.currentValue = int(v[0]) - } else if len(v) > 1 { - var i int - buf := bytes.NewReader(v) - err := binary.Read(buf, binary.LittleEndian, &i) - if err != nil { - fmt.Println("Error reading binary data:", err) - } - *p.currentValue = i + + // need to handle more than 1 byte at a time + // if len(v) == 1 { + // val := int(v[0]) + // b.nwaMemory[i].currentValue = &val + // } else if len(v) > 1 { + // length 1 + var temp_int uint8 + //length 2 + // var temp_int uint16 + // length 4 + // var temp_int uint32 + // length 8 + // var temp_int uint64 + err := binary.Read(bytes.NewReader(v), binary.LittleEndian, &temp_int) + if err != nil { + fmt.Println("Error reading binary data:", err) } + integer := int(temp_int) + b.nwaMemory[i].currentValue = &integer + // } case NWAError: fmt.Printf("%#v\n", v) default: @@ -301,12 +256,10 @@ func (b *NWASplitter) Update() (nwaSummary, error) { } } - start := b.start() reset := b.reset() split := b.split() return nwaSummary{ - Start: start, Reset: reset, Split: split, }, nil @@ -314,46 +267,28 @@ func (b *NWASplitter) Update() (nwaSummary, error) { // private type nwaSummary struct { - Start bool Reset bool Split bool } -func (b *NWASplitter) start() bool { - startState := true - for _, p := range b.startConditions { - var tempstate bool - for _, q := range p { - index, found := b.findInSlice(b.nwaMemory, q.memoryEntryName) - if found { - tempstate = compare(q.compareType, b.nwaMemory[index].currentValue, b.nwaMemory[index].priorValue, q.expectedValue) - } else { - // throw error - } - startState = startState && tempstate - } - if startState { - return true - } - } - return false -} - +// Checks conditions and returns reset state func (b *NWASplitter) reset() bool { + fmt.Printf("Checking reset state\n") if b.ResetTimerOnGameReset { - resetState := true for _, p := range b.resetConditions { + resetState := true var tempstate bool for _, q := range p { index, found := b.findInSlice(b.nwaMemory, q.memoryEntryName) if found { tempstate = compare(q.compareType, b.nwaMemory[index].currentValue, b.nwaMemory[index].priorValue, q.expectedValue) } else { - // throw error + fmt.Printf("How did you get here?\n") } resetState = resetState && tempstate } if resetState { + fmt.Printf("Time to reset\n") return true } } @@ -363,20 +298,23 @@ func (b *NWASplitter) reset() bool { } } +// Checks conditions and returns split state func (b *NWASplitter) split() bool { - splitState := true for _, p := range b.splitConditions { + fmt.Printf("Checking split state\n") + splitState := true var tempstate bool for _, q := range p { index, found := b.findInSlice(b.nwaMemory, q.memoryEntryName) if found { tempstate = compare(q.compareType, b.nwaMemory[index].currentValue, b.nwaMemory[index].priorValue, q.expectedValue) } else { - // throw error + fmt.Printf("How did you get here?\n") } splitState = splitState && tempstate } if splitState { + fmt.Printf("Time to split\n") return true } } @@ -385,118 +323,111 @@ func (b *NWASplitter) split() bool { func (b *NWASplitter) findInSlice(slice []MemoryEntry, target string) (int, bool) { for i, v := range slice { - if v.name == target { + if v.Name == target { return i, true // Return index and true if found } } return -1, false // Return -1 and false if not found } -func compareTypeConverter(input string) compare_type { +func compare(input string, current *int, prior *int, expected *int) bool { switch input { case "ceqp": - return ceqp + fallthrough + case "peqc": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior == *current + } case "ceqe": - return ceqe + fallthrough + case "eeqc": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected == *current + } + case "eeqp": + fallthrough + case "peqe": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior == *expected + } case "cnep": - return cnep + fallthrough + case "pnec": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior != *current + } case "cnee": - return cnee + fallthrough + case "enec": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected != *current + } + case "enep": + fallthrough + case "pnee": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior != *expected + } case "cgtp": - return cgtp + fallthrough + case "pltc": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior < *current + } case "cgte": - return cgte + fallthrough + case "eltc": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected < *current + } + case "egtp": + fallthrough + case "plte": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior < *expected + } case "cltp": - return cltp + fallthrough + case "pgtc": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior > *current + } case "clte": - return clte - case "eeqc": - return eeqc - case "eeqp": - return eeqp - case "enec": - return enec - case "enep": - return enep + fallthrough case "egtc": - return egtc - case "egtp": - return egtp - case "eltc": - return eltc + if (expected == nil) || (current == nil) { + return false + } else { + return *expected > *current + } case "eltp": - return eltp - case "peqc": - return peqc - case "peqe": - return peqe - case "pnec": - return pnec - case "pnee": - return pnee - case "pgtc": - return pgtc + fallthrough case "pgte": - return pgte - case "pltc": - return pltc - case "plte": - return plte - default: - return cter - } -} - -func compare(input compare_type, current *int, prior *int, expected *int) bool { - switch input { - case ceqp: - return *current == *prior - case ceqe: - return *current == *expected - case cnep: - return *current != *prior - case cnee: - return *current != *expected - case cgtp: - return *current > *prior - case cgte: - return *current > *expected - case cltp: - return *current < *prior - case clte: - return *current < *expected - case eeqc: - return *expected == *current - case eeqp: - return *expected == *prior - case enec: - return *expected != *current - case enep: - return *expected != *prior - case egtc: - return *expected > *current - case egtp: - return *expected > *prior - case eltc: - return *expected < *current - case eltp: - return *expected < *prior - case peqc: - return *prior == *current - case peqe: - return *prior == *expected - case pnec: - return *prior != *current - case pnee: - return *prior != *expected - case pgtc: - return *prior > *current - case pgte: - return *prior > *expected - case pltc: - return *prior < *current - case plte: - return *prior < *expected + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior > *expected + } default: return false } From debe48da351b65ca39c39cded10a49799b1eb9d2 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 24 Nov 2025 01:25:22 -0500 Subject: [PATCH 16/52] made Client public --- autosplitters/QUSB2SNES/qusb2snes_client.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/autosplitters/QUSB2SNES/qusb2snes_client.go b/autosplitters/QUSB2SNES/qusb2snes_client.go index a185758..d9cb2cf 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_client.go +++ b/autosplitters/QUSB2SNES/qusb2snes_client.go @@ -1,5 +1,7 @@ package qusb2snes +// TODO: handle errors correctly + import ( "encoding/json" "errors" @@ -97,7 +99,7 @@ type USB2SnesFileInfo struct { } type SyncClient struct { - client *websocket.Conn + Client *websocket.Conn devel bool } @@ -116,7 +118,7 @@ func connect(devel bool) (*SyncClient, error) { return nil, err } return &SyncClient{ - client: conn, + Client: conn, devel: devel, }, nil } @@ -150,12 +152,12 @@ func (sc *SyncClient) sendCommandWithSpace(command Command, space Space, args [] fmt.Println(string(prettyJSON)) } } - err = sc.client.WriteMessage(websocket.TextMessage, jsonData) + err = sc.Client.WriteMessage(websocket.TextMessage, jsonData) return err } func (sc *SyncClient) getReply() (*USB2SnesResult, error) { - _, message, err := sc.client.ReadMessage() + _, message, err := sc.Client.ReadMessage() if err != nil { return nil, err } @@ -282,7 +284,7 @@ func (sc *SyncClient) SendFile(path string, data []byte) error { if stop > len(data) { stop = len(data) } - err = sc.client.WriteMessage(websocket.BinaryMessage, data[start:stop]) + err = sc.Client.WriteMessage(websocket.BinaryMessage, data[start:stop]) if err != nil { return err } @@ -309,7 +311,7 @@ func (sc *SyncClient) getFile(path string) ([]byte, error) { } data := make([]byte, 0, size) for { - _, msgData, err := sc.client.ReadMessage() + _, msgData, err := sc.Client.ReadMessage() if err != nil { return nil, err } @@ -337,7 +339,7 @@ func (sc *SyncClient) getAddress(address uint32, size int) ([]byte, error) { } data := make([]byte, 0, size) for { - _, msgData, err := sc.client.ReadMessage() + _, msgData, err := sc.Client.ReadMessage() if err != nil { return nil, err } @@ -369,7 +371,7 @@ func (sc *SyncClient) getAddresses(pairs [][2]int) ([][]byte, error) { ret := make([][]byte, 0, len(pairs)) for { - _, msgData, err := sc.client.ReadMessage() + _, msgData, err := sc.Client.ReadMessage() if err != nil { return nil, err } From 1f41633985efc073b8db98d9026bd26d0e1cf1ff Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 24 Nov 2025 01:26:37 -0500 Subject: [PATCH 17/52] removed mutex stuff --- autosplitters/QUSB2SNES/qusb2snes_splitter.go | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/autosplitters/QUSB2SNES/qusb2snes_splitter.go b/autosplitters/QUSB2SNES/qusb2snes_splitter.go index 9594f10..e97fb77 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_splitter.go +++ b/autosplitters/QUSB2SNES/qusb2snes_splitter.go @@ -1,9 +1,10 @@ package qusb2snes +// TODO: handle errors correctly + import ( "fmt" "math" - "sync" "time" ) @@ -228,7 +229,7 @@ type Settings struct { parent *string } modifiedAfterCreation bool - mu sync.RWMutex + // mu sync.RWMutex } func NewSettings() *Settings { @@ -593,8 +594,8 @@ func NewSettings() *Settings { } func (s *Settings) Insert(name string, value bool) { - s.mu.Lock() - defer s.mu.Unlock() + // s.mu.Lock() + // defer s.mu.Unlock() s.modifiedAfterCreation = true s.data[name] = struct { value bool @@ -603,8 +604,8 @@ func (s *Settings) Insert(name string, value bool) { } func (s *Settings) InsertWithParent(name string, value bool, parent string) { - s.mu.Lock() - defer s.mu.Unlock() + // s.mu.Lock() + // defer s.mu.Unlock() s.modifiedAfterCreation = true p := parent s.data[name] = struct { @@ -614,15 +615,15 @@ func (s *Settings) InsertWithParent(name string, value bool, parent string) { } func (s *Settings) Contains(varName string) bool { - s.mu.RLock() - defer s.mu.RUnlock() + // s.mu.RLock() + // defer s.mu.RUnlock() _, ok := s.data[varName] return ok } func (s *Settings) Get(varName string) bool { - s.mu.RLock() - defer s.mu.RUnlock() + // s.mu.RLock() + // defer s.mu.RUnlock() return s.getRecursive(varName) } @@ -638,8 +639,8 @@ func (s *Settings) getRecursive(varName string) bool { } func (s *Settings) Set(varName string, value bool) { - s.mu.Lock() - defer s.mu.Unlock() + // s.mu.Lock() + // defer s.mu.Unlock() entry, ok := s.data[varName] if !ok { s.data[varName] = struct { @@ -656,8 +657,8 @@ func (s *Settings) Set(varName string, value bool) { } func (s *Settings) Roots() []string { - s.mu.RLock() - defer s.mu.RUnlock() + // s.mu.RLock() + // defer s.mu.RUnlock() var roots []string for k, v := range s.data { if v.parent == nil { @@ -668,8 +669,8 @@ func (s *Settings) Roots() []string { } func (s *Settings) Children(key string) []string { - s.mu.RLock() - defer s.mu.RUnlock() + // s.mu.RLock() + // defer s.mu.RUnlock() var children []string for k, v := range s.data { if v.parent != nil && *v.parent == key { @@ -680,8 +681,8 @@ func (s *Settings) Children(key string) []string { } func (s *Settings) Lookup(varName string) bool { - s.mu.RLock() - defer s.mu.RUnlock() + // s.mu.RLock() + // defer s.mu.RUnlock() entry, ok := s.data[varName] if !ok { panic("variable not found") @@ -690,8 +691,8 @@ func (s *Settings) Lookup(varName string) bool { } func (s *Settings) LookupMut(varName string) *bool { - s.mu.Lock() - defer s.mu.Unlock() + // s.mu.Lock() + // defer s.mu.Unlock() entry, ok := s.data[varName] if !ok { panic("variable not found") @@ -712,8 +713,8 @@ func (s *Settings) LookupMut(varName string) *bool { } func (s *Settings) HasBeenModified() bool { - s.mu.RLock() - defer s.mu.RUnlock() + // s.mu.RLock() + // defer s.mu.RUnlock() return s.modifiedAfterCreation } @@ -1153,7 +1154,7 @@ type SNESState struct { latencySamples []uint128 data []byte doExtraUpdate bool - mu sync.Mutex + // mu sync.Mutex } type uint128 struct { @@ -1248,8 +1249,8 @@ func (mw MemoryWatcher) ptr() *MemoryWatcher { } func (s *SNESState) update() { - s.mu.Lock() - defer s.mu.Unlock() + // s.mu.Lock() + // defer s.mu.Unlock() for _, watcher := range s.vars { if s.doExtraUpdate { watcher.UpdateValue(s.data) @@ -1372,15 +1373,15 @@ func (s *SNESState) gametimeToSeconds() TimeSpan { } type SuperMetroidAutoSplitter struct { - snes *SNESState - settings *sync.RWMutex + snes *SNESState + // settings *sync.RWMutex settingsData *Settings } -func NewSuperMetroidAutoSplitter(settings *sync.RWMutex, settingsData *Settings) *SuperMetroidAutoSplitter { +func NewSuperMetroidAutoSplitter( /*settings *sync.RWMutex,*/ settingsData *Settings) *SuperMetroidAutoSplitter { return &SuperMetroidAutoSplitter{ - snes: NewSNESState(), - settings: settings, + snes: NewSNESState(), + // settings: settings, settingsData: settingsData, } } From 9659b4507ce9c450c00c45ea8b2262efc6004de8 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 24 Nov 2025 01:30:31 -0500 Subject: [PATCH 18/52] added example NWA autosplitter files --- ...ny% (No WW, NES NTSC-US, Rash (Green)).nwa | 24 ++++++++++ .../Home Improvement (SNES) - Any%.nwa | 44 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa create mode 100644 autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa new file mode 100644 index 0000000..9c0b83c --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa @@ -0,0 +1,24 @@ +ResetTimerOnGameReset = true +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 giant value) +level,RAM,$0010,1 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) +reset:level,0,eeqc level,0,pnee + +#split (some games might have multiple split conditions) (expectedValue is optional) +start:level,0,peqe level,1,ceqe +level:level,255,peqe level,2,ceqe +level:level,255,peqe level,3,ceqe +level:level,255,peqe level,4,ceqe +level:level,255,peqe level,5,ceqe +level:level,255,peqe level,6,ceqe +level:level,255,peqe level,7,ceqe +level:level,255,peqe level,8,ceqe +level:level,255,peqe level,9,ceqe +level:level,255,peqe level,10,ceq +level:level,255,peqe level,11,ceq +level:level,255,peqe level,12,ceq +level:level,255,peqe level,13,ceq diff --git a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa new file mode 100644 index 0000000..9a5be67 --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa @@ -0,0 +1,44 @@ +ResetTimerOnGameReset = true +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 combined value) +act,WRAM,$000AE1,1 #0-3 +level,WRAM,$000AE3,1 #0-4 +crates,WRAM,$001A8A,1 #5,6,7,8 +invuln,$001C05,1 #02 +play_state,$0003B1,1 #00 - dead/complete, 01 - alive/playable +scene,$000886,1 #03 - tool scene, 04 - win screen +state,$00AE5,1 #11 - menus/loading, 13 - gameplay +cutscene,$001400,1 #d8 - bonus countdown active, d0 - between act cutscene +first_boss_HP,$001491,1 #63 +second_boss_P1_HP,$001493,1 #14 +second_boss_P2_HP,$001499,1 # +second_boss_P2_alt_HP,$001491,1 # +third_boss_HP,$001491,1 # +final_boss_HP,$00149D,1 #Starts at 64, Dies at 1, switches to FF after explosion + +#reset (some games might have multiple reset conditions) (expectedValue is optional) +reset:level,0,eeqc level,0,pnee + +#split (some games might have multiple split conditions) (expectedValue is optional) +act1_level1:level,0,peqe level,1,ceqe +act1_level2:level,255,peqe level,2,ceqe +act1_level3:level,255,peqe level,3,ceqe +act1_level4:level,255,peqe level,4,ceqe +act1_boss:level,255,peqe level,5,ceqe +act2_level1:level,0,peqe level,1,ceqe +act2_level2:level,255,peqe level,2,ceqe +act2_level3:level,255,peqe level,3,ceqe +act2_level4:level,255,peqe level,4,ceqe +act2_boss:level,255,peqe level,5,ceqe +act3_level1:level,0,peqe level,1,ceqe +act3_level2:level,255,peqe level,2,ceqe +act3_level3:level,255,peqe level,3,ceqe +act3_level4:level,255,peqe level,4,ceqe +act3_boss:level,255,peqe level,5,ceqe +act4_level1:level,0,peqe level,1,ceqe +act4_level2:level,255,peqe level,2,ceqe +act4_level3:level,255,peqe level,3,ceqe +act4_level4:level,255,peqe level,4,ceqe +act4_boss:level,255,peqe level,5,ceqe From c02b2619cc9aed542fa70d3c593947af276f052a Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 28 Nov 2025 09:10:25 -0500 Subject: [PATCH 19/52] removed debug line --- autosplitters/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autosplitters/service.go b/autosplitters/service.go index 6cbc341..07b1478 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -78,7 +78,7 @@ func (s Splitters) Run(commandDispatcher *dispatcher.Service) { } func (s Splitters) newClient( /*UseAutosplitter bool, ResetTimerOnGameReset bool, Addr string, Port uint32, Type AutosplitterType*/ ) (*nwa.NWASplitter, *qusb2snes.SyncClient) { - fmt.Printf("Creating AutoSplitter Service\n") + // fmt.Printf("Creating AutoSplitter Service\n") if s.UseAutosplitter { if s.Type == NWA { From a4ac80d18c6031a15b2e2e57ee514fcae15fa8dd Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 26 Dec 2025 11:16:19 -0500 Subject: [PATCH 20/52] Added comment and object variable --- main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index cee1b32..80c237b 100644 --- a/main.go +++ b/main.go @@ -66,12 +66,14 @@ func main() { // Build dispatcher that can receive commands from frontend or backend and dispatch them to the state machine commandDispatcher := dispatcher.NewService(machine) - // All the config should come from either the config file or the autosplitter service thread + // UseAutoSplitter and Type should come from the splits config file + // ResetTimerOnGameReset, ResetGameOnTimerReset, Addr, Port should come from the autosplitter config file AutoSplitterService := autosplitters.Splitters{ NWAAutoSplitter: new(nwa.NWASplitter), QUSB2SNESAutoSplitter: new(qusb2snes.SyncClient), UseAutosplitter: true, ResetTimerOnGameReset: true, + ResetGameOnTimerReset: false, Addr: "0.0.0.0", Port: 48879, Type: autosplitters.NWA} From 7beac742a2c33927cef0d611d1abe43dc038c8cd Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 26 Dec 2025 11:20:38 -0500 Subject: [PATCH 21/52] added processex for generic process memory inspection --- go.mod | 7 ++++--- go.sum | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 9b12f3e..886e4c7 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,16 @@ go 1.25 require ( github.com/google/uuid v1.6.0 - github.com/wailsapp/wails/v2 v2.10.2 + github.com/gorilla/websocket v1.5.3 + github.com/wailsapp/wails/v2 v2.11.0 golang.org/x/sys v0.38.0 ) require ( github.com/bep/debounce v1.2.1 // indirect + github.com/biter777/processex v0.0.0-20210102170504-01bb369eda71 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/labstack/echo/v4 v4.13.3 // indirect github.com/labstack/gommon v0.4.2 // indirect @@ -29,7 +30,7 @@ require ( github.com/tkrajina/go-reflector v0.5.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/wailsapp/go-webview2 v1.0.21 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect diff --git a/go.sum b/go.sum index ff23d61..7afb350 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/biter777/processex v0.0.0-20210102170504-01bb369eda71 h1:SYKMUQEyuugaw68KtUlL1MpH36o2QL3ug93/y5Cye60= +github.com/biter777/processex v0.0.0-20210102170504-01bb369eda71/go.mod h1:3+wMnZWvewiNQL4PP4vEXiN1LgRlL4SXgWhj2fNMmeI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= @@ -53,12 +55,12 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= -github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.10.2 h1:29U+c5PI4K4hbx8yFbFvwpCuvqK9VgNv8WGobIlKlXk= -github.com/wailsapp/wails/v2 v2.10.2/go.mod h1:XuN4IUOPpzBrHUkEd7sCU5ln4T/p1wQedfxP7fKik+4= +github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= +github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= From 39181fbca33e9d2c6d494744957ac5d65d6464a3 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 26 Dec 2025 11:27:06 -0500 Subject: [PATCH 22/52] added ResetGameOnTimerReset variable, set sleep timers to 1ms, enabled QUSB, added start condition object, updated autosplit format, added temporary splitcount and run state tracking, QUSB implementation --- autosplitters/service.go | 327 ++++++++++++++++++++++++--------------- 1 file changed, 199 insertions(+), 128 deletions(-) diff --git a/autosplitters/service.go b/autosplitters/service.go index 07b1478..8a1b31d 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -3,8 +3,7 @@ package autosplitters // TODO: // check status of splits file // update object variables -// qusb2snes usage -// load mem and condition data +// add way to cancel autosplitting import ( "fmt" @@ -20,6 +19,7 @@ type Splitters struct { QUSB2SNESAutoSplitter *qusb2snes.SyncClient UseAutosplitter bool ResetTimerOnGameReset bool + ResetGameOnTimerReset bool Addr string Port uint32 Type AutosplitterType @@ -32,25 +32,11 @@ const ( QUSB2SNES ) -// I don't think this should be here not sure why it's not updating the object -// func (s Splitters) Load() { -// useAuto := make(chan bool) -// resetTimer := make(chan bool) -// addr := make(chan string) -// port := make(chan uint32) -// aType := make(chan AutosplitterType) -// s.UseAutosplitter = <-useAuto -// s.ResetTimerOnGameReset = <-resetTimer -// s.Addr = <-addr -// s.Port = <-port -// s.Type = <-aType -// } - func (s Splitters) Run(commandDispatcher *dispatcher.Service) { go func() { // loop trying to connect for { - mil := 2 * time.Millisecond + mil := 1 * time.Millisecond //check for split file loaded // if !splitsFile.loaded { @@ -59,16 +45,16 @@ func (s Splitters) Run(commandDispatcher *dispatcher.Service) { connectStart := time.Now() - s.NWAAutoSplitter, s.QUSB2SNESAutoSplitter = s.newClient( /*s.UseAutosplitter, s.ResetTimerOnGameReset, s.Addr, s.Port, s.Type*/ ) + s.NWAAutoSplitter, s.QUSB2SNESAutoSplitter = s.newClient() if s.NWAAutoSplitter != nil || s.QUSB2SNESAutoSplitter != nil { - if s.NWAAutoSplitter.Client.IsConnected() { + if s.NWAAutoSplitter != nil { s.processNWA(commandDispatcher) } - // if s.QUSB2SNESAutoSplitter != nil { - // s.processQUSB2SNES(commandDispatcher) - // } + if s.QUSB2SNESAutoSplitter != nil { + s.processQUSB2SNES(commandDispatcher) + } } connectElapsed := time.Since(connectStart) // fmt.Println(mil - connectElapsed) @@ -77,7 +63,7 @@ func (s Splitters) Run(commandDispatcher *dispatcher.Service) { }() } -func (s Splitters) newClient( /*UseAutosplitter bool, ResetTimerOnGameReset bool, Addr string, Port uint32, Type AutosplitterType*/ ) (*nwa.NWASplitter, *qusb2snes.SyncClient) { +func (s Splitters) newClient() (*nwa.NWASplitter, *qusb2snes.SyncClient) { // fmt.Printf("Creating AutoSplitter Service\n") if s.UseAutosplitter { @@ -86,15 +72,14 @@ func (s Splitters) newClient( /*UseAutosplitter bool, ResetTimerOnGameReset bool client, connectError := nwa.Connect(s.Addr, s.Port) if connectError == nil { return &nwa.NWASplitter{ - ResetTimerOnGameReset: s.ResetTimerOnGameReset, - Client: *client, + Client: *client, }, nil } else { return nil, nil } } if s.Type == QUSB2SNES { - fmt.Printf("Creating QUSB2SNES AutoSplitter\n") + // fmt.Printf("Creating QUSB2SNES AutoSplitter\n") client, connectError := qusb2snes.Connect() if connectError == nil { return nil, client @@ -108,28 +93,29 @@ func (s Splitters) newClient( /*UseAutosplitter bool, ResetTimerOnGameReset bool // Memory should be moved out of here and received from the config file and sent to the splitter func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { - mil := 2 * time.Millisecond + mil := 1 * time.Millisecond // // Battletoads test data // memData := []string{ - // ("level,RAM,$0010,1")} - // // startConditionImport := []string{} + // ("level,RAM,$0010,1")} + // startConditionImport := []string{ + // ("start:level,prior=0x0 && level,current=0x1")} // resetConditionImport := []string{ - // ("reset:level,0,eeqc level,0,pnee")} + // ("reset:level,current=0x0 && level,prior≠0")} // splitConditionImport := []string{ - // ("start:level,0,peqe level,1,ceqe"), - // ("level:level,255,peqe level,2,ceqe"), - // ("level:level,255,peqe level,3,ceqe"), - // ("level:level,255,peqe level,4,ceqe"), - // ("level:level,255,peqe level,5,ceqe"), - // ("level:level,255,peqe level,6,ceqe"), - // ("level:level,255,peqe level,7,ceqe"), - // ("level:level,255,peqe level,8,ceqe"), - // ("level:level,255,peqe level,9,ceqe"), - // ("level:level,255,peqe level,10,ceqe"), - // ("level:level,255,peqe level,11,ceqe"), - // ("level:level,255,peqe level,12,ceqe"), - // ("level:level,255,peqe level,13,ceqe")} + // ("level2:level,prior=0x255 && level,current=0x2"), + // ("level3:level,prior=0x255 && level,current=0x3"), + // ("level4:level,prior=0x255 && level,current=0x4"), + // ("level5:level,prior=0x255 && level,current=0x5"), + // ("level6:level,prior=0x255 && level,current=0x6"), + // ("level7:level,prior=0x255 && level,current=0x7"), + // ("level8:level,prior=0x255 && level,current=0x8"), + // ("level9:level,prior=0x255 && level,current=0x9"), + // ("level10:level,prior=0x255 && level,current=0xA"), + // ("level11:level,prior=0x255 && level,current=0xB"), + // ("level12:level,prior=0x255 && level,current=0xC"), + // ("level13:level,prior=0x255 && level,current=0xD"), + // } // Home Improvment test data memData := []string{ @@ -150,40 +136,43 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { ("FBossHP,WRAM,$00149D,1"), } + startConditionImport := []string{ + ("start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + ("start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + } + resetConditionImport := []string{ - ("cutscene_reset:state,0,eeqc state,D0,peqe gameplay,11,peqe gameplay,0,eeqc"), - ("tool_reset:gameplay,11,peqe gameplay,0,eeqc scene,4,peqe scene,0,eeqc, scene2,3,peqe scene2,0,eeqc"), - ("level_reset:gameplay,13,peqe gameplay,0,eeqc crates,0,ceqe substage,0,ceqe stage,0,ceqe scene2,0,ceqe"), + ("cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0"), + ("tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0"), + ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0"), } splitConditionImport := []string{ - ("start:state,C0,peqe state,0,ceqe stage,0,ceqe substage,0,ceqe gameplay,11,ceqe play_state,0,ceqe"), - ("start2:state,D0,peqe state,0,ceqe stage,0,ceqe substage,0,ceqe gameplay,11,ceqe play_state,0,ceqe"), - ("1-1:state,C8,peqe state,0,ceqe stage,0,ceqe substage,1,ceqe gameplay,13,ceqe"), - ("1-2:state,C8,peqe state,0,ceqe stage,0,ceqe substage,2,ceqe gameplay,13,ceqe"), - ("1-3:state,C8,peqe state,0,ceqe stage,0,ceqe substage,3,ceqe gameplay,13,ceqe"), - ("1-4:state,C8,peqe state,0,ceqe stage,0,ceqe substage,4,ceqe gameplay,13,ceqe"), - ("1-5:state,C8,peqe state,0,ceqe stage,0,ceqe substage,4,ceqe gameplay,13,ceqe BossHP,0,ceqe"), - ("2-1:state,C8,peqe state,0,ceqe stage,1,ceqe substage,1,ceqe gameplay,13,ceqe"), - ("2-2:state,C8,peqe state,0,ceqe stage,1,ceqe substage,2,ceqe gameplay,13,ceqe"), - ("2-3:state,C8,peqe state,0,ceqe stage,1,ceqe substage,3,ceqe gameplay,13,ceqe"), - ("2-4:state,C8,peqe state,0,ceqe stage,1,ceqe substage,4,ceqe gameplay,13,ceqe"), - ("2-5:state,C8,peqe state,0,ceqe stage,1,ceqe substage,4,ceqe gameplay,13,ceqe W2P2HP,1,ceqe W2P1HP,0,ceqe BossHP,0,ceqe"), - ("3-1:state,C8,peqe state,0,ceqe stage,2,ceqe substage,1,ceqe gameplay,13,ceqe"), - ("3-2:state,C8,peqe state,0,ceqe stage,2,ceqe substage,2,ceqe gameplay,13,ceqe"), - ("3-3:state,C8,peqe state,0,ceqe stage,2,ceqe substage,3,ceqe gameplay,13,ceqe"), - ("3-4:state,C8,peqe state,0,ceqe stage,2,ceqe substage,4,ceqe gameplay,13,ceqe"), - ("3-5:state,C8,peqe state,0,ceqe stage,2,ceqe substage,4,ceqe gameplay,13,ceqe BossHP,0,ceqe"), - ("4-1:state,C8,peqe state,0,ceqe stage,3,ceqe substage,1,ceqe gameplay,13,ceqe"), - ("4-2:state,C8,peqe state,0,ceqe stage,3,ceqe substage,2,ceqe gameplay,13,ceqe"), - ("4-3:state,C8,peqe state,0,ceqe stage,3,ceqe substage,3,ceqe gameplay,13,ceqe"), - ("4-4:state,C8,peqe state,0,ceqe stage,3,ceqe substage,4,ceqe gameplay,13,ceqe"), - ("4-5:state,C8,peqe state,0,ceqe stage,3,ceqe substage,4,ceqe gameplay,13,ceqe FBossHP,FF,ceqe"), + ("1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13"), + ("1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13"), + ("1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13"), + ("1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13"), + ("1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13"), + ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), + ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), + ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), + ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0"), + ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), + ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), + ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), + ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), + ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), + ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), + ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), + ("4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13"), + ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), } // receive setup data...probably through a channel //Setup Memory - s.NWAAutoSplitter.MemAndConditionsSetup(memData /*startConditionImport,*/, resetConditionImport, splitConditionImport) + s.NWAAutoSplitter.MemAndConditionsSetup(memData, startConditionImport, resetConditionImport, splitConditionImport) s.NWAAutoSplitter.EmuInfo() // Gets info about the emu; name, version, nwa_version, id, supported commands s.NWAAutoSplitter.EmuGameInfo() // Gets info about the loaded game @@ -192,86 +181,168 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { s.NWAAutoSplitter.CoreInfo() // Might be useful to display the platform & core names s.NWAAutoSplitter.CoreMemories() // Get info about the memory banks available + splitCount := 0 + runStarted := false // this is the core loop of autosplitting // queries the device (emu, hardware, application) at the rate specified in ms for { processStart := time.Now() fmt.Printf("Checking for autosplitting updates.\n") - autoState, err2 := s.NWAAutoSplitter.Update() + autoState, err2 := s.NWAAutoSplitter.Update( /*TODO: Request Current Split*/ splitCount) if err2 != nil { return } - if autoState.Reset { - //restart run - commandDispatcher.Dispatch(dispatcher.RESET, nil) + if autoState.Start && !runStarted { + //split run + commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + runStarted = !runStarted } - if autoState.Split { + if autoState.Split && runStarted { //split run commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + splitCount++ + } + if autoState.Reset && runStarted { + if s.ResetTimerOnGameReset { + commandDispatcher.Dispatch(dispatcher.RESET, nil) + } + if s.ResetGameOnTimerReset { + s.QUSB2SNESAutoSplitter.Reset() + } + splitCount = 0 + runStarted = !runStarted } // TODO: Close the connection after closing the splits file or receiving a disconnect signal // s.NWAAutoSplitter.Client.Close() + processElapsed := time.Since(processStart) // fmt.Println(processStart) // fmt.Println(processElapsed) + // fmt.Println(mil - processElapsed) time.Sleep(min(mil, max(0, mil-processElapsed))) } } func (s Splitters) processQUSB2SNES(commandDispatcher *dispatcher.Service) { - // // //QUSB2SNES example - // if QUSB2SNESAutoSplitterService != nil { - // // client.SetName("annelid") - - // // version, err := client.AppVersion() - // // fmt.Printf("Server version is %v\n", version) - - // // devices, err := client.ListDevice() - - // // if len(devices) != 1 { - // // if len(devices) == 0 { - // // return errors.New("no devices present") - // // } - // // return errors.Errorf("unexpected devices: %#v", devices) - // // } - // // device := devices[0] - // // fmt.Printf("Using device %v\n", device) - - // // client.Attach(device) - // // fmt.Println("Connected.") - - // // info, err := client.Info() - // // fmt.Printf("%#v\n", info) - - // // var autosplitter AutoSplitter = NewSuperMetroidAutoSplitter(settings) - - // // for { - // // summary, err := autosplitter.Update(client) - // // if summary.Start { - // // timer.Start() - // // } - // // if summary.Reset { - // // if resetTimerOnGameReset == true { - // // timer.Reset(true) - // // } - // // } - // // if summary.Split { - // // // IGT - // // timer.SetGameTime(*t) - // // // RTA - // // timer.Split() - // // } - - // // if ev == TimerReset { - // // // creates a new SNES state - // // autosplitter.ResetGameTracking() - // // if resetGameOnTimerReset == true { - // // client.Reset() - // // } - // // } - - // // time.Sleep(time.Duration(float64(time.Second) / pollingRate)) - // // } - // } + mil := 1 * time.Millisecond + + // Home Improvment test data + memData := []string{ + ("crates,0x1A8A,1"), + ("scene,0x161F,1"), + ("W2P2HP,0x1499,1"), + ("W2P1HP,0x1493,1"), + ("BossHP,0x1491,1"), + ("state,0x1400,1"), + ("gameplay,0x0AE5,1"), + ("substage,0x0AE3,1"), + ("stage,0x0AE1,1"), + ("scene2,0x0886,1"), + ("play_state,0x03B1,1"), + ("power_up,0x03AF,1"), + ("weapon,0x03CD,1"), + ("invul,0x1C05,1"), + ("FBossHP,0x149D,1"), + } + + startConditionImport := []string{ + ("start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + ("start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + } + + resetConditionImport := []string{ + ("cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0"), + ("tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0"), + ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0"), + } + + splitConditionImport := []string{ + ("1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13"), + ("1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13"), + ("1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13"), + ("1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13"), + ("1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13"), + ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), + ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), + ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), + ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0"), + ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), + ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), + ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), + ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), + ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), + ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), + ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), + ("4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13"), + ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), + } + + s.QUSB2SNESAutoSplitter.SetName("OpenSplit") + + version, _ := s.QUSB2SNESAutoSplitter.AppVersion() + fmt.Printf("Server version is %v\n", version) + + devices, _ := s.QUSB2SNESAutoSplitter.ListDevice() + + if len(devices) != 1 { + if len(devices) == 0 { + fmt.Printf("no devices present\n") + return + } + fmt.Printf("unexpected devices: %#v\n", devices) + return + } + device := devices[0] + fmt.Printf("Using device %v\n", device) + + s.QUSB2SNESAutoSplitter.Attach(device) + fmt.Println("Connected.") + + info, _ := s.QUSB2SNESAutoSplitter.Info() + fmt.Printf("%#v\n", info) + + var autosplitter = qusb2snes.NewQUSB2SNESAutoSplitter(memData, startConditionImport, resetConditionImport, splitConditionImport) + + splitCount := 0 + runStarted := false + for { + processStart := time.Now() + + summary, _ := autosplitter.Update(*s.QUSB2SNESAutoSplitter, splitCount) + + if summary.Start && !runStarted { + commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + runStarted = !runStarted + } + if summary.Split && runStarted { + // IGT + // timer.SetGameTime(*t) + // RTA + commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + splitCount++ + } + // need to get timer reset state + if summary.Reset && runStarted /*|| timer is reset*/ { + if s.ResetTimerOnGameReset { + commandDispatcher.Dispatch(dispatcher.RESET, nil) + } + if s.ResetGameOnTimerReset { + s.QUSB2SNESAutoSplitter.Reset() + } + autosplitter.ResetGameTracking() + splitCount = 0 + runStarted = !runStarted + } + // TODO: Close the connection after closing the splits file or receiving a disconnect signal + // s.QUSB2SNESAutoSplitter.Client.Close() + + processElapsed := time.Since(processStart) + fmt.Println(processStart) + fmt.Println(processElapsed) + fmt.Println(mil - processElapsed) + time.Sleep(min(mil, max(0, mil-processElapsed))) + } } From d15fac86b14bfe3c982bb8dfa4dfc6a5a443726e Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 26 Dec 2025 11:32:11 -0500 Subject: [PATCH 23/52] removed unused code, added func pointer, renamed memory struct, rewrote import function for new format, added start conditions to import/update/output, added soft and hard console reset options, rewrote update function for improved performance, rewrote/replaced findInSlice function, added hex to int converter --- autosplitters/NWA/nwa_client.go | 2 +- autosplitters/NWA/nwa_splitter.go | 605 +++++++++++++++++++++--------- 2 files changed, 422 insertions(+), 185 deletions(-) diff --git a/autosplitters/NWA/nwa_client.go b/autosplitters/NWA/nwa_client.go index e665bcb..2dd0397 100644 --- a/autosplitters/NWA/nwa_client.go +++ b/autosplitters/NWA/nwa_client.go @@ -210,7 +210,7 @@ func (c *NWASyncClient) getReply() (emulatorReply, error) { return nil, errors.New("invalid reply") } -// I think this would be used if I actually sent data +// This would be used if I actually sent data // func (c *NWASyncClient) sendData(data []byte) { // buf := make([]byte, 5) // size := len(data) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index ae77553..9c00753 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -3,7 +3,6 @@ package nwa // TODO: handle errors correctly import ( - "bytes" "encoding/binary" "fmt" "log" @@ -13,107 +12,240 @@ import ( // public type NWASplitter struct { - ResetTimerOnGameReset bool - Client NWASyncClient - nwaMemory []MemoryEntry - resetConditions [][]Element - splitConditions [][]Element + Client NWASyncClient + vars map[string]*memoryWatcher + data []byte + nwaMemory []memoryWatcher + startConditions []conditionList + resetConditions []conditionList + splitConditions []conditionList } -// type compare_type int - -// const ( -// ceqp compare_type = iota // current value equal to prior value -// ceqe // current value equal to expected value -// cnep // current value not equal to prior value -// cnee // current value not equal to expected value -// cgtp // current value greater than prior value -// cgte // current value greater than expected value -// cltp // current value less than than prior value -// clte // current value less than than expected value -// eeqc // expected value equal to current value -// eeqp // expected value equal to prior value -// enec // expected value not equal to current value -// enep // expected value not equal to prior value -// egtc // expected value greater than current value -// egtp // expected value greater than prior value -// eltc // expected value less than than current value -// eltp // expected value less than than prior value -// peqc // prior value equal to current value -// peqe // prior value equal to expected value -// pnec // prior value not equal to current value -// pnee // prior value not equal to expected value -// pgtc // prior value greater than current value -// pgte // prior value greater than expected value -// pltc // prior value less than than current value -// plte // prior value less than than expected value -// ) - -type Element struct { +type element struct { memoryEntryName string expectedValue *int compareType string + result compareFunc } -type MemoryEntry struct { - Name string - MemoryBank string - Address string - Size string +type compareFunc func(input string, prior *int, current *int, expected *int) bool + +type memoryWatcher struct { + name string + memoryBank string + address string + size string currentValue *int priorValue *int } +type conditionList struct { + Name string + memory []element +} + // Setup the memory map being read by the NWA splitter and the maps for the reset and split conditions -func (b *NWASplitter) MemAndConditionsSetup(memData []string, resetConditionImport []string, splitConditionImport []string) { - // Populate Start Condition List +func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImport []string, resetConditionImport []string, splitConditionImport []string) { + delimiter1 := "=" + delimiter2 := "≠" + delimiter3 := "<" + delimiter4 := ">" + compareStringCurrent := "current" + compareStringPrior := "prior" + hexPrefix := "0x" + b.data = make([]byte, 0x10000) + b.vars = map[string]*memoryWatcher{} + for _, p := range memData { - // create memory entry - memName := strings.Split(p, ",") - - entry := MemoryEntry{ - Name: memName[0], - MemoryBank: memName[1], - Address: memName[2], - Size: memName[3]} - // add memory map entries to nwaMemory list + mem := strings.Split(p, ",") + + entry := memoryWatcher{ + name: mem[0], + memoryBank: mem[1], + address: mem[2], + size: mem[3], + currentValue: new(int), + priorValue: new(int), + } + b.vars[mem[0]] = &entry b.nwaMemory = append(b.nwaMemory, entry) } + // Populate Start Condition List + for _, p := range startConditionImport { + var condition conditionList + // create elements + // add elements to reset condition list + startName := strings.Split(p, ":")[0] + startCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = startName + + for _, q := range startCon { + if strings.Contains(q, "&&") { + continue + } + + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + if strings.HasPrefix(compStrings[1], hexPrefix) { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + if strings.HasPrefix(compStrings[1], hexPrefix) { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + if strings.HasPrefix(compStrings[1], hexPrefix) { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } + + tempElement.result = compare + + condition.memory = append(condition.memory, tempElement) + } + // add condition lists to Start Conditions list + b.startConditions = append(b.startConditions, condition) + } + // Populate Reset Condition List for _, p := range resetConditionImport { - var condition []Element + var condition conditionList // create elements - // add elements to condition list + // add elements to reset condition list + resetName := strings.Split(p, ":")[0] resetCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = resetName + for _, q := range resetCon { - elements := strings.Split(q, ",") + if strings.Contains(q, "&&") { + continue + } - if len(elements) == 3 { - cT := elements[2] + var tempElement element - // convert hex string to int - num, err := strconv.ParseUint(elements[1], 16, 64) - integer := int(num) - if err != nil { - log.Fatalf("Failed to convert string to integer: %v", err) + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + if strings.HasPrefix(compStrings[1], hexPrefix) { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + if strings.HasPrefix(compStrings[1], hexPrefix) { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + if strings.HasPrefix(compStrings[1], hexPrefix) { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } } - intPtr := new(int) - *intPtr = integer - - condition = append(condition, Element{ - memoryEntryName: elements[0], - expectedValue: intPtr, - compareType: cT}) - } else if len(elements) == 2 { - cT := elements[1] - - condition = append(condition, Element{ - memoryEntryName: elements[0], - compareType: cT}) - } else { - fmt.Printf("Too many or too few conditions given: %#v\n", q) } + + tempElement.result = compare + + condition.memory = append(condition.memory, tempElement) } // add condition lists to Reset Conditions list b.resetConditions = append(b.resetConditions, condition) @@ -121,37 +253,87 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, resetConditionImpo // Populate Split Condition List for _, p := range splitConditionImport { - var condition []Element + var condition conditionList // create elements - // add elements to condition list + // add elements to split condition list + splitName := strings.Split(p, ":")[0] splitCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = splitName + for _, q := range splitCon { - elements := strings.Split(q, ",") + if strings.Contains(q, "&&") { + continue + } - if len(elements) == 3 { - cT := elements[2] + var tempElement element - num, err := strconv.ParseUint(elements[1], 16, 64) - integer := int(num) - if err != nil { - log.Fatalf("Failed to convert string to integer: %v", err) + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + if strings.HasPrefix(compStrings[1], hexPrefix) { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + if strings.HasPrefix(compStrings[1], hexPrefix) { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + if strings.HasPrefix(compStrings[1], hexPrefix) { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } } - intPtr := new(int) - *intPtr = integer - - condition = append(condition, Element{ - memoryEntryName: elements[0], - expectedValue: intPtr, - compareType: cT}) - } else if len(elements) == 2 { - cT := elements[1] - - condition = append(condition, Element{ - memoryEntryName: elements[0], - compareType: cT}) - } else { - fmt.Printf("Too many or too few conditions given: %#v\n", q) } + + tempElement.result = compare + + condition.memory = append(condition.memory, tempElement) } // add condition lists to Split Conditions list b.splitConditions = append(b.splitConditions, condition) @@ -214,123 +396,178 @@ func (b *NWASplitter) CoreMemories() { fmt.Printf("%#v\n", summary) } -// currently only supports 1 byte reads -func (b *NWASplitter) Update() (nwaSummary, error) { +func (b *NWASplitter) SoftResetConsole() { + cmd := "EMULATION_RESET" + summary, err := b.Client.ExecuteCommand(cmd, nil) + if err != nil { + panic(err) + } + fmt.Printf("%#v\n", summary) +} + +func (b *NWASplitter) HardResetConsole() { + // cmd := "EMULATION_STOP" + cmd := "EMULATION_RELOAD" + summary, err := b.Client.ExecuteCommand(cmd, nil) + if err != nil { + panic(err) + } + fmt.Printf("%#v\n", summary) +} + +// currently only suppports 1 memory source at a time +// likely WRAM for SNES and RAM for NES +func (b *NWASplitter) Update(splitIndex int) (nwaSummary, error) { + cmd := "CORE_READ" - for i, p := range b.nwaMemory { - args := p.MemoryBank + ";" + p.Address + ";" + p.Size - summary, err := b.Client.ExecuteCommand(cmd, &args) - if err != nil { - return nwaSummary{}, err - } - fmt.Printf("%#v\n", summary) - - b.nwaMemory[i].priorValue = b.nwaMemory[i].currentValue - switch v := summary.(type) { - case []byte: - - // need to handle more than 1 byte at a time - // if len(v) == 1 { - // val := int(v[0]) - // b.nwaMemory[i].currentValue = &val - // } else if len(v) > 1 { - // length 1 - var temp_int uint8 - //length 2 - // var temp_int uint16 - // length 4 - // var temp_int uint32 - // length 8 - // var temp_int uint64 - err := binary.Read(bytes.NewReader(v), binary.LittleEndian, &temp_int) - if err != nil { - fmt.Println("Error reading binary data:", err) + domain := b.nwaMemory[0].memoryBank + var requestString string + + for _, watcher := range b.nwaMemory { + requestString += ";" + watcher.address + ";" + watcher.size + *watcher.priorValue = *watcher.currentValue + } + + args := domain + requestString + summary, err := b.Client.ExecuteCommand(cmd, &args) + if err != nil { + return nwaSummary{}, err + } + fmt.Printf("%#v\n", summary) + + switch v := summary.(type) { + case []byte: + // update memoryWatcher with data + runningTotal := 0 + for _, watcher := range b.nwaMemory { + size, _ := strconv.Atoi(watcher.size) + switch size { + case 1: + *watcher.currentValue = int(v[runningTotal]) + runningTotal += size + case 2: + *watcher.currentValue = int(binary.LittleEndian.Uint16(v[runningTotal : runningTotal+size])) + runningTotal += size + case 3: + fallthrough + case 4: + *watcher.currentValue = int(binary.LittleEndian.Uint32(v[runningTotal : runningTotal+size])) + runningTotal += size + case 5: + fallthrough + case 6: + fallthrough + case 7: + fallthrough + case 8: + *watcher.currentValue = int(binary.LittleEndian.Uint64(v[runningTotal : runningTotal+size])) + runningTotal += size } - integer := int(temp_int) - b.nwaMemory[i].currentValue = &integer - // } - case NWAError: - fmt.Printf("%#v\n", v) - default: - fmt.Printf("%#v\n", v) } + + case NWAError: + fmt.Printf("%#v\n", v) + default: + fmt.Printf("%#v\n", v) } + start := b.start() reset := b.reset() - split := b.split() + split := b.split(splitIndex) return nwaSummary{ + Start: start, Reset: reset, Split: split, }, nil } -// private type nwaSummary struct { + Start bool Reset bool Split bool } -// Checks conditions and returns reset state -func (b *NWASplitter) reset() bool { +// Checks conditions and returns start state +func (b *NWASplitter) start() bool { fmt.Printf("Checking reset state\n") - if b.ResetTimerOnGameReset { - for _, p := range b.resetConditions { - resetState := true - var tempstate bool - for _, q := range p { - index, found := b.findInSlice(b.nwaMemory, q.memoryEntryName) - if found { - tempstate = compare(q.compareType, b.nwaMemory[index].currentValue, b.nwaMemory[index].priorValue, q.expectedValue) - } else { - fmt.Printf("How did you get here?\n") - } - resetState = resetState && tempstate - } - if resetState { - fmt.Printf("Time to reset\n") - return true - } + startState := true + var tempstate bool + + for _, p := range b.startConditions { + for _, q := range p.memory { + watcher := findMemoryWatcher(b.nwaMemory, q.memoryEntryName) + tempstate = q.result(q.compareType, watcher.priorValue, watcher.currentValue, q.expectedValue) + startState = startState && tempstate + } + if startState { + fmt.Printf("Start: %#v\n", p.Name) + return true } - return false - } else { - return false } + return false } -// Checks conditions and returns split state -func (b *NWASplitter) split() bool { - for _, p := range b.splitConditions { - fmt.Printf("Checking split state\n") - splitState := true - var tempstate bool - for _, q := range p { - index, found := b.findInSlice(b.nwaMemory, q.memoryEntryName) - if found { - tempstate = compare(q.compareType, b.nwaMemory[index].currentValue, b.nwaMemory[index].priorValue, q.expectedValue) - } else { - fmt.Printf("How did you get here?\n") - } - splitState = splitState && tempstate +// Checks conditions and returns reset state +func (b *NWASplitter) reset() bool { + fmt.Printf("Checking reset state\n") + resetState := true + var tempstate bool + + for _, p := range b.resetConditions { + for _, q := range p.memory { + watcher := findMemoryWatcher(b.nwaMemory, q.memoryEntryName) + tempstate = q.result(q.compareType, watcher.priorValue, watcher.currentValue, q.expectedValue) + resetState = resetState && tempstate } - if splitState { - fmt.Printf("Time to split\n") + if resetState { + fmt.Printf("Reset: %#v\n", p.Name) return true } } return false } -func (b *NWASplitter) findInSlice(slice []MemoryEntry, target string) (int, bool) { - for i, v := range slice { - if v.Name == target { - return i, true // Return index and true if found +// Checks conditions and returns split state +func (b *NWASplitter) split(split int) bool { + fmt.Printf("Checking split state\n") + splitState := true + var tempstate bool + + for _, q := range b.splitConditions[split].memory { + watcher := findMemoryWatcher(b.nwaMemory, q.memoryEntryName) + tempstate = q.result(q.compareType, watcher.priorValue, watcher.currentValue, q.expectedValue) + splitState = splitState && tempstate + } + if splitState { + fmt.Printf("Split: %#v\n", b.splitConditions[split].Name) + return true + } + return false +} + +// private +func findMemoryWatcher(memInfo []memoryWatcher, targetWatcher string) *memoryWatcher { + for _, watcher := range memInfo { + if watcher.name == targetWatcher { + return &watcher } } - return -1, false // Return -1 and false if not found + return nil +} + +// convert hex string to int +func hexToInt(hex string) *int { + num, err := strconv.ParseUint(hex, 0, 64) + if err != nil { + log.Fatalf("Failed to convert string to integer: %v", err) + return nil + } + integer := int(num) + return &integer } -func compare(input string, current *int, prior *int, expected *int) bool { +func compare(input string, prior *int, current *int, expected *int) bool { switch input { case "ceqp": fallthrough From a282943a31f5d944d4b4d789d4ed0f3b659ff2c8 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 26 Dec 2025 11:34:16 -0500 Subject: [PATCH 24/52] example autosplitter files in new format --- ...ny% (No WW, NES NTSC-US, Rash (Green)).nwa | 36 +++++---- .../Home Improvement (SNES) - Any%.nwa | 77 +++++++++++-------- .../Home Improvement (SNES) - Any%.qusb2snes | 51 ++++++++++++ 3 files changed, 114 insertions(+), 50 deletions(-) create mode 100644 autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa index 9c0b83c..41c9f96 100644 --- a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa @@ -1,24 +1,28 @@ ResetTimerOnGameReset = true +ResetGameOnTimerReset = false IP = 0.0.0.0 Port = 48879 #memory (Do not combine memory or it will treated as 1 giant value) level,RAM,$0010,1 -#reset (some games might have multiple reset conditions) (expectedValue is optional) -reset:level,0,eeqc level,0,pnee +#start +start:level,prior=0x0 && level,current=0x1 -#split (some games might have multiple split conditions) (expectedValue is optional) -start:level,0,peqe level,1,ceqe -level:level,255,peqe level,2,ceqe -level:level,255,peqe level,3,ceqe -level:level,255,peqe level,4,ceqe -level:level,255,peqe level,5,ceqe -level:level,255,peqe level,6,ceqe -level:level,255,peqe level,7,ceqe -level:level,255,peqe level,8,ceqe -level:level,255,peqe level,9,ceqe -level:level,255,peqe level,10,ceq -level:level,255,peqe level,11,ceq -level:level,255,peqe level,12,ceq -level:level,255,peqe level,13,ceq +#reset (some games might have multiple reset conditions) (expectedValue is optional) (values MUST always be last) +reset:level,current=0x0 && level,prior≠0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) +#= ≠ < > +level2:level,prior=0xFF && level,current=0x2 +level3:level,prior=0xFF && level,current=0x3 +level4:level,prior=0xFF && level,current=0x4 +level5:level,prior=0xFF && level,current=0x5 +level6:level,prior=0xFF && level,current=0x6 +level7:level,prior=0xFF && level,current=0x7 +level8:level,prior=0xFF && level,current=0x8 +level9:level,prior=0xFF && level,current=0x9 +level10:level,prior=0xFF && level,current=0xA +level11:level,prior=0xFF && level,current=0xB +level12:level,prior=0xFF && level,current=0xC +level13:level,prior=0xFF && level,current=0xD diff --git a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa index 9a5be67..0916953 100644 --- a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa +++ b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa @@ -1,44 +1,53 @@ ResetTimerOnGameReset = true +ResetGameOnTimerReset = false IP = 0.0.0.0 Port = 48879 #memory (Do not combine memory or it will treated as 1 combined value) -act,WRAM,$000AE1,1 #0-3 -level,WRAM,$000AE3,1 #0-4 +stage,WRAM,$000AE1,1 #0-3 +substage,WRAM,$000AE3,1 #0-4 crates,WRAM,$001A8A,1 #5,6,7,8 -invuln,$001C05,1 #02 -play_state,$0003B1,1 #00 - dead/complete, 01 - alive/playable -scene,$000886,1 #03 - tool scene, 04 - win screen -state,$00AE5,1 #11 - menus/loading, 13 - gameplay -cutscene,$001400,1 #d8 - bonus countdown active, d0 - between act cutscene -first_boss_HP,$001491,1 #63 -second_boss_P1_HP,$001493,1 #14 -second_boss_P2_HP,$001499,1 # -second_boss_P2_alt_HP,$001491,1 # -third_boss_HP,$001491,1 # -final_boss_HP,$00149D,1 #Starts at 64, Dies at 1, switches to FF after explosion +invul,WRAM,$001C05,1 #02 +play_state,WRAM,$0003B1,1 #00 - dead/complete, 01 - alive/playable +scene2,WRAM,$000886,1 #03 - tool scene, 04 - win screen +scene,WRAM,$00161F,1 +gameplay,WRAM,$00AE5,1 #11 - menus/loading, 13 - gameplay +state,WRAM,$001400,1 #d8 - bonus countdown active, d0 - between act cutscene +BossHP,WRAM,$001491,1 #63 +W2P1HP,WRAM,$001493,1 #14 +W2P2HP,WRAM,$001499,1 # +FBossHP,WRAM,$00149D,1 #Starts at 64, Dies at 1, switches to FF after explosion +power_up,WRAM,$0003AF,1 +weapon,WRAM,$0003CD,1 + +#start (some games might have multiple start conditions) (expectedValue is optional) +start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0 +start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0 #reset (some games might have multiple reset conditions) (expectedValue is optional) -reset:level,0,eeqc level,0,pnee +cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0 +tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0 +level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0 #split (some games might have multiple split conditions) (expectedValue is optional) -act1_level1:level,0,peqe level,1,ceqe -act1_level2:level,255,peqe level,2,ceqe -act1_level3:level,255,peqe level,3,ceqe -act1_level4:level,255,peqe level,4,ceqe -act1_boss:level,255,peqe level,5,ceqe -act2_level1:level,0,peqe level,1,ceqe -act2_level2:level,255,peqe level,2,ceqe -act2_level3:level,255,peqe level,3,ceqe -act2_level4:level,255,peqe level,4,ceqe -act2_boss:level,255,peqe level,5,ceqe -act3_level1:level,0,peqe level,1,ceqe -act3_level2:level,255,peqe level,2,ceqe -act3_level3:level,255,peqe level,3,ceqe -act3_level4:level,255,peqe level,4,ceqe -act3_boss:level,255,peqe level,5,ceqe -act4_level1:level,0,peqe level,1,ceqe -act4_level2:level,255,peqe level,2,ceqe -act4_level3:level,255,peqe level,3,ceqe -act4_level4:level,255,peqe level,4,ceqe -act4_boss:level,255,peqe level,5,ceqe +#= ≠ < > +1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13 +1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13 +1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13 +1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 +1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0 +2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13 +2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13 +2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13 +2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 +2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0 +3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13 +3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13 +3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13 +3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 +3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0 +4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13 +4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13 +4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13 +4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 +4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF diff --git a/autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes b/autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes new file mode 100644 index 0000000..3c4f690 --- /dev/null +++ b/autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes @@ -0,0 +1,51 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false + +#memory (Do not combine memory or it will treated as 1 combined value) +stage,$000AE1,1 #0-3 +substage,$000AE3,1 #0-4 +crates,$001A8A,1 #5,6,7,8 +invul,$001C05,1 #02 +play_state,$0003B1,1 #00 - dead/complete, 01 - alive/playable +scene2,$000886,1 #03 - tool scene, 04 - win screen +scene,$00161F,1 +gameplay,$00AE5,1 #11 - menus/loading, 13 - gameplay +state,$001400,1 #d8 - bonus countdown active, d0 - between act cutscene +BossHP,$001491,1 #63 +W2P1HP,$001493,1 #14 +W2P2HP,$001499,1 # +FBossHP,$00149D,1 #Starts at 64, Dies at 1, switches to FF after explosion +power_up,$0003AF,1 +weapon,$0003CD,1 + +#start +start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0 +start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) +cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0 +tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0 +level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) +#= ≠ < > +1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13 +1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13 +1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13 +1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 +1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0 +2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13 +2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13 +2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13 +2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 +2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0 +3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13 +3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13 +3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13 +3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 +3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0 +4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13 +4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13 +4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13 +4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 +4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF From a769fc1116a24d7656a42cef4a5c0eb20a0d19ea Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 26 Dec 2025 11:36:47 -0500 Subject: [PATCH 25/52] removed unneeded space entry and imports, fixed space entry in USB2SnesQuery object, disabled unused functions --- autosplitters/QUSB2SNES/qusb2snes_client.go | 250 ++++++++++---------- 1 file changed, 128 insertions(+), 122 deletions(-) diff --git a/autosplitters/QUSB2SNES/qusb2snes_client.go b/autosplitters/QUSB2SNES/qusb2snes_client.go index d9cb2cf..55e745f 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_client.go +++ b/autosplitters/QUSB2SNES/qusb2snes_client.go @@ -4,10 +4,8 @@ package qusb2snes import ( "encoding/json" - "errors" "fmt" "net/url" - "strconv" "github.com/gorilla/websocket" ) @@ -57,14 +55,12 @@ type Space int const ( None Space = iota SNES - CMD ) func (s Space) String() string { return [...]string{ "None", "SNES", - "CMD", }[s] } @@ -136,11 +132,21 @@ func (sc *SyncClient) sendCommandWithSpace(command Command, space Space, args [] // s := space.String() // nspace = &s // } - query := USB2SnesQuery{ - Opcode: command.String(), - Space: space.String(), - Flags: []string{}, - Operands: args, + var query USB2SnesQuery + if space == SNES { + query = USB2SnesQuery{ + Opcode: command.String(), + Space: space.String(), + Flags: []string{}, + Operands: args, + } + } else { + query = USB2SnesQuery{ + Opcode: command.String(), + // Space: space.String(), + Flags: []string{}, + Operands: args, + } } jsonData, err := json.Marshal(query) if err != nil { @@ -237,119 +243,119 @@ func (sc *SyncClient) Reset() error { return sc.sendCommand(Reset, []string{}) } -func (sc *SyncClient) Menu() error { - return sc.sendCommand(Menu, []string{}) -} - -func (sc *SyncClient) Boot(toboot string) error { - return sc.sendCommand(Boot, []string{toboot}) -} - -func (sc *SyncClient) Ls(path string) ([]USB2SnesFileInfo, error) { - err := sc.sendCommand(List, []string{path}) - if err != nil { - return nil, err - } - usbreply, err := sc.getReply() - if err != nil { - return nil, err - } - vecInfo := usbreply.Results - var toret []USB2SnesFileInfo - for i := 0; i < len(vecInfo); i += 2 { - if i+1 >= len(vecInfo) { - break - } - fileType := Dir - if vecInfo[i] == "1" { - fileType = File - } - info := USB2SnesFileInfo{ - FileType: fileType, - Name: vecInfo[i+1], - } - toret = append(toret, info) - } - return toret, nil -} - -func (sc *SyncClient) SendFile(path string, data []byte) error { - err := sc.sendCommand(PutFile, []string{path, fmt.Sprintf("%x", len(data))}) - if err != nil { - return err - } - chunkSize := 1024 - for start := 0; start < len(data); start += chunkSize { - stop := start + chunkSize - if stop > len(data) { - stop = len(data) - } - err = sc.Client.WriteMessage(websocket.BinaryMessage, data[start:stop]) - if err != nil { - return err - } - } - return nil -} - -func (sc *SyncClient) getFile(path string) ([]byte, error) { - err := sc.sendCommand(GetFile, []string{path}) - if err != nil { - return nil, err - } - reply, err := sc.getReply() - if err != nil { - return nil, err - } - if len(reply.Results) == 0 { - return nil, errors.New("no results in reply") - } - stringHex := reply.Results[0] - size, err := strconv.ParseUint(stringHex, 16, 0) - if err != nil { - return nil, err - } - data := make([]byte, 0, size) - for { - _, msgData, err := sc.Client.ReadMessage() - if err != nil { - return nil, err - } - // In Rust code, it expects binary message - // Here, msgData is []byte already - data = append(data, msgData...) - if len(data) == int(size) { - break - } - } - return data, nil -} - -func (sc *SyncClient) removePath(path string) error { - return sc.sendCommand(Remove, []string{path}) -} - -func (sc *SyncClient) getAddress(address uint32, size int) ([]byte, error) { - err := sc.sendCommandWithSpace(GetAddress, SNES, []string{ - fmt.Sprintf("%x", address), - fmt.Sprintf("%x", size), - }) - if err != nil { - return nil, err - } - data := make([]byte, 0, size) - for { - _, msgData, err := sc.Client.ReadMessage() - if err != nil { - return nil, err - } - data = append(data, msgData...) - if len(data) == size { - break - } - } - return data, nil -} +// func (sc *SyncClient) Menu() error { +// return sc.sendCommand(Menu, []string{}) +// } + +// func (sc *SyncClient) Boot(toboot string) error { +// return sc.sendCommand(Boot, []string{toboot}) +// } + +// func (sc *SyncClient) Ls(path string) ([]USB2SnesFileInfo, error) { +// err := sc.sendCommand(List, []string{path}) +// if err != nil { +// return nil, err +// } +// usbreply, err := sc.getReply() +// if err != nil { +// return nil, err +// } +// vecInfo := usbreply.Results +// var toret []USB2SnesFileInfo +// for i := 0; i < len(vecInfo); i += 2 { +// if i+1 >= len(vecInfo) { +// break +// } +// fileType := Dir +// if vecInfo[i] == "1" { +// fileType = File +// } +// info := USB2SnesFileInfo{ +// FileType: fileType, +// Name: vecInfo[i+1], +// } +// toret = append(toret, info) +// } +// return toret, nil +// } + +// func (sc *SyncClient) SendFile(path string, data []byte) error { +// err := sc.sendCommand(PutFile, []string{path, fmt.Sprintf("%x", len(data))}) +// if err != nil { +// return err +// } +// chunkSize := 1024 +// for start := 0; start < len(data); start += chunkSize { +// stop := start + chunkSize +// if stop > len(data) { +// stop = len(data) +// } +// err = sc.Client.WriteMessage(websocket.BinaryMessage, data[start:stop]) +// if err != nil { +// return err +// } +// } +// return nil +// } + +// func (sc *SyncClient) getFile(path string) ([]byte, error) { +// err := sc.sendCommand(GetFile, []string{path}) +// if err != nil { +// return nil, err +// } +// reply, err := sc.getReply() +// if err != nil { +// return nil, err +// } +// if len(reply.Results) == 0 { +// return nil, errors.New("no results in reply") +// } +// stringHex := reply.Results[0] +// size, err := strconv.ParseUint(stringHex, 16, 0) +// if err != nil { +// return nil, err +// } +// data := make([]byte, 0, size) +// for { +// _, msgData, err := sc.Client.ReadMessage() +// if err != nil { +// return nil, err +// } +// // In Rust code, it expects binary message +// // Here, msgData is []byte already +// data = append(data, msgData...) +// if len(data) == int(size) { +// break +// } +// } +// return data, nil +// } + +// func (sc *SyncClient) removePath(path string) error { +// return sc.sendCommand(Remove, []string{path}) +// } + +// func (sc *SyncClient) getAddress(address uint32, size int) ([]byte, error) { +// err := sc.sendCommandWithSpace(GetAddress, SNES, []string{ +// fmt.Sprintf("%x", address), +// fmt.Sprintf("%x", size), +// }) +// if err != nil { +// return nil, err +// } +// data := make([]byte, 0, size) +// for { +// _, msgData, err := sc.Client.ReadMessage() +// if err != nil { +// return nil, err +// } +// data = append(data, msgData...) +// if len(data) == size { +// break +// } +// } +// return data, nil +// } func (sc *SyncClient) getAddresses(pairs [][2]int) ([][]byte, error) { args := make([]string, 0, len(pairs)*2) From 7253acfc40940e0aea80f778c80f3e4bcd760e57 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 26 Dec 2025 11:38:17 -0500 Subject: [PATCH 26/52] implements, fixed, and rewrote entire class to enable QUSB splitting functionality --- autosplitters/QUSB2SNES/qusb2snes_splitter.go | 1921 ++++++----------- 1 file changed, 613 insertions(+), 1308 deletions(-) diff --git a/autosplitters/QUSB2SNES/qusb2snes_splitter.go b/autosplitters/QUSB2SNES/qusb2snes_splitter.go index e97fb77..19856bf 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_splitter.go +++ b/autosplitters/QUSB2SNES/qusb2snes_splitter.go @@ -1,1400 +1,705 @@ package qusb2snes -// TODO: handle errors correctly - import ( "fmt" - "math" - "time" -) - -var ( - roomIDEnum = map[string]uint32{ - "landingSite": 0x91F8, - "crateriaPowerBombRoom": 0x93AA, - "westOcean": 0x93FE, - "elevatorToMaridia": 0x94CC, - "crateriaMoat": 0x95FF, - "elevatorToCaterpillar": 0x962A, - "gauntletETankRoom": 0x965B, - "climb": 0x96BA, - "pitRoom": 0x975C, - "elevatorToMorphBall": 0x97B5, - "bombTorizo": 0x9804, - "terminator": 0x990D, - "elevatorToGreenBrinstar": 0x9938, - "greenPirateShaft": 0x99BD, - "crateriaSupersRoom": 0x99F9, - "theFinalMissile": 0x9A90, - "greenBrinstarMainShaft": 0x9AD9, - "sporeSpawnSuper": 0x9B5B, - "earlySupers": 0x9BC8, - "brinstarReserveRoom": 0x9C07, - "bigPink": 0x9D19, - "sporeSpawnKeyhunter": 0x9D9C, - "sporeSpawn": 0x9DC7, - "pinkBrinstarPowerBombRoom": 0x9E11, - "greenHills": 0x9E52, - "noobBridge": 0x9FBA, - "morphBall": 0x9E9F, - "blueBrinstarETankRoom": 0x9F64, - "etecoonETankRoom": 0xA011, - "etecoonSuperRoom": 0xA051, - "waterway": 0xA0D2, - "alphaMissileRoom": 0xA107, - "hopperETankRoom": 0xA15B, - "billyMays": 0xA1D8, - "redTower": 0xA253, - "xRay": 0xA2CE, - "caterpillar": 0xA322, - "betaPowerBombRoom": 0xA37C, - "alphaPowerBombsRoom": 0xA3AE, - "bat": 0xA3DD, - "spazer": 0xA447, - "warehouseETankRoom": 0xA4B1, - "warehouseZeela": 0xA471, - "warehouseKiHunters": 0xA4DA, - "kraidEyeDoor": 0xA56B, - "kraid": 0xA59F, - "statuesHallway": 0xA5ED, - "statues": 0xA66A, - "warehouseEntrance": 0xA6A1, - "varia": 0xA6E2, - "cathedral": 0xA788, - "businessCenter": 0xA7DE, - "iceBeam": 0xA890, - "crumbleShaft": 0xA8F8, - "crocomireSpeedway": 0xA923, - "crocomire": 0xA98D, - "hiJump": 0xA9E5, - "crocomireEscape": 0xAA0E, - "hiJumpShaft": 0xAA41, - "postCrocomirePowerBombRoom": 0xAADE, - "cosineRoom": 0xAB3B, - "preGrapple": 0xAB8F, - "grapple": 0xAC2B, - "norfairReserveRoom": 0xAC5A, - "greenBubblesRoom": 0xAC83, - "bubbleMountain": 0xACB3, - "speedBoostHall": 0xACF0, - "speedBooster": 0xAD1B, - "singleChamber": 0xAD5E, // Exit room from Lower Norfair, also on the path to Wave - "doubleChamber": 0xADAD, - "waveBeam": 0xADDE, - "volcano": 0xAE32, - "kronicBoost": 0xAE74, - "magdolliteTunnel": 0xAEB4, - "lowerNorfairElevator": 0xAF3F, - "risingTide": 0xAFA3, - "spikyAcidSnakes": 0xAFFB, - "acidStatue": 0xB1E5, - "mainHall": 0xB236, // First room in Lower Norfair - "goldenTorizo": 0xB283, - "ridley": 0xB32E, - "lowerNorfairFarming": 0xB37A, - "mickeyMouse": 0xB40A, - "pillars": 0xB457, - "writg": 0xB4AD, - "amphitheatre": 0xB4E5, - "lowerNorfairSpringMaze": 0xB510, - "lowerNorfairEscapePowerBombRoom": 0xB55A, - "redKiShaft": 0xB585, - "wasteland": 0xB5D5, - "metalPirates": 0xB62B, - "threeMusketeers": 0xB656, - "ridleyETankRoom": 0xB698, - "screwAttack": 0xB6C1, - "lowerNorfairFireflea": 0xB6EE, - "bowling": 0xC98E, - "wreckedShipEntrance": 0xCA08, - "attic": 0xCA52, - "atticWorkerRobotRoom": 0xCAAE, - "wreckedShipMainShaft": 0xCAF6, - "wreckedShipETankRoom": 0xCC27, - "basement": 0xCC6F, // Basement of Wrecked Ship - "phantoon": 0xCD13, - "wreckedShipLeftSuperRoom": 0xCDA8, - "wreckedShipRightSuperRoom": 0xCDF1, - "gravity": 0xCE40, - "glassTunnel": 0xCEFB, - "mainStreet": 0xCFC9, - "mamaTurtle": 0xD055, - "wateringHole": 0xD13B, - "beach": 0xD1DD, - "plasmaBeam": 0xD2AA, - "maridiaElevator": 0xD30B, - "plasmaSpark": 0xD340, - "toiletBowl": 0xD408, - "oasis": 0xD48E, - "leftSandPit": 0xD4EF, - "rightSandPit": 0xD51E, - "aqueduct": 0xD5A7, - "butterflyRoom": 0xD5EC, - "botwoonHallway": 0xD617, - "springBall": 0xD6D0, - "precious": 0xD78F, - "botwoonETankRoom": 0xD7E4, - "botwoon": 0xD95E, - "spaceJump": 0xD9AA, - "westCactusAlley": 0xD9FE, - "draygon": 0xDA60, - "tourianElevator": 0xDAAE, - "metroidOne": 0xDAE1, - "metroidTwo": 0xDB31, - "metroidThree": 0xDB7D, - "metroidFour": 0xDBCD, - "dustTorizo": 0xDC65, - "tourianHopper": 0xDC19, - "tourianEyeDoor": 0xDDC4, - "bigBoy": 0xDCB1, - "motherBrain": 0xDD58, - "rinkaShaft": 0xDDF3, - "tourianEscape4": 0xDEDE, - "ceresElevator": 0xDF45, - "flatRoom": 0xE06B, // Placeholder name for the flat room in Ceres Station - "ceresRidley": 0xE0B5, - } - mapInUseEnum = map[string]uint32{ - "crateria": 0x0, - "brinstar": 0x1, - "norfair": 0x2, - "wreckedShip": 0x3, - "maridia": 0x4, - "tourian": 0x5, - "ceres": 0x6, - } - gameStateEnum = map[string]uint32{ - "normalGameplay": 0x8, - "doorTransition": 0xB, - "startOfCeresCutscene": 0x20, - "preEndCutscene": 0x26, // briefly at this value during the black screen transition after the ship fades out - "endCutscene": 0x27, - } - unlockFlagEnum = map[string]uint32{ - // First item byte - "variaSuit": 0x1, - "springBall": 0x2, - "morphBall": 0x4, - "screwAttack": 0x8, - "gravSuit": 0x20, - // Second item byte - "hiJump": 0x1, - "spaceJump": 0x2, - "bomb": 0x10, - "speedBooster": 0x20, - "grapple": 0x40, - "xray": 0x80, - // Beams - "wave": 0x1, - "ice": 0x2, - "spazer": 0x4, - "plasma": 0x8, - // Charge - "chargeBeam": 0x10, - } - motherBrainMaxHPEnum = map[string]uint32{ - "phase1": 0xBB8, // 3000 - "phase2": 0x4650, // 18000 - "phase3": 0x8CA0, // 36000 - } - eventFlagEnum = map[string]uint32{ - "zebesAblaze": 0x40, - "tubeBroken": 0x8, - } - bossFlagEnum = map[string]uint32{ - // Crateria - "bombTorizo": 0x4, - // Brinstar - "sporeSpawn": 0x2, - "kraid": 0x1, - // Norfair - "ridley": 0x1, - "crocomire": 0x2, - "goldenTorizo": 0x4, - // Wrecked Ship - "phantoon": 0x1, - // Maridia - "draygon": 0x1, - "botwoon": 0x2, - // Tourian - "motherBrain": 0x2, - // Ceres - "ceresRidley": 0x1, - } + "log" + "strconv" + "strings" ) -type Settings struct { - data map[string]struct { - value bool - parent *string - } - modifiedAfterCreation bool - // mu sync.RWMutex -} +// TODO: handle errors correctly -func NewSettings() *Settings { - s := &Settings{ - data: make(map[string]struct { - value bool - parent *string - }), - modifiedAfterCreation: false, - } - // Split on Missiles, Super Missiles, and Power Bombs - s.Insert("ammoPickups", true) - // Split on the first Missile pickup - s.InsertWithParent("firstMissile", false, "ammoPickups") - // Split on each Missile upgrade - s.InsertWithParent("allMissiles", false, "ammoPickups") - // Split on specific Missile Pack locations - s.InsertWithParent("specificMissiles", false, "ammoPickups") - // Split on Crateria Missile Pack locations - s.InsertWithParent("crateriaMissiles", false, "specificMissiles") - // Split on picking up the Missile Pack located at the bottom left of the West Ocean - s.InsertWithParent("oceanBottomMissiles", false, "crateriaMissiles") - // Split on picking up the Missile Pack located in the ceiling tile in West Ocean - s.InsertWithParent("oceanTopMissiles", false, "crateriaMissiles") - // Split on picking up the Missile Pack located in the Morphball maze section of West Ocean - s.InsertWithParent("oceanMiddleMissiles", false, "crateriaMissiles") - // Split on picking up the Missile Pack in The Moat, also known as The Lake - s.InsertWithParent("moatMissiles", false, "crateriaMissiles") - // Split on picking up the Missile Pack in the Pit Room - s.InsertWithParent("oldTourianMissiles", false, "crateriaMissiles") - // Split on picking up the right side Missile Pack at the end of Gauntlet(Green Pirates Shaft) - s.InsertWithParent("gauntletRightMissiles", false, "crateriaMissiles") - // Split on picking up the left side Missile Pack at the end of Gauntlet(Green Pirates Shaft) - s.InsertWithParent("gauntletLeftMissiles", false, "crateriaMissiles") - // Split on picking up the Missile Pack located in The Final Missile - s.InsertWithParent("dentalPlan", false, "crateriaMissiles") - // Split on Brinstar Missile Pack locations - s.InsertWithParent("brinstarMissiles", false, "specificMissiles") - // Split on picking up the Missile Pack located below the crumble bridge in the Early Supers Room - s.InsertWithParent("earlySuperBridgeMissiles", false, "brinstarMissiles") - // Split on picking up the first Missile Pack behind the Brinstar Reserve Tank - s.InsertWithParent("greenBrinstarReserveMissiles", false, "brinstarMissiles") - // Split on picking up the second Missile Pack behind the Brinstar Reserve Tank Room - s.InsertWithParent("greenBrinstarExtraReserveMissiles", false, "brinstarMissiles") - // Split on picking up the Missile Pack located left of center in Big Pink - s.InsertWithParent("bigPinkTopMissiles", false, "brinstarMissiles") - // Split on picking up the Missile Pack located at the bottom left of Big Pink - s.InsertWithParent("chargeMissiles", false, "brinstarMissiles") - // Split on picking up the Missile Pack in Green Hill Zone - s.InsertWithParent("greenHillsMissiles", false, "brinstarMissiles") - // Split on picking up the Missile Pack in the Blue Brinstar Energy Tank Room - s.InsertWithParent("blueBrinstarETankMissiles", false, "brinstarMissiles") - // Split on picking up the first Missile Pack of the game(First Missile Room) - s.InsertWithParent("alphaMissiles", false, "brinstarMissiles") - // Split on picking up the Missile Pack located on the pedestal in Billy Mays' Room - s.InsertWithParent("billyMaysMissiles", false, "brinstarMissiles") - // Split on picking up the Missile Pack located in the floor of Billy Mays' Room - s.InsertWithParent("butWaitTheresMoreMissiles", false, "brinstarMissiles") - // Split on picking up the Missile Pack in the Alpha Power Bombs Room - s.InsertWithParent("redBrinstarMissiles", false, "brinstarMissiles") - // Split on picking up the Missile Pack in the Warehouse Kihunter Room - s.InsertWithParent("warehouseMissiles", false, "brinstarMissiles") - // Split on Norfair Missile Pack locations - s.InsertWithParent("norfairMissiles", false, "specificMissiles") - // Split on picking up the Missile Pack in Cathedral - s.InsertWithParent("cathedralMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in Crumble Shaft - s.InsertWithParent("crumbleShaftMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in Crocomire Escape - s.InsertWithParent("crocomireEscapeMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in the Hi Jump Energy Tank Room - s.InsertWithParent("hiJumpMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in the Post Crocomire Missile Room, also known as Cosine Room - s.InsertWithParent("postCrocomireMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in the Post Crocomire Jump Room - s.InsertWithParent("grappleMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in the Norfair Reserve Tank Room - s.InsertWithParent("norfairReserveMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in the Green Bubbles Missile Room - s.InsertWithParent("greenBubblesMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in Bubble Mountain - s.InsertWithParent("bubbleMountainMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in Speed Booster Hall - s.InsertWithParent("speedBoostMissiles", false, "norfairMissiles") - // Split on picking up the Wave Missile Pack in Double Chamber - s.InsertWithParent("waveMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in the Golden Torizo's Room - s.InsertWithParent("goldTorizoMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in the Mickey Mouse Room - s.InsertWithParent("mickeyMouseMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in the Lower Norfair Springball Maze Room - s.InsertWithParent("lowerNorfairSpringMazeMissiles", false, "norfairMissiles") - // Split on picking up the Missile Pack in the The Musketeers' Room - s.InsertWithParent("threeMusketeersMissiles", false, "norfairMissiles") - // Split on Wrecked Ship Missile Pack locations - s.InsertWithParent("wreckedShipMissiles", false, "specificMissiles") - // Split on picking up the Missile Pack in Wrecked Ship Main Shaft - s.InsertWithParent("wreckedShipMainShaftMissiles", false, "wreckedShipMissiles") - // Split on picking up the Missile Pack in Bowling Alley - s.InsertWithParent("bowlingMissiles", false, "wreckedShipMissiles") - // Split on picking up the Missile Pack in the Wrecked Ship East Missile Room - s.InsertWithParent("atticMissiles", false, "wreckedShipMissiles") - // Split on Maridia Missile Pack locations - s.InsertWithParent("maridiaMissiles", false, "specificMissiles") - // Split on picking up the Missile Pack in Main Street - s.InsertWithParent("mainStreetMissiles", false, "maridiaMissiles") - // Split on picking up the Missile Pack in the Mama Turtle Room - s.InsertWithParent("mamaTurtleMissiles", false, "maridiaMissiles") - // Split on picking up the Missile Pack in Watering Hole - s.InsertWithParent("wateringHoleMissiles", false, "maridiaMissiles") - // Split on picking up the Missile Pack in the Pseudo Plasma Spark Room - s.InsertWithParent("beachMissiles", false, "maridiaMissiles") - // Split on picking up the Missile Pack in West Sand Hole - s.InsertWithParent("leftSandPitMissiles", false, "maridiaMissiles") - // Split on picking up the Missile Pack in East Sand Hole - s.InsertWithParent("rightSandPitMissiles", false, "maridiaMissiles") - // Split on picking up the Missile Pack in Aqueduct - s.InsertWithParent("aqueductMissiles", false, "maridiaMissiles") - // Split on picking up the Missile Pack in The Precious Room - s.InsertWithParent("preDraygonMissiles", false, "maridiaMissiles") - // Split on the first Super Missile pickup - s.InsertWithParent("firstSuper", false, "ammoPickups") - // Split on each Super Missile upgrade - s.InsertWithParent("allSupers", false, "ammoPickups") - // Split on specific Super Missile Pack locations - s.InsertWithParent("specificSupers", false, "ammoPickups") - // Split on picking up the Super Missile Pack in the Crateria Super Room - s.InsertWithParent("climbSupers", false, "specificSupers") - // Split on picking up the Super Missile Pack in the Spore Spawn Super Room (NOTE: SSTRA splits when the dialogue box disappears, not on touch. Use Spore Spawn RTA Finish for SSTRA runs.) - s.InsertWithParent("sporeSpawnSupers", false, "specificSupers") - // Split on picking up the Super Missile Pack in the Early Supers Room - s.InsertWithParent("earlySupers", false, "specificSupers") - // Split on picking up the Super Missile Pack in the Etecoon Super Room - s.InsertWithParent("etecoonSupers", false, "specificSupers") - // Split on picking up the Super Missile Pack in the Golden Torizo's Room - s.InsertWithParent("goldTorizoSupers", false, "specificSupers") - // Split on picking up the Super Missile Pack in the Wrecked Ship West Super Room - s.InsertWithParent("wreckedShipLeftSupers", false, "specificSupers") - // Split on picking up the Super Missile Pack in the Wrecked Ship East Super Room - s.InsertWithParent("wreckedShipRightSupers", false, "specificSupers") - // Split on picking up the Super Missile Pack in Main Street - s.InsertWithParent("crabSupers", false, "specificSupers") - // Split on picking up the Super Missile Pack in Watering Hole - s.InsertWithParent("wateringHoleSupers", false, "specificSupers") - // Split on picking up the Super Missile Pack in Aqueduct - s.InsertWithParent("aqueductSupers", false, "specificSupers") - // Split on the first Power Bomb pickup - s.InsertWithParent("firstPowerBomb", true, "ammoPickups") - // Split on each Power Bomb upgrade - s.InsertWithParent("allPowerBombs", false, "ammoPickups") - // Split on specific Power Bomb Pack locations - s.InsertWithParent("specificBombs", false, "ammoPickups") - // Split on picking up the Power Bomb Pack in the Crateria Power Bomb Room - s.InsertWithParent("landingSiteBombs", false, "specificBombs") - // Split on picking up the Power Bomb Pack in the Etecoon Room section of Green Brinstar Main Shaft - s.InsertWithParent("etecoonBombs", false, "specificBombs") - // Split on picking up the Power Bomb Pack in the Pink Brinstar Power Bomb Room - s.InsertWithParent("pinkBrinstarBombs", false, "specificBombs") - // Split on picking up the Power Bomb Pack in the Morph Ball Room - s.InsertWithParent("blueBrinstarBombs", false, "specificBombs") - // Split on picking up the Power Bomb Pack in the Alpha Power Bomb Room - s.InsertWithParent("alphaBombs", false, "specificBombs") - // Split on picking up the Power Bomb Pack in the Beta Power Bomb Room - s.InsertWithParent("betaBombs", false, "specificBombs") - // Split on picking up the Power Bomb Pack in the Post Crocomire Power Bomb Room - s.InsertWithParent("crocomireBombs", false, "specificBombs") - // Split on picking up the Power Bomb Pack in the Lower Norfair Escape Power Bomb Room - s.InsertWithParent("lowerNorfairEscapeBombs", false, "specificBombs") - // Split on picking up the Power Bomb Pack in Wasteland - s.InsertWithParent("shameBombs", false, "specificBombs") - // Split on picking up the Power Bomb Pack in East Sand Hall - s.InsertWithParent("rightSandPitBombs", false, "specificBombs") - - // Split on Varia and Gravity pickups - s.Insert("suitUpgrades", true) - // Split on picking up the Varia Suit - s.InsertWithParent("variaSuit", true, "suitUpgrades") - // Split on picking up the Gravity Suit - s.InsertWithParent("gravSuit", true, "suitUpgrades") - - // Split on beam upgrades - s.Insert("beamUpgrades", true) - // Split on picking up the Charge Beam - s.InsertWithParent("chargeBeam", false, "beamUpgrades") - // Split on picking up the Spazer - s.InsertWithParent("spazer", false, "beamUpgrades") - // Split on picking up the Wave Beam - s.InsertWithParent("wave", true, "beamUpgrades") - // Split on picking up the Ice Beam - s.InsertWithParent("ice", false, "beamUpgrades") - // Split on picking up the Plasma Beam - s.InsertWithParent("plasma", false, "beamUpgrades") - - // Split on boot upgrades - s.Insert("bootUpgrades", false) - // Split on picking up the Hi-Jump Boots - s.InsertWithParent("hiJump", false, "bootUpgrades") - // Split on picking up Space Jump - s.InsertWithParent("spaceJump", false, "bootUpgrades") - // Split on picking up the Speed Booster - s.InsertWithParent("speedBooster", false, "bootUpgrades") - - // Split on Energy Tanks and Reserve Tanks - s.Insert("energyUpgrades", false) - // Split on picking up the first Energy Tank - s.InsertWithParent("firstETank", false, "energyUpgrades") - // Split on picking up each Energy Tank - s.InsertWithParent("allETanks", false, "energyUpgrades") - // Split on specific Energy Tank locations - s.InsertWithParent("specificETanks", false, "energyUpgrades") - // Split on picking up the Energy Tank in the Gauntlet Energy Tank Room - s.InsertWithParent("gauntletETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Terminator Room - s.InsertWithParent("terminatorETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Blue Brinstar Energy Tank Room - s.InsertWithParent("ceilingETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Etecoon Energy Tank Room - s.InsertWithParent("etecoonsETank", false, "specificETanks") - // Split on picking up the Energy Tank in Waterway - s.InsertWithParent("waterwayETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Hopper Energy Tank Room - s.InsertWithParent("waveGateETank", false, "specificETanks") - // Split on picking up the Kraid Energy Tank in the Warehouse Energy Tank Room - s.InsertWithParent("kraidETank", false, "specificETanks") - // Split on picking up the Energy Tank in Crocomire's Room - s.InsertWithParent("crocomireETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Hi Jump Energy Tank Room - s.InsertWithParent("hiJumpETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Ridley Tank Room - s.InsertWithParent("ridleyETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Lower Norfair Fireflea Room - s.InsertWithParent("firefleaETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Wrecked Ship Energy Tank Room - s.InsertWithParent("wreckedShipETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Mama Turtle Room - s.InsertWithParent("tatoriETank", false, "specificETanks") - // Split on picking up the Energy Tank in the Botwoon Energy Tank Room - s.InsertWithParent("botwoonETank", false, "specificETanks") - // Split on picking up each Reserve Tank - s.InsertWithParent("reserveTanks", false, "energyUpgrades") - // Split on specific Reserve Tank locations - s.InsertWithParent("specificRTanks", false, "energyUpgrades") - // Split on picking up the Reserve Tank in the Brinstar Reserve Tank Room - s.InsertWithParent("brinstarReserve", false, "specificRTanks") - // Split on picking up the Reserve Tank in the Norfair Reserve Tank Room - s.InsertWithParent("norfairReserve", false, "specificRTanks") - // Split on picking up the Reserve Tank in Bowling Alley - s.InsertWithParent("wreckedShipReserve", false, "specificRTanks") - // Split on picking up the Reserve Tank in West Sand Hole - s.InsertWithParent("maridiaReserve", false, "specificRTanks") - - // Split on the miscellaneous upgrades - s.Insert("miscUpgrades", false) - // Split on picking up the Morphing Ball - s.InsertWithParent("morphBall", false, "miscUpgrades") - // Split on picking up the Bomb - s.InsertWithParent("bomb", false, "miscUpgrades") - // Split on picking up the Spring Ball - s.InsertWithParent("springBall", false, "miscUpgrades") - // Split on picking up the Screw Attack - s.InsertWithParent("screwAttack", false, "miscUpgrades") - // Split on picking up the Grapple Beam - s.InsertWithParent("grapple", false, "miscUpgrades") - // Split on picking up the X-Ray Scope - s.InsertWithParent("xray", false, "miscUpgrades") - - // Split on transitions between areas - s.Insert("areaTransitions", true) - // Split on entering miniboss rooms (except Bomb Torizo) - s.InsertWithParent("miniBossRooms", false, "areaTransitions") - // Split on entering major boss rooms - s.InsertWithParent("bossRooms", false, "areaTransitions") - // Split on elevator transitions between areas (except Statue Room to Tourian) - s.InsertWithParent("elevatorTransitions", false, "areaTransitions") - // Split on leaving Ceres Station - s.InsertWithParent("ceresEscape", false, "areaTransitions") - // Split on entering the Wrecked Ship Entrance from the lower door of West Ocean - s.InsertWithParent("wreckedShipEntrance", false, "areaTransitions") - // Split on entering Red Tower from Noob Bridge - s.InsertWithParent("redTowerMiddleEntrance", false, "areaTransitions") - // Split on entering Red Tower from Skree Boost room - s.InsertWithParent("redTowerBottomEntrance", false, "areaTransitions") - // Split on entering Kraid's Lair - s.InsertWithParent("kraidsLair", false, "areaTransitions") - // Split on entering Rising Tide from Cathedral - s.InsertWithParent("risingTideEntrance", false, "areaTransitions") - // Split on exiting Attic - s.InsertWithParent("atticExit", false, "areaTransitions") - // Split on blowing up the tube to enter Maridia - s.InsertWithParent("tubeBroken", false, "areaTransitions") - // Split on exiting West Cacattack Alley - s.InsertWithParent("cacExit", false, "areaTransitions") - // Split on entering Toilet Bowl from either direction - s.InsertWithParent("toilet", false, "areaTransitions") - // Split on entering Kronic Boost room - s.InsertWithParent("kronicBoost", false, "areaTransitions") - // Split on the elevator down to Lower Norfair - s.InsertWithParent("lowerNorfairEntrance", false, "areaTransitions") - // Split on entering Worst Room in the Game - s.InsertWithParent("writg", false, "areaTransitions") - // Split on entering Red Kihunter Shaft from either Amphitheatre or Wastelands (NOTE: will split twice) - s.InsertWithParent("redKiShaft", false, "areaTransitions") - // Split on entering Metal Pirates Room from Wasteland - s.InsertWithParent("metalPirates", false, "areaTransitions") - // Split on entering Lower Norfair Springball Maze Room - s.InsertWithParent("lowerNorfairSpringMaze", false, "areaTransitions") - // Split on moving from the Three Musketeers' Room to the Single Chamber - s.InsertWithParent("lowerNorfairExit", false, "areaTransitions") - // Split on entering the Statues Room with all four major bosses defeated - s.InsertWithParent("goldenFour", true, "areaTransitions") - // Split on the elevator down to Tourian - s.InsertWithParent("tourianEntrance", false, "areaTransitions") - // Split on exiting each of the Metroid rooms in Tourian - s.InsertWithParent("metroids", false, "areaTransitions") - // Split on moving from the Dust Torizo Room to the Big Boy Room - s.InsertWithParent("babyMetroidRoom", false, "areaTransitions") - // Split on moving from Tourian Escape Room 4 to The Climb - s.InsertWithParent("escapeClimb", false, "areaTransitions") - - // Split on defeating minibosses - s.Insert("miniBosses", false) - // Split on starting the Ceres Escape - s.InsertWithParent("ceresRidley", false, "miniBosses") - // Split on Bomb Torizo's drops appearing - s.InsertWithParent("bombTorizo", false, "miniBosses") - // Split on the last hit to Spore Spawn - s.InsertWithParent("sporeSpawn", false, "miniBosses") - // Split on Crocomire's drops appearing - s.InsertWithParent("crocomire", false, "miniBosses") - // Split on Botwoon's vertical column being fully destroyed - s.InsertWithParent("botwoon", false, "miniBosses") - // Split on Golden Torizo's drops appearing - s.InsertWithParent("goldenTorizo", false, "miniBosses") - - // Split on defeating major bosses - s.Insert("bosses", true) - // Split shortly after Kraid's drops appear - s.InsertWithParent("kraid", false, "bosses") - // Split on Phantoon's drops appearing - s.InsertWithParent("phantoon", false, "bosses") - // Split on Draygon's drops appearing - s.InsertWithParent("draygon", false, "bosses") - // Split on Ridley's drops appearing - s.InsertWithParent("ridley", true, "bosses") - // Split on Mother Brain's head hitting the ground at the end of the first phase - s.InsertWithParent("mb1", false, "bosses") - // Split on the Baby Metroid detaching from Mother Brain's head - s.InsertWithParent("mb2", true, "bosses") - // Split on the start of the Zebes Escape - s.InsertWithParent("mb3", false, "bosses") - - // Split on facing forward at the end of Zebes Escape - s.Insert("rtaFinish", true) - // Split on In-Game Time finalizing, when the end cutscene starts - s.Insert("igtFinish", false) - // Split on the end of a Spore Spawn RTA run, when the text box clears after collecting the Super Missiles - s.Insert("sporeSpawnRTAFinish", false) - // Split on the end of a 100 Missile RTA run, when the text box clears after collecting the hundredth missile - s.Insert("hundredMissileRTAFinish", false) - s.modifiedAfterCreation = false - return s +type conditionList struct { + Name string + memory []element } -func (s *Settings) Insert(name string, value bool) { - // s.mu.Lock() - // defer s.mu.Unlock() - s.modifiedAfterCreation = true - s.data[name] = struct { - value bool - parent *string - }{value: value, parent: nil} +type element struct { + memoryEntryName string + expectedValue *uint16 + compareType string + result compareFunc } -func (s *Settings) InsertWithParent(name string, value bool, parent string) { - // s.mu.Lock() - // defer s.mu.Unlock() - s.modifiedAfterCreation = true - p := parent - s.data[name] = struct { - value bool - parent *string - }{value: value, parent: &p} -} +type compareFunc func(input string, prior *uint16, current *uint16, expected *uint16) bool -func (s *Settings) Contains(varName string) bool { - // s.mu.RLock() - // defer s.mu.RUnlock() - _, ok := s.data[varName] - return ok +type memoryWatcher struct { + // name string + address uint32 + current *uint16 + old *uint16 + size int } -func (s *Settings) Get(varName string) bool { - // s.mu.RLock() - // defer s.mu.RUnlock() - return s.getRecursive(varName) +type SNESState struct { + vars map[string]*memoryWatcher + data []byte + startConditions []conditionList + resetConditions []conditionList + splitConditions []conditionList + // doExtraUpdate bool + // pickedUpHundredthMissile bool + // pickedUpSporeSpawnSuper bool + // latencySamples []uint128 + // mu sync.Mutex } -func (s *Settings) getRecursive(varName string) bool { - entry, ok := s.data[varName] - if !ok { - return false - } - if entry.parent == nil { - return entry.value - } - return entry.value && s.getRecursive(*entry.parent) +// const NUM_LATENCY_SAMPLES = 10 + +// type uint128 struct { +// hi uint64 +// lo uint64 +// } + +// func (a uint128) Add(b uint128) uint128 { +// lo := a.lo + b.lo +// hi := a.hi + b.hi +// if lo < a.lo { +// hi++ +// } +// return uint128{hi: hi, lo: lo} +// } + +// func (a uint128) Sub(b uint128) uint128 { +// lo := a.lo - b.lo +// hi := a.hi - b.hi +// if a.lo < b.lo { +// hi-- +// } +// return uint128{hi: hi, lo: lo} +// } + +// func (a uint128) ToFloat64() float64 { +// return float64(a.hi)*math.Pow(2, 64) + float64(a.lo) +// } + +// func uint128FromInt(i int64) uint128 { +// if i < 0 { +// return uint128{hi: math.MaxUint64, lo: uint64(i)} +// } +// return uint128{hi: 0, lo: uint64(i)} +// } + +// func averageUint128Slice(arr []uint128) float64 { +// var sum uint128 +// for _, v := range arr { +// sum = sum.Add(v) +// } +// return sum.ToFloat64() / float64(len(arr)) +// } + +// type TimeSpan struct { +// seconds float64 +// } + +// func (t TimeSpan) Seconds() float64 { +// return t.seconds +// } + +// func TimeSpanFromSeconds(seconds float64) TimeSpan { +// return TimeSpan{seconds: seconds} +// } + +// func (s *SNESState) gametimeToSeconds() TimeSpan { +// hours := float64(s.vars["igtHours"].current) +// minutes := float64(s.vars["igtMinutes"].current) +// seconds := float64(s.vars["igtSeconds"].current) + +// totalSeconds := hours*3600 + minutes*60 + seconds +// return TimeSpanFromSeconds(totalSeconds) +// } + +// func (a *QUSB2SNESAutoSplitter) GametimeToSeconds() *TimeSpan { +// t := a.snes.gametimeToSeconds() +// return &t +// } + +func (s *SNESState) split(split int) bool { + splitState := true + var tempstate bool + + for _, q := range s.splitConditions[split].memory { + tempstate = q.result(q.compareType, s.vars[q.memoryEntryName].old, s.vars[q.memoryEntryName].current, q.expectedValue) + splitState = splitState && tempstate + } + if splitState { + fmt.Printf("Split: %#v\n", s.splitConditions[split].Name) + return true + } + return false } -func (s *Settings) Set(varName string, value bool) { - // s.mu.Lock() - // defer s.mu.Unlock() - entry, ok := s.data[varName] - if !ok { - s.data[varName] = struct { - value bool - parent *string - }{value: value, parent: nil} - } else { - s.data[varName] = struct { - value bool - parent *string - }{value: value, parent: entry.parent} - } - s.modifiedAfterCreation = true -} +func (s *SNESState) start() bool { + for _, p := range s.startConditions { + startState := true + var tempstate bool -func (s *Settings) Roots() []string { - // s.mu.RLock() - // defer s.mu.RUnlock() - var roots []string - for k, v := range s.data { - if v.parent == nil { - roots = append(roots, k) + for _, q := range p.memory { + tempstate = q.result(q.compareType, s.vars[q.memoryEntryName].old, s.vars[q.memoryEntryName].current, q.expectedValue) + startState = startState && tempstate } - } - return roots -} - -func (s *Settings) Children(key string) []string { - // s.mu.RLock() - // defer s.mu.RUnlock() - var children []string - for k, v := range s.data { - if v.parent != nil && *v.parent == key { - children = append(children, k) + if startState { + fmt.Printf("Start: %#v\n", p.Name) + return true } } - return children -} - -func (s *Settings) Lookup(varName string) bool { - // s.mu.RLock() - // defer s.mu.RUnlock() - entry, ok := s.data[varName] - if !ok { - panic("variable not found") - } - return entry.value -} - -func (s *Settings) LookupMut(varName string) *bool { - // s.mu.Lock() - // defer s.mu.Unlock() - entry, ok := s.data[varName] - if !ok { - panic("variable not found") - } - s.modifiedAfterCreation = true - // To mutate the value, we need to update the map entry. - // Return a pointer to the value inside the map by re-assigning. - // Since Go does not allow direct pointer to map values, we simulate with a helper struct. - val := entry.value - // parent := entry.parent - // Create a wrapper struct to hold pointer to value - type boolWrapper struct { - val *bool - } - bw := boolWrapper{val: &val} - // Return pointer to val, but user must call Set to update map. - return bw.val -} - -func (s *Settings) HasBeenModified() bool { - // s.mu.RLock() - // defer s.mu.RUnlock() - return s.modifiedAfterCreation -} - -func (s *Settings) SplitOnMiscUpgrades() { - s.Set("miscUpgrades", true) - s.Set("morphBall", true) - s.Set("bomb", true) - s.Set("springBall", true) - s.Set("screwAttack", true) - s.Set("grapple", true) - s.Set("xray", true) -} - -func (s *Settings) SplitOnHundo() { - s.Set("ammoPickups", true) - s.Set("allMissiles", true) - s.Set("allSupers", true) - s.Set("allPowerBombs", true) - s.Set("beamUpgrades", true) - s.Set("chargeBeam", true) - s.Set("spazer", true) - s.Set("wave", true) - s.Set("ice", true) - s.Set("plasma", true) - s.Set("bootUpgrades", true) - s.Set("hiJump", true) - s.Set("spaceJump", true) - s.Set("speedBooster", true) - s.Set("energyUpgrades", true) - s.Set("allETanks", true) - s.Set("reserveTanks", true) - s.SplitOnMiscUpgrades() - s.Set("areaTransitions", true) // should already be true - s.Set("tubeBroken", true) - s.Set("ceresEscape", true) - s.Set("bosses", true) // should already be true - s.Set("kraid", true) - s.Set("phantoon", true) - s.Set("draygon", true) - s.Set("ridley", true) - s.Set("mb1", true) - s.Set("mb2", true) - s.Set("mb3", true) - s.Set("miniBosses", true) - s.Set("ceresRidley", true) - s.Set("bombTorizo", true) - s.Set("crocomire", true) - s.Set("botwoon", true) - s.Set("goldenTorizo", true) - s.Set("babyMetroidRoom", true) -} - -func (s *Settings) SplitOnAnyPercent() { - s.Set("ammoPickups", true) - s.Set("specificMissiles", true) - s.Set("specificSupers", true) - s.Set("wreckedShipLeftSupers", true) - s.Set("specificPowerBombs", true) - s.Set("firstMissile", true) - s.Set("firstSuper", true) - s.Set("firstPowerBomb", true) - s.Set("brinstarMissiles", true) - s.Set("norfairMissiles", true) - s.Set("chargeMissiles", true) - s.Set("waveMissiles", true) - s.Set("beamUpgrades", true) - s.Set("chargeBeam", true) - s.Set("wave", true) - s.Set("ice", true) - s.Set("plasma", true) - s.Set("bootUpgrades", true) - s.Set("hiJump", true) - s.Set("speedBooster", true) - s.Set("specificETanks", true) - s.Set("energyUpgrades", true) - s.Set("terminatorETank", true) - s.Set("hiJumpETank", true) - s.Set("botwoonETank", true) - s.Set("miscUpgrades", true) - s.Set("morphBall", true) - s.Set("spaceJump", true) - s.Set("bomb", true) - s.Set("areaTransitions", true) // should already be true - s.Set("tubeBroken", true) - s.Set("ceresEscape", true) - s.Set("bosses", true) // should already be true - s.Set("kraid", true) - s.Set("phantoon", true) - s.Set("draygon", true) - s.Set("ridley", true) - s.Set("mb1", true) - s.Set("mb2", true) - s.Set("mb3", true) - s.Set("miniBosses", true) - s.Set("ceresRidley", true) - s.Set("bombTorizo", true) - s.Set("botwoon", true) - s.Set("goldenTorizo", true) - s.Set("babyMetroidRoom", true) -} - -// Width enum equivalent -type Width int - -const ( - Byte Width = iota - Word -) - -type MemoryWatcher struct { - address uint32 - current uint32 - old uint32 - width Width + return false } -func NewMemoryWatcher(address uint32, width Width) *MemoryWatcher { - return &MemoryWatcher{ - address: address, - current: 0, - old: 0, - width: width, - } -} +func (s *SNESState) reset() bool { + for _, p := range s.resetConditions { + resetState := true + var tempstate bool -func (mw *MemoryWatcher) UpdateValue(memory []byte) { - mw.old = mw.current - switch mw.width { - case Byte: - mw.current = uint32(memory[mw.address]) - case Word: - addr := mw.address - mw.current = uint32(memory[addr]) | uint32(memory[addr+1])<<8 + for _, q := range p.memory { + tempstate = q.result(q.compareType, s.vars[q.memoryEntryName].old, s.vars[q.memoryEntryName].current, q.expectedValue) + resetState = resetState && tempstate + } + if resetState { + fmt.Printf("Reset: %#v\n", p.Name) + return true + } } + return false } -func split(settings *Settings, snes *SNESState) bool { - firstMissile := settings.Get("firstMissile") && snes.vars["maxMissiles"].old == 0 && snes.vars["maxMissiles"].current == 5 - allMissiles := settings.Get("allMissiles") && (snes.vars["maxMissiles"].old+5) == snes.vars["maxMissiles"].current - oceanBottomMissiles := settings.Get("oceanBottomMissiles") && snes.vars["roomID"].current == roomIDEnum["westOcean"] && (snes.vars["crateriaItems"].old+2) == (snes.vars["crateriaItems"].current) - oceanTopMissiles := settings.Get("oceanTopMissiles") && snes.vars["roomID"].current == roomIDEnum["westOcean"] && (snes.vars["crateriaItems"].old+4) == (snes.vars["crateriaItems"].current) - oceanMiddleMissiles := settings.Get("oceanMiddleMissiles") && snes.vars["roomID"].current == roomIDEnum["westOcean"] && (snes.vars["crateriaItems"].old+8) == (snes.vars["crateriaItems"].current) - moatMissiles := settings.Get("moatMissiles") && snes.vars["roomID"].current == roomIDEnum["crateriaMoat"] && (snes.vars["crateriaItems"].old+16) == (snes.vars["crateriaItems"].current) - oldTourianMissiles := settings.Get("oldTourianMissiles") && snes.vars["roomID"].current == roomIDEnum["pitRoom"] && (snes.vars["crateriaItems"].old+64) == (snes.vars["crateriaItems"].current) - gauntletRightMissiles := settings.Get("gauntletRightMissiles") && snes.vars["roomID"].current == roomIDEnum["greenPirateShaft"] && (snes.vars["brinteriaItems"].old+2) == (snes.vars["brinteriaItems"].current) - gauntletLeftMissiles := settings.Get("gauntletLeftMissiles") && snes.vars["roomID"].current == roomIDEnum["greenPirateShaft"] && (snes.vars["brinteriaItems"].old+4) == (snes.vars["brinteriaItems"].current) - dentalPlan := settings.Get("dentalPlan") && snes.vars["roomID"].current == roomIDEnum["theFinalMissile"] && (snes.vars["brinteriaItems"].old+16) == (snes.vars["brinteriaItems"].current) - earlySuperBridgeMissiles := settings.Get("earlySuperBridgeMissiles") && snes.vars["roomID"].current == roomIDEnum["earlySupers"] && (snes.vars["brinteriaItems"].old+128) == (snes.vars["brinteriaItems"].current) - greenBrinstarReserveMissiles := settings.Get("greenBrinstarReserveMissiles") && snes.vars["roomID"].current == roomIDEnum["brinstarReserveRoom"] && (snes.vars["brinstarItems2"].old+8) == (snes.vars["brinstarItems2"].current) - greenBrinstarExtraReserveMissiles := settings.Get("greenBrinstarExtraReserveMissiles") && snes.vars["roomID"].current == roomIDEnum["brinstarReserveRoom"] && (snes.vars["brinstarItems2"].old+4) == (snes.vars["brinstarItems2"].current) - bigPinkTopMissiles := settings.Get("bigPinkTopMissiles") && snes.vars["roomID"].current == roomIDEnum["bigPink"] && (snes.vars["brinstarItems2"].old+32) == (snes.vars["brinstarItems2"].current) - chargeMissiles := settings.Get("chargeMissiles") && snes.vars["roomID"].current == roomIDEnum["bigPink"] && (snes.vars["brinstarItems2"].old+64) == (snes.vars["brinstarItems2"].current) - greenHillsMissiles := settings.Get("greenHillsMissiles") && snes.vars["roomID"].current == roomIDEnum["greenHills"] && (snes.vars["brinstarItems3"].old+2) == (snes.vars["brinstarItems3"].current) - blueBrinstarETankMissiles := settings.Get("blueBrinstarETankMissiles") && snes.vars["roomID"].current == roomIDEnum["blueBrinstarETankRoom"] && (snes.vars["brinstarItems3"].old+16) == (snes.vars["brinstarItems3"].current) - alphaMissiles := settings.Get("alphaMissiles") && snes.vars["roomID"].current == roomIDEnum["alphaMissileRoom"] && (snes.vars["brinstarItems4"].old+4) == (snes.vars["brinstarItems4"].current) - billyMaysMissiles := settings.Get("billyMaysMissiles") && snes.vars["roomID"].current == roomIDEnum["billyMays"] && (snes.vars["brinstarItems4"].old+16) == (snes.vars["brinstarItems4"].current) - butWaitTheresMoreMissiles := settings.Get("butWaitTheresMoreMissiles") && snes.vars["roomID"].current == roomIDEnum["billyMays"] && (snes.vars["brinstarItems4"].old+32) == (snes.vars["brinstarItems4"].current) - redBrinstarMissiles := settings.Get("redBrinstarMissiles") && snes.vars["roomID"].current == roomIDEnum["alphaPowerBombsRoom"] && (snes.vars["brinstarItems5"].old+2) == (snes.vars["brinstarItems5"].current) - warehouseMissiles := settings.Get("warehouseMissiles") && snes.vars["roomID"].current == roomIDEnum["warehouseKiHunters"] && (snes.vars["brinstarItems5"].old+16) == (snes.vars["brinstarItems5"].current) - cathedralMissiles := settings.Get("cathedralMissiles") && snes.vars["roomID"].current == roomIDEnum["cathedral"] && (snes.vars["norfairItems1"].old+2) == (snes.vars["norfairItems1"].current) - crumbleShaftMissiles := settings.Get("crumbleShaftMissiles") && snes.vars["roomID"].current == roomIDEnum["crumbleShaft"] && (snes.vars["norfairItems1"].old+8) == (snes.vars["norfairItems1"].current) - crocomireEscapeMissiles := settings.Get("crocomireEscapeMissiles") && snes.vars["roomID"].current == roomIDEnum["crocomireEscape"] && (snes.vars["norfairItems1"].old+64) == (snes.vars["norfairItems1"].current) - hiJumpMissiles := settings.Get("hiJumpMissiles") && snes.vars["roomID"].current == roomIDEnum["hiJumpShaft"] && (snes.vars["norfairItems1"].old+128) == (snes.vars["norfairItems1"].current) - postCrocomireMissiles := settings.Get("postCrocomireMissiles") && snes.vars["roomID"].current == roomIDEnum["cosineRoom"] && (snes.vars["norfairItems2"].old+4) == (snes.vars["norfairItems2"].current) - grappleMissiles := settings.Get("grappleMissiles") && snes.vars["roomID"].current == roomIDEnum["preGrapple"] && (snes.vars["norfairItems2"].old+8) == (snes.vars["norfairItems2"].current) - norfairReserveMissiles := settings.Get("norfairReserveMissiles") && snes.vars["roomID"].current == roomIDEnum["norfairReserveRoom"] && (snes.vars["norfairItems2"].old+64) == (snes.vars["norfairItems2"].current) - greenBubblesMissiles := settings.Get("greenBubblesMissiles") && snes.vars["roomID"].current == roomIDEnum["greenBubblesRoom"] && (snes.vars["norfairItems2"].old+128) == (snes.vars["norfairItems2"].current) - bubbleMountainMissiles := settings.Get("bubbleMountainMissiles") && snes.vars["roomID"].current == roomIDEnum["bubbleMountain"] && (snes.vars["norfairItems3"].old+1) == (snes.vars["norfairItems3"].current) - speedBoostMissiles := settings.Get("speedBoostMissiles") && snes.vars["roomID"].current == roomIDEnum["speedBoostHall"] && (snes.vars["norfairItems3"].old+2) == (snes.vars["norfairItems3"].current) - waveMissiles := settings.Get("waveMissiles") && snes.vars["roomID"].current == roomIDEnum["doubleChamber"] && (snes.vars["norfairItems3"].old+8) == (snes.vars["norfairItems3"].current) - goldTorizoMissiles := settings.Get("goldTorizoMissiles") && snes.vars["roomID"].current == roomIDEnum["goldenTorizo"] && (snes.vars["norfairItems3"].old+64) == (snes.vars["norfairItems3"].current) - mickeyMouseMissiles := settings.Get("mickeyMouseMissiles") && snes.vars["roomID"].current == roomIDEnum["mickeyMouse"] && (snes.vars["norfairItems4"].old+2) == (snes.vars["norfairItems4"].current) - lowerNorfairSpringMazeMissiles := settings.Get("lowerNorfairSpringMazeMissiles") && snes.vars["roomID"].current == roomIDEnum["lowerNorfairSpringMaze"] && (snes.vars["norfairItems4"].old+4) == (snes.vars["norfairItems4"].current) - threeMusketeersMissiles := settings.Get("threeMusketeersMissiles") && snes.vars["roomID"].current == roomIDEnum["threeMusketeers"] && (snes.vars["norfairItems4"].old+32) == (snes.vars["norfairItems4"].current) - wreckedShipMainShaftMissiles := settings.Get("wreckedShipMainShaftMissiles") && snes.vars["roomID"].current == roomIDEnum["wreckedShipMainShaft"] && (snes.vars["wreckedShipItems"].old+1) == (snes.vars["wreckedShipItems"].current) - bowlingMissiles := settings.Get("bowlingMissiles") && snes.vars["roomID"].current == roomIDEnum["bowling"] && (snes.vars["wreckedShipItems"].old+4) == (snes.vars["wreckedShipItems"].current) - atticMissiles := settings.Get("atticMissiles") && snes.vars["roomID"].current == roomIDEnum["atticWorkerRobotRoom"] && (snes.vars["wreckedShipItems"].old+8) == (snes.vars["wreckedShipItems"].current) - mainStreetMissiles := settings.Get("mainStreetMissiles") && snes.vars["roomID"].current == roomIDEnum["mainStreet"] && (snes.vars["maridiaItems1"].old+1) == (snes.vars["maridiaItems1"].current) - mamaTurtleMissiles := settings.Get("mamaTurtleMissiles") && snes.vars["roomID"].current == roomIDEnum["mamaTurtle"] && (snes.vars["maridiaItems1"].old+8) == (snes.vars["maridiaItems1"].current) - wateringHoleMissiles := settings.Get("wateringHoleMissiles") && snes.vars["roomID"].current == roomIDEnum["wateringHole"] && (snes.vars["maridiaItems1"].old+32) == (snes.vars["maridiaItems1"].current) - beachMissiles := settings.Get("beachMissiles") && snes.vars["roomID"].current == roomIDEnum["beach"] && (snes.vars["maridiaItems1"].old+64) == (snes.vars["maridiaItems1"].current) - leftSandPitMissiles := settings.Get("leftSandPitMissiles") && snes.vars["roomID"].current == roomIDEnum["leftSandPit"] && (snes.vars["maridiaItems2"].old+1) == (snes.vars["maridiaItems2"].current) - rightSandPitMissiles := settings.Get("rightSandPitMissiles") && snes.vars["roomID"].current == roomIDEnum["rightSandPit"] && (snes.vars["maridiaItems2"].old+4) == (snes.vars["maridiaItems2"].current) - aqueductMissiles := settings.Get("aqueductMissiles") && snes.vars["roomID"].current == roomIDEnum["aqueduct"] && (snes.vars["maridiaItems2"].old+16) == (snes.vars["maridiaItems2"].current) - preDraygonMissiles := settings.Get("preDraygonMissiles") && snes.vars["roomID"].current == roomIDEnum["precious"] && (snes.vars["maridiaItems2"].old+128) == (snes.vars["maridiaItems2"].current) - firstSuper := settings.Get("firstSuper") && snes.vars["maxSupers"].old == 0 && snes.vars["maxSupers"].current == 5 - allSupers := settings.Get("allSupers") && (snes.vars["maxSupers"].old+5) == (snes.vars["maxSupers"].current) - climbSupers := settings.Get("climbSupers") && snes.vars["roomID"].current == roomIDEnum["crateriaSupersRoom"] && (snes.vars["brinteriaItems"].old+8) == (snes.vars["brinteriaItems"].current) - sporeSpawnSupers := settings.Get("sporeSpawnSupers") && snes.vars["roomID"].current == roomIDEnum["sporeSpawnSuper"] && (snes.vars["brinteriaItems"].old+64) == (snes.vars["brinteriaItems"].current) - earlySupers := settings.Get("earlySupers") && snes.vars["roomID"].current == roomIDEnum["earlySupers"] && (snes.vars["brinstarItems2"].old+1) == (snes.vars["brinstarItems2"].current) - etecoonSupers := (settings.Get("etecoonSupers") || settings.Get("etacoonSupers")) && snes.vars["roomID"].current == roomIDEnum["etecoonSuperRoom"] && (snes.vars["brinstarItems3"].old+128) == (snes.vars["brinstarItems3"].current) - goldTorizoSupers := settings.Get("goldTorizoSupers") && snes.vars["roomID"].current == roomIDEnum["goldenTorizo"] && (snes.vars["norfairItems3"].old+128) == (snes.vars["norfairItems3"].current) - wreckedShipLeftSupers := settings.Get("wreckedShipLeftSupers") && snes.vars["roomID"].current == roomIDEnum["wreckedShipLeftSuperRoom"] && (snes.vars["wreckedShipItems"].old+32) == (snes.vars["wreckedShipItems"].current) - wreckedShipRightSupers := settings.Get("wreckedShipRightSupers") && snes.vars["roomID"].current == roomIDEnum["wreckedShipRightSuperRoom"] && (snes.vars["wreckedShipItems"].old+64) == (snes.vars["wreckedShipItems"].current) - crabSupers := settings.Get("crabSupers") && snes.vars["roomID"].current == roomIDEnum["mainStreet"] && (snes.vars["maridiaItems1"].old+2) == (snes.vars["maridiaItems1"].current) - wateringHoleSupers := settings.Get("wateringHoleSupers") && snes.vars["roomID"].current == roomIDEnum["wateringHole"] && (snes.vars["maridiaItems1"].old+16) == (snes.vars["maridiaItems1"].current) - aqueductSupers := settings.Get("aqueductSupers") && snes.vars["roomID"].current == roomIDEnum["aqueduct"] && (snes.vars["maridiaItems2"].old+32) == (snes.vars["maridiaItems2"].current) - firstPowerBomb := settings.Get("firstPowerBomb") && snes.vars["maxPowerBombs"].old == 0 && snes.vars["maxPowerBombs"].current == 5 - allPowerBombs := settings.Get("allPowerBombs") && (snes.vars["maxPowerBombs"].old+5) == (snes.vars["maxPowerBombs"].current) - landingSiteBombs := settings.Get("landingSiteBombs") && snes.vars["roomID"].current == roomIDEnum["crateriaPowerBombRoom"] && (snes.vars["crateriaItems"].old+1) == (snes.vars["crateriaItems"].current) - etecoonBombs := (settings.Get("etecoonBombs") || settings.Get("etacoonBombs")) && snes.vars["roomID"].current == roomIDEnum["greenBrinstarMainShaft"] && (snes.vars["brinteriaItems"].old+32) == (snes.vars["brinteriaItems"].current) - pinkBrinstarBombs := settings.Get("pinkBrinstarBombs") && snes.vars["roomID"].current == roomIDEnum["pinkBrinstarPowerBombRoom"] && (snes.vars["brinstarItems3"].old+1) == (snes.vars["brinstarItems3"].current) - blueBrinstarBombs := settings.Get("blueBrinstarBombs") && snes.vars["roomID"].current == roomIDEnum["morphBall"] && (snes.vars["brinstarItems3"].old+8) == (snes.vars["brinstarItems3"].current) - alphaBombs := settings.Get("alphaBombs") && snes.vars["roomID"].current == roomIDEnum["alphaPowerBombsRoom"] && (snes.vars["brinstarItems5"].old+1) == (snes.vars["brinstarItems5"].current) - betaBombs := settings.Get("betaBombs") && snes.vars["roomID"].current == roomIDEnum["betaPowerBombRoom"] && (snes.vars["brinstarItems4"].old+128) == (snes.vars["brinstarItems4"].current) - crocomireBombs := settings.Get("crocomireBombs") && snes.vars["roomID"].current == roomIDEnum["postCrocomirePowerBombRoom"] && (snes.vars["norfairItems2"].old+2) == (snes.vars["norfairItems2"].current) - lowerNorfairEscapeBombs := settings.Get("lowerNorfairEscapeBombs") && snes.vars["roomID"].current == roomIDEnum["lowerNorfairEscapePowerBombRoom"] && (snes.vars["norfairItems4"].old+8) == (snes.vars["norfairItems4"].current) - shameBombs := settings.Get("shameBombs") && snes.vars["roomID"].current == roomIDEnum["wasteland"] && (snes.vars["norfairItems4"].old+16) == (snes.vars["norfairItems4"].current) - rightSandPitBombs := settings.Get("rightSandPitBombs") && snes.vars["roomID"].current == roomIDEnum["rightSandPit"] && (snes.vars["maridiaItems2"].old+8) == (snes.vars["maridiaItems2"].current) - pickup := firstMissile || allMissiles || oceanBottomMissiles || oceanTopMissiles || oceanMiddleMissiles || moatMissiles || oldTourianMissiles || gauntletRightMissiles || gauntletLeftMissiles || dentalPlan || earlySuperBridgeMissiles || greenBrinstarReserveMissiles || greenBrinstarExtraReserveMissiles || bigPinkTopMissiles || chargeMissiles || greenHillsMissiles || blueBrinstarETankMissiles || alphaMissiles || billyMaysMissiles || butWaitTheresMoreMissiles || redBrinstarMissiles || warehouseMissiles || cathedralMissiles || crumbleShaftMissiles || crocomireEscapeMissiles || hiJumpMissiles || postCrocomireMissiles || grappleMissiles || norfairReserveMissiles || greenBubblesMissiles || bubbleMountainMissiles || speedBoostMissiles || waveMissiles || goldTorizoMissiles || mickeyMouseMissiles || lowerNorfairSpringMazeMissiles || threeMusketeersMissiles || wreckedShipMainShaftMissiles || bowlingMissiles || atticMissiles || mainStreetMissiles || mamaTurtleMissiles || wateringHoleMissiles || beachMissiles || leftSandPitMissiles || rightSandPitMissiles || aqueductMissiles || preDraygonMissiles || firstSuper || allSupers || climbSupers || sporeSpawnSupers || earlySupers || etecoonSupers || goldTorizoSupers || wreckedShipLeftSupers || wreckedShipRightSupers || crabSupers || wateringHoleSupers || aqueductSupers || firstPowerBomb || allPowerBombs || landingSiteBombs || etecoonBombs || pinkBrinstarBombs || blueBrinstarBombs || alphaBombs || betaBombs || crocomireBombs || lowerNorfairEscapeBombs || shameBombs || rightSandPitBombs - - // Item unlock section - varia := settings.Get("variaSuit") && snes.vars["roomID"].current == roomIDEnum["varia"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["variaSuit"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["variaSuit"]) > 0 - springBall := settings.Get("springBall") && snes.vars["roomID"].current == roomIDEnum["springBall"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["springBall"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["springBall"]) > 0 - morphBall := settings.Get("morphBall") && snes.vars["roomID"].current == roomIDEnum["morphBall"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["morphBall"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["morphBall"]) > 0 - screwAttack := settings.Get("screwAttack") && snes.vars["roomID"].current == roomIDEnum["screwAttack"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["screwAttack"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["screwAttack"]) > 0 - gravSuit := settings.Get("gravSuit") && snes.vars["roomID"].current == roomIDEnum["gravity"] && (snes.vars["unlockedEquips2"].old&unlockFlagEnum["gravSuit"]) == 0 && (snes.vars["unlockedEquips2"].current&unlockFlagEnum["gravSuit"]) > 0 - hiJump := settings.Get("hiJump") && snes.vars["roomID"].current == roomIDEnum["hiJump"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["hiJump"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["hiJump"]) > 0 - spaceJump := settings.Get("spaceJump") && snes.vars["roomID"].current == roomIDEnum["spaceJump"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["spaceJump"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["spaceJump"]) > 0 - bomb := settings.Get("bomb") && snes.vars["roomID"].current == roomIDEnum["bombTorizo"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["bomb"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["bomb"]) > 0 - speedBooster := settings.Get("speedBooster") && snes.vars["roomID"].current == roomIDEnum["speedBooster"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["speedBooster"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["speedBooster"]) > 0 - grapple := settings.Get("grapple") && snes.vars["roomID"].current == roomIDEnum["grapple"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["grapple"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["grapple"]) > 0 - xray := settings.Get("xray") && snes.vars["roomID"].current == roomIDEnum["xRay"] && (snes.vars["unlockedEquips"].old&unlockFlagEnum["xray"]) == 0 && (snes.vars["unlockedEquips"].current&unlockFlagEnum["xray"]) > 0 - unlock := varia || springBall || morphBall || screwAttack || gravSuit || hiJump || spaceJump || bomb || speedBooster || grapple || xray - - // Beam unlock section - wave := settings.Get("wave") && snes.vars["roomID"].current == roomIDEnum["waveBeam"] && (snes.vars["unlockedBeams"].old&unlockFlagEnum["wave"]) == 0 && (snes.vars["unlockedBeams"].current&unlockFlagEnum["wave"]) > 0 - ice := settings.Get("ice") && snes.vars["roomID"].current == roomIDEnum["iceBeam"] && (snes.vars["unlockedBeams"].old&unlockFlagEnum["ice"]) == 0 && (snes.vars["unlockedBeams"].current&unlockFlagEnum["ice"]) > 0 - spazer := settings.Get("spazer") && snes.vars["roomID"].current == roomIDEnum["spazer"] && (snes.vars["unlockedBeams"].old&unlockFlagEnum["spazer"]) == 0 && (snes.vars["unlockedBeams"].current&unlockFlagEnum["spazer"]) > 0 - plasma := settings.Get("plasma") && snes.vars["roomID"].current == roomIDEnum["plasmaBeam"] && (snes.vars["unlockedBeams"].old&unlockFlagEnum["plasma"]) == 0 && (snes.vars["unlockedBeams"].current&unlockFlagEnum["plasma"]) > 0 - chargeBeam := settings.Get("chargeBeam") && snes.vars["roomID"].current == roomIDEnum["bigPink"] && (snes.vars["unlockedCharge"].old&unlockFlagEnum["chargeBeam"]) == 0 && (snes.vars["unlockedCharge"].current&unlockFlagEnum["chargeBeam"]) > 0 - beam := wave || ice || spazer || plasma || chargeBeam - - // E-tanks and reserve tanks - firstETank := settings.Get("firstETank") && snes.vars["maxEnergy"].old == 99 && snes.vars["maxEnergy"].current == 199 - allETanks := settings.Get("allETanks") && (snes.vars["maxEnergy"].old+100) == (snes.vars["maxEnergy"].current) - gauntletETank := settings.Get("gauntletETank") && snes.vars["roomID"].current == roomIDEnum["gauntletETankRoom"] && (snes.vars["crateriaItems"].old+32) == (snes.vars["crateriaItems"].current) - terminatorETank := settings.Get("terminatorETank") && snes.vars["roomID"].current == roomIDEnum["terminator"] && (snes.vars["brinteriaItems"].old+1) == (snes.vars["brinteriaItems"].current) - ceilingETank := settings.Get("ceilingETank") && snes.vars["roomID"].current == roomIDEnum["blueBrinstarETankRoom"] && (snes.vars["brinstarItems3"].old+32) == (snes.vars["brinstarItems3"].current) - etecoonsETank := (settings.Get("etecoonsETank") || settings.Get("etacoonsETank")) && snes.vars["roomID"].current == roomIDEnum["etecoonETankRoom"] && (snes.vars["brinstarItems3"].old+64) == (snes.vars["brinstarItems3"].current) - waterwayETank := settings.Get("waterwayETank") && snes.vars["roomID"].current == roomIDEnum["waterway"] && (snes.vars["brinstarItems4"].old+2) == (snes.vars["brinstarItems4"].current) - waveGateETank := settings.Get("waveGateETank") && snes.vars["roomID"].current == roomIDEnum["hopperETankRoom"] && (snes.vars["brinstarItems4"].old+8) == (snes.vars["brinstarItems4"].current) - kraidETank := settings.Get("kraidETank") && snes.vars["roomID"].current == roomIDEnum["warehouseETankRoom"] && (snes.vars["brinstarItems5"].old+8) == (snes.vars["brinstarItems5"].current) - crocomireETank := settings.Get("crocomireETank") && snes.vars["roomID"].current == roomIDEnum["crocomire"] && (snes.vars["norfairItems1"].old+16) == (snes.vars["norfairItems1"].current) - hiJumpETank := settings.Get("hiJumpETank") && snes.vars["roomID"].current == roomIDEnum["hiJumpShaft"] && (snes.vars["norfairItems2"].old+1) == (snes.vars["norfairItems2"].current) - ridleyETank := settings.Get("ridleyETank") && snes.vars["roomID"].current == roomIDEnum["ridleyETankRoom"] && (snes.vars["norfairItems4"].old+64) == (snes.vars["norfairItems4"].current) - firefleaETank := settings.Get("firefleaETank") && snes.vars["roomID"].current == roomIDEnum["lowerNorfairFireflea"] && (snes.vars["norfairItems5"].old+1) == (snes.vars["norfairItems5"].current) - wreckedShipETank := settings.Get("wreckedShipETank") && snes.vars["roomID"].current == roomIDEnum["wreckedShipETankRoom"] && (snes.vars["wreckedShipItems"].old+16) == (snes.vars["wreckedShipItems"].current) - tatoriETank := settings.Get("tatoriETank") && snes.vars["roomID"].current == roomIDEnum["mamaTurtle"] && (snes.vars["maridiaItems1"].old+4) == (snes.vars["maridiaItems1"].current) - botwoonETank := settings.Get("botwoonETank") && snes.vars["roomID"].current == roomIDEnum["botwoonETankRoom"] && (snes.vars["maridiaItems3"].old+1) == (snes.vars["maridiaItems3"].current) - reserveTanks := settings.Get("reserveTanks") && (snes.vars["maxReserve"].old+100) == (snes.vars["maxReserve"].current) - brinstarReserve := settings.Get("brinstarReserve") && snes.vars["roomID"].current == roomIDEnum["brinstarReserveRoom"] && (snes.vars["brinstarItems2"].old+2) == (snes.vars["brinstarItems2"].current) - norfairReserve := settings.Get("norfairReserve") && snes.vars["roomID"].current == roomIDEnum["norfairReserveRoom"] && (snes.vars["norfairItems2"].old+32) == (snes.vars["norfairItems2"].current) - wreckedShipReserve := settings.Get("wreckedShipReserve") && snes.vars["roomID"].current == roomIDEnum["bowling"] && (snes.vars["wreckedShipItems"].old+2) == (snes.vars["wreckedShipItems"].current) - maridiaReserve := settings.Get("maridiaReserve") && snes.vars["roomID"].current == roomIDEnum["leftSandPit"] && (snes.vars["maridiaItems2"].old+2) == (snes.vars["maridiaItems2"].current) - energyUpgrade := firstETank || allETanks || gauntletETank || terminatorETank || ceilingETank || etecoonsETank || waterwayETank || waveGateETank || kraidETank || crocomireETank || hiJumpETank || ridleyETank || firefleaETank || wreckedShipETank || tatoriETank || botwoonETank || reserveTanks || brinstarReserve || norfairReserve || wreckedShipReserve || maridiaReserve - - // Miniboss room transitions - miniBossRooms := false - if settings.Get("miniBossRooms") { - ceresRidleyRoom := snes.vars["roomID"].old == roomIDEnum["flatRoom"] && snes.vars["roomID"].current == roomIDEnum["ceresRidley"] - sporeSpawnRoom := snes.vars["roomID"].old == roomIDEnum["sporeSpawnKeyhunter"] && snes.vars["roomID"].current == roomIDEnum["sporeSpawn"] - crocomireRoom := snes.vars["roomID"].old == roomIDEnum["crocomireSpeedway"] && snes.vars["roomID"].current == roomIDEnum["crocomire"] - botwoonRoom := snes.vars["roomID"].old == roomIDEnum["botwoonHallway"] && snes.vars["roomID"].current == roomIDEnum["botwoon"] - // Allow either vanilla or GGG entry - goldenTorizoRoom := (snes.vars["roomID"].old == roomIDEnum["acidStatue"] || snes.vars["roomID"].old == roomIDEnum["screwAttack"]) && snes.vars["roomID"].current == roomIDEnum["goldenTorizo"] - miniBossRooms = ceresRidleyRoom || sporeSpawnRoom || crocomireRoom || botwoonRoom || goldenTorizoRoom - } +func newSNESState(memData []string, startConditionImport []string, resetConditionImport []string, splitConditionImport []string) *SNESState { + data := make([]byte, 0x10000) + vars := map[string]*memoryWatcher{} + + delimiter1 := "=" + delimiter2 := "≠" + delimiter3 := "<" + delimiter4 := ">" + compareStringCurrent := "current" + compareStringPrior := "prior" + hexPrefix := "0x" + var startConditions []conditionList + var resetConditions []conditionList + var splitConditions []conditionList + + // fill vars map + for _, p := range memData { + mem := strings.Split(p, ",") + size, _ := strconv.Atoi(mem[2]) + tempHex := *hexToInt(mem[1]) + temp32int := uint32(tempHex) + vars[mem[0]] = newMemoryWatcher(temp32int, size) + } + + // Populate Start Condition List + for _, p := range startConditionImport { + var condition conditionList + // create elements + // add elements to reset condition list + startName := strings.Split(p, ":")[0] + startCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = startName + + for _, q := range startCon { + if strings.Contains(q, "&&") { + continue + } - // Boss room transitions - bossRooms := false - if settings.Get("bossRooms") { - kraidRoom := snes.vars["roomID"].old == roomIDEnum["kraidEyeDoor"] && snes.vars["roomID"].current == roomIDEnum["kraid"] - phantoonRoom := snes.vars["roomID"].old == roomIDEnum["basement"] && snes.vars["roomID"].current == roomIDEnum["phantoon"] - draygonRoom := snes.vars["roomID"].old == roomIDEnum["precious"] && snes.vars["roomID"].current == roomIDEnum["draygon"] - ridleyRoom := snes.vars["roomID"].old == roomIDEnum["lowerNorfairFarming"] && snes.vars["roomID"].current == roomIDEnum["ridley"] - motherBrainRoom := snes.vars["roomID"].old == roomIDEnum["rinkaShaft"] && snes.vars["roomID"].current == roomIDEnum["motherBrain"] - bossRooms = kraidRoom || phantoonRoom || draygonRoom || ridleyRoom || motherBrainRoom - } + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } - // Elevator transitions between areas - elevatorTransitions := false - if settings.Get("elevatorTransitions") { - blueBrinstar := (snes.vars["roomID"].old == roomIDEnum["elevatorToMorphBall"] && snes.vars["roomID"].current == roomIDEnum["morphBall"]) || (snes.vars["roomID"].old == roomIDEnum["morphBall"] && snes.vars["roomID"].current == roomIDEnum["elevatorToMorphBall"]) - greenBrinstar := (snes.vars["roomID"].old == roomIDEnum["elevatorToGreenBrinstar"] && snes.vars["roomID"].current == roomIDEnum["greenBrinstarMainShaft"]) || (snes.vars["roomID"].old == roomIDEnum["greenBrinstarMainShaft"] && snes.vars["roomID"].current == roomIDEnum["elevatorToGreenBrinstar"]) - businessCenter := (snes.vars["roomID"].old == roomIDEnum["warehouseEntrance"] && snes.vars["roomID"].current == roomIDEnum["businessCenter"]) || (snes.vars["roomID"].old == roomIDEnum["businessCenter"] && snes.vars["roomID"].current == roomIDEnum["warehouseEntrance"]) - caterpillar := (snes.vars["roomID"].old == roomIDEnum["elevatorToCaterpillar"] && snes.vars["roomID"].current == roomIDEnum["caterpillar"]) || (snes.vars["roomID"].old == roomIDEnum["caterpillar"] && snes.vars["roomID"].current == roomIDEnum["elevatorToCaterpillar"]) - maridiaElevator := (snes.vars["roomID"].old == roomIDEnum["elevatorToMaridia"] && snes.vars["roomID"].current == roomIDEnum["maridiaElevator"]) || (snes.vars["roomID"].old == roomIDEnum["maridiaElevator"] && snes.vars["roomID"].current == roomIDEnum["elevatorToMaridia"]) - elevatorTransitions = blueBrinstar || greenBrinstar || businessCenter || caterpillar || maridiaElevator - } + tempElement.result = compare - // Room transitions - ceresEscape := settings.Get("ceresEscape") && snes.vars["roomID"].current == roomIDEnum["ceresElevator"] && snes.vars["gameState"].old == gameStateEnum["normalGameplay"] && snes.vars["gameState"].current == gameStateEnum["startOfCeresCutscene"] - wreckedShipEntrance := settings.Get("wreckedShipEntrance") && snes.vars["roomID"].old == roomIDEnum["westOcean"] && snes.vars["roomID"].current == roomIDEnum["wreckedShipEntrance"] - redTowerMiddleEntrance := settings.Get("redTowerMiddleEntrance") && snes.vars["roomID"].old == roomIDEnum["noobBridge"] && snes.vars["roomID"].current == roomIDEnum["redTower"] - redTowerBottomEntrance := settings.Get("redTowerBottomEntrance") && snes.vars["roomID"].old == roomIDEnum["bat"] && snes.vars["roomID"].current == roomIDEnum["redTower"] - kraidsLair := settings.Get("kraidsLair") && snes.vars["roomID"].old == roomIDEnum["warehouseEntrance"] && snes.vars["roomID"].current == roomIDEnum["warehouseZeela"] - risingTideEntrance := settings.Get("risingTideEntrance") && snes.vars["roomID"].old == roomIDEnum["cathedral"] && snes.vars["roomID"].current == roomIDEnum["risingTide"] - atticExit := settings.Get("atticExit") && snes.vars["roomID"].old == roomIDEnum["attic"] && snes.vars["roomID"].current == roomIDEnum["westOcean"] - tubeBroken := settings.Get("tubeBroken") && snes.vars["roomID"].current == roomIDEnum["glassTunnel"] && (snes.vars["eventFlags"].old&eventFlagEnum["tubeBroken"]) == 0 && (snes.vars["eventFlags"].current&eventFlagEnum["tubeBroken"]) > 0 - cacExit := settings.Get("cacExit") && snes.vars["roomID"].old == roomIDEnum["westCactusAlley"] && snes.vars["roomID"].current == roomIDEnum["butterflyRoom"] - toilet := settings.Get("toilet") && (snes.vars["roomID"].old == roomIDEnum["plasmaSpark"] && snes.vars["roomID"].current == roomIDEnum["toiletBowl"] || snes.vars["roomID"].old == roomIDEnum["oasis"] && snes.vars["roomID"].current == roomIDEnum["toiletBowl"]) - kronicBoost := settings.Get("kronicBoost") && (snes.vars["roomID"].old == roomIDEnum["magdolliteTunnel"] && snes.vars["roomID"].current == roomIDEnum["kronicBoost"] || snes.vars["roomID"].old == roomIDEnum["spikyAcidSnakes"] && snes.vars["roomID"].current == roomIDEnum["kronicBoost"] || snes.vars["roomID"].old == roomIDEnum["volcano"] && snes.vars["roomID"].current == roomIDEnum["kronicBoost"]) - lowerNorfairEntrance := settings.Get("lowerNorfairEntrance") && snes.vars["roomID"].old == roomIDEnum["lowerNorfairElevator"] && snes.vars["roomID"].current == roomIDEnum["mainHall"] - writg := settings.Get("writg") && snes.vars["roomID"].old == roomIDEnum["pillars"] && snes.vars["roomID"].current == roomIDEnum["writg"] - redKiShaft := settings.Get("redKiShaft") && (snes.vars["roomID"].old == roomIDEnum["amphitheatre"] && snes.vars["roomID"].current == roomIDEnum["redKiShaft"] || snes.vars["roomID"].old == roomIDEnum["wasteland"] && snes.vars["roomID"].current == roomIDEnum["redKiShaft"]) - metalPirates := settings.Get("metalPirates") && snes.vars["roomID"].old == roomIDEnum["wasteland"] && snes.vars["roomID"].current == roomIDEnum["metalPirates"] - lowerNorfairSpringMaze := settings.Get("lowerNorfairSpringMaze") && snes.vars["roomID"].old == roomIDEnum["lowerNorfairFireflea"] && snes.vars["roomID"].current == roomIDEnum["lowerNorfairSpringMaze"] - lowerNorfairExit := settings.Get("lowerNorfairExit") && snes.vars["roomID"].old == roomIDEnum["threeMusketeers"] && snes.vars["roomID"].current == roomIDEnum["singleChamber"] - allBossesFinished := (snes.vars["brinstarBosses"].current&bossFlagEnum["kraid"]) > 0 && (snes.vars["wreckedShipBosses"].current&bossFlagEnum["phantoon"]) > 0 && (snes.vars["maridiaBosses"].current&bossFlagEnum["draygon"]) > 0 && (snes.vars["norfairBosses"].current&bossFlagEnum["ridley"]) > 0 - goldenFour := settings.Get("goldenFour") && snes.vars["roomID"].old == roomIDEnum["statuesHallway"] && snes.vars["roomID"].current == roomIDEnum["statues"] && allBossesFinished - tourianEntrance := settings.Get("tourianEntrance") && snes.vars["roomID"].old == roomIDEnum["statues"] && snes.vars["roomID"].current == roomIDEnum["tourianElevator"] - metroids := settings.Get("metroids") && (snes.vars["roomID"].old == roomIDEnum["metroidOne"] && snes.vars["roomID"].current == roomIDEnum["metroidTwo"] || snes.vars["roomID"].old == roomIDEnum["metroidTwo"] && snes.vars["roomID"].current == roomIDEnum["metroidThree"] || snes.vars["roomID"].old == roomIDEnum["metroidThree"] && snes.vars["roomID"].current == roomIDEnum["metroidFour"] || snes.vars["roomID"].old == roomIDEnum["metroidFour"] && snes.vars["roomID"].current == roomIDEnum["tourianHopper"]) - babyMetroidRoom := settings.Get("babyMetroidRoom") && snes.vars["roomID"].old == roomIDEnum["dustTorizo"] && snes.vars["roomID"].current == roomIDEnum["bigBoy"] - escapeClimb := settings.Get("escapeClimb") && snes.vars["roomID"].old == roomIDEnum["tourianEscape4"] && snes.vars["roomID"].current == roomIDEnum["climb"] - roomTransitions := miniBossRooms || bossRooms || elevatorTransitions || ceresEscape || wreckedShipEntrance || redTowerMiddleEntrance || redTowerBottomEntrance || kraidsLair || risingTideEntrance || atticExit || tubeBroken || cacExit || toilet || kronicBoost || lowerNorfairEntrance || writg || redKiShaft || metalPirates || lowerNorfairSpringMaze || lowerNorfairExit || tourianEntrance || goldenFour || metroids || babyMetroidRoom || escapeClimb - - // Minibosses - ceresRidley := settings.Get("ceresRidley") && (snes.vars["ceresBosses"].old&bossFlagEnum["ceresRidley"]) == 0 && (snes.vars["ceresBosses"].current&bossFlagEnum["ceresRidley"]) > 0 && snes.vars["roomID"].current == roomIDEnum["ceresRidley"] - bombTorizo := settings.Get("bombTorizo") && (snes.vars["crateriaBosses"].old&bossFlagEnum["bombTorizo"]) == 0 && (snes.vars["crateriaBosses"].current&bossFlagEnum["bombTorizo"]) > 0 && snes.vars["roomID"].current == roomIDEnum["bombTorizo"] - sporeSpawn := settings.Get("sporeSpawn") && (snes.vars["brinstarBosses"].old&bossFlagEnum["sporeSpawn"]) == 0 && (snes.vars["brinstarBosses"].current&bossFlagEnum["sporeSpawn"]) > 0 && snes.vars["roomID"].current == roomIDEnum["sporeSpawn"] - crocomire := settings.Get("crocomire") && (snes.vars["norfairBosses"].old&bossFlagEnum["crocomire"]) == 0 && (snes.vars["norfairBosses"].current&bossFlagEnum["crocomire"]) > 0 && snes.vars["roomID"].current == roomIDEnum["crocomire"] - botwoon := settings.Get("botwoon") && (snes.vars["maridiaBosses"].old&bossFlagEnum["botwoon"]) == 0 && (snes.vars["maridiaBosses"].current&bossFlagEnum["botwoon"]) > 0 && snes.vars["roomID"].current == roomIDEnum["botwoon"] - goldenTorizo := settings.Get("goldenTorizo") && (snes.vars["norfairBosses"].old&bossFlagEnum["goldenTorizo"]) == 0 && (snes.vars["norfairBosses"].current&bossFlagEnum["goldenTorizo"]) > 0 && snes.vars["roomID"].current == roomIDEnum["goldenTorizo"] - minibossDefeat := ceresRidley || bombTorizo || sporeSpawn || crocomire || botwoon || goldenTorizo - - // Bosses - kraid := settings.Get("kraid") && (snes.vars["brinstarBosses"].old&bossFlagEnum["kraid"]) == 0 && (snes.vars["brinstarBosses"].current&bossFlagEnum["kraid"]) > 0 && snes.vars["roomID"].current == roomIDEnum["kraid"] - if kraid { - fmt.Println("Split due to kraid defeat") - } - phantoon := settings.Get("phantoon") && (snes.vars["wreckedShipBosses"].old&bossFlagEnum["phantoon"]) == 0 && (snes.vars["wreckedShipBosses"].current&bossFlagEnum["phantoon"]) > 0 && snes.vars["roomID"].current == roomIDEnum["phantoon"] - if phantoon { - fmt.Println("Split due to phantoon defeat") - } - draygon := settings.Get("draygon") && (snes.vars["maridiaBosses"].old&bossFlagEnum["draygon"]) == 0 && (snes.vars["maridiaBosses"].current&bossFlagEnum["draygon"]) > 0 && snes.vars["roomID"].current == roomIDEnum["draygon"] - if draygon { - fmt.Println("Split due to draygon defeat") - } - ridley := settings.Get("ridley") && (snes.vars["norfairBosses"].old&bossFlagEnum["ridley"]) == 0 && (snes.vars["norfairBosses"].current&bossFlagEnum["ridley"]) > 0 && snes.vars["roomID"].current == roomIDEnum["ridley"] - if ridley { - fmt.Println("Split due to ridley defeat") - } - // Mother Brain phases - inMotherBrainRoom := snes.vars["roomID"].current == roomIDEnum["motherBrain"] - mb1 := settings.Get("mb1") && inMotherBrainRoom && snes.vars["gameState"].current == gameStateEnum["normalGameplay"] && snes.vars["motherBrainHP"].old == 0 && snes.vars["motherBrainHP"].current == (motherBrainMaxHPEnum["phase2"]) - if mb1 { - fmt.Println("Split due to mb1 defeat") - } - mb2 := settings.Get("mb2") && inMotherBrainRoom && snes.vars["gameState"].current == gameStateEnum["normalGameplay"] && snes.vars["motherBrainHP"].old == 0 && snes.vars["motherBrainHP"].current == (motherBrainMaxHPEnum["phase3"]) - if mb2 { - fmt.Println("Split due to mb2 defeat") - } - mb3 := settings.Get("mb3") && inMotherBrainRoom && (snes.vars["tourianBosses"].old&bossFlagEnum["motherBrain"]) == 0 && (snes.vars["tourianBosses"].current&bossFlagEnum["motherBrain"]) > 0 - if mb3 { - fmt.Println("Split due to mb3 defeat") + condition.memory = append(condition.memory, tempElement) + } + // add condition lists to Start Conditions list + startConditions = append(startConditions, condition) } - bossDefeat := kraid || phantoon || draygon || ridley || mb1 || mb2 || mb3 - // Run-ending splits - escape := settings.Get("rtaFinish") && (snes.vars["eventFlags"].current&eventFlagEnum["zebesAblaze"]) > 0 && snes.vars["shipAI"].old != 0xaa4f && snes.vars["shipAI"].current == 0xaa4f + // Populate Reset Condition List + for _, p := range resetConditionImport { + var condition conditionList + // create elements + // add elements to reset condition list + resetName := strings.Split(p, ":")[0] + resetCon := strings.Split(strings.Split(p, ":")[1], " ") - takeoff := settings.Get("igtFinish") && snes.vars["roomID"].current == roomIDEnum["landingSite"] && snes.vars["gameState"].old == gameStateEnum["preEndCutscene"] && snes.vars["gameState"].current == gameStateEnum["endCutscene"] + condition.Name = resetName - sporeSpawnRTAFinish := false - if settings.Get("sporeSpawnRTAFinish") { - if snes.pickedUpSporeSpawnSuper { - if snes.vars["igtFrames"].old != snes.vars["igtFrames"].current { - sporeSpawnRTAFinish = true - snes.pickedUpSporeSpawnSuper = false + for _, q := range resetCon { + if strings.Contains(q, "&&") { + continue } - } else { - snes.pickedUpSporeSpawnSuper = snes.vars["roomID"].current == roomIDEnum["sporeSpawnSuper"] && (snes.vars["maxSupers"].old+5) == (snes.vars["maxSupers"].current) && (snes.vars["brinstarBosses"].current&bossFlagEnum["sporeSpawn"]) > 0 - } - } - hundredMissileRTAFinish := false - if settings.Get("hundredMissileRTAFinish") { - if snes.pickedUpHundredthMissile { - if snes.vars["igtFrames"].old != snes.vars["igtFrames"].current { - hundredMissileRTAFinish = true - snes.pickedUpHundredthMissile = false + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } + } } - } else { - snes.pickedUpHundredthMissile = snes.vars["maxMissiles"].old == 95 && snes.vars["maxMissiles"].current == 100 - } - } - - nonStandardCategoryFinish := sporeSpawnRTAFinish || hundredMissileRTAFinish - if pickup { - fmt.Println("Split due to pickup") - } - - if unlock { - fmt.Println("Split due to unlock") - } + tempElement.result = compare - if beam { - fmt.Println("Split due to beam upgrade") + condition.memory = append(condition.memory, tempElement) + } + // add condition lists to Reset Conditions list + resetConditions = append(resetConditions, condition) } - if energyUpgrade { - fmt.Println("Split due to energy upgrade") - } + // Populate Split Condition List + for _, p := range splitConditionImport { + var condition conditionList + // create elements + // add elements to split condition list + splitName := strings.Split(p, ":")[0] + splitCon := strings.Split(strings.Split(p, ":")[1], " ") - if roomTransitions { - fmt.Println("Split due to room transition") - } + condition.Name = splitName - if minibossDefeat { - fmt.Println("Split due to miniboss defeat") - } + for _, q := range splitCon { + if strings.Contains(q, "&&") { + continue + } - // individual boss defeat conditions already covered above - if escape { - fmt.Println("Split due to escape") - } + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1]) + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } + } + } - if takeoff { - fmt.Println("Split due to takeoff") - } + tempElement.result = compare - if nonStandardCategoryFinish { - fmt.Println("Split due to non standard category finish") + condition.memory = append(condition.memory, tempElement) + } + // add condition lists to Split Conditions list + splitConditions = append(splitConditions, condition) } - return pickup || unlock || beam || energyUpgrade || roomTransitions || minibossDefeat || bossDefeat || escape || takeoff || nonStandardCategoryFinish -} - -const NUM_LATENCY_SAMPLES = 10 - -type SNESState struct { - vars map[string]*MemoryWatcher - pickedUpHundredthMissile bool - pickedUpSporeSpawnSuper bool - latencySamples []uint128 - data []byte - doExtraUpdate bool - // mu sync.Mutex -} - -type uint128 struct { - hi uint64 - lo uint64 -} - -func (a uint128) Add(b uint128) uint128 { - lo := a.lo + b.lo - hi := a.hi + b.hi - if lo < a.lo { - hi++ + return &SNESState{ + // doExtraUpdate: true, + data: data, + startConditions: startConditions, + resetConditions: resetConditions, + splitConditions: splitConditions, + // latencySamples: make([]uint128, 0), + // pickedUpHundredthMissile: false, + // pickedUpSporeSpawnSuper: false, + vars: vars, } - return uint128{hi: hi, lo: lo} } -func (a uint128) Sub(b uint128) uint128 { - lo := a.lo - b.lo - hi := a.hi - b.hi - if a.lo < b.lo { - hi-- +func newMemoryWatcher(address uint32, size int) *memoryWatcher { + return &memoryWatcher{ + address: address, + current: new(uint16), + old: new(uint16), + size: size, } - return uint128{hi: hi, lo: lo} } -func (a uint128) ToFloat64() float64 { - return float64(a.hi)*math.Pow(2, 64) + float64(a.lo) -} - -func NewSNESState() *SNESState { - data := make([]byte, 0x10000) - vars := map[string]*MemoryWatcher{ - // Word - "controller": NewMemoryWatcher(0x008B, Word), - "roomID": NewMemoryWatcher(0x079B, Word), - "enemyHP": NewMemoryWatcher(0x0F8C, Word), - "shipAI": NewMemoryWatcher(0x0FB2, Word), - "motherBrainHP": NewMemoryWatcher(0x0FCC, Word), - // Byte - "mapInUse": NewMemoryWatcher(0x079F, Byte), - "gameState": NewMemoryWatcher(0x0998, Byte), - "unlockedEquips2": NewMemoryWatcher(0x09A4, Byte), - "unlockedEquips": NewMemoryWatcher(0x09A5, Byte), - "unlockedBeams": NewMemoryWatcher(0x09A8, Byte), - "unlockedCharge": NewMemoryWatcher(0x09A9, Byte), - "maxEnergy": NewMemoryWatcher(0x09C4, Word), - "maxMissiles": NewMemoryWatcher(0x09C8, Byte), - "maxSupers": NewMemoryWatcher(0x09CC, Byte), - "maxPowerBombs": NewMemoryWatcher(0x09D0, Byte), - "maxReserve": NewMemoryWatcher(0x09D4, Word), - "igtFrames": NewMemoryWatcher(0x09DA, Byte), - "igtSeconds": NewMemoryWatcher(0x09DC, Byte), - "igtMinutes": NewMemoryWatcher(0x09DE, Byte), - "igtHours": NewMemoryWatcher(0x09E0, Byte), - "playerState": NewMemoryWatcher(0x0A28, Byte), - "eventFlags": NewMemoryWatcher(0xD821, Byte), - "crateriaBosses": NewMemoryWatcher(0xD828, Byte), - "brinstarBosses": NewMemoryWatcher(0xD829, Byte), - "norfairBosses": NewMemoryWatcher(0xD82A, Byte), - "wreckedShipBosses": NewMemoryWatcher(0xD82B, Byte), - "maridiaBosses": NewMemoryWatcher(0xD82C, Byte), - "tourianBosses": NewMemoryWatcher(0xD82D, Byte), - "ceresBosses": NewMemoryWatcher(0xD82E, Byte), - "crateriaItems": NewMemoryWatcher(0xD870, Byte), - "brinteriaItems": NewMemoryWatcher(0xD871, Byte), - "brinstarItems2": NewMemoryWatcher(0xD872, Byte), - "brinstarItems3": NewMemoryWatcher(0xD873, Byte), - "brinstarItems4": NewMemoryWatcher(0xD874, Byte), - "brinstarItems5": NewMemoryWatcher(0xD875, Byte), - "norfairItems1": NewMemoryWatcher(0xD876, Byte), - "norfairItems2": NewMemoryWatcher(0xD877, Byte), - "norfairItems3": NewMemoryWatcher(0xD878, Byte), - "norfairItems4": NewMemoryWatcher(0xD879, Byte), - "norfairItems5": NewMemoryWatcher(0xD87A, Byte), - "wreckedShipItems": NewMemoryWatcher(0xD880, Byte), - "maridiaItems1": NewMemoryWatcher(0xD881, Byte), - "maridiaItems2": NewMemoryWatcher(0xD882, Byte), - "maridiaItems3": NewMemoryWatcher(0xD883, Byte), - } - return &SNESState{ - doExtraUpdate: true, - data: data, - latencySamples: make([]uint128, 0), - pickedUpHundredthMissile: false, - pickedUpSporeSpawnSuper: false, - vars: vars, +func (mw *memoryWatcher) updateValue(memory []byte) { + *mw.old = *mw.current + switch mw.size { + case 1: + *mw.current = uint16(memory[mw.address]) + case 2: + addr := mw.address + *mw.current = uint16(memory[addr]) | uint16(memory[addr+1])<<8 } } -func (mw MemoryWatcher) ptr() *MemoryWatcher { - return &mw -} - func (s *SNESState) update() { // s.mu.Lock() // defer s.mu.Unlock() for _, watcher := range s.vars { - if s.doExtraUpdate { - watcher.UpdateValue(s.data) - s.doExtraUpdate = false - } - watcher.UpdateValue(s.data) + // if s.doExtraUpdate { + // watcher.updateValue(s.data) + // s.doExtraUpdate = false + // } + watcher.updateValue(s.data) } } type SNESSummary struct { - LatencyAverage float64 - LatencyStddev float64 - Start bool - Reset bool - Split bool + // LatencyAverage float64 + // LatencyStddev float64 + Start bool + Reset bool + Split bool +} + +type QUSB2SNESAutoSplitter struct { + snes *SNESState + // settings *sync.RWMutex + // settingsData *Settings +} + +func NewQUSB2SNESAutoSplitter(memData []string, startConditionImport []string, resetConditionImport []string, splitConditionImport []string /*settings *sync.RWMutex, settingsData *Settings*/) *QUSB2SNESAutoSplitter { + return &QUSB2SNESAutoSplitter{ + snes: newSNESState(memData, startConditionImport, resetConditionImport, splitConditionImport), + // settings: settings, + // settingsData: settingsData, + } } -func (s *SNESState) FetchAll(client SyncClient, settings *Settings) (*SNESSummary, error) { - startTime := time.Now() - addresses := [][2]int{ - {0xF5008B, 2}, // Controller 1 Input - {0xF5079B, 3}, // ROOM ID + ROOM # for region + Region Number - {0xF50998, 1}, // GAME STATE - {0xF509A4, 61}, // ITEMS - {0xF50A28, 1}, - {0xF50F8C, 66}, - {0xF5D821, 14}, - {0xF5D870, 20}, +func (a *QUSB2SNESAutoSplitter) Update(client SyncClient, splitNum int) (*SNESSummary, error) { + addresses := [][2]int{} + + for _, watcher := range a.snes.vars { + fullAddress := int(watcher.address | 0xF50000) + + newRow := []int{fullAddress, int(watcher.size)} + addresses = append(addresses, [2]int(newRow)) } + snesData, err := client.getAddresses(addresses) if err != nil { return nil, err } - copy(s.data[0x008B:0x008B+2], snesData[0]) - copy(s.data[0x079B:0x079B+3], snesData[1]) - s.data[0x0998] = snesData[2][0] - copy(s.data[0x09A4:0x09A4+61], snesData[3]) - s.data[0x0A28] = snesData[4][0] - copy(s.data[0x0F8C:0x0F8C+66], snesData[5]) - copy(s.data[0xD821:0xD821+14], snesData[6]) - copy(s.data[0xD870:0xD870+20], snesData[7]) - - s.update() + for index, row := range addresses { + copy(a.snes.data[(row[0]^0xF50000):(row[0]^0xF50000)+row[1]], snesData[index]) + } + a.snes.update() - start := s.start() - reset := s.reset() - split := split(settings, s) + start := a.snes.start() + reset := a.snes.reset() + split := a.snes.split(splitNum) - elapsed := time.Since(startTime).Milliseconds() + // elapsed := time.Since(startTime).Milliseconds() - if len(s.latencySamples) == NUM_LATENCY_SAMPLES { - s.latencySamples = s.latencySamples[1:] - } - s.latencySamples = append(s.latencySamples, uint128FromInt(elapsed)) + // if len(s.latencySamples) == NUM_LATENCY_SAMPLES { + // s.latencySamples = s.latencySamples[1:] + // } + // s.latencySamples = append(s.latencySamples, uint128FromInt(elapsed)) - averageLatency := averageUint128Slice(s.latencySamples) + // averageLatency := averageUint128Slice(s.latencySamples) - var sdevSum float64 - for _, x := range s.latencySamples { - diff := x.ToFloat64() - averageLatency - sdevSum += diff * diff - } - stddev := math.Sqrt(sdevSum / float64(len(s.latencySamples)-1)) + // var sdevSum float64 + // for _, x := range s.latencySamples { + // diff := x.ToFloat64() - averageLatency + // sdevSum += diff * diff + // } + // stddev := math.Sqrt(sdevSum / float64(len(s.latencySamples)-1)) return &SNESSummary{ - LatencyAverage: averageLatency, - LatencyStddev: stddev, - Start: start, - Reset: reset, - Split: split, + // LatencyAverage: averageLatency, + // LatencyStddev: stddev, + Start: start, + Reset: reset, + Split: split, }, nil } -func uint128FromInt(i int64) uint128 { - if i < 0 { - return uint128{hi: math.MaxUint64, lo: uint64(i)} +func (a *QUSB2SNESAutoSplitter) ResetGameTracking() { + // a.snes = newSNESState() + clear(a.snes.data[:]) + for _, watcher := range a.snes.vars { + *watcher.current = 0 + *watcher.old = 0 } - return uint128{hi: 0, lo: uint64(i)} + // a.snes.doExtraUpdate = true } -func averageUint128Slice(arr []uint128) float64 { - var sum uint128 - for _, v := range arr { - sum = sum.Add(v) +// convert hex string to int +func hexToInt(hex string) *uint16 { + num, err := strconv.ParseUint(hex, 0, 64) + if err != nil { + log.Fatalf("Failed to convert string to integer: %v", err) + return nil } - return sum.ToFloat64() / float64(len(arr)) -} - -func (s *SNESState) start() bool { - normalStart := s.vars["gameState"].old == 2 && s.vars["gameState"].current == 0x1f - cutsceneEnded := s.vars["gameState"].old == 0x1E && s.vars["gameState"].current == 0x1F - zebesStart := s.vars["gameState"].old == 5 && s.vars["gameState"].current == 6 - return normalStart || cutsceneEnded || zebesStart -} - -func (s *SNESState) reset() bool { - return s.vars["roomID"].old != 0 && s.vars["roomID"].current == 0 + integer := uint16(num) + return &integer } -type TimeSpan struct { - seconds float64 -} - -func (t TimeSpan) Seconds() float64 { - return t.seconds -} - -func TimeSpanFromSeconds(seconds float64) TimeSpan { - return TimeSpan{seconds: seconds} -} - -func (s *SNESState) gametimeToSeconds() TimeSpan { - hours := float64(s.vars["igtHours"].current) - minutes := float64(s.vars["igtMinutes"].current) - seconds := float64(s.vars["igtSeconds"].current) - - totalSeconds := hours*3600 + minutes*60 + seconds - return TimeSpanFromSeconds(totalSeconds) -} - -type SuperMetroidAutoSplitter struct { - snes *SNESState - // settings *sync.RWMutex - settingsData *Settings -} - -func NewSuperMetroidAutoSplitter( /*settings *sync.RWMutex,*/ settingsData *Settings) *SuperMetroidAutoSplitter { - return &SuperMetroidAutoSplitter{ - snes: NewSNESState(), - // settings: settings, - settingsData: settingsData, +func compare(input string, prior *uint16, current *uint16, expected *uint16) bool { + switch input { + case "ceqp": + fallthrough + case "peqc": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior == *current + } + case "ceqe": + fallthrough + case "eeqc": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected == *current + } + case "eeqp": + fallthrough + case "peqe": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior == *expected + } + case "cnep": + fallthrough + case "pnec": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior != *current + } + case "cnee": + fallthrough + case "enec": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected != *current + } + case "enep": + fallthrough + case "pnee": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior != *expected + } + case "cgtp": + fallthrough + case "pltc": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior < *current + } + case "cgte": + fallthrough + case "eltc": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected < *current + } + case "egtp": + fallthrough + case "plte": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior < *expected + } + case "cltp": + fallthrough + case "pgtc": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior > *current + } + case "clte": + fallthrough + case "egtc": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected > *current + } + case "eltp": + fallthrough + case "pgte": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior > *expected + } + default: + return false } } - -func (a *SuperMetroidAutoSplitter) Update(client SyncClient) (*SNESSummary, error) { - return a.snes.FetchAll(client, a.settingsData) -} - -func (a *SuperMetroidAutoSplitter) GametimeToSeconds() *TimeSpan { - t := a.snes.gametimeToSeconds() - return &t -} - -func (a *SuperMetroidAutoSplitter) ResetGameTracking() { - a.snes = NewSNESState() -} From 23b5a62a3e4c214d77f9ec3cc89c5fc335bc57b5 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 26 Dec 2025 11:40:02 -0500 Subject: [PATCH 27/52] new function added --- frontend/wailsjs/runtime/runtime.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js index 623397b..7cb89d7 100644 --- a/frontend/wailsjs/runtime/runtime.js +++ b/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName, ...additionalEventNames) { return window.runtime.EventsOff(eventName, ...additionalEventNames); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { return EventsOnMultiple(eventName, callback, 1); } From d1169bb7911baeae5e38d503d9a07ab0d67bd631 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 11:30:42 -0500 Subject: [PATCH 28/52] added qusb2snes service config example --- main.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/main.go b/main.go index 80c237b..85f4147 100644 --- a/main.go +++ b/main.go @@ -68,6 +68,7 @@ func main() { // UseAutoSplitter and Type should come from the splits config file // ResetTimerOnGameReset, ResetGameOnTimerReset, Addr, Port should come from the autosplitter config file + // // NWA AutoSplitterService := autosplitters.Splitters{ NWAAutoSplitter: new(nwa.NWASplitter), QUSB2SNESAutoSplitter: new(qusb2snes.SyncClient), @@ -78,6 +79,17 @@ func main() { Port: 48879, Type: autosplitters.NWA} + // // QUSB2SNES + // AutoSplitterService := autosplitters.Splitters{ + // NWAAutoSplitter: new(nwa.NWASplitter), + // QUSB2SNESAutoSplitter: new(qusb2snes.SyncClient), + // UseAutosplitter: true, + // ResetTimerOnGameReset: true, + // ResetGameOnTimerReset: false, + // Addr: "0.0.0.0", + // Port: 23074, + // Type: autosplitters.QUSB2SNES} + var hotkeyProvider statemachine.HotkeyProvider err := wails.Run(&options.App{ From 67e092c59e417bb8d2c7bfd213261e00b56bbdc9 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 11:31:19 -0500 Subject: [PATCH 29/52] added server and port config to qusb --- autosplitters/QUSB2SNES/qusb2snes_client.go | 14 ++++++++------ autosplitters/service.go | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/autosplitters/QUSB2SNES/qusb2snes_client.go b/autosplitters/QUSB2SNES/qusb2snes_client.go index 55e745f..b480928 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_client.go +++ b/autosplitters/QUSB2SNES/qusb2snes_client.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "net/url" + "strconv" "github.com/gorilla/websocket" ) @@ -99,16 +100,17 @@ type SyncClient struct { devel bool } -func Connect() (*SyncClient, error) { - return connect(false) +func Connect(host string, port uint32) (*SyncClient, error) { + return connect(host, port, false) } -func ConnectWithDevel() (*SyncClient, error) { - return connect(true) +func ConnectWithDevel(host string, port uint32) (*SyncClient, error) { + return connect(host, port, true) } -func connect(devel bool) (*SyncClient, error) { - u := url.URL{Scheme: "ws", Host: "localhost:23074", Path: "/"} +func connect(host string, port uint32, devel bool) (*SyncClient, error) { + numStr := strconv.FormatUint(uint64(port), 10) + u := url.URL{Scheme: "ws", Host: host + ":" + numStr, Path: "/"} conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { return nil, err diff --git a/autosplitters/service.go b/autosplitters/service.go index 8a1b31d..4c615ad 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -80,7 +80,7 @@ func (s Splitters) newClient() (*nwa.NWASplitter, *qusb2snes.SyncClient) { } if s.Type == QUSB2SNES { // fmt.Printf("Creating QUSB2SNES AutoSplitter\n") - client, connectError := qusb2snes.Connect() + client, connectError := qusb2snes.Connect(s.Addr, s.Port) if connectError == nil { return nil, client } else { @@ -217,9 +217,9 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { // s.NWAAutoSplitter.Client.Close() processElapsed := time.Since(processStart) - // fmt.Println(processStart) - // fmt.Println(processElapsed) - // fmt.Println(mil - processElapsed) + fmt.Println(processStart) + fmt.Println(processElapsed) + fmt.Println(mil - processElapsed) time.Sleep(min(mil, max(0, mil-processElapsed))) } } From 555eaa0823e0e6708ec633313fbdb4819086b2c0 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 11:39:31 -0500 Subject: [PATCH 30/52] added bitwise operators --- ...ny% (No WW, NES NTSC-US, Rash (Green)).nwa | 2 +- .../Home Improvement (SNES) - Any%.nwa | 2 +- autosplitters/NWA/nwa_splitter.go | 209 +++++++++++++++-- .../Home Improvement (SNES) - Any%.qusb2snes | 4 +- autosplitters/QUSB2SNES/qusb2snes_splitter.go | 218 +++++++++++++++--- 5 files changed, 377 insertions(+), 58 deletions(-) diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa index 41c9f96..b7e2c8b 100644 --- a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa @@ -13,7 +13,7 @@ start:level,prior=0x0 && level,current=0x1 reset:level,current=0x0 && level,prior≠0x0 #split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) -#= ≠ < > +#= ≠ < > & | ^ level2:level,prior=0xFF && level,current=0x2 level3:level,prior=0xFF && level,current=0x3 level4:level,prior=0xFF && level,current=0x4 diff --git a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa index 0916953..746e7c6 100644 --- a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa +++ b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa @@ -30,7 +30,7 @@ tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && sce level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0 #split (some games might have multiple split conditions) (expectedValue is optional) -#= ≠ < > +#= ≠ < > & | ^ 1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13 1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13 1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13 diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index 9c00753..db1c055 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -50,9 +50,11 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo delimiter2 := "≠" delimiter3 := "<" delimiter4 := ">" + delimiter5 := "&" + delimiter6 := "|" + delimiter7 := "^" compareStringCurrent := "current" compareStringPrior := "prior" - hexPrefix := "0x" b.data = make([]byte, 0x10000) b.vars = map[string]*memoryWatcher{} @@ -91,11 +93,12 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo components := strings.Split(q, ",") tempElement.memoryEntryName = components[0] + tempElement.result = compare if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "ceqe" @@ -105,11 +108,14 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" } } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cnee" @@ -119,11 +125,14 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" } } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "clte" @@ -139,7 +148,8 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cgte" @@ -153,10 +163,41 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo tempElement.compareType = "pgtc" } } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" + } + } } - tempElement.result = compare - condition.memory = append(condition.memory, tempElement) } // add condition lists to Start Conditions list @@ -183,11 +224,12 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo components := strings.Split(q, ",") tempElement.memoryEntryName = components[0] + tempElement.result = compare if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "ceqe" @@ -197,11 +239,14 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" } } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cnee" @@ -211,11 +256,14 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" } } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "clte" @@ -225,11 +273,14 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" } } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cgte" @@ -239,12 +290,45 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" } } } - tempElement.result = compare - condition.memory = append(condition.memory, tempElement) } // add condition lists to Reset Conditions list @@ -271,11 +355,12 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo components := strings.Split(q, ",") tempElement.memoryEntryName = components[0] + tempElement.result = compare if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "ceqe" @@ -285,11 +370,14 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" } } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cnee" @@ -299,11 +387,14 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" } } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "clte" @@ -313,11 +404,14 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" } } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - if strings.HasPrefix(compStrings[1], hexPrefix) { + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cgte" @@ -327,12 +421,45 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" } } } - tempElement.result = compare - condition.memory = append(condition.memory, tempElement) } // add condition lists to Split Conditions list @@ -560,7 +687,7 @@ func findMemoryWatcher(memInfo []memoryWatcher, targetWatcher string) *memoryWat func hexToInt(hex string) *int { num, err := strconv.ParseUint(hex, 0, 64) if err != nil { - log.Fatalf("Failed to convert string to integer: %v", err) + log.Printf("Failed to convert string to integer: %v", err) return nil } integer := int(num) @@ -665,6 +792,42 @@ func compare(input string, prior *int, current *int, expected *int) bool { } else { return *prior > *expected } + case "pbae": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior & *expected) != 0 + } + case "cbae": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current & *expected) != 0 + } + case "pboe": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior | *expected) != 0 + } + case "cboe": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current | *expected) != 0 + } + case "pbne": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior ^ *expected) != 0 + } + case "cbne": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current ^ *expected) != 0 + } default: return false } diff --git a/autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes b/autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes index 3c4f690..bfa2c09 100644 --- a/autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes +++ b/autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes @@ -1,5 +1,7 @@ ResetTimerOnGameReset = true ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 23074 #memory (Do not combine memory or it will treated as 1 combined value) stage,$000AE1,1 #0-3 @@ -28,7 +30,7 @@ tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && sce level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0 #split (some games might have multiple split conditions) (expectedValue is optional) -#= ≠ < > +#= ≠ < > & | ^ 1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13 1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13 1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13 diff --git a/autosplitters/QUSB2SNES/qusb2snes_splitter.go b/autosplitters/QUSB2SNES/qusb2snes_splitter.go index 19856bf..59eb32a 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_splitter.go +++ b/autosplitters/QUSB2SNES/qusb2snes_splitter.go @@ -171,9 +171,11 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio delimiter2 := "≠" delimiter3 := "<" delimiter4 := ">" + delimiter5 := "&" + delimiter6 := "|" + delimiter7 := "^" compareStringCurrent := "current" compareStringPrior := "prior" - hexPrefix := "0x" var startConditions []conditionList var resetConditions []conditionList var splitConditions []conditionList @@ -207,11 +209,12 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio components := strings.Split(q, ",") tempElement.memoryEntryName = components[0] + tempElement.result = compare if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "ceqe" @@ -221,12 +224,14 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" } } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cnee" @@ -236,12 +241,14 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" } } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "clte" @@ -257,8 +264,8 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cgte" @@ -272,10 +279,41 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio tempElement.compareType = "pgtc" } } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" + } + } } - tempElement.result = compare - condition.memory = append(condition.memory, tempElement) } // add condition lists to Start Conditions list @@ -302,11 +340,12 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio components := strings.Split(q, ",") tempElement.memoryEntryName = components[0] + tempElement.result = compare if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "ceqe" @@ -316,12 +355,14 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" } } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cnee" @@ -331,12 +372,14 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" } } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "clte" @@ -346,12 +389,14 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" } } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cgte" @@ -361,12 +406,45 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" } } } - tempElement.result = compare - condition.memory = append(condition.memory, tempElement) } // add condition lists to Reset Conditions list @@ -393,11 +471,12 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio components := strings.Split(q, ",") tempElement.memoryEntryName = components[0] + tempElement.result = compare if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "ceqe" @@ -407,12 +486,14 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" } } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cnee" @@ -422,12 +503,14 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" } } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "clte" @@ -437,12 +520,14 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" } } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - if strings.HasPrefix(compStrings[1], hexPrefix) { - tempElement.expectedValue = hexToInt(compStrings[1]) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: tempElement.compareType = "cgte" @@ -452,12 +537,45 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } else { if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1][2:]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" } } } - tempElement.result = compare - condition.memory = append(condition.memory, tempElement) } // add condition lists to Split Conditions list @@ -594,7 +712,7 @@ func (a *QUSB2SNESAutoSplitter) ResetGameTracking() { func hexToInt(hex string) *uint16 { num, err := strconv.ParseUint(hex, 0, 64) if err != nil { - log.Fatalf("Failed to convert string to integer: %v", err) + log.Printf("Failed to convert string to integer: %v", err) return nil } integer := uint16(num) @@ -699,6 +817,42 @@ func compare(input string, prior *uint16, current *uint16, expected *uint16) boo } else { return *prior > *expected } + case "pbae": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior & *expected) != 0 + } + case "cbae": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current & *expected) != 0 + } + case "pboe": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior | *expected) != 0 + } + case "cboe": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current | *expected) != 0 + } + case "pbne": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior ^ *expected) != 0 + } + case "cbne": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current ^ *expected) != 0 + } default: return false } From bd216009fefaf3ff097f449163360b6f501f2859 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 17:27:07 -0500 Subject: [PATCH 31/52] swapped and fixed NWA test data --- autosplitters/service.go | 138 +++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/autosplitters/service.go b/autosplitters/service.go index 4c615ad..b9b8788 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -95,81 +95,81 @@ func (s Splitters) newClient() (*nwa.NWASplitter, *qusb2snes.SyncClient) { func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { mil := 1 * time.Millisecond - // // Battletoads test data - // memData := []string{ - // ("level,RAM,$0010,1")} - // startConditionImport := []string{ - // ("start:level,prior=0x0 && level,current=0x1")} - // resetConditionImport := []string{ - // ("reset:level,current=0x0 && level,prior≠0")} - // splitConditionImport := []string{ - // ("level2:level,prior=0x255 && level,current=0x2"), - // ("level3:level,prior=0x255 && level,current=0x3"), - // ("level4:level,prior=0x255 && level,current=0x4"), - // ("level5:level,prior=0x255 && level,current=0x5"), - // ("level6:level,prior=0x255 && level,current=0x6"), - // ("level7:level,prior=0x255 && level,current=0x7"), - // ("level8:level,prior=0x255 && level,current=0x8"), - // ("level9:level,prior=0x255 && level,current=0x9"), - // ("level10:level,prior=0x255 && level,current=0xA"), - // ("level11:level,prior=0x255 && level,current=0xB"), - // ("level12:level,prior=0x255 && level,current=0xC"), - // ("level13:level,prior=0x255 && level,current=0xD"), - // } - - // Home Improvment test data + // Battletoads test data memData := []string{ - ("crates,WRAM,$001A8A,1"), - ("scene,WRAM,$00161F,1"), - ("W2P2HP,WRAM,$001499,1"), - ("W2P1HP,WRAM,$001493,1"), - ("BossHP,WRAM,$001491,1"), - ("state,WRAM,$001400,1"), - ("gameplay,WRAM,$000AE5,1"), - ("substage,WRAM,$000AE3,1"), - ("stage,WRAM,$000AE1,1"), - ("scene2,WRAM,$000886,1"), - ("play_state,WRAM,$0003B1,1"), - ("power_up,WRAM,$0003AF,1"), - ("weapon,WRAM,$0003CD,1"), - ("invul,WRAM,$001C05,1"), - ("FBossHP,WRAM,$00149D,1"), - } - + ("level,RAM,$0010,1")} startConditionImport := []string{ - ("start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), - ("start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), - } - + ("start:level,prior=0x0 && level,current=0x1")} resetConditionImport := []string{ - ("cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0"), - ("tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0"), - ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0"), - } - + ("reset:level,current=0x0 && level,prior≠0")} splitConditionImport := []string{ - ("1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13"), - ("1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13"), - ("1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13"), - ("1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13"), - ("1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), - ("2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13"), - ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), - ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), - ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), - ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0"), - ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), - ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), - ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), - ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), - ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), - ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), - ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), - ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), - ("4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13"), - ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), + // ("level2:level,prior=0xFF && level,current=0x2"), + ("level3:level,prior=0xFF && level,current=0x3"), + ("level4:level,prior=0xFF && level,current=0x4"), + // ("level5:level,prior=0xFF && level,current=0x5"), + ("level6:level,prior=0xFF && level,current=0x6"), + // ("level7:level,prior=0xFF && level,current=0x7"), + ("level8:level,prior=0xFF && level,current=0x8"), + ("level9:level,prior=0xFF && level,current=0x9"), + ("level10:level,prior=0xFF && level,current=0xA"), + ("level11:level,prior=0xFF && level,current=0xB"), + ("level12:level,prior=0xFF && level,current=0xC"), + ("level13:level,prior=0xFF && level,current=0xD"), } + // // Home Improvment test data + // memData := []string{ + // ("crates,WRAM,$001A8A,1"), + // ("scene,WRAM,$00161F,1"), + // ("W2P2HP,WRAM,$001499,1"), + // ("W2P1HP,WRAM,$001493,1"), + // ("BossHP,WRAM,$001491,1"), + // ("state,WRAM,$001400,1"), + // ("gameplay,WRAM,$000AE5,1"), + // ("substage,WRAM,$000AE3,1"), + // ("stage,WRAM,$000AE1,1"), + // ("scene2,WRAM,$000886,1"), + // ("play_state,WRAM,$0003B1,1"), + // ("power_up,WRAM,$0003AF,1"), + // ("weapon,WRAM,$0003CD,1"), + // ("invul,WRAM,$001C05,1"), + // ("FBossHP,WRAM,$00149D,1"), + // } + + // startConditionImport := []string{ + // ("start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + // ("start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + // } + + // resetConditionImport := []string{ + // ("cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0"), + // ("tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0"), + // ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0"), + // } + + // splitConditionImport := []string{ + // ("1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13"), + // ("1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13"), + // ("1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13"), + // ("1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13"), + // ("1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + // ("2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13"), + // ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), + // ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), + // ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), + // ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0"), + // ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), + // ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), + // ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), + // ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), + // ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + // ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), + // ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), + // ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), + // ("4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13"), + // ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), + // } + // receive setup data...probably through a channel //Setup Memory s.NWAAutoSplitter.MemAndConditionsSetup(memData, startConditionImport, resetConditionImport, splitConditionImport) From bea934178909aa46cf9498c9b54ae15c2a085c21 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 17:28:42 -0500 Subject: [PATCH 32/52] fixed start and reset state variable location; removed unneeded string trim --- autosplitters/NWA/nwa_splitter.go | 56 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index db1c055..fd15f96 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -97,7 +97,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -114,7 +114,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -131,7 +131,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -148,7 +148,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -165,7 +165,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "&") { compStrings := strings.Split(components[1], delimiter5) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -176,7 +176,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "|") { compStrings := strings.Split(components[1], delimiter6) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -187,7 +187,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "^") { compStrings := strings.Split(components[1], delimiter7) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -228,7 +228,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -245,7 +245,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -262,7 +262,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -279,7 +279,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -296,7 +296,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "&") { compStrings := strings.Split(components[1], delimiter5) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -307,7 +307,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "|") { compStrings := strings.Split(components[1], delimiter6) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -318,7 +318,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "^") { compStrings := strings.Split(components[1], delimiter7) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -359,7 +359,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -376,7 +376,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -393,7 +393,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -410,7 +410,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -427,7 +427,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "&") { compStrings := strings.Split(components[1], delimiter5) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -438,7 +438,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "|") { compStrings := strings.Split(components[1], delimiter6) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -449,7 +449,7 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo } } else if strings.Contains(components[1], "^") { compStrings := strings.Split(components[1], delimiter7) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -617,11 +617,11 @@ type nwaSummary struct { // Checks conditions and returns start state func (b *NWASplitter) start() bool { - fmt.Printf("Checking reset state\n") - startState := true - var tempstate bool - + fmt.Printf("Checking start state\n") for _, p := range b.startConditions { + startState := true + var tempstate bool + for _, q := range p.memory { watcher := findMemoryWatcher(b.nwaMemory, q.memoryEntryName) tempstate = q.result(q.compareType, watcher.priorValue, watcher.currentValue, q.expectedValue) @@ -638,10 +638,10 @@ func (b *NWASplitter) start() bool { // Checks conditions and returns reset state func (b *NWASplitter) reset() bool { fmt.Printf("Checking reset state\n") - resetState := true - var tempstate bool - for _, p := range b.resetConditions { + resetState := true + var tempstate bool + for _, q := range p.memory { watcher := findMemoryWatcher(b.nwaMemory, q.memoryEntryName) tempstate = q.result(q.compareType, watcher.priorValue, watcher.currentValue, q.expectedValue) From 607b66b785f95db5f1a47429dee1e826f1176c7c Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 17:29:17 -0500 Subject: [PATCH 33/52] example NWA files --- ...ges (No WW, NES NTSC-US, Rash (Green)).nwa | 28 +++++++++++++++++++ ...-op (No WW, NES NTSC-US, Rash (Green)).nwa | 28 +++++++++++++++++++ ...ny% (No WW, NES NTSC-US, Rash (Green)).nwa | 8 +++--- ...-op (No WW, NES NTSC-US, Rash (Green)).nwa | 28 +++++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa create mode 100644 autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa create mode 100644 autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa new file mode 100644 index 0000000..c9592a0 --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa @@ -0,0 +1,28 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 giant value) +level,RAM,$0010,1 + +#start +start:level,prior=0x0 && level,current=0x1 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) (values MUST always be last) +reset:level,current=0x0 && level,prior≠0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) +#= ≠ < > & | ^ +level2:level,prior=0xFF && level,current=0x2 +level3:level,prior=0xFF && level,current=0x3 +level4:level,prior=0xFF && level,current=0x4 +level5:level,prior=0xFF && level,current=0x5 +level6:level,prior=0xFF && level,current=0x6 +level7:level,prior=0xFF && level,current=0x7 +level8:level,prior=0xFF && level,current=0x8 +level9:level,prior=0xFF && level,current=0x9 +level10:level,prior=0xFF && level,current=0xA +level11:level,prior=0xFF && level,current=0xB +level12:level,prior=0xFF && level,current=0xC +level13:level,prior=0xFF && level,current=0xD \ No newline at end of file diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa new file mode 100644 index 0000000..c9592a0 --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa @@ -0,0 +1,28 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 giant value) +level,RAM,$0010,1 + +#start +start:level,prior=0x0 && level,current=0x1 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) (values MUST always be last) +reset:level,current=0x0 && level,prior≠0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) +#= ≠ < > & | ^ +level2:level,prior=0xFF && level,current=0x2 +level3:level,prior=0xFF && level,current=0x3 +level4:level,prior=0xFF && level,current=0x4 +level5:level,prior=0xFF && level,current=0x5 +level6:level,prior=0xFF && level,current=0x6 +level7:level,prior=0xFF && level,current=0x7 +level8:level,prior=0xFF && level,current=0x8 +level9:level,prior=0xFF && level,current=0x9 +level10:level,prior=0xFF && level,current=0xA +level11:level,prior=0xFF && level,current=0xB +level12:level,prior=0xFF && level,current=0xC +level13:level,prior=0xFF && level,current=0xD \ No newline at end of file diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa index b7e2c8b..0907ae1 100644 --- a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa @@ -14,15 +14,15 @@ reset:level,current=0x0 && level,prior≠0x0 #split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) #= ≠ < > & | ^ -level2:level,prior=0xFF && level,current=0x2 +#level2:level,prior=0xFF && level,current=0x2 level3:level,prior=0xFF && level,current=0x3 level4:level,prior=0xFF && level,current=0x4 -level5:level,prior=0xFF && level,current=0x5 +#level5:level,prior=0xFF && level,current=0x5 level6:level,prior=0xFF && level,current=0x6 -level7:level,prior=0xFF && level,current=0x7 +#level7:level,prior=0xFF && level,current=0x7 level8:level,prior=0xFF && level,current=0x8 level9:level,prior=0xFF && level,current=0x9 level10:level,prior=0xFF && level,current=0xA level11:level,prior=0xFF && level,current=0xB level12:level,prior=0xFF && level,current=0xC -level13:level,prior=0xFF && level,current=0xD +level13:level,prior=0xFF && level,current=0xD \ No newline at end of file diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa new file mode 100644 index 0000000..0907ae1 --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa @@ -0,0 +1,28 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 giant value) +level,RAM,$0010,1 + +#start +start:level,prior=0x0 && level,current=0x1 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) (values MUST always be last) +reset:level,current=0x0 && level,prior≠0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) +#= ≠ < > & | ^ +#level2:level,prior=0xFF && level,current=0x2 +level3:level,prior=0xFF && level,current=0x3 +level4:level,prior=0xFF && level,current=0x4 +#level5:level,prior=0xFF && level,current=0x5 +level6:level,prior=0xFF && level,current=0x6 +#level7:level,prior=0xFF && level,current=0x7 +level8:level,prior=0xFF && level,current=0x8 +level9:level,prior=0xFF && level,current=0x9 +level10:level,prior=0xFF && level,current=0xA +level11:level,prior=0xFF && level,current=0xB +level12:level,prior=0xFF && level,current=0xC +level13:level,prior=0xFF && level,current=0xD \ No newline at end of file From 7b4735a7e3e7ab7a35f6fa3c10e6aa2933497326 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 17:29:37 -0500 Subject: [PATCH 34/52] removed unneeded string trims --- autosplitters/QUSB2SNES/qusb2snes_splitter.go | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/autosplitters/QUSB2SNES/qusb2snes_splitter.go b/autosplitters/QUSB2SNES/qusb2snes_splitter.go index 59eb32a..c63f7fd 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_splitter.go +++ b/autosplitters/QUSB2SNES/qusb2snes_splitter.go @@ -213,7 +213,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -230,7 +230,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -247,7 +247,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -264,7 +264,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -281,7 +281,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "&") { compStrings := strings.Split(components[1], delimiter5) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -292,7 +292,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "|") { compStrings := strings.Split(components[1], delimiter6) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -303,7 +303,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "^") { compStrings := strings.Split(components[1], delimiter7) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -344,7 +344,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -361,7 +361,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -378,7 +378,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -395,7 +395,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -412,7 +412,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "&") { compStrings := strings.Split(components[1], delimiter5) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -423,7 +423,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "|") { compStrings := strings.Split(components[1], delimiter6) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -434,7 +434,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "^") { compStrings := strings.Split(components[1], delimiter7) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -475,7 +475,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio if strings.Contains(components[1], "=") { compStrings := strings.Split(components[1], delimiter1) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -492,7 +492,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "≠") { compStrings := strings.Split(components[1], delimiter2) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -509,7 +509,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "<") { compStrings := strings.Split(components[1], delimiter3) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -526,7 +526,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], ">") { compStrings := strings.Split(components[1], delimiter4) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -543,7 +543,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "&") { compStrings := strings.Split(components[1], delimiter5) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -554,7 +554,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "|") { compStrings := strings.Split(components[1], delimiter6) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: @@ -565,7 +565,7 @@ func newSNESState(memData []string, startConditionImport []string, resetConditio } } else if strings.Contains(components[1], "^") { compStrings := strings.Split(components[1], delimiter7) - tempElement.expectedValue = hexToInt(compStrings[1][2:]) + tempElement.expectedValue = hexToInt(compStrings[1]) if tempElement.expectedValue != nil { switch compStrings[0] { case compareStringCurrent: From f6a14f4b8ad04a6b05db7b5b161130fb25322517 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 17:38:16 -0500 Subject: [PATCH 35/52] fmt --- frontend/src/components/editor/SplitEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/editor/SplitEditor.tsx b/frontend/src/components/editor/SplitEditor.tsx index c47500d..5d21b51 100644 --- a/frontend/src/components/editor/SplitEditor.tsx +++ b/frontend/src/components/editor/SplitEditor.tsx @@ -1,4 +1,4 @@ -import { faFolder,faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faFolder, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { useEffect, useRef, useState } from "react"; From 9cc4cbb1c165ab4a7fcd34e51dde8a4da3e08a61 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 17:43:17 -0500 Subject: [PATCH 36/52] Lint and ignoring unused return values --- autosplitters/NWA/nwa_client.go | 48 ++++++++++++++++----------------- autosplitters/service.go | 20 +++++++------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/autosplitters/NWA/nwa_client.go b/autosplitters/NWA/nwa_client.go index 2dd0397..e3585ff 100644 --- a/autosplitters/NWA/nwa_client.go +++ b/autosplitters/NWA/nwa_client.go @@ -45,7 +45,7 @@ func Connect(ip string, port uint32) (*NWASyncClient, error) { func (c *NWASyncClient) ExecuteCommand(cmd string, argString *string) (emulatorReply, error) { var command string - c.Connection.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _ = c.Connection.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) if argString == nil { command = fmt.Sprintf("%s\n", cmd) } else { @@ -62,7 +62,7 @@ func (c *NWASyncClient) ExecuteCommand(cmd string, argString *string) (emulatorR func (c *NWASyncClient) ExecuteRawCommand(cmd string, argString *string) { var command string - c.Connection.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _ = c.Connection.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) if argString == nil { command = fmt.Sprintf("%s\n", cmd) } else { @@ -73,31 +73,31 @@ func (c *NWASyncClient) ExecuteRawCommand(cmd string, argString *string) { _, _ = io.WriteString(c.Connection, command) } -func (c *NWASyncClient) IsConnected() bool { - // net.Conn in Go does not have a Peek method. - // We can try to set a read deadline and read with a zero-length buffer to check connection. - // But zero-length read returns immediately, so we try to read 1 byte with deadline. - buf := make([]byte, 1) - c.Connection.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) - n, err := c.Connection.Read(buf) - if err != nil { - // If timeout or no data, consider connected - netErr, ok := err.(net.Error) - if ok && netErr.Timeout() { - return true - } - return false - } - if n > 0 { - // Data was read, connection is alive - return true - } - return false -} +// func (c *NWASyncClient) IsConnected() bool { +// // net.Conn in Go does not have a Peek method. +// // We can try to set a read deadline and read with a zero-length buffer to check connection. +// // But zero-length read returns immediately, so we try to read 1 byte with deadline. +// buf := make([]byte, 1) +// _ = c.Connection.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) +// n, err := c.Connection.Read(buf) +// if err != nil { +// // If timeout or no data, consider connected +// netErr, ok := err.(net.Error) +// if ok && netErr.Timeout() { +// return true +// } +// return false +// } +// if n > 0 { +// // Data was read, connection is alive +// return true +// } +// return false +// } func (c *NWASyncClient) Close() { // TODO: handle the error - c.Connection.Close() + _ = c.Connection.Close() } func (c *NWASyncClient) Reconnected() (bool, error) { diff --git a/autosplitters/service.go b/autosplitters/service.go index b9b8788..4986a85 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -195,20 +195,20 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { } if autoState.Start && !runStarted { //split run - commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + _, _ = commandDispatcher.Dispatch(dispatcher.SPLIT, nil) runStarted = !runStarted } if autoState.Split && runStarted { //split run - commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + _, _ = commandDispatcher.Dispatch(dispatcher.SPLIT, nil) splitCount++ } if autoState.Reset && runStarted { if s.ResetTimerOnGameReset { - commandDispatcher.Dispatch(dispatcher.RESET, nil) + _, _ = commandDispatcher.Dispatch(dispatcher.RESET, nil) } if s.ResetGameOnTimerReset { - s.QUSB2SNESAutoSplitter.Reset() + _ = s.QUSB2SNESAutoSplitter.Reset() } splitCount = 0 runStarted = !runStarted @@ -280,7 +280,7 @@ func (s Splitters) processQUSB2SNES(commandDispatcher *dispatcher.Service) { ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), } - s.QUSB2SNESAutoSplitter.SetName("OpenSplit") + _ = s.QUSB2SNESAutoSplitter.SetName("OpenSplit") version, _ := s.QUSB2SNESAutoSplitter.AppVersion() fmt.Printf("Server version is %v\n", version) @@ -298,7 +298,7 @@ func (s Splitters) processQUSB2SNES(commandDispatcher *dispatcher.Service) { device := devices[0] fmt.Printf("Using device %v\n", device) - s.QUSB2SNESAutoSplitter.Attach(device) + _ = s.QUSB2SNESAutoSplitter.Attach(device) fmt.Println("Connected.") info, _ := s.QUSB2SNESAutoSplitter.Info() @@ -314,23 +314,23 @@ func (s Splitters) processQUSB2SNES(commandDispatcher *dispatcher.Service) { summary, _ := autosplitter.Update(*s.QUSB2SNESAutoSplitter, splitCount) if summary.Start && !runStarted { - commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + _, _ = commandDispatcher.Dispatch(dispatcher.SPLIT, nil) runStarted = !runStarted } if summary.Split && runStarted { // IGT // timer.SetGameTime(*t) // RTA - commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + _, _ = commandDispatcher.Dispatch(dispatcher.SPLIT, nil) splitCount++ } // need to get timer reset state if summary.Reset && runStarted /*|| timer is reset*/ { if s.ResetTimerOnGameReset { - commandDispatcher.Dispatch(dispatcher.RESET, nil) + _, _ = commandDispatcher.Dispatch(dispatcher.RESET, nil) } if s.ResetGameOnTimerReset { - s.QUSB2SNESAutoSplitter.Reset() + _ = s.QUSB2SNESAutoSplitter.Reset() } autosplitter.ResetGameTracking() splitCount = 0 From e395211535562191ad64a499218f263f782c80a6 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 18:00:26 -0500 Subject: [PATCH 37/52] removed comment --- autosplitters/NWA/nwa_splitter.go | 2 -- autosplitters/QUSB2SNES/qusb2snes_client.go | 2 -- autosplitters/QUSB2SNES/qusb2snes_splitter.go | 2 -- 3 files changed, 6 deletions(-) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index fd15f96..0c65465 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -1,7 +1,5 @@ package nwa -// TODO: handle errors correctly - import ( "encoding/binary" "fmt" diff --git a/autosplitters/QUSB2SNES/qusb2snes_client.go b/autosplitters/QUSB2SNES/qusb2snes_client.go index b480928..e440f4a 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_client.go +++ b/autosplitters/QUSB2SNES/qusb2snes_client.go @@ -1,7 +1,5 @@ package qusb2snes -// TODO: handle errors correctly - import ( "encoding/json" "fmt" diff --git a/autosplitters/QUSB2SNES/qusb2snes_splitter.go b/autosplitters/QUSB2SNES/qusb2snes_splitter.go index c63f7fd..e8dcabe 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_splitter.go +++ b/autosplitters/QUSB2SNES/qusb2snes_splitter.go @@ -7,8 +7,6 @@ import ( "strings" ) -// TODO: handle errors correctly - type conditionList struct { Name string memory []element From e4ebb76d89a46677a760ba4b2b68730443275b01 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 28 Dec 2025 18:21:00 -0500 Subject: [PATCH 38/52] fixed folder name --- .../Home Improvement (SNES) - Any%.qusb2snes | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename autosplitters/QUSB2SNES/{esxample_QUSB2SNES_files => example_QUSB2SNES_files}/Home Improvement (SNES) - Any%.qusb2snes (100%) diff --git a/autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes similarity index 100% rename from autosplitters/QUSB2SNES/esxample_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes rename to autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes From e897dde138157152c07c528631901083058a22b9 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Tue, 30 Dec 2025 10:19:49 -0500 Subject: [PATCH 39/52] fixed reseting in levels --- .../Home Improvement (SNES) - Any%.qusb2snes | 2 +- autosplitters/service.go | 140 +++++++++--------- 2 files changed, 71 insertions(+), 71 deletions(-) diff --git a/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes index bfa2c09..2f1321a 100644 --- a/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes +++ b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes @@ -27,7 +27,7 @@ start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,cur #reset (some games might have multiple reset conditions) (expectedValue is optional) cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0 tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0 -level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0 +level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 #split (some games might have multiple split conditions) (expectedValue is optional) #= ≠ < > & | ^ diff --git a/autosplitters/service.go b/autosplitters/service.go index 4986a85..762a179 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -95,81 +95,81 @@ func (s Splitters) newClient() (*nwa.NWASplitter, *qusb2snes.SyncClient) { func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { mil := 1 * time.Millisecond - // Battletoads test data - memData := []string{ - ("level,RAM,$0010,1")} - startConditionImport := []string{ - ("start:level,prior=0x0 && level,current=0x1")} - resetConditionImport := []string{ - ("reset:level,current=0x0 && level,prior≠0")} - splitConditionImport := []string{ - // ("level2:level,prior=0xFF && level,current=0x2"), - ("level3:level,prior=0xFF && level,current=0x3"), - ("level4:level,prior=0xFF && level,current=0x4"), - // ("level5:level,prior=0xFF && level,current=0x5"), - ("level6:level,prior=0xFF && level,current=0x6"), - // ("level7:level,prior=0xFF && level,current=0x7"), - ("level8:level,prior=0xFF && level,current=0x8"), - ("level9:level,prior=0xFF && level,current=0x9"), - ("level10:level,prior=0xFF && level,current=0xA"), - ("level11:level,prior=0xFF && level,current=0xB"), - ("level12:level,prior=0xFF && level,current=0xC"), - ("level13:level,prior=0xFF && level,current=0xD"), - } - - // // Home Improvment test data + // // Battletoads test data // memData := []string{ - // ("crates,WRAM,$001A8A,1"), - // ("scene,WRAM,$00161F,1"), - // ("W2P2HP,WRAM,$001499,1"), - // ("W2P1HP,WRAM,$001493,1"), - // ("BossHP,WRAM,$001491,1"), - // ("state,WRAM,$001400,1"), - // ("gameplay,WRAM,$000AE5,1"), - // ("substage,WRAM,$000AE3,1"), - // ("stage,WRAM,$000AE1,1"), - // ("scene2,WRAM,$000886,1"), - // ("play_state,WRAM,$0003B1,1"), - // ("power_up,WRAM,$0003AF,1"), - // ("weapon,WRAM,$0003CD,1"), - // ("invul,WRAM,$001C05,1"), - // ("FBossHP,WRAM,$00149D,1"), - // } - + // ("level,RAM,$0010,1")} // startConditionImport := []string{ - // ("start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), - // ("start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), - // } - + // ("start:level,prior=0x0 && level,current=0x1")} // resetConditionImport := []string{ - // ("cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0"), - // ("tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0"), - // ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0"), - // } - + // ("reset:level,current=0x0 && level,prior≠0")} // splitConditionImport := []string{ - // ("1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13"), - // ("1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13"), - // ("1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13"), - // ("1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13"), - // ("1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), - // ("2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13"), - // ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), - // ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), - // ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), - // ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0"), - // ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), - // ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), - // ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), - // ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), - // ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), - // ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), - // ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), - // ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), - // ("4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13"), - // ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), + // // ("level2:level,prior=0xFF && level,current=0x2"), + // ("level3:level,prior=0xFF && level,current=0x3"), + // ("level4:level,prior=0xFF && level,current=0x4"), + // // ("level5:level,prior=0xFF && level,current=0x5"), + // ("level6:level,prior=0xFF && level,current=0x6"), + // // ("level7:level,prior=0xFF && level,current=0x7"), + // ("level8:level,prior=0xFF && level,current=0x8"), + // ("level9:level,prior=0xFF && level,current=0x9"), + // ("level10:level,prior=0xFF && level,current=0xA"), + // ("level11:level,prior=0xFF && level,current=0xB"), + // ("level12:level,prior=0xFF && level,current=0xC"), + // ("level13:level,prior=0xFF && level,current=0xD"), // } + // Home Improvment test data + memData := []string{ + ("crates,WRAM,$001A8A,1"), + ("scene,WRAM,$00161F,1"), + ("W2P2HP,WRAM,$001499,1"), + ("W2P1HP,WRAM,$001493,1"), + ("BossHP,WRAM,$001491,1"), + ("state,WRAM,$001400,1"), + ("gameplay,WRAM,$000AE5,1"), + ("substage,WRAM,$000AE3,1"), + ("stage,WRAM,$000AE1,1"), + ("scene2,WRAM,$000886,1"), + ("play_state,WRAM,$0003B1,1"), + ("power_up,WRAM,$0003AF,1"), + ("weapon,WRAM,$0003CD,1"), + ("invul,WRAM,$001C05,1"), + ("FBossHP,WRAM,$00149D,1"), + } + + startConditionImport := []string{ + ("start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + ("start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + } + + resetConditionImport := []string{ + ("cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0"), + ("tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0"), + ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0"), + } + + splitConditionImport := []string{ + ("1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13"), + ("1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13"), + ("1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13"), + ("1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13"), + ("1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13"), + ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), + ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), + ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), + ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0"), + ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), + ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), + ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), + ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), + ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), + ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), + ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), + ("4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13"), + ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), + } + // receive setup data...probably through a channel //Setup Memory s.NWAAutoSplitter.MemAndConditionsSetup(memData, startConditionImport, resetConditionImport, splitConditionImport) @@ -254,7 +254,7 @@ func (s Splitters) processQUSB2SNES(commandDispatcher *dispatcher.Service) { resetConditionImport := []string{ ("cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0"), ("tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0"), - ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0"), + ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0"), } splitConditionImport := []string{ From 2bbc3cbf8ad8f414eec1217dd6d964477d007fe7 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Thu, 1 Jan 2026 16:44:59 -0500 Subject: [PATCH 40/52] added support for checking set and unset bits; removed u --- ...ges (No WW, NES NTSC-US, Rash (Green)).nwa | 2 +- ...-op (No WW, NES NTSC-US, Rash (Green)).nwa | 2 +- ...ny% (No WW, NES NTSC-US, Rash (Green)).nwa | 2 +- ...-op (No WW, NES NTSC-US, Rash (Green)).nwa | 2 +- .../Home Improvement (SNES) - Any%.nwa | 2 +- autosplitters/NWA/nwa_splitter.go | 48 ++++++++++++------- .../Home Improvement (SNES) - Any%.qusb2snes | 2 +- autosplitters/QUSB2SNES/qusb2snes_splitter.go | 48 ++++++++++++------- 8 files changed, 66 insertions(+), 42 deletions(-) diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa index c9592a0..acdc3b7 100644 --- a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa @@ -13,7 +13,7 @@ start:level,prior=0x0 && level,current=0x1 reset:level,current=0x0 && level,prior≠0x0 #split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) -#= ≠ < > & | ^ +#= ≠ < > & ~ level2:level,prior=0xFF && level,current=0x2 level3:level,prior=0xFF && level,current=0x3 level4:level,prior=0xFF && level,current=0x4 diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa index c9592a0..acdc3b7 100644 --- a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa @@ -13,7 +13,7 @@ start:level,prior=0x0 && level,current=0x1 reset:level,current=0x0 && level,prior≠0x0 #split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) -#= ≠ < > & | ^ +#= ≠ < > & ~ level2:level,prior=0xFF && level,current=0x2 level3:level,prior=0xFF && level,current=0x3 level4:level,prior=0xFF && level,current=0x4 diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa index 0907ae1..265a52b 100644 --- a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa @@ -13,7 +13,7 @@ start:level,prior=0x0 && level,current=0x1 reset:level,current=0x0 && level,prior≠0x0 #split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) -#= ≠ < > & | ^ +#= ≠ < > & ~ #level2:level,prior=0xFF && level,current=0x2 level3:level,prior=0xFF && level,current=0x3 level4:level,prior=0xFF && level,current=0x4 diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa index 0907ae1..265a52b 100644 --- a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa @@ -13,7 +13,7 @@ start:level,prior=0x0 && level,current=0x1 reset:level,current=0x0 && level,prior≠0x0 #split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) -#= ≠ < > & | ^ +#= ≠ < > & ~ #level2:level,prior=0xFF && level,current=0x2 level3:level,prior=0xFF && level,current=0x3 level4:level,prior=0xFF && level,current=0x4 diff --git a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa index 746e7c6..8e61594 100644 --- a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa +++ b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa @@ -30,7 +30,7 @@ tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && sce level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0 #split (some games might have multiple split conditions) (expectedValue is optional) -#= ≠ < > & | ^ +#= ≠ < > & ~ 1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13 1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13 1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13 diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index 0c65465..ccb170b 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -790,42 +790,54 @@ func compare(input string, prior *int, current *int, expected *int) bool { } else { return *prior > *expected } - case "pbae": + case "pbse": if (expected == nil) || (prior == nil) { return false } else { return (*prior & *expected) != 0 } - case "cbae": + case "cbse": if (expected == nil) || (current == nil) { return false } else { return (*current & *expected) != 0 } - case "pboe": + case "pbue": if (expected == nil) || (prior == nil) { return false } else { - return (*prior | *expected) != 0 + return (*prior & *expected) == 0 } - case "cboe": + case "cbue": if (expected == nil) || (current == nil) { return false } else { - return (*current | *expected) != 0 - } - case "pbne": - if (expected == nil) || (prior == nil) { - return false - } else { - return (*prior ^ *expected) != 0 - } - case "cbne": - if (expected == nil) || (current == nil) { - return false - } else { - return (*current ^ *expected) != 0 + return (*current & *expected) == 0 } + // case "pboe": + // if (expected == nil) || (prior == nil) { + // return false + // } else { + // return (*prior | *expected) != 0 + // } + // case "cboe": + // if (expected == nil) || (current == nil) { + // return false + // } else { + // return (*current | *expected) != 0 + // } + // case "pbne": + // if (expected == nil) || (prior == nil) { + // return false + // } else { + // return (*prior ^ *expected) != 0 + // } + // case "cbne": + // if (expected == nil) || (current == nil) { + // return false + // } else { + // return (*current ^ *expected) != 0 + // } default: return false } diff --git a/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes index 2f1321a..403303f 100644 --- a/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes +++ b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes @@ -30,7 +30,7 @@ tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && sce level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 #split (some games might have multiple split conditions) (expectedValue is optional) -#= ≠ < > & | ^ +#= ≠ < > & ~ 1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13 1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13 1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13 diff --git a/autosplitters/QUSB2SNES/qusb2snes_splitter.go b/autosplitters/QUSB2SNES/qusb2snes_splitter.go index e8dcabe..aa1f999 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_splitter.go +++ b/autosplitters/QUSB2SNES/qusb2snes_splitter.go @@ -815,42 +815,54 @@ func compare(input string, prior *uint16, current *uint16, expected *uint16) boo } else { return *prior > *expected } - case "pbae": + case "pbse": if (expected == nil) || (prior == nil) { return false } else { return (*prior & *expected) != 0 } - case "cbae": + case "cbse": if (expected == nil) || (current == nil) { return false } else { return (*current & *expected) != 0 } - case "pboe": + case "pbue": if (expected == nil) || (prior == nil) { return false } else { - return (*prior | *expected) != 0 + return (*prior & *expected) == 0 } - case "cboe": + case "cbue": if (expected == nil) || (current == nil) { return false } else { - return (*current | *expected) != 0 - } - case "pbne": - if (expected == nil) || (prior == nil) { - return false - } else { - return (*prior ^ *expected) != 0 - } - case "cbne": - if (expected == nil) || (current == nil) { - return false - } else { - return (*current ^ *expected) != 0 + return (*current & *expected) == 0 } + // case "pboe": + // if (expected == nil) || (prior == nil) { + // return false + // } else { + // return (*prior | *expected) != 0 + // } + // case "cboe": + // if (expected == nil) || (current == nil) { + // return false + // } else { + // return (*current | *expected) != 0 + // } + // case "pbne": + // if (expected == nil) || (prior == nil) { + // return false + // } else { + // return (*prior ^ *expected) != 0 + // } + // case "cbne": + // if (expected == nil) || (current == nil) { + // return false + // } else { + // return (*current ^ *expected) != 0 + // } default: return false } From 64c103045cad33c44671629e4da6b02671e3a318 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 2 Jan 2026 14:00:45 -0500 Subject: [PATCH 41/52] fixed Home Improvement 2-5 addresses --- .../Home Improvement (SNES) - Any%.nwa | 2 +- .../Home Improvement (SNES) - Any%.qusb2snes | 2 +- autosplitters/service.go | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa index 8e61594..23a13dd 100644 --- a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa +++ b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa @@ -40,7 +40,7 @@ level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && 2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13 2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13 2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 -2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0 +2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P1HP,current=0x0 3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13 3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13 3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13 diff --git a/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes index 403303f..c1de714 100644 --- a/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes +++ b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes @@ -40,7 +40,7 @@ level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && 2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13 2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13 2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 -2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0 +2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P1HP,current=0x0 3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13 3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13 3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13 diff --git a/autosplitters/service.go b/autosplitters/service.go index 762a179..54d7ab8 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -157,7 +157,7 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), - ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0"), + ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P1HP,current=0x0"), ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), @@ -170,10 +170,6 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), } - // receive setup data...probably through a channel - //Setup Memory - s.NWAAutoSplitter.MemAndConditionsSetup(memData, startConditionImport, resetConditionImport, splitConditionImport) - s.NWAAutoSplitter.EmuInfo() // Gets info about the emu; name, version, nwa_version, id, supported commands s.NWAAutoSplitter.EmuGameInfo() // Gets info about the loaded game s.NWAAutoSplitter.EmuStatus() // Gets the status of the emu @@ -181,6 +177,10 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { s.NWAAutoSplitter.CoreInfo() // Might be useful to display the platform & core names s.NWAAutoSplitter.CoreMemories() // Get info about the memory banks available + // receive setup data...probably through a channel + //Setup Memory + s.NWAAutoSplitter.MemAndConditionsSetup(memData, startConditionImport, resetConditionImport, splitConditionImport) + splitCount := 0 runStarted := false // this is the core loop of autosplitting From 3dc89cc5caccc0d3194eddf432bb19cffb5e9a73 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 2 Jan 2026 14:01:09 -0500 Subject: [PATCH 42/52] removed used code --- autosplitters/NWA/nwa_splitter.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index ccb170b..ffb6df6 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -11,8 +11,6 @@ import ( // public type NWASplitter struct { Client NWASyncClient - vars map[string]*memoryWatcher - data []byte nwaMemory []memoryWatcher startConditions []conditionList resetConditions []conditionList @@ -53,8 +51,6 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo delimiter7 := "^" compareStringCurrent := "current" compareStringPrior := "prior" - b.data = make([]byte, 0x10000) - b.vars = map[string]*memoryWatcher{} for _, p := range memData { mem := strings.Split(p, ",") @@ -67,7 +63,6 @@ func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImpo currentValue: new(int), priorValue: new(int), } - b.vars[mem[0]] = &entry b.nwaMemory = append(b.nwaMemory, entry) } From e90a0257be54a44a4942a829fa3e1f2d9d643269 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 4 Jan 2026 01:25:01 -0500 Subject: [PATCH 43/52] wails version fix --- go.mod | 1 - go.sum | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index c96f3df..a67677f 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( require ( github.com/bep/debounce v1.2.1 // indirect - github.com/biter777/processex v0.0.0-20210102170504-01bb369eda71 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect diff --git a/go.sum b/go.sum index 3da98bc..8d2dffa 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/biter777/processex v0.0.0-20210102170504-01bb369eda71 h1:SYKMUQEyuugaw68KtUlL1MpH36o2QL3ug93/y5Cye60= -github.com/biter777/processex v0.0.0-20210102170504-01bb369eda71/go.mod h1:3+wMnZWvewiNQL4PP4vEXiN1LgRlL4SXgWhj2fNMmeI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= @@ -59,8 +57,8 @@ github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6N github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= -github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= +github.com/wailsapp/wails/v2 v2.10.2 h1:29U+c5PI4K4hbx8yFbFvwpCuvqK9VgNv8WGobIlKlXk= +github.com/wailsapp/wails/v2 v2.10.2/go.mod h1:XuN4IUOPpzBrHUkEd7sCU5ln4T/p1wQedfxP7fKik+4= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= From 284547224e4fd01fc6b75f3cab1c118e4b9c4df3 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 4 Jan 2026 01:52:50 -0500 Subject: [PATCH 44/52] corrected console reset function --- autosplitters/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autosplitters/service.go b/autosplitters/service.go index 54d7ab8..6aa8d09 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -208,7 +208,7 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { _, _ = commandDispatcher.Dispatch(dispatcher.RESET, nil) } if s.ResetGameOnTimerReset { - _ = s.QUSB2SNESAutoSplitter.Reset() + s.NWAAutoSplitter.SoftResetConsole() } splitCount = 0 runStarted = !runStarted From 3f8f0a613cea60efeb339241f2ffa2fe8b7ade1a Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 4 Jan 2026 01:53:05 -0500 Subject: [PATCH 45/52] fixed crashing --- autosplitters/NWA/nwa_splitter.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index ffb6df6..4f950b3 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -465,7 +465,8 @@ func (b *NWASplitter) ClientID() { args := "OpenSplit" summary, err := b.Client.ExecuteCommand(cmd, &args) if err != nil { - panic(err) + // panic(err) + println(err) } fmt.Printf("%#v\n", summary) } @@ -475,7 +476,8 @@ func (b *NWASplitter) EmuInfo() { args := "0" summary, err := b.Client.ExecuteCommand(cmd, &args) if err != nil { - panic(err) + println(err) + // panic(err) } fmt.Printf("%#v\n", summary) } @@ -484,7 +486,8 @@ func (b *NWASplitter) EmuGameInfo() { cmd := "GAME_INFO" summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { - panic(err) + // panic(err) + println(err) } fmt.Printf("%#v\n", summary) } @@ -493,7 +496,8 @@ func (b *NWASplitter) EmuStatus() { cmd := "EMULATION_STATUS" summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { - panic(err) + // panic(err) + println(err) } fmt.Printf("%#v\n", summary) } @@ -502,7 +506,8 @@ func (b *NWASplitter) CoreInfo() { cmd := "CORE_CURRENT_INFO" summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { - panic(err) + // panic(err) + println(err) } fmt.Printf("%#v\n", summary) } @@ -511,7 +516,8 @@ func (b *NWASplitter) CoreMemories() { cmd := "CORE_MEMORIES" summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { - panic(err) + // panic(err) + println(err) } fmt.Printf("%#v\n", summary) } @@ -520,7 +526,8 @@ func (b *NWASplitter) SoftResetConsole() { cmd := "EMULATION_RESET" summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { - panic(err) + // panic(err) + println(err) } fmt.Printf("%#v\n", summary) } @@ -530,7 +537,8 @@ func (b *NWASplitter) HardResetConsole() { cmd := "EMULATION_RELOAD" summary, err := b.Client.ExecuteCommand(cmd, nil) if err != nil { - panic(err) + // panic(err) + println(err) } fmt.Printf("%#v\n", summary) } From 5ea32befb7d82d6108acb8f94ec3684cd846e5a6 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 5 Jan 2026 02:39:21 -0500 Subject: [PATCH 46/52] added return if client response is 0 length --- autosplitters/NWA/nwa_splitter.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index 4f950b3..bd0decd 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -563,6 +563,14 @@ func (b *NWASplitter) Update(splitIndex int) (nwaSummary, error) { } fmt.Printf("%#v\n", summary) + if len(summary.([]byte)) == 0 { + return nwaSummary{ + Start: false, + Reset: false, + Split: false, + }, nil + } + switch v := summary.(type) { case []byte: // update memoryWatcher with data From 1f4ccf1a510a3199dbc0aa830abbe8b341cdce3d Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Mon, 5 Jan 2026 22:38:03 -0500 Subject: [PATCH 47/52] 500 Hz should be plenty for any game --- autosplitters/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autosplitters/service.go b/autosplitters/service.go index 6aa8d09..4a88e5d 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -93,7 +93,7 @@ func (s Splitters) newClient() (*nwa.NWASplitter, *qusb2snes.SyncClient) { // Memory should be moved out of here and received from the config file and sent to the splitter func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { - mil := 1 * time.Millisecond + mil := 2 * time.Millisecond // // Battletoads test data // memData := []string{ @@ -225,7 +225,7 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { } func (s Splitters) processQUSB2SNES(commandDispatcher *dispatcher.Service) { - mil := 1 * time.Millisecond + mil := 2 * time.Millisecond // Home Improvment test data memData := []string{ From 7cf139f19fdf3e10424700f34cce5ef904a5dc21 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Tue, 6 Jan 2026 22:34:08 -0500 Subject: [PATCH 48/52] corrected check for incomplete data --- autosplitters/NWA/nwa_splitter.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go index bd0decd..fb86a83 100644 --- a/autosplitters/NWA/nwa_splitter.go +++ b/autosplitters/NWA/nwa_splitter.go @@ -551,9 +551,11 @@ func (b *NWASplitter) Update(splitIndex int) (nwaSummary, error) { domain := b.nwaMemory[0].memoryBank var requestString string + var watcherCount int for _, watcher := range b.nwaMemory { requestString += ";" + watcher.address + ";" + watcher.size *watcher.priorValue = *watcher.currentValue + watcherCount++ } args := domain + requestString @@ -563,7 +565,7 @@ func (b *NWASplitter) Update(splitIndex int) (nwaSummary, error) { } fmt.Printf("%#v\n", summary) - if len(summary.([]byte)) == 0 { + if len(summary.([]byte)) != watcherCount { return nwaSummary{ Start: false, Reset: false, From ec089a2733e0a6e83a898dd93030fca8699f1a89 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sat, 10 Jan 2026 02:38:09 -0500 Subject: [PATCH 49/52] removed unneeded structs --- autosplitters/QUSB2SNES/qusb2snes_client.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/autosplitters/QUSB2SNES/qusb2snes_client.go b/autosplitters/QUSB2SNES/qusb2snes_client.go index e440f4a..8abf718 100644 --- a/autosplitters/QUSB2SNES/qusb2snes_client.go +++ b/autosplitters/QUSB2SNES/qusb2snes_client.go @@ -81,17 +81,17 @@ type USB2SnesResult struct { Results []string `json:"Results"` } -type USB2SnesFileType int +// type USB2SnesFileType int -const ( - File USB2SnesFileType = iota - Dir -) +// const ( +// File USB2SnesFileType = iota +// Dir +// ) -type USB2SnesFileInfo struct { - Name string - FileType USB2SnesFileType -} +// type USB2SnesFileInfo struct { +// Name string +// FileType USB2SnesFileType +// } type SyncClient struct { Client *websocket.Conn From 7043252cb184b3d8eb36fcd8a2c2ad337b9c9723 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 11 Jan 2026 03:42:04 -0500 Subject: [PATCH 50/52] switched to NWA --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 85f4147..04881fb 100644 --- a/main.go +++ b/main.go @@ -68,7 +68,7 @@ func main() { // UseAutoSplitter and Type should come from the splits config file // ResetTimerOnGameReset, ResetGameOnTimerReset, Addr, Port should come from the autosplitter config file - // // NWA + // NWA AutoSplitterService := autosplitters.Splitters{ NWAAutoSplitter: new(nwa.NWASplitter), QUSB2SNESAutoSplitter: new(qusb2snes.SyncClient), From e6f7fc5b8a88711a228289085e108308c7f0684e Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 11 Jan 2026 03:43:11 -0500 Subject: [PATCH 51/52] fixed 2nd and 3rd boss tracking --- .../example_NWA_files/Home Improvement (SNES) - Any%.nwa | 2 +- .../Home Improvement (SNES) - Any%.qusb2snes | 2 +- autosplitters/service.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa index 23a13dd..9449dc4 100644 --- a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa +++ b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa @@ -45,7 +45,7 @@ level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && 3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13 3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13 3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 -3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0 +3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && crates,current=0x7 4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13 4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13 4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13 diff --git a/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes index c1de714..f5be492 100644 --- a/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes +++ b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes @@ -45,7 +45,7 @@ level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && 3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13 3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13 3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 -3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0 +3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && crates,current=0x7 4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13 4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13 4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13 diff --git a/autosplitters/service.go b/autosplitters/service.go index 4a88e5d..a1ee8e8 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -162,7 +162,7 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), - ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && crates,current=0x7"), ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), @@ -267,12 +267,12 @@ func (s Splitters) processQUSB2SNES(commandDispatcher *dispatcher.Service) { ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), - ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P2HP,current=0x1 && W2P1HP,current=0x0 && BossHP,current=0x0"), + ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P1HP,current=0x0"), ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), - ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && crates,current=0x7"), ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), From 59e7212f3f3b63bb952fc7c100f85d5fa6252f34 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 11 Jan 2026 04:17:46 -0500 Subject: [PATCH 52/52] BT queen split added --- ...toads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa | 4 +++- autosplitters/service.go | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa index 265a52b..6c78e88 100644 --- a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa @@ -5,6 +5,7 @@ Port = 48879 #memory (Do not combine memory or it will treated as 1 giant value) level,RAM,$0010,1 +qdead,RAM,$0005,1 #start start:level,prior=0x0 && level,current=0x1 @@ -25,4 +26,5 @@ level9:level,prior=0xFF && level,current=0x9 level10:level,prior=0xFF && level,current=0xA level11:level,prior=0xFF && level,current=0xB level12:level,prior=0xFF && level,current=0xC -level13:level,prior=0xFF && level,current=0xD \ No newline at end of file +level13:level,prior=0xFF && level,current=0xD +queen:level,current=0xD && qdead,prior=0x0 && qdead,current=0x5 \ No newline at end of file diff --git a/autosplitters/service.go b/autosplitters/service.go index a1ee8e8..4524cfc 100644 --- a/autosplitters/service.go +++ b/autosplitters/service.go @@ -97,7 +97,9 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { // // Battletoads test data // memData := []string{ - // ("level,RAM,$0010,1")} + // ("level,RAM,$0010,1"), + // ("qdead,RAM,$0005,1"), + // } // startConditionImport := []string{ // ("start:level,prior=0x0 && level,current=0x1")} // resetConditionImport := []string{ @@ -115,6 +117,7 @@ func (s Splitters) processNWA(commandDispatcher *dispatcher.Service) { // ("level11:level,prior=0xFF && level,current=0xB"), // ("level12:level,prior=0xFF && level,current=0xC"), // ("level13:level,prior=0xFF && level,current=0xD"), + // ("queen:level,current=0xD && qdead,prior=0x0 && qdead,current=0x5"), // } // Home Improvment test data