package main import "fmt" import "os" import "github.com/joho/godotenv" import "github.com/veandco/go-sdl2/sdl" import _ "embed" import "crypto/rand" import "encoding/hex" import "encoding/json" import "encoding/binary" import "path/filepath" import "os/exec" import "runtime" import "strconv" import "bytes" import "reflect" import "unsafe" //go:embed Profiles.json var DefaultProfilesJson []byte //go:embed banner.txt var Banner string func BEUint32(n uint32) []byte { b := make([]byte, 4) binary.BigEndian.PutUint32(b, n) return b } func BEUint16(n uint16) []byte { b := make([]byte, 4) binary.BigEndian.PutUint16(b, n) return b } func IPSWritePatchOffset(buffer *bytes.Buffer, offset uint, patch []byte) { b := make([]byte, 4) binary.BigEndian.PutUint32(b, uint32(offset)) buffer.Write(b) binary.BigEndian.PutUint16(b[2:], uint16(len(patch))) buffer.Write(b[2:]) buffer.Write(patch) } func interpolateBool(v string) bool { return v == "1" || v == "true" || v == "t" || v == "yes" || v == "y" } func WriteDefaultProfilesConfig(profilesJsonPath string) { fmt.Println("Writing default config to", profilesJsonPath) err := os.WriteFile(profilesJsonPath, DefaultProfilesJson, 0666) if err != nil { panic(err) } } func BootstrapProfilesConfig(profilesJsonPath string, customNickname string, fallbackToDefaultConfig bool) string { defer func(profilesJsonPath string, customNickname string) { if(!fallbackToDefaultConfig) { return } if recover() == nil { return } WriteDefaultProfilesConfig(profilesJsonPath) BootstrapProfilesConfig(profilesJsonPath, customNickname, false) }(profilesJsonPath, customNickname) byteValue, err := os.ReadFile(profilesJsonPath) if(err != nil) { panic(err) } var result map[string]interface{} json.Unmarshal([]byte(byteValue), &result) var profiles = result["profiles"].([]interface{}) if(len(profiles) != 2) { panic("Invalid profile count") } var selectedProfile map[string]interface{} for i, _ := range profiles { var profile = profiles[i].(map[string]interface{}) if profile["user_id"].(string) != "00000000000000010000000000000000" { selectedProfile = profile } } var currentNickname = selectedProfile["name"].(string) var userId = selectedProfile["user_id"].(string) var changed = false if(customNickname != "" && currentNickname != customNickname) { fmt.Println("Setting nickname to:", customNickname) selectedProfile["name"] = customNickname changed = true } if(userId == "00000000000000000000000000000000" || len(userId) != 32) { var userIdBytes = make([]byte, 16) _, err := rand.Read(userIdBytes) if err != nil { panic(err) } userId = hex.EncodeToString(userIdBytes) selectedProfile["user_id"] = userId changed = true } if(result["last_opened"].(string) != userId) { result["last_opened"] = userId changed = true } if(changed) { byteValue, err = json.Marshal(result) err = os.WriteFile(profilesJsonPath, byteValue, 0666) if err != nil { panic(err) } } return userId } func writeIPSPatch(ryujinxDir string, server string) { var patchDir = filepath.Join(ryujinxDir, "portable/mods/contents/0100277011f1a000/customserver/exefs") var patchPath = filepath.Join(patchDir, "EF1E5EA4171B94B0F22A6C565BB300175A6BF5EE000000000000000000000000.ips") err := os.MkdirAll(patchDir, 0750) if(err != nil) { panic(err) } fmt.Println("Patching server to:", server) var buf = bytes.NewBufferString("IPS32") IPSWritePatchOffset(buf, 0x957D4, []byte{0x1F,0x20,0x03,0xD5}) IPSWritePatchOffset(buf, 0x9EBA8, []byte{0x1F,0x20,0x03,0xD5}) IPSWritePatchOffset(buf, 0xC40E0, []byte{0xE0,0x03,0x1F,0xAA,0x20,0,0,0xF9,0x40,0,0,0xF9,0xC0,0x03,0x5F,0xD6}) IPSWritePatchOffset(buf, 0xB6C957, append([]byte(server), 0)) err = os.WriteFile(patchPath, buf.Bytes(), 0666) if err != nil { panic(err) } } func getControllerGUID(joystickIndex int) string { if err := sdl.Init(sdl.INIT_JOYSTICK); err != nil { return "" } defer sdl.Quit() var tmp [16]byte var guid = sdl.JoystickGetDeviceGUID(joystickIndex) rs := reflect.ValueOf(&guid).Elem() rf := rs.Field(0) rf = reflect.NewAt(reflect.TypeOf(tmp), unsafe.Pointer(rf.UnsafeAddr())).Elem() var guidBytes = rf.Interface().([16]byte) return strconv.Itoa(joystickIndex)+"-"+hex.EncodeToString(guidBytes[0:4])+"-"+hex.EncodeToString(guidBytes[4:6])+"-"+hex.EncodeToString(guidBytes[6:8])+"-"+hex.EncodeToString(guidBytes[8:10])+"-"+hex.EncodeToString(guidBytes[10:16]) } func main() { fmt.Println(Banner) exePath, err := os.Executable() if err != nil { panic(err) } exeDir := filepath.Dir(exePath) if os.Getenv("MARIO35_EXE_DIR") != "" { exeDir = os.Getenv("MARIO35_EXE_DIR") } exeDir, err = filepath.Abs(exeDir) fmt.Println("Dir:", exeDir) env, err := godotenv.Read(filepath.Join(exeDir, "mario35.conf")) if err != nil { fmt.Println("Conf error:", err) env = make(map[string]string) } fmt.Println("Conf:", env) var userId = BootstrapProfilesConfig(filepath.Join(exeDir, "portable/system/Profiles.json"), env["NICKNAME"], true) fmt.Println("UserId:", userId) var exeName = "Ryujinx.Headless.SDL2" if runtime.GOOS == "windows" { exeName += ".exe" } if(env["EXENAME"] != "") { exeName = env["EXENAME"] } var nsp = "portable/games/smb35_v0.nsp" if(env["NSP"] != "") { nsp = env["NSP"] } var args = []string{"--enable-internet-connection"} if(interpolateBool(env["FULLSCREEN"])) { args = append(args, "--fullscreen") } if(interpolateBool(env["NOVSYNC"])) { args = append(args, "--disable-vsync") } scale, _ := strconv.Atoi(env["SCALE"]) if(scale > 1) { args = append(args, "--resolution-scale"+strconv.Itoa(scale)) } if(interpolateBool(env["HANDHELD"])) { args = append(args, "--disable-docked-mode") } if(interpolateBool(env["VULKAN"])) { args = append(args, "--graphics-backend=Vulkan") } if(interpolateBool(env["VULKAN"]) && env["GPUVENDOR"] != "") { args = append(args, "--preferred-gpu-vendor="+env["GPUVENDOR"]) } gamepad, _ := strconv.Atoi(env["GAMEPAD"]) if(gamepad >= 1) { var gamepadId = getControllerGUID(gamepad-1) if(gamepadId != "") { args = append(args, "--input-id-1="+gamepadId) } } if(env["SERVER"] != "") { writeIPSPatch(exeDir, env["SERVER"]) } args = append(args, "--", nsp) fmt.Println("ExeName:", exeName) fmt.Println("Args:", args) cmd := exec.Command(filepath.Join(exeDir, exeName), args...) cmd.Dir = exeDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr fmt.Println("\n:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::") err = cmd.Run() if(err != nil) { panic(err) } }