package dalamud import ( "bufio" "bytes" "encoding/base64" "encoding/json" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "time" ) // LaunchConfig carries everything Launch needs beyond the Paths from Ensure. type LaunchConfig struct { GameExe string // ffxiv_dx11.exe ConfigDir string // holds dalamudConfig.json + installedPlugins/ LogDir string Language int // ClientLanguage: 0=JP 1=EN 2=DE 3=FR GameArgs string // the encrypted launch argument string } const injectorTimeout = 60 * time.Second // Launch runs Dalamud.Injector.exe, which itself spawns ffxiv_dx11.exe with // GameArgs and exits once the game process is up. It returns the game PID the // injector reported (0 if the output did not carry one). The argument shape // mirrors FFXIVQuickLauncher's WindowsDalamudRunner. func Launch(p *Paths, lc LaunchConfig) (int, error) { if err := os.MkdirAll(filepath.Join(lc.ConfigDir, "installedPlugins"), 0o755); err != nil { return 0, err } if err := os.MkdirAll(lc.LogDir, 0o755); err != nil { return 0, err } args := buildInjectorArgs(p, lc) cmd := exec.Command(p.Runner, args...) logf("launch: %s launch --mode=inject", p.Runner) runtimeDir := p.RuntimeDir cmd.Env = append(os.Environ(), "DALAMUD_RUNTIME="+runtimeDir, "DOTNET_ROOT="+runtimeDir, ) stdout, err := cmd.StdoutPipe() if err != nil { return 0, err } cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { return 0, fmt.Errorf("starting injector: %w", err) } pidCh := make(chan int, 1) go readInjectorOutput(stdout, pidCh) done := make(chan error, 1) go func() { done <- cmd.Wait() }() var waitErr error select { case waitErr = <-done: case <-time.After(injectorTimeout): _ = cmd.Process.Kill() return 0, errors.New("injector did not exit within the timeout") } if waitErr != nil { return 0, fmt.Errorf("injector exited with error: %w", waitErr) } select { case pid := <-pidCh: logf("game started via dalamud injector (pid %d)", pid) return pid, nil default: } logf("game started via dalamud injector") return 0, nil } // buildInjectorArgs assembles the command line after the executable. Paths are // passed as plain argv elements (no embedded quoting), which is what // CreateProcess expects from exec.Command-style argument vectors. func buildInjectorArgs(p *Paths, lc LaunchConfig) []string { return []string{ "launch", "--mode=inject", "--game=" + lc.GameExe, "--dalamud-working-directory=" + p.HooksDir, "--dalamud-configuration-path=" + filepath.Join(lc.ConfigDir, "dalamudConfig.json"), "--logpath=" + lc.LogDir, "--dalamud-plugin-directory=" + filepath.Join(lc.ConfigDir, "installedPlugins"), "--dalamud-asset-directory=" + p.AssetDir, fmt.Sprintf("--dalamud-client-language=%d", lc.Language), "--dalamud-delay-initialize=0", // The injector embeds this blob in its logs for support purposes. "--dalamud-tspack-b64=" + base64.StdEncoding.EncodeToString([]byte(`{"launcher":"ffxiv-tiny-launcher"}`)), "--", lc.GameArgs, } } // readInjectorOutput tails the injector's stdout: the first line is JSON with // the game process id, later lines are log output. func readInjectorOutput(stdout io.Reader, pidCh chan<- int) { defer close(pidCh) sc := bufio.NewScanner(stdout) first := true for sc.Scan() { line := bytes.TrimSpace(sc.Bytes()) if len(line) == 0 { continue } if first { first = false var out struct { Pid int `json:"pid"` } if err := json.Unmarshal(line, &out); err == nil && out.Pid > 0 { pidCh <- out.Pid } continue } logf("%s", line) } }