From 110720c8154b658ee58ca05d8f169a617c6a7f57 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Wed, 8 Oct 2025 00:44:52 -0400 Subject: [PATCH 01/36] 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/36] - 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/36] 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/36] 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/36] 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/36] - 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/36] - 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/36] - 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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 0ab30a69916733b511501359ba030fd88a1f988c Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 28 Nov 2025 09:07:42 -0500 Subject: [PATCH 19/36] removed autosplitter from this branch --- ...ny% (No WW, NES NTSC-US, Rash (Green)).nwa | 24 - .../Home Improvement (SNES) - Any%.nwa | 44 - autosplitters/NWA/nwa_client.go | 226 --- autosplitters/NWA/nwa_splitter.go | 434 ----- autosplitters/QUSB2SNES/qusb2snes_client.go | 394 ----- autosplitters/QUSB2SNES/qusb2snes_splitter.go | 1400 ----------------- autosplitters/service.go | 277 ---- 7 files changed, 2799 deletions(-) delete mode 100644 autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa delete mode 100644 autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa delete mode 100644 autosplitters/NWA/nwa_client.go delete mode 100644 autosplitters/NWA/nwa_splitter.go delete mode 100644 autosplitters/QUSB2SNES/qusb2snes_client.go delete mode 100644 autosplitters/QUSB2SNES/qusb2snes_splitter.go delete mode 100644 autosplitters/service.go 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 deleted file mode 100644 index 9c0b83c..0000000 --- a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 9a5be67..0000000 --- a/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa +++ /dev/null @@ -1,44 +0,0 @@ -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 diff --git a/autosplitters/NWA/nwa_client.go b/autosplitters/NWA/nwa_client.go deleted file mode 100644 index e665bcb..0000000 --- a/autosplitters/NWA/nwa_client.go +++ /dev/null @@ -1,226 +0,0 @@ -package nwa - -import ( - "bufio" - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "net" - "strings" - "time" -) - -// public -type NWAError struct { - Kind errorKind - Reason string -} - -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) 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 { - 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 - c.Connection.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) - 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 { - if err == io.EOF { - return nil, errors.New("connection aborted") - } - return nil, err - } - - // Ascii - // stops reading when the only result is a new line - 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 nil, 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 - } - - // Binary - if firstByte == 0 { - 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") -} - -// 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) -// } diff --git a/autosplitters/NWA/nwa_splitter.go b/autosplitters/NWA/nwa_splitter.go deleted file mode 100644 index ae77553..0000000 --- a/autosplitters/NWA/nwa_splitter.go +++ /dev/null @@ -1,434 +0,0 @@ -package nwa - -// TODO: handle errors correctly - -import ( - "bytes" - "encoding/binary" - "fmt" - "log" - "strconv" - "strings" -) - -// public -type NWASplitter struct { - ResetTimerOnGameReset bool - Client NWASyncClient - nwaMemory []MemoryEntry - 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 -// ) - -type Element struct { - memoryEntryName string - expectedValue *int - compareType string -} - -type MemoryEntry struct { - Name string - MemoryBank string - Address string - Size string - currentValue *int - priorValue *int -} - -// 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, ",") - - entry := MemoryEntry{ - 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 Reset Condition List - for _, p := range resetConditionImport { - var condition []Element - // create elements - // add elements to condition list - resetCon := strings.Split(strings.Split(p, ":")[1], " ") - for _, q := range resetCon { - elements := strings.Split(q, ",") - - 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) - } - 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) - } - } - // add condition lists to Reset Conditions 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(strings.Split(p, ":")[1], " ") - for _, q := range splitCon { - elements := strings.Split(q, ",") - - if len(elements) == 3 { - cT := elements[2] - - num, err := strconv.ParseUint(elements[1], 16, 64) - integer := int(num) - if err != nil { - log.Fatalf("Failed to convert string to integer: %v", err) - } - 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) - } - } - // add condition lists to Split Conditions list - b.splitConditions = append(b.splitConditions, condition) - } -} - -func (b *NWASplitter) ClientID() { - cmd := "MY_NAME_IS" - args := "OpenSplit" - 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) -} - -// currently only supports 1 byte reads -func (b *NWASplitter) Update() (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) - } - integer := int(temp_int) - b.nwaMemory[i].currentValue = &integer - // } - case NWAError: - fmt.Printf("%#v\n", v) - default: - fmt.Printf("%#v\n", v) - } - } - - reset := b.reset() - split := b.split() - - return nwaSummary{ - Reset: reset, - Split: split, - }, nil -} - -// private -type nwaSummary struct { - Reset bool - Split bool -} - -// Checks conditions and returns reset state -func (b *NWASplitter) reset() 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 - } - } - return false - } else { - 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 - } - if splitState { - fmt.Printf("Time to split\n") - 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 - } - } - return -1, false // Return -1 and false if not found -} - -func compare(input string, current *int, prior *int, expected *int) 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 - } -} diff --git a/autosplitters/QUSB2SNES/qusb2snes_client.go b/autosplitters/QUSB2SNES/qusb2snes_client.go deleted file mode 100644 index d9cb2cf..0000000 --- a/autosplitters/QUSB2SNES/qusb2snes_client.go +++ /dev/null @@ -1,394 +0,0 @@ -package qusb2snes - -// TODO: handle errors correctly - -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 deleted file mode 100644 index e97fb77..0000000 --- a/autosplitters/QUSB2SNES/qusb2snes_splitter.go +++ /dev/null @@ -1,1400 +0,0 @@ -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, - } -) - -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 bool - Reset bool - Split bool -} - -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() -} diff --git a/autosplitters/service.go b/autosplitters/service.go deleted file mode 100644 index 6cbc341..0000000 --- a/autosplitters/service.go +++ /dev/null @@ -1,277 +0,0 @@ -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 ( - NWA AutosplitterType = iota - 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 - - //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") - - 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 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))) - } -} - -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 e00d8966e13b7c1d6ef36e6e4c948baaed4c4e9a Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 28 Nov 2025 09:08:09 -0500 Subject: [PATCH 20/36] removed autosplitter; added racetime --- main.go | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/main.go b/main.go index cee1b32..5a8a151 100644 --- a/main.go +++ b/main.go @@ -15,15 +15,13 @@ import ( "time" "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" "github.com/zellydev-games/opensplit/hotkeys" "github.com/zellydev-games/opensplit/logger" "github.com/zellydev-games/opensplit/platform" + "github.com/zellydev-games/opensplit/racetime" "github.com/zellydev-games/opensplit/repo" "github.com/zellydev-games/opensplit/session" "github.com/zellydev-games/opensplit/statemachine" @@ -66,16 +64,6 @@ 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 - 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{ @@ -106,8 +94,8 @@ func main() { timerUIBridge.StartUIPump() configUIBridge.StartUIPump() - //Start autosplitter - AutoSplitterService.Run(commandDispatcher) + // Start racetime integration + racetime.Run() startInterruptListener(ctx, hotkeyProvider) runtime.WindowSetAlwaysOnTop(ctx, true) From 6286eee6ba863240f8e323325da1530649ff1fb6 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Fri, 28 Nov 2025 09:09:06 -0500 Subject: [PATCH 21/36] initial racetime integration implementation --- racetime/racetime_integration.go | 585 +++++++++++++++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 racetime/racetime_integration.go diff --git a/racetime/racetime_integration.go b/racetime/racetime_integration.go new file mode 100644 index 0000000..30de92a --- /dev/null +++ b/racetime/racetime_integration.go @@ -0,0 +1,585 @@ +package racetime + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "strconv" + "time" + + "github.com/gorilla/websocket" +) + +// type UserRole int + +// const ( +// Unknown UserRole = iota +// Anonymous +// Regular +// ChannelCreator UserRole = 4 +// Monitor UserRole = 8 +// Moderator UserRole = 16 +// Staff UserRole = 32 +// Bot UserRole = 64 +// System UserRole = 128 +// ) + +// type UserStatus int + +// const ( +// Unknown UserStatus = iota +// NotInRace +// NotReady +// Ready +// Finished +// Disqualified +// Forfeit +// Racing +// ) + +// type RaceState int + +// const ( +// Unknown UserStatus = iota +// Open +// OpenInviteOnly +// Ready +// Starting +// Started +// Ended +// Cancelled +// ) + +// type MessageType int + +// const ( +// Unknown MessageType = iota +// User +// Error +// Race +// System +// LiveSplit +// SplitUpdate +// Bot +// ) + +// // domain (Domain or IP of the Race-Server) +// racetime.gg +var addr = flag.String("addr", "localhost:8000", "http service address") + +var client_id = "x4oiff8OAiWwtfQUboFhFlYfgmDMHmxduOFOQgve" +var client_secret = "1BYxBFqyO495W8VCYiZxAEXgortlLa5trpzY0xxDHNAuAWaqfxhgy4435Gq5yp6P76Hw1EIFdp8JjnKvDtDfzLZ2lo6D1TrrWlp0yNbmBTPpNxYVePSqE7eX72ZDAmaU" + +// Gets all current races +func GetRaces() ([]byte, error) { + u := url.URL{Scheme: "http", Host: *addr, Path: "/races/data"} + req, err := http.NewRequest("GET", u.String(), nil) + + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) + // Example Response Body: {"races": [{"name": "alttp/perfect-ivysaur-9765", "status": {"value": "open", "verbose_value": "Open", "help_text": "Anyone may join this race"}, "url": "/alttp/perfect-ivysaur-9765", "data_url": "/alttp/perfect-ivysaur-9765/data", "goal": {"name": "100%", "custom": false}, "info": "", "entrants_count": 0, "entrants_count_finished": 0, "entrants_count_inactive": 0, "opened_at": "2025-11-25T14:22:38.834Z", "started_at": null, "time_limit": "P1DT00H00M00S", "opened_by_bot": null, "category": {"name": "The Legend of Zelda: A Link to the Past", "short_name": "ALttP", "slug": "alttp", "url": "/alttp", "data_url": "/alttp/data", "image": null}}]} + + return body, err +} + +// Get category details +func GetCategoryDetails(category string) ([]byte, error) { + // category = "alttp" + u := url.URL{Scheme: "http", Host: *addr, Path: "/" + category + "/data"} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) + // Example Response Body: {"name": "The Legend of Zelda: A Link to the Past", "short_name": "ALttP", "slug": "alttp", "url": "/alttp", "data_url": "/alttp/data", "image": null, "info": null, "streaming_required": true, "owners": [{"id": "GvzqPgEyPdZ0RKnr", "full_name": "Luigi#5557", "name": "Luigi", "discriminator": "5557", "url": "/user/GvzqPgEyPdZ0RKnr/luigi", "avatar": null, "pronouns": null, "flair": "moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}], "moderators": [{"id": "xr85vpEMBoX32zJ4", "full_name": "Bowser#7723", "name": "Bowser", "discriminator": "7723", "url": "/user/xr85vpEMBoX32zJ4/bowser", "avatar": null, "pronouns": null, "flair": "moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}], "goals": ["100%", "16 stars", "Beat the game"], "current_races": [], "emotes": {}} + + return body, err +} + +// Get category past races +// show_entrants can be either true, false, or empty +// page selects a page of the returned data, -1 ignores option +// per_page controls how many results to return per page +func GetCategoryPastRaces(category string, show_entrants string, page int, per_page int) ([]byte, error) { + // Generate json request body + data := make(map[string]string) + switch show_entrants { + case "": + if page != -1 { + data["page"] = strconv.Itoa(page) + data["per_page"] = strconv.Itoa(per_page) + } + case "true": + if page == -1 { + data["show_entrants"] = show_entrants + } else { + data["show_entrants"] = show_entrants + data["page"] = strconv.Itoa(page) + data["per_page"] = strconv.Itoa(per_page) + } + case "false": + if page == -1 { + data["show_entrants"] = show_entrants + } else { + data["show_entrants"] = show_entrants + data["page"] = strconv.Itoa(page) + data["per_page"] = strconv.Itoa(per_page) + } + } + + jsonData, err := json.Marshal(data) + if err != nil { + log.Fatalf("Error marshalling JSON: %v", err) + } + + // category = "alttp" + u := url.URL{Scheme: "http", Host: *addr, Path: "/" + category + "/races/data"} + req, err := http.NewRequest("GET", u.String(), bytes.NewBuffer(jsonData)) + // Example Response Body: {"count": 0, "num_pages": 1, "races": []} + + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) + // Example Response Body: {"name": "The Legend of Zelda: A Link to the Past", "short_name": "ALttP", "slug": "alttp", "url": "/alttp", "data_url": "/alttp/data", "image": null, "info": null, "streaming_required": true, "owners": [{"id": "GvzqPgEyPdZ0RKnr", "full_name": "Luigi#5557", "name": "Luigi", "discriminator": "5557", "url": "/user/GvzqPgEyPdZ0RKnr/luigi", "avatar": null, "pronouns": null, "flair": "moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}], "moderators": [{"id": "xr85vpEMBoX32zJ4", "full_name": "Bowser#7723", "name": "Bowser", "discriminator": "7723", "url": "/user/xr85vpEMBoX32zJ4/bowser", "avatar": null, "pronouns": null, "flair": "moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}], "goals": ["100%", "16 stars", "Beat the game"], "current_races": [], "emotes": {}} + + return body, err +} + +// Gets category leaderboard data +func GetCategoryLeaderboards(category string) ([]byte, error) { + // category = "alttp" + u := url.URL{Scheme: "http", Host: *addr, Path: "/" + category + "/leaderboards/data"} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) + // Example Response Body: {"leaderboards": [{"goal": "100%", "num_ranked": 0, "rankings": []}, {"goal": "16 stars", "num_ranked": 0, "rankings": []}, {"goal": "Beat the game", "num_ranked": 0, "rankings": []}]} + + return body, err +} + +// Gets category race info +func GetCategoryRaceInfo(category string, race string) ([]byte, error) { + // category = "alttp" + // race := "funky-link-3070" + u := url.URL{Scheme: "http", Host: *addr, Path: "/" + category + "/" + race + "/data"} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) + // Example Response Body: {"version": 1, "name": "alttp/funky-link-3070", "slug": "funky-link-3070", "status": {"value": "open", "verbose_value": "Open", "help_text": "Anyone may join this race"}, "url": "/alttp/funky-link-3070", "data_url": "/alttp/funky-link-3070/data", "websocket_url": "/ws/race/funky-link-3070", "websocket_bot_url": "/ws/o/bot/funky-link-3070", "websocket_oauth_url": "/ws/o/race/funky-link-3070", "category": {"name": "The Legend of Zelda: A Link to the Past", "short_name": "ALttP", "slug": "alttp", "url": "/alttp", "data_url": "/alttp/data", "image": null}, "goal": {"name": "100%", "custom": false}, "info": "", "info_bot": null, "info_user": "", "team_race": false, "entrants_count": 0, "entrants_count_finished": 0, "entrants_count_inactive": 0, "entrants": [], "opened_at": "2025-11-25T15:05:51.047Z", "start_delay": "P0DT00H00M15S", "started_at": null, "ended_at": null, "cancelled_at": null, "ranked": true, "unlisted": false, "time_limit": "P1DT00H00M00S", "time_limit_auto_complete": false, "require_even_teams": false, "streaming_required": true, "auto_start": true, "opened_by": {"id": "5BRGVMd30E368Lzv", "full_name": "Douglas Kirby", "name": "Douglas Kirby", "discriminator": null, "url": "/user/5BRGVMd30E368Lzv/douglas-kirby", "avatar": null, "pronouns": null, "flair": "staff moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}, "opened_by_bot": null, "monitors": [], "recordable": true, "recorded": false, "recorded_by": null, "disqualify_unready": false, "allow_comments": true, "hide_comments": false, "hide_entrants": false, "chat_restricted": false, "allow_prerace_chat": true, "allow_midrace_chat": true, "allow_non_entrant_chat": true, "chat_message_delay": "P0DT00H00M00S", "bot_meta": {}} + + return body, err +} + +// Gets user past race info +// show_entrants can be either true, false, or empty +// page selects a page of the returned data, -1 ignores option +// per_page controls how many results to return per page +func GetUserPastRaces(category string, user string, show_entrants string, page int, per_page int) ([]byte, error) { + // Generate json request body + data := make(map[string]string) + switch show_entrants { + case "": + if page != -1 { + data["page"] = strconv.Itoa(page) + data["per_page"] = strconv.Itoa(per_page) + } + case "true": + if page == -1 { + data["show_entrants"] = show_entrants + } else { + data["show_entrants"] = show_entrants + data["page"] = strconv.Itoa(page) + data["per_page"] = strconv.Itoa(per_page) + } + case "false": + if page == -1 { + data["show_entrants"] = show_entrants + } else { + data["show_entrants"] = show_entrants + data["page"] = strconv.Itoa(page) + data["per_page"] = strconv.Itoa(per_page) + } + } + + jsonData, err := json.Marshal(data) + if err != nil { + log.Fatalf("Error marshalling JSON: %v", err) + } + + u := url.URL{Scheme: "http", Host: *addr, Path: "/user/" + user + "/races/data"} + req, err := http.NewRequest("GET", u.String(), bytes.NewBuffer(jsonData)) + + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) + + return body, err +} + +// User search +func UserSearch(user string, discriminator string) ([]byte, error) { + // Generate json request body + data := make(map[string]string) + data["name"] = user + if discriminator != "" { + data["discriminator"] = discriminator + } + + jsonData, err := json.Marshal(data) + if err != nil { + log.Fatalf("Error marshalling JSON: %v", err) + } + + u := url.URL{Scheme: "http", Host: *addr, Path: "/user/search"} + req, err := http.NewRequest("GET", u.String(), bytes.NewBuffer(jsonData)) + + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) + + return body, err +} + +func Authorize() { + // app := application.Get() + // // New windows inherit the same frontend assets by default + // err := runtime.NewWindow(ctx, options.Window{ + // Title: "My Popup Window", + // Width: 400, + // Height: 300, + // // You can set other options like Frameless, MinSize, etc. here + // }) + // if err != nil { + // // Handle error appropriately + // println("Error opening new window:", err.Error()) + // } + + // This requires processing the login page. + req, err := http.NewRequest("GET", "http://localhost:8000/o/authorize", nil) + + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) +} + +func GenTokens() { + // Generate json request body + data := map[string]string{"client_id": client_id, "client_secret": client_secret, "grant_type": "authorization_code"} + // data := map[string]string{"client_id": client_id, "client_secret": client_secret, "grant_type": "client_credentials"} + jsonData, err := json.Marshal(data) + if err != nil { + log.Fatalf("Error marshalling JSON: %v", err) + } + // Can only be done if the user is authorized. Creates access and refresh tokens that needs to be stored. Expires eventually and needs to be refreshed with the refresh token. + u := url.URL{Scheme: "http", Host: *addr, Path: "/o/token"} + req, err := http.NewRequest("POST", u.String(), bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) + // Example response should include: access_token, refresh_token, token_type, expires_in, scope +} + +func RefreshToken(refreshToken string) { + // Generate json request body + data := map[string]string{"client_id": client_id, "client_secret": client_secret, "grant_type": "refresh_token", "refresh_token": refreshToken} + jsonData, err := json.Marshal(data) + if err != nil { + log.Fatalf("Error marshalling JSON: %v", err) + } + // Can only be done if the user is logged in. Creates access and refresh tokens that needs to be stored. Expires eventually and needs to be refreshed with the refresh token. + req, err := http.NewRequest("POST", "http://localhost:8000/o/token", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Body: %s\n", body) + // Example response should include: access_token, refresh_token, token_type, expires_in, scope +} + +// var assets embed.FS + +func Run() { + // err := wails.Run(&options.App{ + // Title: "Authorize", + // Width: 1024, + // Height: 768, + // Frameless: true, + // AssetServer: &assetserver.Options{ + // Assets: assets, + // Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // if len(r.URL.Path) > 7 && r.URL.Path[:7] == "/skins/" { + // //skinsFileServer.ServeHTTP(w, r) + // return + // } + // http.NotFound(w, r) + // }), + // }, + // BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + + // OnStartup: func(ctx context.Context) { + // Authorize() + // }, + // }) + + // if err != nil { + // println("Error:", err.Error()) + // } + + // Connect to race + flag.Parse() + log.SetFlags(0) + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + // requires authentication + u := url.URL{Scheme: "ws", Host: *addr, Path: "/ws/o/bot/clean-pacman-8175"} + log.Printf("connecting to %s", u.String()) + + dialer := websocket.DefaultDialer + dialer.HandshakeTimeout = 45 * time.Second + + // Add custom headers to the WebSocket handshake request + requestHeader := http.Header{} + // requestHeader.Set("Authorization", access_token) + + c, _, err := dialer.Dial(u.String(), requestHeader) + if err != nil { + log.Fatal("Dial error:", err) + } + defer c.Close() + + done := make(chan struct{}) + + go func() { + defer close(done) + for { + _, message, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + return + } + log.Printf("recv: %s", message) + } + }() + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for { + select { + case <-done: + return + case t := <-ticker.C: + err := c.WriteMessage(websocket.TextMessage, []byte(t.String())) + if err != nil { + log.Println("write:", err) + return + } + case <-interrupt: + log.Println("interrupt") + + // Cleanly close the connection by sending a close message and then + // waiting (with timeout) for the server to close the connection. + err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + log.Println("write close:", err) + return + } + select { + case <-done: + case <-time.After(time.Second): + } + return + } + } +} + +// // create race address (Webpage to be opened when clicking on New Race. Relative to race server) +// / + +// // OAUTH_CHALLENGE_METHOD (Plain or S256) +// S256 + +// // OAUTH_ENDPOINT_FAILURE +// o/done?error=access_denied + +// // OAUTH_ENDPOINT_REVOKE +// o/revoke_token + +// // OAUTH_ENDPOINT_SUCCESS +// o/done + +// // OAUTH_ENDPOINT_USERINFO +// o/userinfo + +// // OAUTH_REDIRECT_ADDRESS +// 127.0.0.1 + +// // OAUTH_REDIRECT_PORT +// 4888 + +// // OAUTH_SCOPES +// read chat_message race_action + +// // OAUTH_SERVER +// https://racetime.gg/ + +// // PROTOCOL_REST (http or https) +// https + +// // PROTOCOL_WEBSOCKET (ws or wss) +// wss From 131012723052e1c48d5569c136ad4fe6f3cdac38 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 30 Nov 2025 18:36:12 -0500 Subject: [PATCH 22/36] added OAuth2 module --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index 9b12f3e..cc34ff6 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/wailsapp/mimetype v1.4.1 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/text v0.22.0 // indirect ) From 933a5c429bb1a0231c782f89119069834a8357eb Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 30 Nov 2025 18:36:37 -0500 Subject: [PATCH 23/36] added OAuth2 module --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index ff23d61..207dc48 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5 golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 59cc881292f3ba8244e820494199223c78086974 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 30 Nov 2025 18:41:47 -0500 Subject: [PATCH 24/36] Racetime.gg generated go binding --- frontend/wailsjs/go/racetime/WebRace.d.ts | 12 ++++++++++++ frontend/wailsjs/go/racetime/WebRace.js | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100755 frontend/wailsjs/go/racetime/WebRace.d.ts create mode 100755 frontend/wailsjs/go/racetime/WebRace.js diff --git a/frontend/wailsjs/go/racetime/WebRace.d.ts b/frontend/wailsjs/go/racetime/WebRace.d.ts new file mode 100755 index 0000000..a7b507e --- /dev/null +++ b/frontend/wailsjs/go/racetime/WebRace.d.ts @@ -0,0 +1,12 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Authorize():Promise; + +export function CheckTokens():Promise; + +export function GenTokens(arg1:string):Promise; + +export function GetAccessToken():Promise; + +export function RefreshTokens():Promise; diff --git a/frontend/wailsjs/go/racetime/WebRace.js b/frontend/wailsjs/go/racetime/WebRace.js new file mode 100755 index 0000000..37a1704 --- /dev/null +++ b/frontend/wailsjs/go/racetime/WebRace.js @@ -0,0 +1,23 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Authorize() { + return window['go']['racetime']['WebRace']['Authorize'](); +} + +export function CheckTokens() { + return window['go']['racetime']['WebRace']['CheckTokens'](); +} + +export function GenTokens(arg1) { + return window['go']['racetime']['WebRace']['GenTokens'](arg1); +} + +export function GetAccessToken() { + return window['go']['racetime']['WebRace']['GetAccessToken'](); +} + +export function RefreshTokens() { + return window['go']['racetime']['WebRace']['RefreshTokens'](); +} From adbf1256d1fc4ea4f5b79172b8a812d9ea858117 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 30 Nov 2025 18:43:37 -0500 Subject: [PATCH 25/36] test racetime service creation and binding --- main.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 5a8a151..b19cbe9 100644 --- a/main.go +++ b/main.go @@ -64,6 +64,10 @@ func main() { // Build dispatcher that can receive commands from frontend or backend and dispatch them to the state machine commandDispatcher := dispatcher.NewService(machine) + // TODO: + // Convert client_id and client_secret to live site (AFTER getting approval from racetime.gg staff) + race := racetime.NewService("http", "localhost:8000", "localhost:9999") + var hotkeyProvider statemachine.HotkeyProvider err := wails.Run(&options.App{ @@ -94,9 +98,6 @@ func main() { timerUIBridge.StartUIPump() configUIBridge.StartUIPump() - // Start racetime integration - racetime.Run() - startInterruptListener(ctx, hotkeyProvider) runtime.WindowSetAlwaysOnTop(ctx, true) logger.Info("application startup complete") @@ -107,6 +108,7 @@ func main() { }, Bind: []interface{}{ commandDispatcher, + race, }, }) From 01dd15104363d70517e7fe0074049bb20e789371 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 30 Nov 2025 18:44:21 -0500 Subject: [PATCH 26/36] added button for racetime.gg integration; not visible though --- frontend/src/components/splitter/Welcome.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/src/components/splitter/Welcome.tsx b/frontend/src/components/splitter/Welcome.tsx index 8f07032..7afc1e0 100644 --- a/frontend/src/components/splitter/Welcome.tsx +++ b/frontend/src/components/splitter/Welcome.tsx @@ -2,6 +2,7 @@ import { Dispatch } from "../../../wailsjs/go/dispatcher/Service"; import { WindowSetSize } from "../../../wailsjs/runtime"; import { Command } from "../../App"; import zdgLogo from "../../assets/images/ZG512.png"; +import { WebSocketManager, RaceData, RaceList, LoginWithOAuth } from "../racetime_gg"; export default function Welcome() { WindowSetSize(320, 580); @@ -17,6 +18,7 @@ export default function Welcome() { > Create New Split File + + + + + ))} + + ); +} \ No newline at end of file From 8cc1e575f113083720c0edfd872ccb46e637c077 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 7 Dec 2025 06:05:49 -0500 Subject: [PATCH 31/36] probably related to the wails update --- 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 bf462ccb495003b7258c9555ba7375d50d1f9bc7 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 7 Dec 2025 06:06:22 -0500 Subject: [PATCH 32/36] removed old code; --- racetime/racetime_integration.go | 476 +------------------------------ 1 file changed, 6 insertions(+), 470 deletions(-) diff --git a/racetime/racetime_integration.go b/racetime/racetime_integration.go index fd7ec75..d989508 100644 --- a/racetime/racetime_integration.go +++ b/racetime/racetime_integration.go @@ -12,13 +12,7 @@ import ( ) type WebRace struct { - Token *oauth2.Token - // restProtocol string - // restAddr string - // websocketProtocol string - // websocketAddr string - // client_id string - // client_secret string + Token *oauth2.Token verifier string conf *oauth2.Config } @@ -28,18 +22,12 @@ func NewService(RestProtocol string, WebRaceServer string, RedirectURL string) * client_secret := "1BYxBFqyO495W8VCYiZxAEXgortlLa5trpzY0xxDHNAuAWaqfxhgy4435Gq5yp6P76Hw1EIFdp8JjnKvDtDfzLZ2lo6D1TrrWlp0yNbmBTPpNxYVePSqE7eX72ZDAmaU" return &WebRace{ - // restProtocol: RestProtocol, - // restAddr: "://" + WebRaceServer, - // websocketProtocol: "ws", - // websocketAddr: "://" + WebRaceServer, verifier: oauth2.GenerateVerifier(), - // client_id: client_id, - // client_secret: client_secret, conf: &oauth2.Config{ ClientID: client_id, ClientSecret: client_secret, Scopes: []string{"read", "chat_message", "race_action"}, - RedirectURL: RestProtocol + "://" + RedirectURL + "/oauth/callback", + // RedirectURL: RestProtocol + "://" + RedirectURL + "/oauth/callback", Endpoint: oauth2.Endpoint{ AuthURL: RestProtocol + "://" + WebRaceServer + "/o/authorize", TokenURL: RestProtocol + "://" + WebRaceServer + "/o/token", @@ -69,286 +57,6 @@ func NewService(RestProtocol string, WebRaceServer string, RedirectURL string) * // // domain (Domain or IP of the Race-Server) // racetime.gg -// // Gets all current races -// func (w *WebRace) GetRaces() ([]byte, error) { -// // u := url.URL{Scheme: "http", Host: *addr, Path: "/races/data"} -// req, err := http.NewRequest("GET", w.restProtocol+w.restAddr+"/races/data", nil) - -// if err != nil { -// log.Fatalf("Error creating request: %v", err) -// } -// client := &http.Client{} -// resp, err := client.Do(req) -// if err != nil { -// log.Fatalf("Error sending request: %v", err) -// } -// defer resp.Body.Close() - -// body, err := io.ReadAll(resp.Body) -// if err != nil { -// log.Fatalf("Error reading response body: %v", err) -// } - -// fmt.Printf("Response Status: %s\n", resp.Status) -// fmt.Printf("Response Body: %s\n", body) -// // Example Response Body: {"races": [{"name": "alttp/perfect-ivysaur-9765", "status": {"value": "open", "verbose_value": "Open", "help_text": "Anyone may join this race"}, "url": "/alttp/perfect-ivysaur-9765", "data_url": "/alttp/perfect-ivysaur-9765/data", "goal": {"name": "100%", "custom": false}, "info": "", "entrants_count": 0, "entrants_count_finished": 0, "entrants_count_inactive": 0, "opened_at": "2025-11-25T14:22:38.834Z", "started_at": null, "time_limit": "P1DT00H00M00S", "opened_by_bot": null, "category": {"name": "The Legend of Zelda: A Link to the Past", "short_name": "ALttP", "slug": "alttp", "url": "/alttp", "data_url": "/alttp/data", "image": null}}]} - -// return body, err -// } - -// Get category details -// func (w *WebRace) GetCategoryDetails(category string) ([]byte, error) { -// // category = "alttp" -// // u := url.URL{Scheme: "http", Host: *addr, Path: "/" + category + "/data"} -// req, err := http.NewRequest("GET", w.restProtocol+w.restAddr+category+"/data", nil) -// if err != nil { -// log.Fatalf("Error creating request: %v", err) -// } -// client := &http.Client{} -// resp, err := client.Do(req) -// if err != nil { -// log.Fatalf("Error sending request: %v", err) -// } -// defer resp.Body.Close() - -// body, err := io.ReadAll(resp.Body) -// if err != nil { -// log.Fatalf("Error reading response body: %v", err) -// } - -// fmt.Printf("Response Status: %s\n", resp.Status) -// fmt.Printf("Response Body: %s\n", body) -// // Example Response Body: {"name": "The Legend of Zelda: A Link to the Past", "short_name": "ALttP", "slug": "alttp", "url": "/alttp", "data_url": "/alttp/data", "image": null, "info": null, "streaming_required": true, "owners": [{"id": "GvzqPgEyPdZ0RKnr", "full_name": "Luigi#5557", "name": "Luigi", "discriminator": "5557", "url": "/user/GvzqPgEyPdZ0RKnr/luigi", "avatar": null, "pronouns": null, "flair": "moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}], "moderators": [{"id": "xr85vpEMBoX32zJ4", "full_name": "Bowser#7723", "name": "Bowser", "discriminator": "7723", "url": "/user/xr85vpEMBoX32zJ4/bowser", "avatar": null, "pronouns": null, "flair": "moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}], "goals": ["100%", "16 stars", "Beat the game"], "current_races": [], "emotes": {}} - -// return body, err -// } - -// Get category past races -// show_entrants can be either true, false, or empty -// page selects a page of the returned data, -1 ignores option -// per_page controls how many results to return per page -// func (w *WebRace) GetCategoryPastRaces(category string, show_entrants string, page int, per_page int) ([]byte, error) { -// // Generate json request body -// data := make(map[string]string) -// switch show_entrants { -// case "": -// if page != -1 { -// data["page"] = strconv.Itoa(page) -// data["per_page"] = strconv.Itoa(per_page) -// } -// case "true": -// if page == -1 { -// data["show_entrants"] = show_entrants -// } else { -// data["show_entrants"] = show_entrants -// data["page"] = strconv.Itoa(page) -// data["per_page"] = strconv.Itoa(per_page) -// } -// case "false": -// if page == -1 { -// data["show_entrants"] = show_entrants -// } else { -// data["show_entrants"] = show_entrants -// data["page"] = strconv.Itoa(page) -// data["per_page"] = strconv.Itoa(per_page) -// } -// } - -// jsonData, err := json.Marshal(data) -// if err != nil { -// log.Fatalf("Error marshalling JSON: %v", err) -// } - -// // category = "alttp" -// // u := url.URL{Scheme: "http", Host: *addr, Path: "/" + category + "/races/data"} -// req, err := http.NewRequest("GET", w.restProtocol+w.restAddr+category+"/races/data", bytes.NewBuffer(jsonData)) -// // Example Response Body: {"count": 0, "num_pages": 1, "races": []} - -// if err != nil { -// log.Fatalf("Error creating request: %v", err) -// } -// client := &http.Client{} -// resp, err := client.Do(req) -// if err != nil { -// log.Fatalf("Error sending request: %v", err) -// } -// defer resp.Body.Close() - -// body, err := io.ReadAll(resp.Body) -// if err != nil { -// log.Fatalf("Error reading response body: %v", err) -// } - -// fmt.Printf("Response Status: %s\n", resp.Status) -// fmt.Printf("Response Body: %s\n", body) -// // Example Response Body: {"name": "The Legend of Zelda: A Link to the Past", "short_name": "ALttP", "slug": "alttp", "url": "/alttp", "data_url": "/alttp/data", "image": null, "info": null, "streaming_required": true, "owners": [{"id": "GvzqPgEyPdZ0RKnr", "full_name": "Luigi#5557", "name": "Luigi", "discriminator": "5557", "url": "/user/GvzqPgEyPdZ0RKnr/luigi", "avatar": null, "pronouns": null, "flair": "moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}], "moderators": [{"id": "xr85vpEMBoX32zJ4", "full_name": "Bowser#7723", "name": "Bowser", "discriminator": "7723", "url": "/user/xr85vpEMBoX32zJ4/bowser", "avatar": null, "pronouns": null, "flair": "moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}], "goals": ["100%", "16 stars", "Beat the game"], "current_races": [], "emotes": {}} - -// return body, err -// } - -// Gets category leaderboard data -// func (w *WebRace) GetCategoryLeaderboards(category string) ([]byte, error) { -// // category = "alttp" -// // u := url.URL{Scheme: "http", Host: *addr, Path: "/" + category + "/leaderboards/data"} -// req, err := http.NewRequest("GET", w.restProtocol+w.restAddr+"/leaderboards/data", nil) -// if err != nil { -// log.Fatalf("Error creating request: %v", err) -// } -// client := &http.Client{} -// resp, err := client.Do(req) -// if err != nil { -// log.Fatalf("Error sending request: %v", err) -// } -// defer resp.Body.Close() - -// body, err := io.ReadAll(resp.Body) -// if err != nil { -// log.Fatalf("Error reading response body: %v", err) -// } - -// fmt.Printf("Response Status: %s\n", resp.Status) -// fmt.Printf("Response Body: %s\n", body) -// // Example Response Body: {"leaderboards": [{"goal": "100%", "num_ranked": 0, "rankings": []}, {"goal": "16 stars", "num_ranked": 0, "rankings": []}, {"goal": "Beat the game", "num_ranked": 0, "rankings": []}]} - -// return body, err -// } - -// Gets category race info -// func (w *WebRace) GetCategoryRaceInfo(category string, race string) ([]byte, error) { -// // category = "alttp" -// // race := "funky-link-3070" -// // u := url.URL{Scheme: "http", Host: *addr, Path: "/" + category + "/" + race + "/data"} -// req, err := http.NewRequest("GET", w.restProtocol+w.restAddr+category+race+"/data", nil) -// if err != nil { -// log.Fatalf("Error creating request: %v", err) -// } -// client := &http.Client{} -// resp, err := client.Do(req) -// if err != nil { -// log.Fatalf("Error sending request: %v", err) -// } -// defer resp.Body.Close() - -// body, err := io.ReadAll(resp.Body) -// if err != nil { -// log.Fatalf("Error reading response body: %v", err) -// } - -// fmt.Printf("Response Status: %s\n", resp.Status) -// fmt.Printf("Response Body: %s\n", body) -// // Example Response Body: {"version": 1, "name": "alttp/funky-link-3070", "slug": "funky-link-3070", "status": {"value": "open", "verbose_value": "Open", "help_text": "Anyone may join this race"}, "url": "/alttp/funky-link-3070", "data_url": "/alttp/funky-link-3070/data", "websocket_url": "/ws/race/funky-link-3070", "websocket_bot_url": "/ws/o/bot/funky-link-3070", "websocket_oauth_url": "/ws/o/race/funky-link-3070", "category": {"name": "The Legend of Zelda: A Link to the Past", "short_name": "ALttP", "slug": "alttp", "url": "/alttp", "data_url": "/alttp/data", "image": null}, "goal": {"name": "100%", "custom": false}, "info": "", "info_bot": null, "info_user": "", "team_race": false, "entrants_count": 0, "entrants_count_finished": 0, "entrants_count_inactive": 0, "entrants": [], "opened_at": "2025-11-25T15:05:51.047Z", "start_delay": "P0DT00H00M15S", "started_at": null, "ended_at": null, "cancelled_at": null, "ranked": true, "unlisted": false, "time_limit": "P1DT00H00M00S", "time_limit_auto_complete": false, "require_even_teams": false, "streaming_required": true, "auto_start": true, "opened_by": {"id": "5BRGVMd30E368Lzv", "full_name": "Douglas Kirby", "name": "Douglas Kirby", "discriminator": null, "url": "/user/5BRGVMd30E368Lzv/douglas-kirby", "avatar": null, "pronouns": null, "flair": "staff moderator", "twitch_name": null, "twitch_display_name": null, "twitch_channel": null, "can_moderate": true}, "opened_by_bot": null, "monitors": [], "recordable": true, "recorded": false, "recorded_by": null, "disqualify_unready": false, "allow_comments": true, "hide_comments": false, "hide_entrants": false, "chat_restricted": false, "allow_prerace_chat": true, "allow_midrace_chat": true, "allow_non_entrant_chat": true, "chat_message_delay": "P0DT00H00M00S", "bot_meta": {}} - -// return body, err -// } - -// Gets user past race info -// show_entrants can be either true, false, or empty -// page selects a page of the returned data, -1 ignores option -// per_page controls how many results to return per page -// func (w *WebRace) GetUserPastRaces(user string, show_entrants string, page int, per_page int) ([]byte, error) { -// // Generate json request body -// data := make(map[string]string) -// switch show_entrants { -// case "": -// if page != -1 { -// data["page"] = strconv.Itoa(page) -// data["per_page"] = strconv.Itoa(per_page) -// } -// case "true": -// if page == -1 { -// data["show_entrants"] = show_entrants -// } else { -// data["show_entrants"] = show_entrants -// data["page"] = strconv.Itoa(page) -// data["per_page"] = strconv.Itoa(per_page) -// } -// case "false": -// if page == -1 { -// data["show_entrants"] = show_entrants -// } else { -// data["show_entrants"] = show_entrants -// data["page"] = strconv.Itoa(page) -// data["per_page"] = strconv.Itoa(per_page) -// } -// } - -// jsonData, err := json.Marshal(data) -// if err != nil { -// log.Fatalf("Error marshalling JSON: %v", err) -// } - -// // u := url.URL{Scheme: "http", Host: *addr, Path: "/user/" + user + "/races/data"} -// req, err := http.NewRequest("GET", w.restProtocol+w.restAddr+user+"/races/data", bytes.NewBuffer(jsonData)) - -// if err != nil { -// log.Fatalf("Error creating request: %v", err) -// } -// client := &http.Client{} -// resp, err := client.Do(req) -// if err != nil { -// log.Fatalf("Error sending request: %v", err) -// } -// defer resp.Body.Close() - -// body, err := io.ReadAll(resp.Body) -// if err != nil { -// log.Fatalf("Error reading response body: %v", err) -// } - -// fmt.Printf("Response Status: %s\n", resp.Status) -// fmt.Printf("Response Body: %s\n", body) - -// return body, err -// } - -// User search -// func (w *WebRace) UserSearch(user string, discriminator string) ([]byte, error) { -// // Generate json request body -// data := make(map[string]string) -// data["name"] = user -// if discriminator != "" { -// data["discriminator"] = discriminator -// } - -// jsonData, err := json.Marshal(data) -// if err != nil { -// log.Fatalf("Error marshalling JSON: %v", err) -// } - -// // u := url.URL{Scheme: "http", Host: *addr, Path: "/user/search"} -// req, err := http.NewRequest("GET", w.restProtocol+w.restAddr+"/user/search", bytes.NewBuffer(jsonData)) - -// if err != nil { -// log.Fatalf("Error creating request: %v", err) -// } -// client := &http.Client{} -// resp, err := client.Do(req) -// if err != nil { -// log.Fatalf("Error sending request: %v", err) -// } -// defer resp.Body.Close() - -// body, err := io.ReadAll(resp.Body) -// if err != nil { -// log.Fatalf("Error reading response body: %v", err) -// } - -// fmt.Printf("Response Status: %s\n", resp.Status) -// fmt.Printf("Response Body: %s\n", body) - -// return body, err -// } - -// TODO: make token available -// func (*WebRace) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) { -// ctx := context.Background() -// code := r.URL.Query().Get("code") -// tok, err := conf.Exchange(ctx, code, oauth2.VerifierOption(verifier)) -// if err != nil { -// log.Fatal(err) -// } -// } - func (w *WebRace) CheckTokens() bool { if w.Token == nil || (w.Token.AccessToken == "" && w.Token.RefreshToken == "") { return false @@ -365,40 +73,15 @@ func (w *WebRace) CheckTokens() bool { } func (w *WebRace) Authorize() (url string) { - // generates "correctly". Throws a csrf error constantly url = w.conf.AuthCodeURL("state", oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(w.verifier)) - fmt.Printf("URL for the auth dialog: %v", url) + fmt.Printf("URL for the auth dialog: %v\n", url) return url } -// func Authorize() { -// req, err := http.NewRequest("GET", "http://localhost:8000/o/authorize", nil) - -// if err != nil { -// log.Fatalf("Error creating request: %v", err) -// } - -// client := &http.Client{} -// resp, err := client.Do(req) -// if err != nil { -// log.Fatalf("Error sending request: %v", err) -// } -// defer resp.Body.Close() - -// body, err := io.ReadAll(resp.Body) -// if err != nil { -// log.Fatalf("Error reading response body: %v", err) -// } - -// fmt.Printf("Response Status: %s\n", resp.Status) -// //returns full html page -// fmt.Printf("Response Body: %s\n", body) -// } - // Requests tokens from authorization code // Can only be done if the user is authorized. Creates access and refresh tokens that needs to be stored. Expires eventually and needs to be refreshed with the refresh token. // Example response should include: access_token, refresh_token, token_type, expires_in, scope -func (w *WebRace) GenTokens(code string) { +func (w *WebRace) GenTokens(code string) (accessToken string, refreshToken string) { // // Use the authorization code that is pushed to the redirect // // URL. Exchange will do the handshake to retrieve the // // initial access token. @@ -418,6 +101,8 @@ func (w *WebRace) GenTokens(code string) { fmt.Printf("Refresh token: %s\n", w.Token.RefreshToken) fmt.Printf("Access token expires: %s\n", w.Token.Expiry) fmt.Printf("Access token expires: %v\n", w.Token.ExpiresIn) + + return w.Token.AccessToken, w.Token.RefreshToken } // Can only be done if the user is logged in. Refreshes tokens that needs to be stored. @@ -438,152 +123,3 @@ func (w *WebRace) RefreshTokens() { func (w *WebRace) GetAccessToken() (accessToken string) { return w.Token.AccessToken } - -// type UserRole int - -// const ( -// Unknown UserRole = iota -// Anonymous -// Regular -// ChannelCreator UserRole = 4 -// Monitor UserRole = 8 -// Moderator UserRole = 16 -// Staff UserRole = 32 -// Bot UserRole = 64 -// System UserRole = 128 -// ) - -// type UserStatus int - -// const ( -// Unknown UserStatus = iota -// NotInRace -// NotReady -// Ready -// Finished -// Disqualified -// Forfeit -// Racing -// ) - -// type RaceState int - -// const ( -// Unknown UserStatus = iota -// Open -// OpenInviteOnly -// Ready -// Starting -// Started -// Ended -// Cancelled -// ) - -// func (w *WebRace) RaceData(dataURL string) { -// "data_url": "/alttp/funky-link-3070/data" -// } - -// type MessageType int - -// const ( -// Unknown MessageType = iota -// User -// Error -// Race -// System -// LiveSplit -// SplitUpdate -// Bot -// ) - -// Connects to websocket to get and send chat commands -// Example socket url "websocket_oauth_url": "/ws/o/race/funky-link-3070" -// func (w *WebRace) RaceChat(chatURL string) { -// // Connect to race -// interrupt := make(chan os.Signal, 1) -// signal.Notify(interrupt, os.Interrupt) - -// // chatURL = "/ws/o/race/funky-link-3070" -// // requires authentication -// addr := w.websocketProtocol + w.websocketAddr + chatURL -// log.Printf("connecting to %s", addr) - -// dialer := websocket.DefaultDialer -// dialer.HandshakeTimeout = 45 * time.Second - -// // // Add custom headers to the WebSocket handshake request -// requestHeader := http.Header{} -// requestHeader.Set("Authorization", w.Token.AccessToken) - -// c, _, err := dialer.Dial(addr, requestHeader) -// if err != nil { -// log.Fatal("Dial error:", err) -// } -// defer c.Close() - -// done := make(chan struct{}) - -// go func() { -// defer close(done) -// for { -// _, message, err := c.ReadMessage() -// if err != nil { -// log.Println("read:", err) -// return -// } -// log.Printf("recv: %s", message) -// } -// }() - -// ticker := time.NewTicker(time.Second) -// defer ticker.Stop() - -// for { -// select { -// case <-done: -// return -// case t := <-ticker.C: -// err := c.WriteMessage(websocket.TextMessage, []byte(t.String())) -// if err != nil { -// log.Println("write:", err) -// return -// } -// case <-interrupt: -// log.Println("interrupt") - -// // Cleanly close the connection by sending a close message and then -// // waiting (with timeout) for the server to close the connection. -// err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) -// if err != nil { -// log.Println("write close:", err) -// return -// } -// select { -// case <-done: -// case <-time.After(time.Second): -// } -// return -// } -// } -// } - -// func (w *WebRace) Run() { -// } - -// // create race address (Webpage to be opened when clicking on New Race. Relative to race server) -// / - -// // OAUTH_CHALLENGE_METHOD (Plain or S256) -// S256 - -// // OAUTH_ENDPOINT_FAILURE -// o/done?error=access_denied - -// // OAUTH_ENDPOINT_REVOKE -// o/revoke_token - -// // OAUTH_ENDPOINT_SUCCESS -// o/done - -// // OAUTH_ENDPOINT_USERINFO -// o/userinfo From d1b2ddd99078c0e0818df727deff1f40824d40c8 Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 7 Dec 2025 06:06:37 -0500 Subject: [PATCH 33/36] updated signature --- frontend/wailsjs/go/racetime/WebRace.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/wailsjs/go/racetime/WebRace.d.ts b/frontend/wailsjs/go/racetime/WebRace.d.ts index a7b507e..3b78985 100755 --- a/frontend/wailsjs/go/racetime/WebRace.d.ts +++ b/frontend/wailsjs/go/racetime/WebRace.d.ts @@ -5,7 +5,7 @@ export function Authorize():Promise; export function CheckTokens():Promise; -export function GenTokens(arg1:string):Promise; +export function GenTokens(arg1:string):Promise; export function GetAccessToken():Promise; From f3e311954dffbc3634e1612c1ca07bc14792ddcc Mon Sep 17 00:00:00 2001 From: Douglas Kirby Date: Sun, 7 Dec 2025 06:07:34 -0500 Subject: [PATCH 34/36] added buttons and import calls for racetime integration --- frontend/src/components/splitter/Welcome.tsx | 26 +++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/splitter/Welcome.tsx b/frontend/src/components/splitter/Welcome.tsx index 7afc1e0..48d1f32 100644 --- a/frontend/src/components/splitter/Welcome.tsx +++ b/frontend/src/components/splitter/Welcome.tsx @@ -2,7 +2,7 @@ import { Dispatch } from "../../../wailsjs/go/dispatcher/Service"; import { WindowSetSize } from "../../../wailsjs/runtime"; import { Command } from "../../App"; import zdgLogo from "../../assets/images/ZG512.png"; -import { WebSocketManager, RaceData, RaceList, LoginWithOAuth } from "../racetime_gg"; +import { LoginWithOAuth, RaceListWindow } from "../racetime_gg"; export default function Welcome() { WindowSetSize(320, 580); @@ -37,10 +37,30 @@ export default function Welcome() { + +