diff --git a/.gitignore b/.gitignore index 1e7ebef..1904770 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ frontend/package-lock* frontend/package.json.md5 cover.out .DS_Store +.vscode/* coverage.out diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa new file mode 100644 index 0000000..acdc3b7 --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages (No WW, NES NTSC-US, Rash (Green)).nwa @@ -0,0 +1,28 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 giant value) +level,RAM,$0010,1 + +#start +start:level,prior=0x0 && level,current=0x1 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) (values MUST always be last) +reset:level,current=0x0 && level,prior≠0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) +#= ≠ < > & ~ +level2:level,prior=0xFF && level,current=0x2 +level3:level,prior=0xFF && level,current=0x3 +level4:level,prior=0xFF && level,current=0x4 +level5:level,prior=0xFF && level,current=0x5 +level6:level,prior=0xFF && level,current=0x6 +level7:level,prior=0xFF && level,current=0x7 +level8:level,prior=0xFF && level,current=0x8 +level9:level,prior=0xFF && level,current=0x9 +level10:level,prior=0xFF && level,current=0xA +level11:level,prior=0xFF && level,current=0xB +level12:level,prior=0xFF && level,current=0xC +level13:level,prior=0xFF && level,current=0xD \ No newline at end of file diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa new file mode 100644 index 0000000..acdc3b7 --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - All Stages Co-op (No WW, NES NTSC-US, Rash (Green)).nwa @@ -0,0 +1,28 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 giant value) +level,RAM,$0010,1 + +#start +start:level,prior=0x0 && level,current=0x1 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) (values MUST always be last) +reset:level,current=0x0 && level,prior≠0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) +#= ≠ < > & ~ +level2:level,prior=0xFF && level,current=0x2 +level3:level,prior=0xFF && level,current=0x3 +level4:level,prior=0xFF && level,current=0x4 +level5:level,prior=0xFF && level,current=0x5 +level6:level,prior=0xFF && level,current=0x6 +level7:level,prior=0xFF && level,current=0x7 +level8:level,prior=0xFF && level,current=0x8 +level9:level,prior=0xFF && level,current=0x9 +level10:level,prior=0xFF && level,current=0xA +level11:level,prior=0xFF && level,current=0xB +level12:level,prior=0xFF && level,current=0xC +level13:level,prior=0xFF && level,current=0xD \ No newline at end of file diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa new file mode 100644 index 0000000..6c78e88 --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% (No WW, NES NTSC-US, Rash (Green)).nwa @@ -0,0 +1,30 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 giant value) +level,RAM,$0010,1 +qdead,RAM,$0005,1 + +#start +start:level,prior=0x0 && level,current=0x1 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) (values MUST always be last) +reset:level,current=0x0 && level,prior≠0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) +#= ≠ < > & ~ +#level2:level,prior=0xFF && level,current=0x2 +level3:level,prior=0xFF && level,current=0x3 +level4:level,prior=0xFF && level,current=0x4 +#level5:level,prior=0xFF && level,current=0x5 +level6:level,prior=0xFF && level,current=0x6 +#level7:level,prior=0xFF && level,current=0x7 +level8:level,prior=0xFF && level,current=0x8 +level9:level,prior=0xFF && level,current=0x9 +level10:level,prior=0xFF && level,current=0xA +level11:level,prior=0xFF && level,current=0xB +level12:level,prior=0xFF && level,current=0xC +level13:level,prior=0xFF && level,current=0xD +queen:level,current=0xD && qdead,prior=0x0 && qdead,current=0x5 \ No newline at end of file diff --git a/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa new file mode 100644 index 0000000..265a52b --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Battletoads (NES) - Any% Co-op (No WW, NES NTSC-US, Rash (Green)).nwa @@ -0,0 +1,28 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 giant value) +level,RAM,$0010,1 + +#start +start:level,prior=0x0 && level,current=0x1 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) (values MUST always be last) +reset:level,current=0x0 && level,prior≠0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) (values MUST always be last) +#= ≠ < > & ~ +#level2:level,prior=0xFF && level,current=0x2 +level3:level,prior=0xFF && level,current=0x3 +level4:level,prior=0xFF && level,current=0x4 +#level5:level,prior=0xFF && level,current=0x5 +level6:level,prior=0xFF && level,current=0x6 +#level7:level,prior=0xFF && level,current=0x7 +level8:level,prior=0xFF && level,current=0x8 +level9:level,prior=0xFF && level,current=0x9 +level10:level,prior=0xFF && level,current=0xA +level11:level,prior=0xFF && level,current=0xB +level12:level,prior=0xFF && level,current=0xC +level13:level,prior=0xFF && level,current=0xD \ No newline at end of file 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..9449dc4 --- /dev/null +++ b/autosplitters/NWA/example_NWA_files/Home Improvement (SNES) - Any%.nwa @@ -0,0 +1,53 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 48879 + +#memory (Do not combine memory or it will treated as 1 combined value) +stage,WRAM,$000AE1,1 #0-3 +substage,WRAM,$000AE3,1 #0-4 +crates,WRAM,$001A8A,1 #5,6,7,8 +invul,WRAM,$001C05,1 #02 +play_state,WRAM,$0003B1,1 #00 - dead/complete, 01 - alive/playable +scene2,WRAM,$000886,1 #03 - tool scene, 04 - win screen +scene,WRAM,$00161F,1 +gameplay,WRAM,$00AE5,1 #11 - menus/loading, 13 - gameplay +state,WRAM,$001400,1 #d8 - bonus countdown active, d0 - between act cutscene +BossHP,WRAM,$001491,1 #63 +W2P1HP,WRAM,$001493,1 #14 +W2P2HP,WRAM,$001499,1 # +FBossHP,WRAM,$00149D,1 #Starts at 64, Dies at 1, switches to FF after explosion +power_up,WRAM,$0003AF,1 +weapon,WRAM,$0003CD,1 + +#start (some games might have multiple start conditions) (expectedValue is optional) +start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0 +start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) +cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0 +tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0 +level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 && scene2,current=0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) +#= ≠ < > & ~ +1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13 +1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13 +1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13 +1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 +1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0 +2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13 +2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13 +2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13 +2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 +2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P1HP,current=0x0 +3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13 +3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13 +3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13 +3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 +3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && crates,current=0x7 +4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13 +4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13 +4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13 +4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 +4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF diff --git a/autosplitters/NWA/nwa_client.go b/autosplitters/NWA/nwa_client.go new file mode 100644 index 0000000..e3585ff --- /dev/null +++ b/autosplitters/NWA/nwa_client.go @@ -0,0 +1,226 @@ +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") +} + +// 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 new file mode 100644 index 0000000..fb86a83 --- /dev/null +++ b/autosplitters/NWA/nwa_splitter.go @@ -0,0 +1,857 @@ +package nwa + +import ( + "encoding/binary" + "fmt" + "log" + "strconv" + "strings" +) + +// public +type NWASplitter struct { + Client NWASyncClient + nwaMemory []memoryWatcher + startConditions []conditionList + resetConditions []conditionList + splitConditions []conditionList +} + +type element struct { + memoryEntryName string + expectedValue *int + compareType string + result compareFunc +} + +type compareFunc func(input string, prior *int, current *int, expected *int) bool + +type memoryWatcher struct { + name string + memoryBank string + address string + size string + currentValue *int + priorValue *int +} + +type conditionList struct { + Name string + memory []element +} + +// Setup the memory map being read by the NWA splitter and the maps for the reset and split conditions +func (b *NWASplitter) MemAndConditionsSetup(memData []string, startConditionImport []string, resetConditionImport []string, splitConditionImport []string) { + delimiter1 := "=" + delimiter2 := "≠" + delimiter3 := "<" + delimiter4 := ">" + delimiter5 := "&" + delimiter6 := "|" + delimiter7 := "^" + compareStringCurrent := "current" + compareStringPrior := "prior" + + for _, p := range memData { + mem := strings.Split(p, ",") + + entry := memoryWatcher{ + name: mem[0], + memoryBank: mem[1], + address: mem[2], + size: mem[3], + currentValue: new(int), + priorValue: new(int), + } + b.nwaMemory = append(b.nwaMemory, entry) + } + + // Populate Start Condition List + for _, p := range startConditionImport { + var condition conditionList + // create elements + // add elements to reset condition list + startName := strings.Split(p, ":")[0] + startCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = startName + + for _, q := range startCon { + if strings.Contains(q, "&&") { + continue + } + + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + tempElement.result = compare + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" + } + } + } + + condition.memory = append(condition.memory, tempElement) + } + // add condition lists to Start Conditions list + b.startConditions = append(b.startConditions, condition) + } + + // Populate Reset Condition List + for _, p := range resetConditionImport { + var condition conditionList + // create elements + // add elements to reset condition list + resetName := strings.Split(p, ":")[0] + resetCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = resetName + + for _, q := range resetCon { + if strings.Contains(q, "&&") { + continue + } + + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + tempElement.result = compare + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" + } + } + } + + condition.memory = append(condition.memory, tempElement) + } + // add condition lists to Reset Conditions list + b.resetConditions = append(b.resetConditions, condition) + } + + // Populate Split Condition List + for _, p := range splitConditionImport { + var condition conditionList + // create elements + // add elements to split condition list + splitName := strings.Split(p, ":")[0] + splitCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = splitName + + for _, q := range splitCon { + if strings.Contains(q, "&&") { + continue + } + + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + tempElement.result = compare + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" + } + } + } + + condition.memory = append(condition.memory, tempElement) + } + // 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) + println(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 { + println(err) + // 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) + println(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) + println(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) + println(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) + println(err) + } + fmt.Printf("%#v\n", summary) +} + +func (b *NWASplitter) SoftResetConsole() { + cmd := "EMULATION_RESET" + summary, err := b.Client.ExecuteCommand(cmd, nil) + if err != nil { + // panic(err) + println(err) + } + fmt.Printf("%#v\n", summary) +} + +func (b *NWASplitter) HardResetConsole() { + // cmd := "EMULATION_STOP" + cmd := "EMULATION_RELOAD" + summary, err := b.Client.ExecuteCommand(cmd, nil) + if err != nil { + // panic(err) + println(err) + } + fmt.Printf("%#v\n", summary) +} + +// currently only suppports 1 memory source at a time +// likely WRAM for SNES and RAM for NES +func (b *NWASplitter) Update(splitIndex int) (nwaSummary, error) { + + cmd := "CORE_READ" + domain := b.nwaMemory[0].memoryBank + var requestString string + + var watcherCount int + for _, watcher := range b.nwaMemory { + requestString += ";" + watcher.address + ";" + watcher.size + *watcher.priorValue = *watcher.currentValue + watcherCount++ + } + + args := domain + requestString + summary, err := b.Client.ExecuteCommand(cmd, &args) + if err != nil { + return nwaSummary{}, err + } + fmt.Printf("%#v\n", summary) + + if len(summary.([]byte)) != watcherCount { + return nwaSummary{ + Start: false, + Reset: false, + Split: false, + }, nil + } + + switch v := summary.(type) { + case []byte: + // update memoryWatcher with data + runningTotal := 0 + for _, watcher := range b.nwaMemory { + size, _ := strconv.Atoi(watcher.size) + switch size { + case 1: + *watcher.currentValue = int(v[runningTotal]) + runningTotal += size + case 2: + *watcher.currentValue = int(binary.LittleEndian.Uint16(v[runningTotal : runningTotal+size])) + runningTotal += size + case 3: + fallthrough + case 4: + *watcher.currentValue = int(binary.LittleEndian.Uint32(v[runningTotal : runningTotal+size])) + runningTotal += size + case 5: + fallthrough + case 6: + fallthrough + case 7: + fallthrough + case 8: + *watcher.currentValue = int(binary.LittleEndian.Uint64(v[runningTotal : runningTotal+size])) + runningTotal += size + } + } + + case NWAError: + fmt.Printf("%#v\n", v) + default: + fmt.Printf("%#v\n", v) + } + + start := b.start() + reset := b.reset() + split := b.split(splitIndex) + + return nwaSummary{ + Start: start, + Reset: reset, + Split: split, + }, nil +} + +type nwaSummary struct { + Start bool + Reset bool + Split bool +} + +// Checks conditions and returns start state +func (b *NWASplitter) start() bool { + fmt.Printf("Checking start state\n") + for _, p := range b.startConditions { + startState := true + var tempstate bool + + for _, q := range p.memory { + watcher := findMemoryWatcher(b.nwaMemory, q.memoryEntryName) + tempstate = q.result(q.compareType, watcher.priorValue, watcher.currentValue, q.expectedValue) + startState = startState && tempstate + } + if startState { + fmt.Printf("Start: %#v\n", p.Name) + return true + } + } + return false +} + +// Checks conditions and returns reset state +func (b *NWASplitter) reset() bool { + fmt.Printf("Checking reset state\n") + for _, p := range b.resetConditions { + resetState := true + var tempstate bool + + for _, q := range p.memory { + watcher := findMemoryWatcher(b.nwaMemory, q.memoryEntryName) + tempstate = q.result(q.compareType, watcher.priorValue, watcher.currentValue, q.expectedValue) + resetState = resetState && tempstate + } + if resetState { + fmt.Printf("Reset: %#v\n", p.Name) + return true + } + } + return false +} + +// Checks conditions and returns split state +func (b *NWASplitter) split(split int) bool { + fmt.Printf("Checking split state\n") + splitState := true + var tempstate bool + + for _, q := range b.splitConditions[split].memory { + watcher := findMemoryWatcher(b.nwaMemory, q.memoryEntryName) + tempstate = q.result(q.compareType, watcher.priorValue, watcher.currentValue, q.expectedValue) + splitState = splitState && tempstate + } + if splitState { + fmt.Printf("Split: %#v\n", b.splitConditions[split].Name) + return true + } + return false +} + +// private +func findMemoryWatcher(memInfo []memoryWatcher, targetWatcher string) *memoryWatcher { + for _, watcher := range memInfo { + if watcher.name == targetWatcher { + return &watcher + } + } + return nil +} + +// convert hex string to int +func hexToInt(hex string) *int { + num, err := strconv.ParseUint(hex, 0, 64) + if err != nil { + log.Printf("Failed to convert string to integer: %v", err) + return nil + } + integer := int(num) + return &integer +} + +func compare(input string, prior *int, current *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 + } + case "pbse": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior & *expected) != 0 + } + case "cbse": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current & *expected) != 0 + } + case "pbue": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior & *expected) == 0 + } + case "cbue": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current & *expected) == 0 + } + // case "pboe": + // if (expected == nil) || (prior == nil) { + // return false + // } else { + // return (*prior | *expected) != 0 + // } + // case "cboe": + // if (expected == nil) || (current == nil) { + // return false + // } else { + // return (*current | *expected) != 0 + // } + // case "pbne": + // if (expected == nil) || (prior == nil) { + // return false + // } else { + // return (*prior ^ *expected) != 0 + // } + // case "cbne": + // if (expected == nil) || (current == nil) { + // return false + // } else { + // return (*current ^ *expected) != 0 + // } + default: + return false + } +} diff --git a/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes new file mode 100644 index 0000000..f5be492 --- /dev/null +++ b/autosplitters/QUSB2SNES/example_QUSB2SNES_files/Home Improvement (SNES) - Any%.qusb2snes @@ -0,0 +1,53 @@ +ResetTimerOnGameReset = true +ResetGameOnTimerReset = false +IP = 0.0.0.0 +Port = 23074 + +#memory (Do not combine memory or it will treated as 1 combined value) +stage,$000AE1,1 #0-3 +substage,$000AE3,1 #0-4 +crates,$001A8A,1 #5,6,7,8 +invul,$001C05,1 #02 +play_state,$0003B1,1 #00 - dead/complete, 01 - alive/playable +scene2,$000886,1 #03 - tool scene, 04 - win screen +scene,$00161F,1 +gameplay,$00AE5,1 #11 - menus/loading, 13 - gameplay +state,$001400,1 #d8 - bonus countdown active, d0 - between act cutscene +BossHP,$001491,1 #63 +W2P1HP,$001493,1 #14 +W2P2HP,$001499,1 # +FBossHP,$00149D,1 #Starts at 64, Dies at 1, switches to FF after explosion +power_up,$0003AF,1 +weapon,$0003CD,1 + +#start +start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0 +start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0 + +#reset (some games might have multiple reset conditions) (expectedValue is optional) +cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0 +tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0 +level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0 + +#split (some games might have multiple split conditions) (expectedValue is optional) +#= ≠ < > & ~ +1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13 +1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13 +1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13 +1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 +1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0 +2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13 +2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13 +2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13 +2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 +2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P1HP,current=0x0 +3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13 +3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13 +3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13 +3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 +3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && crates,current=0x7 +4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13 +4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13 +4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13 +4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 +4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF diff --git a/autosplitters/QUSB2SNES/qusb2snes_client.go b/autosplitters/QUSB2SNES/qusb2snes_client.go new file mode 100644 index 0000000..8abf718 --- /dev/null +++ b/autosplitters/QUSB2SNES/qusb2snes_client.go @@ -0,0 +1,400 @@ +package qusb2snes + +import ( + "encoding/json" + "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 +) + +func (s Space) String() string { + return [...]string{ + "None", + "SNES", + }[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(host string, port uint32) (*SyncClient, error) { + return connect(host, port, false) +} + +func ConnectWithDevel(host string, port uint32) (*SyncClient, error) { + return connect(host, port, true) +} + +func connect(host string, port uint32, devel bool) (*SyncClient, error) { + numStr := strconv.FormatUint(uint64(port), 10) + u := url.URL{Scheme: "ws", Host: host + ":" + numStr, Path: "/"} + conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + return nil, err + } + 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 + // } + var query USB2SnesQuery + if space == SNES { + query = USB2SnesQuery{ + Opcode: command.String(), + Space: space.String(), + Flags: []string{}, + Operands: args, + } + } else { + query = USB2SnesQuery{ + Opcode: command.String(), + // Space: space.String(), + Flags: []string{}, + Operands: args, + } + } + jsonData, err := json.Marshal(query) + if err != nil { + 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..aa1f999 --- /dev/null +++ b/autosplitters/QUSB2SNES/qusb2snes_splitter.go @@ -0,0 +1,869 @@ +package qusb2snes + +import ( + "fmt" + "log" + "strconv" + "strings" +) + +type conditionList struct { + Name string + memory []element +} + +type element struct { + memoryEntryName string + expectedValue *uint16 + compareType string + result compareFunc +} + +type compareFunc func(input string, prior *uint16, current *uint16, expected *uint16) bool + +type memoryWatcher struct { + // name string + address uint32 + current *uint16 + old *uint16 + size int +} + +type SNESState struct { + vars map[string]*memoryWatcher + data []byte + startConditions []conditionList + resetConditions []conditionList + splitConditions []conditionList + // doExtraUpdate bool + // pickedUpHundredthMissile bool + // pickedUpSporeSpawnSuper bool + // latencySamples []uint128 + // mu sync.Mutex +} + +// const NUM_LATENCY_SAMPLES = 10 + +// type uint128 struct { +// hi uint64 +// lo uint64 +// } + +// func (a uint128) Add(b uint128) uint128 { +// lo := a.lo + b.lo +// hi := a.hi + b.hi +// if lo < a.lo { +// hi++ +// } +// return uint128{hi: hi, lo: lo} +// } + +// func (a uint128) Sub(b uint128) uint128 { +// lo := a.lo - b.lo +// hi := a.hi - b.hi +// if a.lo < b.lo { +// hi-- +// } +// return uint128{hi: hi, lo: lo} +// } + +// func (a uint128) ToFloat64() float64 { +// return float64(a.hi)*math.Pow(2, 64) + float64(a.lo) +// } + +// func uint128FromInt(i int64) uint128 { +// if i < 0 { +// return uint128{hi: math.MaxUint64, lo: uint64(i)} +// } +// return uint128{hi: 0, lo: uint64(i)} +// } + +// func averageUint128Slice(arr []uint128) float64 { +// var sum uint128 +// for _, v := range arr { +// sum = sum.Add(v) +// } +// return sum.ToFloat64() / float64(len(arr)) +// } + +// type TimeSpan struct { +// seconds float64 +// } + +// func (t TimeSpan) Seconds() float64 { +// return t.seconds +// } + +// func TimeSpanFromSeconds(seconds float64) TimeSpan { +// return TimeSpan{seconds: seconds} +// } + +// func (s *SNESState) gametimeToSeconds() TimeSpan { +// hours := float64(s.vars["igtHours"].current) +// minutes := float64(s.vars["igtMinutes"].current) +// seconds := float64(s.vars["igtSeconds"].current) + +// totalSeconds := hours*3600 + minutes*60 + seconds +// return TimeSpanFromSeconds(totalSeconds) +// } + +// func (a *QUSB2SNESAutoSplitter) GametimeToSeconds() *TimeSpan { +// t := a.snes.gametimeToSeconds() +// return &t +// } + +func (s *SNESState) split(split int) bool { + splitState := true + var tempstate bool + + for _, q := range s.splitConditions[split].memory { + tempstate = q.result(q.compareType, s.vars[q.memoryEntryName].old, s.vars[q.memoryEntryName].current, q.expectedValue) + splitState = splitState && tempstate + } + if splitState { + fmt.Printf("Split: %#v\n", s.splitConditions[split].Name) + return true + } + return false +} + +func (s *SNESState) start() bool { + for _, p := range s.startConditions { + startState := true + var tempstate bool + + for _, q := range p.memory { + tempstate = q.result(q.compareType, s.vars[q.memoryEntryName].old, s.vars[q.memoryEntryName].current, q.expectedValue) + startState = startState && tempstate + } + if startState { + fmt.Printf("Start: %#v\n", p.Name) + return true + } + } + return false +} + +func (s *SNESState) reset() bool { + for _, p := range s.resetConditions { + resetState := true + var tempstate bool + + for _, q := range p.memory { + tempstate = q.result(q.compareType, s.vars[q.memoryEntryName].old, s.vars[q.memoryEntryName].current, q.expectedValue) + resetState = resetState && tempstate + } + if resetState { + fmt.Printf("Reset: %#v\n", p.Name) + return true + } + } + return false +} + +func newSNESState(memData []string, startConditionImport []string, resetConditionImport []string, splitConditionImport []string) *SNESState { + data := make([]byte, 0x10000) + vars := map[string]*memoryWatcher{} + + delimiter1 := "=" + delimiter2 := "≠" + delimiter3 := "<" + delimiter4 := ">" + delimiter5 := "&" + delimiter6 := "|" + delimiter7 := "^" + compareStringCurrent := "current" + compareStringPrior := "prior" + var startConditions []conditionList + var resetConditions []conditionList + var splitConditions []conditionList + + // fill vars map + for _, p := range memData { + mem := strings.Split(p, ",") + size, _ := strconv.Atoi(mem[2]) + tempHex := *hexToInt(mem[1]) + temp32int := uint32(tempHex) + vars[mem[0]] = newMemoryWatcher(temp32int, size) + } + + // Populate Start Condition List + for _, p := range startConditionImport { + var condition conditionList + // create elements + // add elements to reset condition list + startName := strings.Split(p, ":")[0] + startCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = startName + + for _, q := range startCon { + if strings.Contains(q, "&&") { + continue + } + + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + tempElement.result = compare + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" + } + } + } + + condition.memory = append(condition.memory, tempElement) + } + // add condition lists to Start Conditions list + startConditions = append(startConditions, condition) + } + + // Populate Reset Condition List + for _, p := range resetConditionImport { + var condition conditionList + // create elements + // add elements to reset condition list + resetName := strings.Split(p, ":")[0] + resetCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = resetName + + for _, q := range resetCon { + if strings.Contains(q, "&&") { + continue + } + + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + tempElement.result = compare + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" + } + } + } + + condition.memory = append(condition.memory, tempElement) + } + // add condition lists to Reset Conditions list + resetConditions = append(resetConditions, condition) + } + + // Populate Split Condition List + for _, p := range splitConditionImport { + var condition conditionList + // create elements + // add elements to split condition list + splitName := strings.Split(p, ":")[0] + splitCon := strings.Split(strings.Split(p, ":")[1], " ") + + condition.Name = splitName + + for _, q := range splitCon { + if strings.Contains(q, "&&") { + continue + } + + var tempElement element + + components := strings.Split(q, ",") + + tempElement.memoryEntryName = components[0] + tempElement.result = compare + + if strings.Contains(components[1], "=") { + compStrings := strings.Split(components[1], delimiter1) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "ceqe" + case compareStringPrior: + tempElement.compareType = "peqe" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "ceqp" + } else { + tempElement.compareType = "peqc" + } + } + } else if strings.Contains(components[1], "≠") { + compStrings := strings.Split(components[1], delimiter2) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cnee" + case compareStringPrior: + tempElement.compareType = "pnee" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cnep" + } else { + tempElement.compareType = "pnec" + } + } + } else if strings.Contains(components[1], "<") { + compStrings := strings.Split(components[1], delimiter3) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "clte" + case compareStringPrior: + tempElement.compareType = "plte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cltp" + } else { + tempElement.compareType = "pltc" + } + } + } else if strings.Contains(components[1], ">") { + compStrings := strings.Split(components[1], delimiter4) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cgte" + case compareStringPrior: + tempElement.compareType = "pgte" + } + } else { + if compStrings[0] == compareStringCurrent && compStrings[1] == compareStringPrior { + tempElement.compareType = "cgtp" + } else { + tempElement.compareType = "pgtc" + } + } + } else if strings.Contains(components[1], "&") { + compStrings := strings.Split(components[1], delimiter5) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbae" + case compareStringPrior: + tempElement.compareType = "pbae" + } + } + } else if strings.Contains(components[1], "|") { + compStrings := strings.Split(components[1], delimiter6) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cboe" + case compareStringPrior: + tempElement.compareType = "pboe" + } + } + } else if strings.Contains(components[1], "^") { + compStrings := strings.Split(components[1], delimiter7) + tempElement.expectedValue = hexToInt(compStrings[1]) + if tempElement.expectedValue != nil { + switch compStrings[0] { + case compareStringCurrent: + tempElement.compareType = "cbne" + case compareStringPrior: + tempElement.compareType = "pbne" + } + } + } + + condition.memory = append(condition.memory, tempElement) + } + // add condition lists to Split Conditions list + splitConditions = append(splitConditions, condition) + } + + return &SNESState{ + // doExtraUpdate: true, + data: data, + startConditions: startConditions, + resetConditions: resetConditions, + splitConditions: splitConditions, + // latencySamples: make([]uint128, 0), + // pickedUpHundredthMissile: false, + // pickedUpSporeSpawnSuper: false, + vars: vars, + } +} + +func newMemoryWatcher(address uint32, size int) *memoryWatcher { + return &memoryWatcher{ + address: address, + current: new(uint16), + old: new(uint16), + size: size, + } +} + +func (mw *memoryWatcher) updateValue(memory []byte) { + *mw.old = *mw.current + switch mw.size { + case 1: + *mw.current = uint16(memory[mw.address]) + case 2: + addr := mw.address + *mw.current = uint16(memory[addr]) | uint16(memory[addr+1])<<8 + } +} + +func (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 +} + +type QUSB2SNESAutoSplitter struct { + snes *SNESState + // settings *sync.RWMutex + // settingsData *Settings +} + +func NewQUSB2SNESAutoSplitter(memData []string, startConditionImport []string, resetConditionImport []string, splitConditionImport []string /*settings *sync.RWMutex, settingsData *Settings*/) *QUSB2SNESAutoSplitter { + return &QUSB2SNESAutoSplitter{ + snes: newSNESState(memData, startConditionImport, resetConditionImport, splitConditionImport), + // settings: settings, + // settingsData: settingsData, + } +} + +func (a *QUSB2SNESAutoSplitter) Update(client SyncClient, splitNum int) (*SNESSummary, error) { + addresses := [][2]int{} + + for _, watcher := range a.snes.vars { + fullAddress := int(watcher.address | 0xF50000) + + newRow := []int{fullAddress, int(watcher.size)} + addresses = append(addresses, [2]int(newRow)) + } + + snesData, err := client.getAddresses(addresses) + if err != nil { + return nil, err + } + + for index, row := range addresses { + copy(a.snes.data[(row[0]^0xF50000):(row[0]^0xF50000)+row[1]], snesData[index]) + } + a.snes.update() + + start := a.snes.start() + reset := a.snes.reset() + split := a.snes.split(splitNum) + + // 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 (a *QUSB2SNESAutoSplitter) ResetGameTracking() { + // a.snes = newSNESState() + clear(a.snes.data[:]) + for _, watcher := range a.snes.vars { + *watcher.current = 0 + *watcher.old = 0 + } + // a.snes.doExtraUpdate = true +} + +// convert hex string to int +func hexToInt(hex string) *uint16 { + num, err := strconv.ParseUint(hex, 0, 64) + if err != nil { + log.Printf("Failed to convert string to integer: %v", err) + return nil + } + integer := uint16(num) + return &integer +} + +func compare(input string, prior *uint16, current *uint16, expected *uint16) bool { + switch input { + case "ceqp": + fallthrough + case "peqc": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior == *current + } + case "ceqe": + fallthrough + case "eeqc": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected == *current + } + case "eeqp": + fallthrough + case "peqe": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior == *expected + } + case "cnep": + fallthrough + case "pnec": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior != *current + } + case "cnee": + fallthrough + case "enec": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected != *current + } + case "enep": + fallthrough + case "pnee": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior != *expected + } + case "cgtp": + fallthrough + case "pltc": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior < *current + } + case "cgte": + fallthrough + case "eltc": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected < *current + } + case "egtp": + fallthrough + case "plte": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior < *expected + } + case "cltp": + fallthrough + case "pgtc": + if (prior == nil) || (current == nil) { + return false + } else { + return *prior > *current + } + case "clte": + fallthrough + case "egtc": + if (expected == nil) || (current == nil) { + return false + } else { + return *expected > *current + } + case "eltp": + fallthrough + case "pgte": + if (expected == nil) || (prior == nil) { + return false + } else { + return *prior > *expected + } + case "pbse": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior & *expected) != 0 + } + case "cbse": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current & *expected) != 0 + } + case "pbue": + if (expected == nil) || (prior == nil) { + return false + } else { + return (*prior & *expected) == 0 + } + case "cbue": + if (expected == nil) || (current == nil) { + return false + } else { + return (*current & *expected) == 0 + } + // case "pboe": + // if (expected == nil) || (prior == nil) { + // return false + // } else { + // return (*prior | *expected) != 0 + // } + // case "cboe": + // if (expected == nil) || (current == nil) { + // return false + // } else { + // return (*current | *expected) != 0 + // } + // case "pbne": + // if (expected == nil) || (prior == nil) { + // return false + // } else { + // return (*prior ^ *expected) != 0 + // } + // case "cbne": + // if (expected == nil) || (current == nil) { + // return false + // } else { + // return (*current ^ *expected) != 0 + // } + default: + return false + } +} diff --git a/autosplitters/service.go b/autosplitters/service.go new file mode 100644 index 0000000..4524cfc --- /dev/null +++ b/autosplitters/service.go @@ -0,0 +1,351 @@ +package autosplitters + +// TODO: +// check status of splits file +// update object variables +// add way to cancel autosplitting + +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 + ResetGameOnTimerReset bool + Addr string + Port uint32 + Type AutosplitterType +} + +type AutosplitterType int + +const ( + NWA AutosplitterType = iota + QUSB2SNES +) + +func (s Splitters) Run(commandDispatcher *dispatcher.Service) { + go func() { + // loop trying to connect + for { + mil := 1 * time.Millisecond + + //check for split file loaded + // if !splitsFile.loaded { + // continue + // } + + connectStart := time.Now() + + s.NWAAutoSplitter, s.QUSB2SNESAutoSplitter = s.newClient() + + if s.NWAAutoSplitter != nil || s.QUSB2SNESAutoSplitter != nil { + + if s.NWAAutoSplitter != nil { + 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() (*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{ + Client: *client, + }, nil + } else { + return nil, nil + } + } + if s.Type == QUSB2SNES { + // fmt.Printf("Creating QUSB2SNES AutoSplitter\n") + client, connectError := qusb2snes.Connect(s.Addr, s.Port) + 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"), + // ("qdead,RAM,$0005,1"), + // } + // startConditionImport := []string{ + // ("start:level,prior=0x0 && level,current=0x1")} + // resetConditionImport := []string{ + // ("reset:level,current=0x0 && level,prior≠0")} + // splitConditionImport := []string{ + // // ("level2:level,prior=0xFF && level,current=0x2"), + // ("level3:level,prior=0xFF && level,current=0x3"), + // ("level4:level,prior=0xFF && level,current=0x4"), + // // ("level5:level,prior=0xFF && level,current=0x5"), + // ("level6:level,prior=0xFF && level,current=0x6"), + // // ("level7:level,prior=0xFF && level,current=0x7"), + // ("level8:level,prior=0xFF && level,current=0x8"), + // ("level9:level,prior=0xFF && level,current=0x9"), + // ("level10:level,prior=0xFF && level,current=0xA"), + // ("level11:level,prior=0xFF && level,current=0xB"), + // ("level12:level,prior=0xFF && level,current=0xC"), + // ("level13:level,prior=0xFF && level,current=0xD"), + // ("queen:level,current=0xD && qdead,prior=0x0 && qdead,current=0x5"), + // } + + // Home Improvment test data + memData := []string{ + ("crates,WRAM,$001A8A,1"), + ("scene,WRAM,$00161F,1"), + ("W2P2HP,WRAM,$001499,1"), + ("W2P1HP,WRAM,$001493,1"), + ("BossHP,WRAM,$001491,1"), + ("state,WRAM,$001400,1"), + ("gameplay,WRAM,$000AE5,1"), + ("substage,WRAM,$000AE3,1"), + ("stage,WRAM,$000AE1,1"), + ("scene2,WRAM,$000886,1"), + ("play_state,WRAM,$0003B1,1"), + ("power_up,WRAM,$0003AF,1"), + ("weapon,WRAM,$0003CD,1"), + ("invul,WRAM,$001C05,1"), + ("FBossHP,WRAM,$00149D,1"), + } + + startConditionImport := []string{ + ("start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + ("start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + } + + resetConditionImport := []string{ + ("cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0"), + ("tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0"), + ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0"), + } + + splitConditionImport := []string{ + ("1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13"), + ("1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13"), + ("1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13"), + ("1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13"), + ("1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13"), + ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), + ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), + ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), + ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P1HP,current=0x0"), + ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), + ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), + ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), + ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), + ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && crates,current=0x7"), + ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), + ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), + ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), + ("4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13"), + ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), + } + + s.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 + + // receive setup data...probably through a channel + //Setup Memory + s.NWAAutoSplitter.MemAndConditionsSetup(memData, startConditionImport, resetConditionImport, splitConditionImport) + + splitCount := 0 + runStarted := false + // this is the core loop of autosplitting + // 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( /*TODO: Request Current Split*/ splitCount) + if err2 != nil { + return + } + if autoState.Start && !runStarted { + //split run + _, _ = commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + runStarted = !runStarted + } + if autoState.Split && runStarted { + //split run + _, _ = commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + splitCount++ + } + if autoState.Reset && runStarted { + if s.ResetTimerOnGameReset { + _, _ = commandDispatcher.Dispatch(dispatcher.RESET, nil) + } + if s.ResetGameOnTimerReset { + s.NWAAutoSplitter.SoftResetConsole() + } + splitCount = 0 + runStarted = !runStarted + } + // TODO: Close the connection after closing the splits file or receiving a disconnect signal + // s.NWAAutoSplitter.Client.Close() + + processElapsed := time.Since(processStart) + fmt.Println(processStart) + fmt.Println(processElapsed) + fmt.Println(mil - processElapsed) + time.Sleep(min(mil, max(0, mil-processElapsed))) + } +} + +func (s Splitters) processQUSB2SNES(commandDispatcher *dispatcher.Service) { + mil := 2 * time.Millisecond + + // Home Improvment test data + memData := []string{ + ("crates,0x1A8A,1"), + ("scene,0x161F,1"), + ("W2P2HP,0x1499,1"), + ("W2P1HP,0x1493,1"), + ("BossHP,0x1491,1"), + ("state,0x1400,1"), + ("gameplay,0x0AE5,1"), + ("substage,0x0AE3,1"), + ("stage,0x0AE1,1"), + ("scene2,0x0886,1"), + ("play_state,0x03B1,1"), + ("power_up,0x03AF,1"), + ("weapon,0x03CD,1"), + ("invul,0x1C05,1"), + ("FBossHP,0x149D,1"), + } + + startConditionImport := []string{ + ("start:state,prior=0xC0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + ("start:state,prior=0xD0 && state,current=0x0 && stage,current=0x0 && substage,current=0x0 && gameplay,current=0x11 && play_state,current=0x0"), + } + + resetConditionImport := []string{ + ("cutscene_reset:state,current=0x0 && state,prior=0xD0 && gameplay,prior=0x11 && gameplay,current=0x0"), + ("tool_reset:gameplay,prior=0x11 && gameplay,current=0x0 && scene,prior=0x4 && scene,current=0x0 && scene2,prior=0x3 && scene2,current=0x0"), + ("level_reset:gameplay,prior=0x13 && gameplay,current=0x0 && crates,current=0x0 && substage,current=0x0 && stage,current=0x0"), + } + + splitConditionImport := []string{ + ("1-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x1 && gameplay,current=0x13"), + ("1-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x2 && gameplay,current=0x13"), + ("1-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x3 && gameplay,current=0x13"), + ("1-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13"), + ("1-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x0 && substage,current=0x4 && gameplay,current=0x13 && BossHP,current=0x0"), + ("2-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x1 && gameplay,current=0x13"), + ("2-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x2 && gameplay,current=0x13"), + ("2-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x3 && gameplay,current=0x13"), + ("2-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13"), + ("2-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x1 && substage,current=0x4 && gameplay,current=0x13 && W2P1HP,current=0x0"), + ("3-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x1 && gameplay,current=0x13"), + ("3-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x2 && gameplay,current=0x13"), + ("3-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x3 && gameplay,current=0x13"), + ("3-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13"), + ("3-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x2 && substage,current=0x4 && gameplay,current=0x13 && crates,current=0x7"), + ("4-1:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x1 && gameplay,current=0x13"), + ("4-2:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x2 && gameplay,current=0x13"), + ("4-3:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x3 && gameplay,current=0x13"), + ("4-4:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13"), + ("4-5:state,prior=0xC8 && state,current=0x0 && stage,current=0x3 && substage,current=0x4 && gameplay,current=0x13 && FBossHP,current=0xFF"), + } + + _ = s.QUSB2SNESAutoSplitter.SetName("OpenSplit") + + version, _ := s.QUSB2SNESAutoSplitter.AppVersion() + fmt.Printf("Server version is %v\n", version) + + devices, _ := s.QUSB2SNESAutoSplitter.ListDevice() + + if len(devices) != 1 { + if len(devices) == 0 { + fmt.Printf("no devices present\n") + return + } + fmt.Printf("unexpected devices: %#v\n", devices) + return + } + device := devices[0] + fmt.Printf("Using device %v\n", device) + + _ = s.QUSB2SNESAutoSplitter.Attach(device) + fmt.Println("Connected.") + + info, _ := s.QUSB2SNESAutoSplitter.Info() + fmt.Printf("%#v\n", info) + + var autosplitter = qusb2snes.NewQUSB2SNESAutoSplitter(memData, startConditionImport, resetConditionImport, splitConditionImport) + + splitCount := 0 + runStarted := false + for { + processStart := time.Now() + + summary, _ := autosplitter.Update(*s.QUSB2SNESAutoSplitter, splitCount) + + if summary.Start && !runStarted { + _, _ = commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + runStarted = !runStarted + } + if summary.Split && runStarted { + // IGT + // timer.SetGameTime(*t) + // RTA + _, _ = commandDispatcher.Dispatch(dispatcher.SPLIT, nil) + splitCount++ + } + // need to get timer reset state + if summary.Reset && runStarted /*|| timer is reset*/ { + if s.ResetTimerOnGameReset { + _, _ = commandDispatcher.Dispatch(dispatcher.RESET, nil) + } + if s.ResetGameOnTimerReset { + _ = s.QUSB2SNESAutoSplitter.Reset() + } + autosplitter.ResetGameTracking() + splitCount = 0 + runStarted = !runStarted + } + // TODO: Close the connection after closing the splits file or receiving a disconnect signal + // s.QUSB2SNESAutoSplitter.Client.Close() + + processElapsed := time.Since(processStart) + fmt.Println(processStart) + fmt.Println(processElapsed) + fmt.Println(mil - processElapsed) + time.Sleep(min(mil, max(0, mil-processElapsed))) + } +} 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); } diff --git a/go.mod b/go.mod index 989138c..c982d37 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25 require ( github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 github.com/wailsapp/wails/v2 v2.10.2 golang.org/x/sys v0.40.0 ) @@ -12,7 +13,6 @@ require ( github.com/bep/debounce v1.2.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/labstack/echo/v4 v4.13.3 // indirect github.com/labstack/gommon v0.4.2 // indirect @@ -29,7 +29,7 @@ require ( github.com/tkrajina/go-reflector v0.5.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/wailsapp/go-webview2 v1.0.21 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect diff --git a/go.sum b/go.sum index fb44a81..664be2f 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= -github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v2 v2.10.2 h1:29U+c5PI4K4hbx8yFbFvwpCuvqK9VgNv8WGobIlKlXk= diff --git a/opensplit.go b/opensplit.go index 642ab7d..9efc469 100644 --- a/opensplit.go +++ b/opensplit.go @@ -18,6 +18,9 @@ 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" @@ -69,6 +72,30 @@ func main() { // Build dispatcher that can receive commands from frontend or backend and dispatch them to the state machine commandDispatcher := dispatcher.NewService(machine) + // UseAutoSplitter and Type should come from the splits config file + // ResetTimerOnGameReset, ResetGameOnTimerReset, Addr, Port should come from the autosplitter config file + // NWA + AutoSplitterService := autosplitters.Splitters{ + NWAAutoSplitter: new(nwa.NWASplitter), + QUSB2SNESAutoSplitter: new(qusb2snes.SyncClient), + UseAutosplitter: true, + ResetTimerOnGameReset: true, + ResetGameOnTimerReset: false, + Addr: "0.0.0.0", + Port: 48879, + Type: autosplitters.NWA} + + // // QUSB2SNES + // AutoSplitterService := autosplitters.Splitters{ + // NWAAutoSplitter: new(nwa.NWASplitter), + // QUSB2SNESAutoSplitter: new(qusb2snes.SyncClient), + // UseAutosplitter: true, + // ResetTimerOnGameReset: true, + // ResetGameOnTimerReset: false, + // Addr: "0.0.0.0", + // Port: 23074, + // Type: autosplitters.QUSB2SNES} + var hotkeyProvider statemachine.HotkeyProvider err := wails.Run(&options.App{ @@ -99,6 +126,9 @@ func main() { timerUIBridge.StartUIPump() configUIBridge.StartUIPump() + //Start autosplitter + AutoSplitterService.Run(commandDispatcher) + startInterruptListener(ctx, hotkeyProvider) runtime.WindowSetAlwaysOnTop(ctx, true) runtime.WindowSetMinSize(ctx, 100, 100)