package main import ( "bufio" "bytes" "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" wruntime "github.com/wailsapp/wails/v2/pkg/runtime" "github.com/zhyee/zipstream" ) type MrData struct { FormatVersion int Game string VersionId string Name string Summary string Files []MrFile Dependencies map[string]string } type MrFile struct { Path string Hashes map[string]string Env map[string]string Downloads []string FileSize int } type Instance struct { InstanceName string ModpackId string ModpackVersion string MinecraftVersion string ForgeVersion string NeoForgeVersion string FabricVersion string QuiltVersion string JavaVersion int Libraries []string MainClass string } type InstanceManager struct { instances []Instance app *App } type mmcpack struct { Components []component } type component struct { Uid string Version string } func (i *InstanceManager) SearchInstances() { i.instances = []Instance{} dir, _ := os.UserConfigDir() dir = filepath.Join(dir, "FCLauncher", "instances") if _, err := os.Stat(dir); err != nil { return } subdirs, _ := os.ReadDir(dir) for _, d := range subdirs { if !d.IsDir() { continue } if _, err := os.Stat(filepath.Join(dir, d.Name(), "instance.json")); err != nil { continue } f, _ := os.OpenFile(filepath.Join(dir, d.Name(), "instance.json"), os.O_RDONLY, 0755) defer f.Close() buff := new(bytes.Buffer) io.Copy(buff, f) instance := Instance{} json.Unmarshal(buff.Bytes(), &instance) i.instances = append(i.instances, instance) } } func (i *InstanceManager) checkJavaVersion(instance Instance) { infoPath := filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "mmc-pack.json") f, _ := os.OpenFile(infoPath, os.O_RDONLY, 0755) defer f.Close() dataStr, _ := io.ReadAll(f) var data mmcpack json.Unmarshal(dataStr, &data) mc_version := "0.0" for _, comp := range data.Components { if comp.Uid == "net.minecraft" { mc_version = comp.Version break } } fmt.Printf("MC Version: %s", mc_version) tokensStr := strings.Split(mc_version, ".") tokens := []int{0, 0, 0} tokens[0], _ = strconv.Atoi(tokensStr[0]) tokens[1], _ = strconv.Atoi(tokensStr[1]) if len(tokensStr) > 2 { tokens[2], _ = strconv.Atoi(tokensStr[2]) } javaVer := 8 if tokens[1] == 17 { javaVer = 17 } else if tokens[1] == 18 || tokens[1] == 19 { javaVer = 17 } else if tokens[1] > 19 { if tokens[1] == 20 && tokens[2] < 5 { javaVer = 17 } else { javaVer = 21 } } fmt.Printf("Req Java Version: %d", javaVer) if !i.app.Java.CheckJavaVer(javaVer) { i.app.Java.InstallJavaVer(javaVer) } confPath := filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "instance.cfg") f, _ = os.OpenFile(confPath, os.O_RDONLY, 0755) defer f.Close() buff := new(bytes.Buffer) io.Copy(buff, f) sc := bufio.NewScanner(buff) f, _ = os.OpenFile(confPath, os.O_CREATE|os.O_RDWR, 0755) plat := "lin" exe := "java" if runtime.GOOS == "windows" { plat = "win" exe = "Java.exe" } confDir, _ := os.UserConfigDir() found := false for sc.Scan() { line := sc.Text() if strings.HasPrefix(line, "JavaPath=") { line = fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-%d-%s/bin/%s", strings.ReplaceAll(confDir, "\\", "/"), javaVer, plat, exe) found = true } f.WriteString(line + "\n") } if !found { line := fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-%d-%s/bin/%s", strings.ReplaceAll(confDir, "\\", "/"), javaVer, plat, exe) f.WriteString(line + "\n") f.WriteString("OverrideJavaLocation=true\nOverrideJava=true\n") } f.Close() } func (i *InstanceManager) InstallModpack(modpack Modpack, instanceName string) { i.app.Status(fmt.Sprintf("Installing %s", modpack.Name)) version := modpack.Versions[len(modpack.Versions)-1] dname, _ := os.MkdirTemp("", "fclauncher-*") f, _ := os.OpenFile(filepath.Join(dname, instanceName+".mrpack"), os.O_CREATE|os.O_RDWR, 0755) defer f.Close() HttpDownload(modpack.Id+"/"+version.File, f, i.app.Ctx) i.app.PrismLauncher.ImportModpack(f.Name()) instance := Instance{InstanceName: instanceName, ModpackVersion: version.Version, ModpackId: modpack.Id} i.instances = append(i.instances, instance) f, _ = os.OpenFile(filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755) defer f.Close() data, _ := json.Marshal(instance) f.Write(data) i.checkJavaVersion(instance) } func (i *InstanceManager) InstallVanilla(version string, instanceName string) { dir, _ := os.UserConfigDir() err := DownloadAssets(version, filepath.Join(dir, "FCLauncher", "assets"), *i.app) if err != nil { fmt.Printf("Unable to download assets: %s\n", err) } else { fmt.Printf("Assets Downloaded") } err = DownloadLibraries(version, filepath.Join(dir, "FCLauncher", "lib"), *i.app) if err != nil { fmt.Printf("Unable to download libs: %s\n", err) } else { fmt.Printf("Libs Downloaded") } InstallNatives(version, filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft", "natives")) DownloadLoggingConfig(version, filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft")) err = DownloadExecutable(version, filepath.Join(dir, "FCLauncher", "bin"), *i.app) if err != nil { fmt.Printf("Unable to download binaries: %s\n", err) } else { fmt.Printf("Binaries Downloaded") } metadata, err := GetVersionMetadata(version) if err != nil { fmt.Printf("unable to pull metadata: %s\n", err) } err = os.MkdirAll(filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft"), 0755) if err != nil { fmt.Printf("unable to create directory: %s\n", err) } instance := Instance{InstanceName: instanceName, MinecraftVersion: version, JavaVersion: metadata.JavaVersion.MajorVersion, MainClass: metadata.MainClass} for _, lib := range metadata.Libraries { instance.Libraries = append(instance.Libraries, lib.Downloads.Artifact.Path) } data, err := json.Marshal(instance) if err != nil { fmt.Printf("unable to marshal json data: %s\n", err) } f, err := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755) if err != nil { fmt.Printf("unable to open file: %s\n", err) } defer f.Close() _, err = f.Write(data) if err != nil { fmt.Printf("unable to write data: %s\n", err) } i.instances = append(i.instances, instance) if !i.app.Java.CheckJavaVer(instance.JavaVersion) { i.app.Status(fmt.Sprintf("Installing Java Version %d", instance.JavaVersion)) i.app.Java.InstallJavaVer(instance.JavaVersion) } return } func (i *InstanceManager) GetInstances() []string { names := []string{} for _, inst := range i.instances { names = append(names, inst.InstanceName) } return names } func (i *InstanceManager) CheckUpdate(instance Instance) { return i.app.Status("Checking for Updates") i.app.Modpacks.QuerryModpacks() pack := i.app.Modpacks.GetModpack(instance.ModpackId) if pack.Versions[len(pack.Versions)-1].Version == instance.ModpackVersion { return } i.app.Status(fmt.Sprintf("Updating %s", instance.InstanceName)) version := pack.Versions[len(pack.Versions)-1] dname, _ := os.MkdirTemp("", "fclauncher-*") f, _ := os.OpenFile(filepath.Join(dname, instance.InstanceName+".mrpack"), os.O_CREATE|os.O_RDWR, 0755) defer f.Close() HttpDownload(pack.Id+"/"+version.File, f, i.app.Ctx) i.app.PrismLauncher.ImportModpack(f.Name()) instance.ModpackVersion = version.Version f, _ = os.OpenFile(filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755) defer f.Close() data, _ := json.Marshal(instance) f.Write(data) i.checkJavaVersion(instance) i.SearchInstances() } func (i *InstanceManager) GetInstance(instance string) (Instance, error) { instanceObject := Instance{} found := false for _, inst := range i.instances { if inst.InstanceName == instance { instanceObject = inst found = true break } } if !found { return Instance{}, fmt.Errorf("unable to find instance %s\n", instance) } return instanceObject, nil } func (i *InstanceManager) LaunchInstance(instance string) { i.app.Status(fmt.Sprintf("Launching %s", instance)) dir, err := os.UserConfigDir() if err != nil { fmt.Printf("unable to get config directory\n") } instanceObject, err := i.GetInstance(instance) if err != nil { fmt.Printf("Unable to find instance\n") } execName := "java" suffix := "lin" if runtime.GOOS == "windows" { execName = "Java.exe" suffix = "win" } dir = filepath.Join(dir, "FCLauncher") auth, err := MicrosoftAuth(i.app.Auth) if err != nil { fmt.Printf("unable to authenticate: %s\n", err) return } args, err := GetOnlineLaunchArgs(instanceObject.MinecraftVersion, instanceObject, filepath.Join(dir, "lib"), filepath.Join(dir, "bin"), filepath.Join(dir, "assets"), filepath.Join(dir, "instances", instance, "minecraft"), auth) if err != nil { fmt.Printf("unable to get launch args: %s\n", err) } if instanceObject.ForgeVersion != "" { args = append(args, "--launchTarget") args = append(args, "forge_client") } fmt.Printf("Args: %+v", args) child := exec.Command(filepath.Join(dir, "java", fmt.Sprintf("java-%d-%s", instanceObject.JavaVersion, suffix), "bin", execName), args...) child.Dir = filepath.Join(dir, "instances", instance, "minecraft") wruntime.WindowHide(i.app.Ctx) data, err := child.CombinedOutput() wruntime.WindowShow(i.app.Ctx) fmt.Printf("Command Output: %s\n", data) } func (i *InstanceManager) InstallFabric(instance string, fabricVersion string) { i.app.Status("Installing Fabric") instanceObject, err := i.GetInstance(instance) if err != nil { fmt.Printf("Instance does not exist\n") } metadata, err := GetFabricMetadata(instanceObject.MinecraftVersion, fabricVersion) if err != nil { fmt.Printf("unable to get version metadata\n") } client: for _, lib := range metadata.LauncherMeta.Libraries.Client { tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator)) pkg := tokens[len(tokens)-2] instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))) for ind, path := range instanceObject.Libraries { tokens := strings.Split(path, string(os.PathSeparator)) if pkg == tokens[len(tokens)-3] { instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)) fmt.Printf("duplicate library %s\n", pkg) continue client } } } common: for _, lib := range metadata.LauncherMeta.Libraries.Common { tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator)) pkg := tokens[len(tokens)-2] instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))) for ind, path := range instanceObject.Libraries { tokens := strings.Split(path, string(os.PathSeparator)) if pkg == tokens[len(tokens)-3] { instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)) fmt.Printf("duplicate library %s\n", pkg) continue common } } } instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Loader.Maven), ProcessMavenFilename(metadata.Loader.Maven))) instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Intermediary.Maven), ProcessMavenFilename(metadata.Intermediary.Maven))) instanceObject.MainClass = metadata.LauncherMeta.MainClass["client"] instanceObject.FabricVersion = fabricVersion dir, _ := os.UserConfigDir() InstallFabricLibs(instanceObject.MinecraftVersion, fabricVersion, filepath.Join(dir, "FCLauncher", "lib"), i.app) f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755) data, _ := json.Marshal(instanceObject) defer f.Close() f.Write(data) for ind, inst := range i.instances { if inst.InstanceName == instance { i.instances[ind] = instanceObject break } } } func (i *InstanceManager) InstallQuilt(instance string, quiltVersion string) { i.app.Status("Installing Quilt") instanceObject, err := i.GetInstance(instance) if err != nil { fmt.Printf("Instance does not exist\n") } metadata, err := GetQuiltMetadata(instanceObject.MinecraftVersion, quiltVersion) if err != nil { fmt.Printf("unable to get version metadata\n") } client: for _, lib := range metadata.LauncherMeta.Libraries.Client { tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator)) pkg := tokens[len(tokens)-2] instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))) for ind, path := range instanceObject.Libraries { tokens := strings.Split(path, string(os.PathSeparator)) if pkg == tokens[len(tokens)-3] { instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)) fmt.Printf("duplicate library %s\n", pkg) continue client } } } common: for _, lib := range metadata.LauncherMeta.Libraries.Common { tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator)) pkg := tokens[len(tokens)-2] instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))) for ind, path := range instanceObject.Libraries { tokens := strings.Split(path, string(os.PathSeparator)) if pkg == tokens[len(tokens)-3] { instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)) fmt.Printf("duplicate library %s\n", pkg) continue common } } } instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Loader.Maven), ProcessMavenFilename(metadata.Loader.Maven))) instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Intermediary.Maven), ProcessMavenFilename(metadata.Intermediary.Maven))) instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Hashed.Maven), ProcessMavenFilename(metadata.Hashed.Maven))) instanceObject.MainClass = metadata.LauncherMeta.MainClass["client"] instanceObject.QuiltVersion = quiltVersion dir, _ := os.UserConfigDir() InstallQuiltLibs(instanceObject.MinecraftVersion, quiltVersion, filepath.Join(dir, "FCLauncher", "lib"), i.app) f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755) data, _ := json.Marshal(instanceObject) defer f.Close() f.Write(data) for ind, inst := range i.instances { if inst.InstanceName == instance { i.instances[ind] = instanceObject break } } } func (i *InstanceManager) InstallForge(instance string, forgeVersion string) { instanceObject, err := i.GetInstance(instance) if err != nil { fmt.Printf("Unable to find instance: %s\n", err) } installData, err := GetForgeInstallData(instanceObject.MinecraftVersion, forgeVersion) if err != nil { fmt.Printf("Unable to get install data: %s\n", err) } dir, _ := os.UserConfigDir() InstallForgeLibs(instanceObject.MinecraftVersion, forgeVersion, filepath.Join(dir, "FCLauncher", "lib")) instanceObject.ForgeVersion = forgeVersion outer: for _, lib := range installData.Libraries { tokens := strings.Split(lib.Downloads.Artifact.Path, string(os.PathSeparator)) pkg := tokens[len(tokens)-2] instanceObject.Libraries = append(instanceObject.Libraries, lib.Downloads.Artifact.Path) for ind, path := range instanceObject.Libraries { tokens := strings.Split(path, string(os.PathSeparator)) if pkg == tokens[len(tokens)-3] { instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)) fmt.Printf("duplicate library %s\n", pkg) continue outer } } } instanceObject.MainClass = installData.MainClass f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755) data, _ := json.Marshal(instanceObject) defer f.Close() f.Write(data) for ind, inst := range i.instances { if inst.InstanceName == instance { i.instances[ind] = instanceObject break } } } func (i *InstanceManager) ImportModpack(modpack Modpack, name string) { i.app.Status(fmt.Sprintf("Downloading %s", modpack.Name)) buff := new(bytes.Buffer) HttpDownload(filepath.Join(modpack.Id, modpack.Versions[len(modpack.Versions)-1].File), buff, i.app.Ctx) i.ImportMrpack(buff, name) } func (i *InstanceManager) ImportMrpack(data io.Reader, name string) { dir, _ := os.UserConfigDir() InstancePath := filepath.Join(dir, "FCLauncher", "instances", name, "minecraft") zr := zipstream.NewReader(data) mrdata := MrData{} i.app.Status("Unpacking modpack File") for { entry, err := zr.GetNextEntry() if err == io.EOF { break } if err != nil { fmt.Printf("Error unpacking modpack file: %s\n", err) } if entry.Name == "modrinth.index.json" { i.app.Status("Loading metadata") file, _ := entry.Open() data, _ := io.ReadAll(file) json.Unmarshal(data, &mrdata) } else { i.app.Status(fmt.Sprintf("Unpacking %s", entry.Name)) prefix := strings.Split(entry.Name, "/")[0] if prefix == "overrides" || prefix == "client-overrides" { path := strings.SplitN(entry.Name, "/", 2)[1] if entry.IsDir() { fmt.Printf("creating directory %s\n", filepath.Join(InstancePath, path)) if _, err := os.Stat(filepath.Join(InstancePath, path)); err != nil { os.MkdirAll(filepath.Join(InstancePath, path), 0755) } } else { zf, _ := entry.Open() defer zf.Close() fileDir := "" tokens := strings.Split(path, "/") for ind, token := range tokens { if ind != len(tokens)-1 { fileDir = filepath.Join(fileDir, token) } } fmt.Printf("creating directory %s\n", filepath.Join(InstancePath, fileDir)) if _, err := os.Stat(filepath.Join(InstancePath, fileDir)); err != nil { os.MkdirAll(filepath.Join(InstancePath, fileDir), 0755) } file, _ := os.OpenFile(filepath.Join(InstancePath, path), os.O_CREATE|os.O_RDWR, 0755) defer file.Close() io.Copy(file, zf) } } } } i.InstallVanilla(mrdata.Dependencies["minecraft"], name) if mrdata.Dependencies["forge"] != "" { fmt.Printf("Forge not implemented!") //implement forge } else if mrdata.Dependencies["neoforge"] != "" { fmt.Printf("Neoforge not implemented!") //implement neoforge } else if mrdata.Dependencies["fabric-loader"] != "" { i.InstallFabric(name, mrdata.Dependencies["fabric-loader"]) } else if mrdata.Dependencies["quilt-loader"] != "" { i.InstallQuilt(name, mrdata.Dependencies["quilt-loader"]) } i.app.Status("Downloading Mods") for _, f := range mrdata.Files { fmt.Printf("Downloading %s\n", f.Path) i.app.Status(fmt.Sprintf("Downloading %s", f.Path)) resp, err := http.Get(f.Downloads[0]) if err != nil { fmt.Printf("Unable to download file %s\n", err) continue } defer resp.Body.Close() buff := new(bytes.Buffer) downloaded := 0 for { count, err := io.CopyN(buff, resp.Body, BlockSize) if err == io.EOF { downloaded += int(count) break } if err != nil { fmt.Printf("Error Downloading libs: %e\n", err) return } downloaded += int(count) wruntime.EventsEmit(i.app.Ctx, "download", downloaded, f.FileSize) } fileDir := "" tokens := strings.Split(f.Path, "/") for ind, token := range tokens { if ind != len(tokens)-1 { fileDir = filepath.Join(fileDir, token) } } if _, err := os.Stat(filepath.Join(InstancePath, fileDir)); err != nil { os.MkdirAll(filepath.Join(InstancePath, fileDir), 0755) } file, _ := os.OpenFile(filepath.Join(InstancePath, f.Path), os.O_CREATE|os.O_RDWR, 0755) defer file.Close() io.Copy(file, buff) wruntime.EventsEmit(i.app.Ctx, "download_complete") } }