From 1f9e3891acd82ddf326ae66f29fcf13e57b6b21b Mon Sep 17 00:00:00 2001 From: aomizu Date: Sun, 18 May 2025 21:46:58 +0900 Subject: [PATCH] refactored project, added makefile --- FyneApp.toml | 5 +- Makefile | 25 ++ README.md | 22 +- main.go | 728 +-------------------------------------- pkg/launcher/launcher.go | 106 ++++++ pkg/patching/patching.go | 259 ++++++++++++++ pkg/paths/paths.go | 94 +++++ pkg/ui/ui.go | 173 ++++++++++ pkg/utils/utils.go | 119 +++++++ 9 files changed, 806 insertions(+), 725 deletions(-) create mode 100644 Makefile create mode 100644 pkg/launcher/launcher.go create mode 100644 pkg/patching/patching.go create mode 100644 pkg/paths/paths.go create mode 100644 pkg/ui/ui.go create mode 100644 pkg/utils/utils.go diff --git a/FyneApp.toml b/FyneApp.toml index 50c8d27..7b525b9 100644 --- a/FyneApp.toml +++ b/FyneApp.toml @@ -1,5 +1,6 @@ [Details] + Icon = "Icon.png" Name = "TurtleSilicon" ID = "com.tairasu.turtlesilicon" - Version = "0.1.0" - Build = 15 + Version = "1.0.3" + Build = 6 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..62556d1 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.PHONY: build clean + +# Default target +all: build + +# Build the application with custom resource copying +build: + GOOS=darwin GOARCH=arm64 fyne package + @echo "Copying additional resources to app bundle..." + @mkdir -p TurtleSilicon.app/Contents/Resources/rosettax87 + @mkdir -p TurtleSilicon.app/Contents/Resources/winerosetta + @cp -R rosettax87/* TurtleSilicon.app/Contents/Resources/rosettax87/ + @cp -R winerosetta/* TurtleSilicon.app/Contents/Resources/winerosetta/ + @echo "Build complete!" + +# Clean build artifacts +clean: + rm -rf TurtleSilicon.app + rm -f TurtleSilicon.dmg + +# Build DMG without code signing +dmg: build + @echo "Creating DMG file..." + @hdiutil create -volname TurtleSilicon -srcfolder TurtleSilicon.app -ov -format UDZO TurtleSilicon.dmg + @echo "DMG created: TurtleSilicon.dmg" diff --git a/README.md b/README.md index 5928478..c07d867 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,29 @@ To build this application yourself, you will need: Once Go and Fyne are set up, navigate to the project directory in your terminal and run the following command to build the application for Apple Silicon (ARM64) macOS: +### Option 1: Using the Makefile (Recommended) + +The included Makefile automates the build process and handles copying the required resource files: + ```sh -GOOS=darwin GOARCH=arm64 fyne package +make ``` -This will create a `TurtleSilicon.app` file in the project directory, which you can then run. +This will: +1. Build the application for Apple Silicon macOS +2. Automatically copy the rosettax87 and winerosetta directories to the app bundle + +### Option 2: Manual Build + +If you prefer to build manually: + +```sh +GOOS=darwin GOARCH=arm64 fyne package +# Then manually copy the resource directories +cp -R rosettax87 winerosetta TurtleSilicon.app/Contents/Resources/ +``` + +In either case, this will create a `TurtleSilicon.app` file in the project directory, which you can then run. Make sure you have an `Icon.png` file in the root of the project directory before building. diff --git a/main.go b/main.go index 68f5416..712ab88 100644 --- a/main.go +++ b/main.go @@ -1,735 +1,21 @@ package main import ( - "bytes" // Added import - "fmt" - "io" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" + "turtlesilicon/pkg/ui" // Updated import path ) -const defaultCrossOverPath = "/Applications/CrossOver.app" - -var ( - crossoverPath string - turtlewowPath string - patchesAppliedTurtleWoW = false - patchesAppliedCrossOver = false - enableMetalHud = true // Default to enabled -) - -// Helper function to check if a path exists -func pathExists(path string) bool { - _, err := os.Stat(path) - return err == nil -} - -// Helper function to check if a path exists and is a directory -func dirExists(path string) bool { - info, err := os.Stat(path) - if err != nil { - return false - } - return info.IsDir() -} - -// Helper function to copy a file -func copyFile(src, dst string) error { - sourceFileStat, err := os.Stat(src) - if err != nil { - return err - } - - if !sourceFileStat.Mode().IsRegular() { - return fmt.Errorf("%s is not a regular file", src) - } - - source, err := os.Open(src) - if err != nil { - return err - } - defer source.Close() - - destination, err := os.Create(dst) - if err != nil { - return err - } - defer destination.Close() - _, err = io.Copy(destination, source) - return err -} - -// Helper function to copy a directory recursively -func copyDir(src string, dst string) error { - srcInfo, err := os.Stat(src) - if err != nil { - return err - } - - err = os.MkdirAll(dst, srcInfo.Mode()) - if err != nil { - return err - } - - dir, err := os.ReadDir(src) - if err != nil { - return err - } - - for _, entry := range dir { - srcPath := filepath.Join(src, entry.Name()) - dstPath := filepath.Join(dst, entry.Name()) - - if entry.IsDir() { - err = copyDir(srcPath, dstPath) - if err != nil { - return err - } - } else { - err = copyFile(srcPath, dstPath) - if err != nil { - return err - } - } - } - return nil -} - -// Helper function to run an AppleScript command using osascript -func runOsascript(scriptString string, myWindow fyne.Window) bool { - log.Printf("Executing AppleScript: %s", scriptString) - cmd := exec.Command("osascript", "-e", scriptString) - output, err := cmd.CombinedOutput() // Changed variable name to avoid conflict if 'output' is used later - if err != nil { - errMsg := fmt.Sprintf("AppleScript failed: %v\\nOutput: %s", err, string(output)) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - return false - } - log.Printf("osascript output: %s", string(output)) - return true -} - -// escapeStringForAppleScript escapes a string to be safely embedded in an AppleScript double-quoted string. -func escapeStringForAppleScript(s string) string { - s = strings.ReplaceAll(s, "\\", "\\\\") // Escape backslashes first: \ -> \\ - s = strings.ReplaceAll(s, "\"", "\\\"") // Escape double quotes: " -> \" - return s -} +const appVersion = "1.0.3" // Added version constant func main() { - myApp := app.NewWithID("com.example.turtlesilicon") - myWindow := myApp.NewWindow("TurtleSilicon") - myWindow.Resize(fyne.NewSize(650, 450)) // Slightly wider for clarity + myApp := app.NewWithID("com.tairasu.turtlesilicon") + myWindow := myApp.NewWindow("TurtleSilicon v" + appVersion) // Updated title + myWindow.Resize(fyne.NewSize(650, 450)) myWindow.SetFixedSize(true) - // --- Path Labels --- - crossoverPathLabel := widget.NewRichText() // Changed to RichText - turtlewowPathLabel := widget.NewRichText() // Changed to RichText + content := ui.CreateUI(myWindow) // Use the CreateUI function from the ui package + myWindow.SetContent(content) - // --- Status Labels (Changed to RichText for color) --- - var turtlewowStatusLabel *widget.RichText - var crossoverStatusLabel *widget.RichText - - turtlewowStatusLabel = widget.NewRichText() - crossoverStatusLabel = widget.NewRichText() - - // --- Checkbox for Metal HUD --- - metalHudCheckbox := widget.NewCheck("Enable Metal Hud (show FPS)", func(checked bool) { - enableMetalHud = checked - log.Printf("Metal HUD enabled: %v", enableMetalHud) - }) - metalHudCheckbox.SetChecked(enableMetalHud) // Set initial state - - // --- Buttons (declared here to be accessible in updateAllStatuses) --- - var launchButton *widget.Button - var patchTurtleWoWButton *widget.Button - var patchCrossOverButton *widget.Button - - // --- Helper to update all statuses and button states --- - updateAllStatuses := func() { - // Update Crossover Path and Status - if crossoverPath == "" { - crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} - crossoverPathLabel.Refresh() - patchesAppliedCrossOver = false // Reset if path is cleared - } else { - crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: crossoverPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} - crossoverPathLabel.Refresh() - wineloader2Path := filepath.Join(crossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application", "wineloader2") - if pathExists(wineloader2Path) { - patchesAppliedCrossOver = true - } else { - // patchesAppliedCrossOver = false // Only set to false if not already true from a patch action this session - } - } - if patchesAppliedCrossOver { - crossoverStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} // Changed to ColorNameSuccess - crossoverStatusLabel.Refresh() - if patchCrossOverButton != nil { - patchCrossOverButton.Disable() - } - } else { - crossoverStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} - crossoverStatusLabel.Refresh() - if patchCrossOverButton != nil { - if crossoverPath != "" { - patchCrossOverButton.Enable() - } else { - patchCrossOverButton.Disable() - } - } - } - - // Update TurtleWoW Path and Status - if turtlewowPath == "" { - turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} - turtlewowPathLabel.Refresh() - patchesAppliedTurtleWoW = false // Reset if path is cleared - } else { - turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: turtlewowPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} - turtlewowPathLabel.Refresh() - winerosettaDllPath := filepath.Join(turtlewowPath, "winerosetta.dll") - d3d9DllPath := filepath.Join(turtlewowPath, "d3d9.dll") - libSiliconPatchDllPath := filepath.Join(turtlewowPath, "libSiliconPatch.dll") // Added check for libSiliconPatch.dll - rosettaX87DirPath := filepath.Join(turtlewowPath, "rosettax87") - dllsTextFile := filepath.Join(turtlewowPath, "dlls.txt") - // Check for libRuntimeRosettax87 as well - rosettaX87ExePath := filepath.Join(rosettaX87DirPath, "rosettax87") - libRuntimeRosettaX87Path := filepath.Join(rosettaX87DirPath, "libRuntimeRosettax87") - - // Check if dlls.txt exists and contains winerosetta.dll and libSiliconPatch.dll - dllsFileValid := false - if pathExists(dllsTextFile) { - if fileContent, err := os.ReadFile(dllsTextFile); err == nil { - // Updated to check for both entries for dllsFileValid to be true - if strings.Contains(string(fileContent), "winerosetta.dll") && strings.Contains(string(fileContent), "libSiliconPatch.dll") { - dllsFileValid = true - } - } - } - - if pathExists(winerosettaDllPath) && pathExists(d3d9DllPath) && pathExists(libSiliconPatchDllPath) && dirExists(rosettaX87DirPath) && - pathExists(rosettaX87ExePath) && pathExists(libRuntimeRosettaX87Path) && dllsFileValid { - patchesAppliedTurtleWoW = true - } else { - // patchesAppliedTurtleWoW = false - } - } - if patchesAppliedTurtleWoW { - turtlewowStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} // Changed to ColorNameSuccess - turtlewowStatusLabel.Refresh() - if patchTurtleWoWButton != nil { - patchTurtleWoWButton.Disable() - } - } else { - turtlewowStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} - turtlewowStatusLabel.Refresh() - if patchTurtleWoWButton != nil { - if turtlewowPath != "" { - patchTurtleWoWButton.Enable() - } else { - patchTurtleWoWButton.Disable() - } - } - } - - // Update Launch Button State - if launchButton != nil { - if patchesAppliedTurtleWoW && patchesAppliedCrossOver && turtlewowPath != "" && crossoverPath != "" { - launchButton.Enable() - } else { - launchButton.Disable() - } - } - } - - // --- Path Selection Functions --- - selectCrossOverPath := func() { - dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { - if err != nil { - dialog.ShowError(err, myWindow) - return - } - if uri == nil { // User cancelled - log.Println("CrossOver path selection cancelled.") - // Do not reset crossoverPath if user cancels, keep previous valid path - updateAllStatuses() // Re-evaluate with existing path - return - } - selectedPath := uri.Path() - if filepath.Ext(selectedPath) == ".app" && dirExists(selectedPath) { // Check if it's a directory too - crossoverPath = selectedPath - patchesAppliedCrossOver = false // Reset patch status on new path, updateAllStatuses will re-check - log.Println("CrossOver path set to:", crossoverPath) - } else { - // Don't reset crossoverPath, show error and keep old one if any - dialog.ShowError(fmt.Errorf("invalid selection: '%s'. Please select a valid .app bundle", selectedPath), myWindow) - log.Println("Invalid CrossOver path selected:", selectedPath) - } - updateAllStatuses() - }, myWindow) - } - - selectTurtleWoWPath := func() { - dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { - if err != nil { - dialog.ShowError(err, myWindow) - return - } - if uri == nil { // User cancelled - log.Println("TurtleWoW path selection cancelled.") - updateAllStatuses() // Re-evaluate - return - } - selectedPath := uri.Path() - if dirExists(selectedPath) { // Basic check for directory - turtlewowPath = selectedPath - patchesAppliedTurtleWoW = false // Reset patch status on new path, updateAllStatuses will re-check - log.Println("TurtleWoW path set to:", turtlewowPath) - } else { - dialog.ShowError(fmt.Errorf("invalid selection: '%s' is not a valid directory", selectedPath), myWindow) - log.Println("Invalid TurtleWoW path selected:", selectedPath) - } - updateAllStatuses() - }, myWindow) - } - - // --- Patching Functions --- - patchTurtleWoWFunc := func() { - log.Println("Patch TurtleWoW clicked") - if turtlewowPath == "" { - dialog.ShowError(fmt.Errorf("TurtleWoW path not set. Please set it first."), myWindow) - return - } - - // Target paths - targetWinerosettaDll := filepath.Join(turtlewowPath, "winerosetta.dll") - targetD3d9Dll := filepath.Join(turtlewowPath, "d3d9.dll") - targetRosettaX87Dir := filepath.Join(turtlewowPath, "rosettax87") - dllsTextFile := filepath.Join(turtlewowPath, "dlls.txt") - - // Files to copy directly into turtlewowPath - filesToCopy := map[string]string{ - "winerosetta/winerosetta.dll": targetWinerosettaDll, // Adjusted path - "winerosetta/d3d9.dll": targetD3d9Dll, // Adjusted path - "winerosetta/libSiliconPatch.dll": filepath.Join(turtlewowPath, "libSiliconPatch.dll"), // Added libSiliconPatch.dll - } - - for resourceName, destPath := range filesToCopy { - log.Printf("Processing resource: %s to %s", resourceName, destPath) - - resource, err := fyne.LoadResourceFromPath(resourceName) - if err != nil { - errMsg := fmt.Sprintf("failed to open bundled resource %s: %v", resourceName, err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - patchesAppliedTurtleWoW = false - updateAllStatuses() - return - } - - destinationFile, err := os.Create(destPath) - if err != nil { - errMsg := fmt.Sprintf("failed to create destination file %s: %v", destPath, err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - patchesAppliedTurtleWoW = false - updateAllStatuses() - return - } - defer destinationFile.Close() - - _, err = io.Copy(destinationFile, bytes.NewReader(resource.Content())) // Changed to use bytes.NewReader(resource.Content()) - if err != nil { - errMsg := fmt.Sprintf("failed to copy bundled resource %s to %s: %v", resourceName, destPath, err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - patchesAppliedTurtleWoW = false - updateAllStatuses() - return - } - log.Printf("Successfully copied %s to %s", resourceName, destPath) - } - - // Handle rosettax87 folder and its contents - log.Printf("Preparing rosettax87 directory at: %s", targetRosettaX87Dir) - if err := os.RemoveAll(targetRosettaX87Dir); err != nil { - log.Printf("Warning: could not remove existing rosettax87 folder '%s': %v", targetRosettaX87Dir, err) - // Not necessarily fatal, MkdirAll will handle creation. - } - if err := os.MkdirAll(targetRosettaX87Dir, 0755); err != nil { - errMsg := fmt.Sprintf("failed to create directory %s: %v", targetRosettaX87Dir, err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - patchesAppliedTurtleWoW = false - updateAllStatuses() - return - } - - rosettaFilesToCopy := map[string]string{ - "rosettax87/rosettax87": filepath.Join(targetRosettaX87Dir, "rosettax87"), // Adjusted path - "rosettax87/libRuntimeRosettax87": filepath.Join(targetRosettaX87Dir, "libRuntimeRosettax87"), // Added libRuntimeRosettax87 - } - - for resourceName, destPath := range rosettaFilesToCopy { - log.Printf("Processing rosetta resource: %s to %s", resourceName, destPath) - resource, err := fyne.LoadResourceFromPath(resourceName) - if err != nil { - errMsg := fmt.Sprintf("failed to open bundled resource %s: %v", resourceName, err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - patchesAppliedTurtleWoW = false - updateAllStatuses() - return - } - - destinationFile, err := os.Create(destPath) - if err != nil { - errMsg := fmt.Sprintf("failed to create destination file %s: %v", destPath, err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - patchesAppliedTurtleWoW = false - updateAllStatuses() - return - } - // No defer destinationFile.Close() here, because we chmod after copy and then it can be closed. - - _, err = io.Copy(destinationFile, bytes.NewReader(resource.Content())) // Changed to use bytes.NewReader(resource.Content()) - if err != nil { - destinationFile.Close() // Close before erroring out - errMsg := fmt.Sprintf("failed to copy bundled resource %s to %s: %v", resourceName, destPath, err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - patchesAppliedTurtleWoW = false - updateAllStatuses() - return - } - destinationFile.Close() // Close after successful copy - - // Set execute permissions for the rosettax87 executable - if filepath.Base(destPath) == "rosettax87" { // Corrected condition - log.Printf("Setting execute permission for %s", destPath) - if err := os.Chmod(destPath, 0755); err != nil { - errMsg := fmt.Sprintf("failed to set execute permission for %s: %v", destPath, err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - // Decide if this is fatal or a warning. For now, treat as fatal for patching. - patchesAppliedTurtleWoW = false - updateAllStatuses() - return - } - } - log.Printf("Successfully copied %s to %s", resourceName, destPath) - } - - // Check and update dlls.txt file - log.Printf("Checking dlls.txt file at: %s", dllsTextFile) - winerosettaEntry := "winerosetta.dll" - libSiliconPatchEntry := "libSiliconPatch.dll" // New entry - needsWinerosettaUpdate := true - needsLibSiliconPatchUpdate := true - - // Check if file exists and what it contains - if fileContentBytes, err := os.ReadFile(dllsTextFile); err == nil { - fileContent := string(fileContentBytes) - // File exists, check if it contains winerosetta.dll - if strings.Contains(fileContent, winerosettaEntry) { - log.Printf("dlls.txt already contains %s", winerosettaEntry) - needsWinerosettaUpdate = false - } - // File exists, check if it contains libSiliconPatch.dll - if strings.Contains(fileContent, libSiliconPatchEntry) { - log.Printf("dlls.txt already contains %s", libSiliconPatchEntry) - needsLibSiliconPatchUpdate = false - } - } else { - // File doesn't exist, we'll create a new one, so both need to be added - log.Printf("dlls.txt not found, will create a new one with both entries") - } - - // Update the file if needed - if needsWinerosettaUpdate || needsLibSiliconPatchUpdate { - var fileContentBytes []byte - var err error - if pathExists(dllsTextFile) { - fileContentBytes, err = os.ReadFile(dllsTextFile) - if err != nil { - errMsg := fmt.Sprintf("failed to read dlls.txt for update: %v", err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - // Not treating this as fatal, will try to create/overwrite - } - } - - currentContent := string(fileContentBytes) - updatedContent := currentContent - - // Ensure newline at the end of existing content if it's not empty - if len(updatedContent) > 0 && !strings.HasSuffix(updatedContent, "\n") { - updatedContent += "\n" - } - - if needsWinerosettaUpdate { - if !strings.Contains(updatedContent, winerosettaEntry+"\n") { // Check with newline to avoid partial matches - updatedContent += winerosettaEntry + "\n" - log.Printf("Adding %s to dlls.txt", winerosettaEntry) - } - } - if needsLibSiliconPatchUpdate { - if !strings.Contains(updatedContent, libSiliconPatchEntry+"\n") { // Check with newline - updatedContent += libSiliconPatchEntry + "\n" - log.Printf("Adding %s to dlls.txt", libSiliconPatchEntry) - } - } - - // Write updated content back to file - if err := os.WriteFile(dllsTextFile, []byte(updatedContent), 0644); err != nil { - errMsg := fmt.Sprintf("failed to update dlls.txt: %v", err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - // Not treating this as fatal for patching process - } else { - log.Printf("Successfully updated dlls.txt") - } - } - - log.Println("TurtleWoW patching with bundled resources completed successfully.") - dialog.ShowInformation("Success", "TurtleWoW patching process completed using bundled resources.", myWindow) - updateAllStatuses() - } - - patchCrossOverFunc := func() { - log.Println("Patch CrossOver clicked") - if crossoverPath == "" { - dialog.ShowError(fmt.Errorf("CrossOver path not set. Please set it first."), myWindow) - return - } - - wineloaderBasePath := filepath.Join(crossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application") - wineloaderOrig := filepath.Join(wineloaderBasePath, "wineloader") - wineloaderCopy := filepath.Join(wineloaderBasePath, "wineloader2") - - if !pathExists(wineloaderOrig) { - dialog.ShowError(fmt.Errorf("original wineloader not found at %s", wineloaderOrig), myWindow) - patchesAppliedCrossOver = false - updateAllStatuses() - return - } - - // 1. Make a copy of wineloader - log.Printf("Copying %s to %s", wineloaderOrig, wineloaderCopy) - if err := copyFile(wineloaderOrig, wineloaderCopy); err != nil { - dialog.ShowError(fmt.Errorf("failed to copy wineloader: %w", err), myWindow) - patchesAppliedCrossOver = false - updateAllStatuses() - return - } - - // 2. Execute codesign --remove-signature - log.Printf("Executing: codesign --remove-signature %s", wineloaderCopy) - cmd := exec.Command("codesign", "--remove-signature", wineloaderCopy) - combinedOutput, err := cmd.CombinedOutput() // Store result in a new variable - if err != nil { - // Corrected variable name for error message and use combinedOutput - derrMsg := fmt.Sprintf("failed to remove signature from %s: %v\\nOutput: %s", wineloaderCopy, err, string(combinedOutput)) - dialog.ShowError(fmt.Errorf(derrMsg), myWindow) - log.Println(derrMsg) - patchesAppliedCrossOver = false - // Attempt to clean up the copied file if codesign fails - if err := os.Remove(wineloaderCopy); err != nil { - log.Printf("Warning: failed to cleanup wineloader2 after codesign failure: %v", err) - } - updateAllStatuses() - return - } - log.Printf("codesign output: %s", string(combinedOutput)) // Use combinedOutput here - - // 3. Make wineloader2 executable - log.Printf("Setting execute permissions for %s", wineloaderCopy) - if err := os.Chmod(wineloaderCopy, 0755); err != nil { - errMsg := fmt.Sprintf("failed to set executable permissions for %s: %v", wineloaderCopy, err) - dialog.ShowError(fmt.Errorf(errMsg), myWindow) - log.Println(errMsg) - patchesAppliedCrossOver = false - updateAllStatuses() - return - } - - log.Println("CrossOver patching completed successfully.") - patchesAppliedCrossOver = true - dialog.ShowInformation("Success", "CrossOver patching process completed.", myWindow) - updateAllStatuses() - } - - // --- Launch Function --- - launchGameFunc := func(myWindow fyne.Window) { - log.Println("Launch Game button clicked") - - // crossoverBinPath := filepath.Join(crossoverPath, "Contents", "MacOS", "CrossOver") // No longer used - turtlewowExePath := filepath.Join(turtlewowPath, "WoW.exe") - - // Pre-launch checks - if crossoverPath == "" { - dialog.ShowError(fmt.Errorf("CrossOver path not set. Please set it in the patcher."), myWindow) - return - } - if turtlewowPath == "" { - dialog.ShowError(fmt.Errorf("TurtleWoW path not set. Please set it in the patcher."), myWindow) - return - } - if !patchesAppliedTurtleWoW || !patchesAppliedCrossOver { - confirmed := false - dialog.ShowConfirm("Warning", "Not all patches confirmed applied. Continue with launch?", func(c bool) { - confirmed = c - }, myWindow) - if !confirmed { - return - } - } - - log.Println("Preparing to launch TurtleSilicon...") - - rosettaInTurtlePath := filepath.Join(turtlewowPath, "rosettax87") - rosettaExecutable := filepath.Join(rosettaInTurtlePath, "rosettax87") - wineloader2Path := filepath.Join(crossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application", "wineloader2") - wowExePath := filepath.Join(turtlewowPath, "wow.exe") - - if !pathExists(rosettaExecutable) { - dialog.ShowError(fmt.Errorf("rosetta executable not found at %s. Ensure TurtleWoW patching was successful", rosettaExecutable), myWindow) - return - } - if !pathExists(wineloader2Path) { - dialog.ShowError(fmt.Errorf("patched wineloader2 not found at %s. Ensure CrossOver patching was successful", wineloader2Path), myWindow) - return - } - if !pathExists(wowExePath) { - dialog.ShowError(fmt.Errorf("wow.exe not found at %s. Ensure your TurtleWoW directory is correct", wowExePath), myWindow) - return - } - - // Command 1: Launch rosettax87 - // The path itself needs to be an AppleScript string literal, so we use escapeStringForAppleScript. - // Corrected: cd into the rosettax87 directory itself. - appleScriptSafeRosettaDir := escapeStringForAppleScript(rosettaInTurtlePath) - // Construct the AppleScript command using a regular Go string literal for the format string. - // This ensures that \" correctly escapes double quotes for Go, producing literal quotes in the AppleScript command. - cmd1Script := fmt.Sprintf("tell application \"Terminal\" to do script \"cd \" & quoted form of \"%s\" & \" && sudo ./rosettax87\"", appleScriptSafeRosettaDir) - - log.Println("Launching rosettax87 (requires sudo password in new terminal)...") - if !runOsascript(cmd1Script, myWindow) { - // Error already shown by runOsascript - return - } - - dialog.ShowConfirm("Action Required", // Changed from ShowInformation to ShowConfirm - "The rosetta x87 terminal has been initiated.\n\n"+ - "1. Please enter your sudo password in that new terminal window.\n"+ - "2. Wait for rosetta x87 to fully start.\n\n"+ - "Click Yes once rosetta x87 is running and you have entered the password.\n"+ - "Click No to abort launching WoW.", - func(confirmed bool) { - if confirmed { - log.Println("User confirmed rosetta x87 is running. Proceeding to launch WoW.") - // Command 2: Launch WoW.exe via rosettax87 - if crossoverPath == "" || turtlewowPath == "" { - dialog.ShowError(fmt.Errorf("CrossOver path or TurtleWoW path is not set. Cannot launch WoW."), myWindow) - return - } - - // Determine MTL_HUD_ENABLED value based on checkbox - mtlHudValue := "0" - if enableMetalHud { - mtlHudValue = "1" - } - - // Construct the new shell command - shellCmd := fmt.Sprintf(`cd %s && WINEDLLOVERRIDES="d3d9=n,b" MTL_HUD_ENABLED=%s %s %s %s`, - quotePathForShell(turtlewowPath), // Added cd to turtlewowPath - mtlHudValue, // Use dynamic value - quotePathForShell(rosettaExecutable), - quotePathForShell(wineloader2Path), - quotePathForShell(turtlewowExePath)) // turtlewowExePath is defined at the start of launchGameFunc - - // Escape the entire shell command for AppleScript - escapedShellCmd := escapeStringForAppleScript(shellCmd) - - cmd2Script := fmt.Sprintf("tell application \"Terminal\" to do script \"%s\"", escapedShellCmd) - - log.Println("Executing updated WoW launch command via AppleScript...") - if !runOsascript(cmd2Script, myWindow) { - // Error already shown by runOsascript - return - } - - log.Println("Launch commands executed. Check the new terminal windows.") - dialog.ShowInformation("Launched", "World of Warcraft is starting. Enjoy.", myWindow) - } else { - log.Println("User cancelled WoW launch after rosetta x87 initiation.") - dialog.ShowInformation("Cancelled", "WoW launch was cancelled.", myWindow) - } - }, myWindow) - - // The following lines are now moved inside the dialog.ShowConfirm callback - // // Command 2: Launch WoW.exe via CrossOver - // ... (rest of the original cmd2 logic was here) - } - - // --- Button Definitions --- - patchTurtleWoWButton = widget.NewButton("Patch TurtleWoW", patchTurtleWoWFunc) - patchCrossOverButton = widget.NewButton("Patch CrossOver", patchCrossOverFunc) - launchButton = widget.NewButton("Launch Game", func() { // Wrap the call in an anonymous function - launchGameFunc(myWindow) // Pass myWindow to the actual handler - }) - - // --- Initial Check for Default CrossOver Path --- - if info, err := os.Stat(defaultCrossOverPath); err == nil && info.IsDir() { - crossoverPath = defaultCrossOverPath - log.Println("Pre-set CrossOver to default:", defaultCrossOverPath) - // No need to reset patchesAppliedCrossOver here, updateAllStatuses will check - } - - // --- UI Layout --- - // Using Form layout for better alignment of labels and controls - pathSelectionForm := widget.NewForm( - widget.NewFormItem("CrossOver Path:", container.NewBorder(nil, nil, nil, widget.NewButton("Set/Change", selectCrossOverPath), crossoverPathLabel)), - widget.NewFormItem("TurtleWoW Path:", container.NewBorder(nil, nil, nil, widget.NewButton("Set/Change", selectTurtleWoWPath), turtlewowPathLabel)), - ) - - patchOperationsLayout := container.NewVBox( - widget.NewSeparator(), - container.NewGridWithColumns(3, // Label, Status, Button - widget.NewLabel("TurtleWoW Patch:"), turtlewowStatusLabel, patchTurtleWoWButton, - ), - container.NewGridWithColumns(3, - widget.NewLabel("CrossOver Patch:"), crossoverStatusLabel, patchCrossOverButton, - ), - widget.NewSeparator(), - ) - - myWindow.SetContent(container.NewVBox( - pathSelectionForm, - patchOperationsLayout, - metalHudCheckbox, // Added Metal HUD checkbox - container.NewPadded(launchButton), // Added padding to launchButton - )) - - updateAllStatuses() // Initial UI state update, including button states myWindow.ShowAndRun() } - -// Helper to quote paths for shell commands if they contain spaces or special chars -func quotePathForShell(path string) string { - // A simple approach: always quote. More robust parsing might be needed for complex paths. - return fmt.Sprintf(`"%s"`, path) -} diff --git a/pkg/launcher/launcher.go b/pkg/launcher/launcher.go new file mode 100644 index 0000000..b3a33aa --- /dev/null +++ b/pkg/launcher/launcher.go @@ -0,0 +1,106 @@ +package launcher + +import ( + "fmt" + "log" + "path/filepath" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/dialog" + "turtlesilicon/pkg/paths" // Corrected import path + "turtlesilicon/pkg/utils" // Corrected import path +) + +var EnableMetalHud = true // Default to enabled + +func LaunchGame(myWindow fyne.Window) { + log.Println("Launch Game button clicked") + + if paths.CrossoverPath == "" { + dialog.ShowError(fmt.Errorf("CrossOver path not set. Please set it in the patcher."), myWindow) + return + } + if paths.TurtlewowPath == "" { + dialog.ShowError(fmt.Errorf("TurtleWoW path not set. Please set it in the patcher."), myWindow) + return + } + if !paths.PatchesAppliedTurtleWoW || !paths.PatchesAppliedCrossOver { + confirmed := false + dialog.ShowConfirm("Warning", "Not all patches confirmed applied. Continue with launch?", func(c bool) { + confirmed = c + }, myWindow) + if !confirmed { + return + } + } + + log.Println("Preparing to launch TurtleSilicon...") + + rosettaInTurtlePath := filepath.Join(paths.TurtlewowPath, "rosettax87") + rosettaExecutable := filepath.Join(rosettaInTurtlePath, "rosettax87") + wineloader2Path := filepath.Join(paths.CrossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application", "wineloader2") + wowExePath := filepath.Join(paths.TurtlewowPath, "wow.exe") // Corrected to wow.exe + + if !utils.PathExists(rosettaExecutable) { + dialog.ShowError(fmt.Errorf("rosetta executable not found at %s. Ensure TurtleWoW patching was successful", rosettaExecutable), myWindow) + return + } + if !utils.PathExists(wineloader2Path) { + dialog.ShowError(fmt.Errorf("patched wineloader2 not found at %s. Ensure CrossOver patching was successful", wineloader2Path), myWindow) + return + } + if !utils.PathExists(wowExePath) { + dialog.ShowError(fmt.Errorf("wow.exe not found at %s. Ensure your TurtleWoW directory is correct", wowExePath), myWindow) + return + } + + appleScriptSafeRosettaDir := utils.EscapeStringForAppleScript(rosettaInTurtlePath) + cmd1Script := fmt.Sprintf("tell application \"Terminal\" to do script \"cd \" & quoted form of \"%s\" & \" && sudo ./rosettax87\"", appleScriptSafeRosettaDir) + + log.Println("Launching rosettax87 (requires sudo password in new terminal)...") + if !utils.RunOsascript(cmd1Script, myWindow) { + return + } + + dialog.ShowConfirm("Action Required", + "The rosetta x87 terminal has been initiated.\n\n"+ + "1. Please enter your sudo password in that new terminal window.\n"+ + "2. Wait for rosetta x87 to fully start.\n\n"+ + "Click Yes once rosetta x87 is running and you have entered the password.\n"+ + "Click No to abort launching WoW.", + func(confirmed bool) { + if confirmed { + log.Println("User confirmed rosetta x87 is running. Proceeding to launch WoW.") + if paths.CrossoverPath == "" || paths.TurtlewowPath == "" { + dialog.ShowError(fmt.Errorf("CrossOver path or TurtleWoW path is not set. Cannot launch WoW."), myWindow) + return + } + + mtlHudValue := "0" + if EnableMetalHud { + mtlHudValue = "1" + } + + shellCmd := fmt.Sprintf(`cd %s && WINEDLLOVERRIDES="d3d9=n,b" MTL_HUD_ENABLED=%s %s %s %s`, + utils.QuotePathForShell(paths.TurtlewowPath), + mtlHudValue, + utils.QuotePathForShell(rosettaExecutable), + utils.QuotePathForShell(wineloader2Path), + utils.QuotePathForShell(wowExePath)) + + escapedShellCmd := utils.EscapeStringForAppleScript(shellCmd) + cmd2Script := fmt.Sprintf("tell application \"Terminal\" to do script \"%s\"", escapedShellCmd) + + log.Println("Executing updated WoW launch command via AppleScript...") + if !utils.RunOsascript(cmd2Script, myWindow) { + return + } + + log.Println("Launch commands executed. Check the new terminal windows.") + dialog.ShowInformation("Launched", "World of Warcraft is starting. Enjoy.", myWindow) + } else { + log.Println("User cancelled WoW launch after rosetta x87 initiation.") + dialog.ShowInformation("Cancelled", "WoW launch was cancelled.", myWindow) + } + }, myWindow) +} diff --git a/pkg/patching/patching.go b/pkg/patching/patching.go new file mode 100644 index 0000000..fa5ab04 --- /dev/null +++ b/pkg/patching/patching.go @@ -0,0 +1,259 @@ +package patching + +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/dialog" + "turtlesilicon/pkg/paths" // Corrected import path + "turtlesilicon/pkg/utils" // Corrected import path +) + +func PatchTurtleWoW(myWindow fyne.Window, updateAllStatuses func()) { + log.Println("Patch TurtleWoW clicked") + if paths.TurtlewowPath == "" { + dialog.ShowError(fmt.Errorf("TurtleWoW path not set. Please set it first."), myWindow) + return + } + + targetWinerosettaDll := filepath.Join(paths.TurtlewowPath, "winerosetta.dll") + targetD3d9Dll := filepath.Join(paths.TurtlewowPath, "d3d9.dll") + targetRosettaX87Dir := filepath.Join(paths.TurtlewowPath, "rosettax87") + dllsTextFile := filepath.Join(paths.TurtlewowPath, "dlls.txt") + + filesToCopy := map[string]string{ + "winerosetta/winerosetta.dll": targetWinerosettaDll, + "winerosetta/d3d9.dll": targetD3d9Dll, + } + + for resourceName, destPath := range filesToCopy { + log.Printf("Processing resource: %s to %s", resourceName, destPath) + resource, err := fyne.LoadResourceFromPath(resourceName) + if err != nil { + errMsg := fmt.Sprintf("failed to open bundled resource %s: %v", resourceName, err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + paths.PatchesAppliedTurtleWoW = false + updateAllStatuses() + return + } + + destinationFile, err := os.Create(destPath) + if err != nil { + errMsg := fmt.Sprintf("failed to create destination file %s: %v", destPath, err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + paths.PatchesAppliedTurtleWoW = false + updateAllStatuses() + return + } + defer destinationFile.Close() + + _, err = io.Copy(destinationFile, bytes.NewReader(resource.Content())) + if err != nil { + errMsg := fmt.Sprintf("failed to copy bundled resource %s to %s: %v", resourceName, destPath, err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + paths.PatchesAppliedTurtleWoW = false + updateAllStatuses() + return + } + log.Printf("Successfully copied %s to %s", resourceName, destPath) + } + + log.Printf("Preparing rosettax87 directory at: %s", targetRosettaX87Dir) + if err := os.RemoveAll(targetRosettaX87Dir); err != nil { + log.Printf("Warning: could not remove existing rosettax87 folder '%s': %v", targetRosettaX87Dir, err) + } + if err := os.MkdirAll(targetRosettaX87Dir, 0755); err != nil { + errMsg := fmt.Sprintf("failed to create directory %s: %v", targetRosettaX87Dir, err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + paths.PatchesAppliedTurtleWoW = false + updateAllStatuses() + return + } + + rosettaFilesToCopy := map[string]string{ + "rosettax87/rosettax87": filepath.Join(targetRosettaX87Dir, "rosettax87"), + "rosettax87/libRuntimeRosettax87": filepath.Join(targetRosettaX87Dir, "libRuntimeRosettax87"), + } + + for resourceName, destPath := range rosettaFilesToCopy { + log.Printf("Processing rosetta resource: %s to %s", resourceName, destPath) + resource, err := fyne.LoadResourceFromPath(resourceName) + if err != nil { + errMsg := fmt.Sprintf("failed to open bundled resource %s: %v", resourceName, err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + paths.PatchesAppliedTurtleWoW = false + updateAllStatuses() + return + } + + destinationFile, err := os.Create(destPath) + if err != nil { + errMsg := fmt.Sprintf("failed to create destination file %s: %v", destPath, err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + paths.PatchesAppliedTurtleWoW = false + updateAllStatuses() + return + } + + _, err = io.Copy(destinationFile, bytes.NewReader(resource.Content())) + if err != nil { + destinationFile.Close() + errMsg := fmt.Sprintf("failed to copy bundled resource %s to %s: %v", resourceName, destPath, err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + paths.PatchesAppliedTurtleWoW = false + updateAllStatuses() + return + } + destinationFile.Close() + + if filepath.Base(destPath) == "rosettax87" { + log.Printf("Setting execute permission for %s", destPath) + if err := os.Chmod(destPath, 0755); err != nil { + errMsg := fmt.Sprintf("failed to set execute permission for %s: %v", destPath, err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + paths.PatchesAppliedTurtleWoW = false + updateAllStatuses() + return + } + } + log.Printf("Successfully copied %s to %s", resourceName, destPath) + } + + log.Printf("Checking dlls.txt file at: %s", dllsTextFile) + winerosettaEntry := "winerosetta.dll" + libSiliconPatchEntry := "libSiliconPatch.dll" + needsWinerosettaUpdate := true + needsLibSiliconPatchUpdate := true + + if fileContentBytes, err := os.ReadFile(dllsTextFile); err == nil { + fileContent := string(fileContentBytes) + if strings.Contains(fileContent, winerosettaEntry) { + log.Printf("dlls.txt already contains %s", winerosettaEntry) + needsWinerosettaUpdate = false + } + if strings.Contains(fileContent, libSiliconPatchEntry) { + log.Printf("dlls.txt already contains %s", libSiliconPatchEntry) + needsLibSiliconPatchUpdate = false + } + } else { + log.Printf("dlls.txt not found, will create a new one with both entries") + } + + if needsWinerosettaUpdate || needsLibSiliconPatchUpdate { + var fileContentBytes []byte + var err error + if utils.PathExists(dllsTextFile) { + fileContentBytes, err = os.ReadFile(dllsTextFile) + if err != nil { + errMsg := fmt.Sprintf("failed to read dlls.txt for update: %v", err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + } + } + + currentContent := string(fileContentBytes) + updatedContent := currentContent + + if len(updatedContent) > 0 && !strings.HasSuffix(updatedContent, "\n") { + updatedContent += "\n" + } + + if needsWinerosettaUpdate { + if !strings.Contains(updatedContent, winerosettaEntry+"\n") { + updatedContent += winerosettaEntry + "\n" + log.Printf("Adding %s to dlls.txt", winerosettaEntry) + } + } + if needsLibSiliconPatchUpdate { + if !strings.Contains(updatedContent, libSiliconPatchEntry+"\n") { + updatedContent += libSiliconPatchEntry + "\n" + log.Printf("Adding %s to dlls.txt", libSiliconPatchEntry) + } + } + + if err := os.WriteFile(dllsTextFile, []byte(updatedContent), 0644); err != nil { + errMsg := fmt.Sprintf("failed to update dlls.txt: %v", err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + } else { + log.Printf("Successfully updated dlls.txt") + } + } + + log.Println("TurtleWoW patching with bundled resources completed successfully.") + dialog.ShowInformation("Success", "TurtleWoW patching process completed using bundled resources.", myWindow) + updateAllStatuses() +} + +func PatchCrossOver(myWindow fyne.Window, updateAllStatuses func()) { + log.Println("Patch CrossOver clicked") + if paths.CrossoverPath == "" { + dialog.ShowError(fmt.Errorf("CrossOver path not set. Please set it first."), myWindow) + return + } + + wineloaderBasePath := filepath.Join(paths.CrossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application") + wineloaderOrig := filepath.Join(wineloaderBasePath, "wineloader") + wineloaderCopy := filepath.Join(wineloaderBasePath, "wineloader2") + + if !utils.PathExists(wineloaderOrig) { + dialog.ShowError(fmt.Errorf("original wineloader not found at %s", wineloaderOrig), myWindow) + paths.PatchesAppliedCrossOver = false + updateAllStatuses() + return + } + + log.Printf("Copying %s to %s", wineloaderOrig, wineloaderCopy) + if err := utils.CopyFile(wineloaderOrig, wineloaderCopy); err != nil { + dialog.ShowError(fmt.Errorf("failed to copy wineloader: %w", err), myWindow) + paths.PatchesAppliedCrossOver = false + updateAllStatuses() + return + } + + log.Printf("Executing: codesign --remove-signature %s", wineloaderCopy) + cmd := exec.Command("codesign", "--remove-signature", wineloaderCopy) + combinedOutput, err := cmd.CombinedOutput() + if err != nil { + derrMsg := fmt.Sprintf("failed to remove signature from %s: %v\nOutput: %s", wineloaderCopy, err, string(combinedOutput)) + dialog.ShowError(fmt.Errorf(derrMsg), myWindow) + log.Println(derrMsg) + paths.PatchesAppliedCrossOver = false + if err := os.Remove(wineloaderCopy); err != nil { + log.Printf("Warning: failed to cleanup wineloader2 after codesign failure: %v", err) + } + updateAllStatuses() + return + } + log.Printf("codesign output: %s", string(combinedOutput)) + + log.Printf("Setting execute permissions for %s", wineloaderCopy) + if err := os.Chmod(wineloaderCopy, 0755); err != nil { + errMsg := fmt.Sprintf("failed to set executable permissions for %s: %v", wineloaderCopy, err) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + paths.PatchesAppliedCrossOver = false + updateAllStatuses() + return + } + + log.Println("CrossOver patching completed successfully.") + paths.PatchesAppliedCrossOver = true + dialog.ShowInformation("Success", "CrossOver patching process completed.", myWindow) + updateAllStatuses() +} diff --git a/pkg/paths/paths.go b/pkg/paths/paths.go new file mode 100644 index 0000000..13287fb --- /dev/null +++ b/pkg/paths/paths.go @@ -0,0 +1,94 @@ +package paths + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "turtlesilicon/pkg/utils" +) + +const DefaultCrossOverPath = "/Applications/CrossOver.app" + +var ( + CrossoverPath string + TurtlewowPath string + PatchesAppliedTurtleWoW = false + PatchesAppliedCrossOver = false +) + +func SelectCrossOverPath(myWindow fyne.Window, crossoverPathLabel *widget.RichText, updateAllStatuses func()) { + dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { + if err != nil { + dialog.ShowError(err, myWindow) + return + } + if uri == nil { + log.Println("CrossOver path selection cancelled.") + updateAllStatuses() + return + } + selectedPath := uri.Path() + if filepath.Ext(selectedPath) == ".app" && utils.DirExists(selectedPath) { + CrossoverPath = selectedPath + PatchesAppliedCrossOver = false + log.Println("CrossOver path set to:", CrossoverPath) + } else { + dialog.ShowError(fmt.Errorf("invalid selection: '%s'. Please select a valid .app bundle", selectedPath), myWindow) + log.Println("Invalid CrossOver path selected:", selectedPath) + } + updateAllStatuses() + }, myWindow) +} + +func SelectTurtleWoWPath(myWindow fyne.Window, turtlewowPathLabel *widget.RichText, updateAllStatuses func()) { + dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { + if err != nil { + dialog.ShowError(err, myWindow) + return + } + if uri == nil { + log.Println("TurtleWoW path selection cancelled.") + updateAllStatuses() + return + } + selectedPath := uri.Path() + if utils.DirExists(selectedPath) { + TurtlewowPath = selectedPath + PatchesAppliedTurtleWoW = false + log.Println("TurtleWoW path set to:", TurtlewowPath) + } else { + dialog.ShowError(fmt.Errorf("invalid selection: '%s' is not a valid directory", selectedPath), myWindow) + log.Println("Invalid TurtleWoW path selected:", selectedPath) + } + updateAllStatuses() + }, myWindow) +} + +func UpdatePathLabels(crossoverPathLabel, turtlewowPathLabel *widget.RichText) { + if CrossoverPath == "" { + crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + } else { + crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: CrossoverPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + } + crossoverPathLabel.Refresh() + + if TurtlewowPath == "" { + turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + } else { + turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: TurtlewowPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + } + turtlewowPathLabel.Refresh() +} + +func CheckDefaultCrossOverPath() { + if info, err := os.Stat(DefaultCrossOverPath); err == nil && info.IsDir() { + CrossoverPath = DefaultCrossOverPath + log.Println("Pre-set CrossOver to default:", DefaultCrossOverPath) + } +} diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go new file mode 100644 index 0000000..5dd3340 --- /dev/null +++ b/pkg/ui/ui.go @@ -0,0 +1,173 @@ +package ui + +import ( + "log" + "os" // Added import for os.ReadFile + "path/filepath" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "turtlesilicon/pkg/launcher" // Corrected import path + "turtlesilicon/pkg/patching" // Corrected import path + "turtlesilicon/pkg/paths" // Corrected import path + "turtlesilicon/pkg/utils" // Corrected import path +) + +var ( + crossoverPathLabel *widget.RichText + turtlewowPathLabel *widget.RichText + turtlewowStatusLabel *widget.RichText + crossoverStatusLabel *widget.RichText + launchButton *widget.Button + patchTurtleWoWButton *widget.Button + patchCrossOverButton *widget.Button + metalHudCheckbox *widget.Check +) + +func UpdateAllStatuses() { + // Update Crossover Path and Status + if paths.CrossoverPath == "" { + crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + paths.PatchesAppliedCrossOver = false // Reset if path is cleared + } else { + crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: paths.CrossoverPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + wineloader2Path := filepath.Join(paths.CrossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application", "wineloader2") + if utils.PathExists(wineloader2Path) { + paths.PatchesAppliedCrossOver = true + } else { + // paths.PatchesAppliedCrossOver = false // Only set to false if not already true from a patch action this session + } + } + crossoverPathLabel.Refresh() + + if paths.PatchesAppliedCrossOver { + crossoverStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + if patchCrossOverButton != nil { + patchCrossOverButton.Disable() + } + } else { + crossoverStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + if patchCrossOverButton != nil { + if paths.CrossoverPath != "" { + patchCrossOverButton.Enable() + } else { + patchCrossOverButton.Disable() + } + } + } + crossoverStatusLabel.Refresh() + + // Update TurtleWoW Path and Status + if paths.TurtlewowPath == "" { + turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + paths.PatchesAppliedTurtleWoW = false // Reset if path is cleared + } else { + turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: paths.TurtlewowPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + winerosettaDllPath := filepath.Join(paths.TurtlewowPath, "winerosetta.dll") + d3d9DllPath := filepath.Join(paths.TurtlewowPath, "d3d9.dll") + rosettaX87DirPath := filepath.Join(paths.TurtlewowPath, "rosettax87") + dllsTextFile := filepath.Join(paths.TurtlewowPath, "dlls.txt") + rosettaX87ExePath := filepath.Join(rosettaX87DirPath, "rosettax87") + libRuntimeRosettaX87Path := filepath.Join(rosettaX87DirPath, "libRuntimeRosettax87") + + dllsFileValid := false + if utils.PathExists(dllsTextFile) { + if fileContent, err := os.ReadFile(dllsTextFile); err == nil { + if strings.Contains(string(fileContent), "winerosetta.dll") { + dllsFileValid = true + } + } + } + + if utils.PathExists(winerosettaDllPath) && utils.PathExists(d3d9DllPath) && utils.DirExists(rosettaX87DirPath) && + utils.PathExists(rosettaX87ExePath) && utils.PathExists(libRuntimeRosettaX87Path) && dllsFileValid { + paths.PatchesAppliedTurtleWoW = true + } else { + // paths.PatchesAppliedTurtleWoW = false + } + } + turtlewowPathLabel.Refresh() + + if paths.PatchesAppliedTurtleWoW { + turtlewowStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + if patchTurtleWoWButton != nil { + patchTurtleWoWButton.Disable() + } + } else { + turtlewowStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + if patchTurtleWoWButton != nil { + if paths.TurtlewowPath != "" { + patchTurtleWoWButton.Enable() + } else { + patchTurtleWoWButton.Disable() + } + } + } + turtlewowStatusLabel.Refresh() + + // Update Launch Button State + if launchButton != nil { + if paths.PatchesAppliedTurtleWoW && paths.PatchesAppliedCrossOver && paths.TurtlewowPath != "" && paths.CrossoverPath != "" { + launchButton.Enable() + } else { + launchButton.Disable() + } + } +} + +func CreateUI(myWindow fyne.Window) fyne.CanvasObject { + crossoverPathLabel = widget.NewRichText() + turtlewowPathLabel = widget.NewRichText() + turtlewowStatusLabel = widget.NewRichText() + crossoverStatusLabel = widget.NewRichText() + + metalHudCheckbox = widget.NewCheck("Enable Metal Hud (show FPS)", func(checked bool) { + launcher.EnableMetalHud = checked + log.Printf("Metal HUD enabled: %v", launcher.EnableMetalHud) + }) + metalHudCheckbox.SetChecked(launcher.EnableMetalHud) + + patchTurtleWoWButton = widget.NewButton("Patch TurtleWoW", func() { + patching.PatchTurtleWoW(myWindow, UpdateAllStatuses) + }) + patchCrossOverButton = widget.NewButton("Patch CrossOver", func() { + patching.PatchCrossOver(myWindow, UpdateAllStatuses) + }) + launchButton = widget.NewButton("Launch Game", func() { + launcher.LaunchGame(myWindow) + }) + + paths.CheckDefaultCrossOverPath() + + pathSelectionForm := widget.NewForm( + widget.NewFormItem("CrossOver Path:", container.NewBorder(nil, nil, nil, widget.NewButton("Set/Change", func() { + paths.SelectCrossOverPath(myWindow, crossoverPathLabel, UpdateAllStatuses) + }), crossoverPathLabel)), + widget.NewFormItem("TurtleWoW Path:", container.NewBorder(nil, nil, nil, widget.NewButton("Set/Change", func() { + paths.SelectTurtleWoWPath(myWindow, turtlewowPathLabel, UpdateAllStatuses) + }), turtlewowPathLabel)), + ) + + patchOperationsLayout := container.NewVBox( + widget.NewSeparator(), + container.NewGridWithColumns(3, + widget.NewLabel("TurtleWoW Patch:"), turtlewowStatusLabel, patchTurtleWoWButton, + ), + container.NewGridWithColumns(3, + widget.NewLabel("CrossOver Patch:"), crossoverStatusLabel, patchCrossOverButton, + ), + widget.NewSeparator(), + ) + + UpdateAllStatuses() // Initial UI state update + + return container.NewVBox( + pathSelectionForm, + patchOperationsLayout, + metalHudCheckbox, + container.NewPadded(launchButton), + ) +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..93ee0bb --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,119 @@ +package utils + +import ( + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/dialog" +) + +// PathExists checks if a path exists. +func PathExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// DirExists checks if a path exists and is a directory. +func DirExists(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + return info.IsDir() +} + +// CopyFile copies a single file from src to dst. +func CopyFile(src, dst string) error { + sourceFileStat, err := os.Stat(src) + if err != nil { + return err + } + + if !sourceFileStat.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return err + } + defer destination.Close() + _, err = io.Copy(destination, source) + return err +} + +// CopyDir copies a directory recursively from src to dst. +func CopyDir(src string, dst string) error { + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + err = os.MkdirAll(dst, srcInfo.Mode()) + if err != nil { + return err + } + + dir, err := os.ReadDir(src) + if err != nil { + return err + } + + for _, entry := range dir { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + err = CopyDir(srcPath, dstPath) + if err != nil { + return err + } + } else { + err = CopyFile(srcPath, dstPath) + if err != nil { + return err + } + } + } + return nil +} + +// RunOsascript runs an AppleScript command using osascript. +func RunOsascript(scriptString string, myWindow fyne.Window) bool { + log.Printf("Executing AppleScript: %s", scriptString) + cmd := exec.Command("osascript", "-e", scriptString) + output, err := cmd.CombinedOutput() + if err != nil { + errMsg := fmt.Sprintf("AppleScript failed: %v\nOutput: %s", err, string(output)) + dialog.ShowError(fmt.Errorf(errMsg), myWindow) + log.Println(errMsg) + return false + } + log.Printf("osascript output: %s", string(output)) + return true +} + +// EscapeStringForAppleScript escapes a string for AppleScript. +func EscapeStringForAppleScript(s string) string { + s = strings.ReplaceAll(s, "\\", "\\\\") + s = strings.ReplaceAll(s, "\"", "\\\"") + return s +} + +// QuotePathForShell quotes paths for shell commands. +func QuotePathForShell(path string) string { + return fmt.Sprintf(`"%s"`, path) +} +