package main import ( "bytes" "crypto/sha1" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "runtime" "strings" "time" wruntime "github.com/wailsapp/wails/v2/pkg/runtime" "github.com/zhyee/zipstream" ) type McVersionManifestEntry struct { Id string Type string Url string Time time.Time ReleaseTime time.Time Sha1 string ComplianceLevel int } type McLatestEntry struct { Release string Snapshot string } type McVersionManifest struct { Latest McLatestEntry Versions []McVersionManifestEntry } type McArguments struct { Game []string Jvm []string } type McAssetIndex struct { Id string Sha1 string Size int TotalSize int Url string } type McJavaVersion struct { Component string MajorVersion int } type McLibraryArtifact struct { Path string Sha1 string Size int Url string } type McLibraryDownload struct { Artifact McLibraryArtifact Classifiers map[string]interface{} } type McRuleOs struct { Name string Version string ARch string } type McRule struct { Action string Features map[string]bool Os McRuleOs } type McLibrary struct { Downloads McLibraryDownload Name string Rules []McRule Natives map[string]string } type McDownload struct { Sha1 string Size int Url string } type McLogging struct { Client McLoggingClient } type McLoggingClient struct { Argument string File McDownload } type McMetadata struct { Arguments McArguments AssetIndex McAssetIndex Assets string ComplianceLevel int Downloads map[string]McDownload Id string JavaVersion McJavaVersion Libraries []McLibrary MainClass string MinimumLauncherVersion int ReleaseTime time.Time Time time.Time Type string MinecraftArguments string Logging McLogging } func GetVersionManifest() (McVersionManifest, error) { resp, err := http.Get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json") var returnError error = nil if err == nil { defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err == nil { versionManifest := McVersionManifest{} err = json.Unmarshal(data, &versionManifest) if err == nil { dir, err := os.UserConfigDir() if err != nil { return versionManifest, nil } err = os.MkdirAll(filepath.Join(dir, "FCLauncher", "cache"), 0755) f, err := os.OpenFile(filepath.Join(dir, "FCLauncher", "cache", "versionManifest.json"), os.O_CREATE|os.O_RDWR, 0755) defer f.Close() f.Write(data) return versionManifest, nil } else { returnError = fmt.Errorf("Unable to parse Json: %e\n", err) } } else { returnError = fmt.Errorf("Unable to read version manifest: %e\n", err) } } else { returnError = fmt.Errorf("Unable to download version manifest: %e\n", err) } dir, err := os.UserConfigDir() if err != nil { return McVersionManifest{}, returnError } path := filepath.Join(dir, "FCLauncher", "cache", "versionManifest.json") if _, err = os.Stat(path); err != nil { return McVersionManifest{}, returnError } f, err := os.OpenFile(path, os.O_RDONLY, 0755) if err != nil { return McVersionManifest{}, returnError } data, _ := io.ReadAll(f) if err != nil { return McVersionManifest{}, returnError } versionManifest := McVersionManifest{} err = json.Unmarshal(data, &versionManifest) if err != nil { return McVersionManifest{}, returnError } return versionManifest, nil } func GetVersionMetadata(wantedVersion string) (McMetadata, error) { manifest, err := GetVersionManifest() if err != nil { return McMetadata{}, fmt.Errorf("GetVersionMetadata: %e\n", err) } for _, version := range manifest.Versions { if wantedVersion == version.Id { //found it dir, err := os.UserConfigDir() if err == nil { path := filepath.Join(dir, "FCLauncher", "cache", "versionMetadata", wantedVersion+".json") if _, err := os.Stat(path); err == nil { if f, err := os.OpenFile(path, os.O_RDONLY, 0755); err == nil { defer f.Close() if data, err := io.ReadAll(f); err == nil { sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) == version.Sha1{ metadata := McMetadata{} json.Unmarshal(data, &metadata) return metadata, nil } } } } } resp, err := http.Get(version.Url) if err != nil { return McMetadata{}, fmt.Errorf("Unable to download metadata: %e\n", err) } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return McMetadata{}, fmt.Errorf("Unable to download metadata: %e\n", err) } sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) != version.Sha1 { return McMetadata{}, fmt.Errorf("GetMetadata: Sha1 hash does not match\n") } metadata := McMetadata{} err = json.Unmarshal(data, &metadata) dir, err = os.UserConfigDir() if err == nil { path := filepath.Join(dir, "FCLauncher", "cache", "versionMetadata") if os.MkdirAll(path, 0755) == nil { if f, err := os.OpenFile(filepath.Join(path, wantedVersion+".json"), os.O_CREATE|os.O_RDWR, 0755); err == nil { defer f.Close() f.Write(data) } } } return metadata, nil } } return McMetadata{}, fmt.Errorf("Unable to find version %s\n", wantedVersion) } func GetAssetIndex(mcVersion string) ([]string, error) { found := false var data []byte metadata, err := GetVersionMetadata(mcVersion) if err != nil { return []string{}, fmt.Errorf("Unable to pull manifest: %e\n", err) } dir, _ := os.UserConfigDir() path := filepath.Join(dir, "FCLauncher", "assets", "indexes") if _, err := os.Stat(filepath.Join(path, metadata.Assets+".json")); err == nil { //cache file exists if f, err := os.OpenFile(filepath.Join(path, metadata.Assets+".json"), os.O_RDONLY, 0755); err == nil { defer f.Close() if data, err = io.ReadAll(f); err == nil { sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) == metadata.AssetIndex.Sha1 { found = true } } } } if !found { //no cache file data = []byte{} resp, err := http.Get(metadata.AssetIndex.Url) if err != nil { return []string{}, fmt.Errorf("Unable to pull asset index: %e\n", err) } defer resp.Body.Close() data, err = io.ReadAll(resp.Body) if err != nil { return []string{}, fmt.Errorf("Unable to pull asset index: %e\n", err) } sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) != metadata.AssetIndex.Sha1 { return []string{}, fmt.Errorf("Sha mismatch!\n") } os.MkdirAll(path, 0755) f, _ := os.OpenFile(filepath.Join(path, metadata.Assets+".json"), os.O_CREATE|os.O_RDWR, 0755) defer f.Close() f.Write(data) } //at this point data is populated var index map[string]interface{} json.Unmarshal(data, &index) index = index["objects"].(map[string]interface{}) hashes := []string{} for _, val := range index { index_map := val.(map[string]interface{}) hashes = append(hashes, index_map["hash"].(string)) } return hashes, nil } func DownloadAssets(mcVersion string, assetPath string, a App) error { a.Status("Downloading Minecraft Assets") assets, err := GetAssetIndex(mcVersion) if err != nil { return fmt.Errorf("Unable to get asset index: %e\n", err) } metadata, err := GetVersionMetadata(mcVersion) if err != nil { return fmt.Errorf("Unable to get version metadata: %e\n", err) } total := metadata.AssetIndex.TotalSize downloaded := 0 wruntime.EventsEmit(a.Ctx, "download", downloaded, total) for _, hash := range assets { if _, err := os.Stat(filepath.Join(assetPath, "objects", hash[:2], hash)); err == nil { f, _ := os.OpenFile(filepath.Join(assetPath, "objects", hash[:2], hash), os.O_RDONLY, 0755) defer f.Close() data, _ := io.ReadAll(f) sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) == hash { downloaded += len(data) wruntime.EventsEmit(a.Ctx, "download", downloaded, total) continue } } resp, err := http.Get(fmt.Sprintf("https://resources.download.minecraft.net/%s/%s", hash[:2], hash)) if err != nil { return fmt.Errorf("unable to download assets: %e\n", err) } defer resp.Body.Close() buff := new(bytes.Buffer) for { count, err := io.CopyN(buff, resp.Body, BlockSize) if err == io.EOF { downloaded += int(count) break } if err != nil { return fmt.Errorf("Error Downloading assets: %e\n", err) } downloaded += int(count) wruntime.EventsEmit(a.Ctx, "download", downloaded, total) } data := buff.Bytes() sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) != hash { return fmt.Errorf("unable to download assets: Sha1 Mismatch\n") } err = os.MkdirAll(filepath.Join(assetPath, "objects", hash[:2]), 0755) if err != nil { return fmt.Errorf("unable to download assets: Unable to create directory\n") } f, err := os.OpenFile(filepath.Join(assetPath, "objects", hash[:2], hash), os.O_CREATE|os.O_RDWR, 0755) if err != nil { return fmt.Errorf("unable to download assets: Unable to open file\n") } defer f.Close() f.Write(data) wruntime.EventsEmit(a.Ctx, "download", downloaded, total) } wruntime.EventsEmit(a.Ctx, "download_complete") return nil } func DownloadLibraries(mcVersion string, libPath string, a App) error { metadata, err := GetVersionMetadata(mcVersion) if err != nil { return fmt.Errorf("unable to pull version metadata: %e\n", err) } for _, lib := range metadata.Libraries { a.Status(fmt.Sprintf("Checking %s\n", lib.Name)) if _, err := os.Stat(filepath.Join(libPath, lib.Downloads.Artifact.Path)); err == nil { f, _ := os.OpenFile(filepath.Join(libPath, lib.Downloads.Artifact.Path), os.O_CREATE|os.O_RDWR, 0755) defer f.Close() data, _ := io.ReadAll(f) sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) == lib.Downloads.Artifact.Sha1 { continue } } a.Status(fmt.Sprintf("Downloading %s\n", lib.Name)) resp, err := http.Get(lib.Downloads.Artifact.Url) if err != nil { return fmt.Errorf("unable to download libs: %e\n", err) } 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 { return fmt.Errorf("Error Downloading libs: %e\n", err) } downloaded += int(count) wruntime.EventsEmit(a.Ctx, "download", downloaded, resp.ContentLength) } data := buff.Bytes() sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) != lib.Downloads.Artifact.Sha1 { return fmt.Errorf("unable to download libs: Sha1 Mismatch\n") } path := "" tokens := strings.Split(lib.Downloads.Artifact.Path, "/") for ind, token := range tokens { if ind != len(tokens)-1 { path = filepath.Join(path, token) } } err = os.MkdirAll(filepath.Join(libPath, path), 0755) if err != nil { return fmt.Errorf("unable to download libs: Unable to create directory\n") } f, err := os.OpenFile(filepath.Join(libPath, lib.Downloads.Artifact.Path), os.O_CREATE|os.O_RDWR, 0755) if err != nil { return fmt.Errorf("unable to download libs: Unable to open file\n") } defer f.Close() f.Write(data) wruntime.EventsEmit(a.Ctx, "download_complete") } return nil } func InstallNatives(mcVersion string, nativesDir string){ metadata, _ := GetVersionMetadata(mcVersion) for _, lib := range metadata.Libraries { if lib.Natives != nil { glob := lib.Natives[runtime.GOOS] fmt.Printf("glob is: %s\n", glob) if lib.Downloads.Classifiers[glob] != nil { artifact := lib.Downloads.Classifiers[glob].(map[string]interface{}) resp, _ := http.Get(artifact["url"].(string)) defer resp.Body.Close() zr := zipstream.NewReader(resp.Body) for { e, err := zr.GetNextEntry() if err == io.EOF { break } if e.IsDir() { os.MkdirAll(filepath.Join(nativesDir, e.Name), 0755) } else { zc, _ := e.Open() f, _ := os.OpenFile(filepath.Join(nativesDir, e.Name), os.O_CREATE|os.O_RDWR, 0755) defer zc.Close() defer f.Close() io.Copy(f, zc) } } } } } } func DownloadLoggingConfig(mcVersion string, gameDir string){ metadata, _ := GetVersionMetadata(mcVersion) resp, err := http.Get(metadata.Logging.Client.File.Url) if err != nil { fmt.Printf("Error downloading logging config: %s\n", err) } defer resp.Body.Close() os.MkdirAll(gameDir, 0755) f, _ := os.OpenFile(filepath.Join(gameDir, "log4j2.xml"), os.O_CREATE|os.O_RDWR, 0755) defer f.Close() io.Copy(f, resp.Body) } func DownloadExecutable(mcVersion string, binDir string, a App) error { metadata, err := GetVersionMetadata(mcVersion) if err != nil { return fmt.Errorf("unable to pull version metadata: %e\n", err) } a.Status(fmt.Sprintf("Checking Minecraft %s Executable\n", mcVersion)) if _, err := os.Stat(filepath.Join(binDir, mcVersion, "client.jar")); err == nil { f, _ := os.OpenFile(filepath.Join(binDir, mcVersion, "client.jar"), os.O_CREATE|os.O_RDWR, 0755) defer f.Close() data, _ := io.ReadAll(f) sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) == metadata.Downloads["client"].Sha1 { return nil } } a.Status(fmt.Sprintf("Downloading Minecraft %s Executable\n", mcVersion)) resp, err := http.Get(metadata.Downloads["client"].Url) if err != nil { return fmt.Errorf("unable to download executable: %e\n", err) } 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 { return fmt.Errorf("Error Downloading executable: %e\n", err) } downloaded += int(count) wruntime.EventsEmit(a.Ctx, "download", downloaded, resp.ContentLength) } data := buff.Bytes() sha := sha1.Sum(data) if hex.EncodeToString(sha[:20]) != metadata.Downloads["client"].Sha1 { return fmt.Errorf("unable to download executable: Sha1 Mismatch\n") } err = os.MkdirAll(filepath.Join(binDir, mcVersion), 0755) if err != nil { return fmt.Errorf("unable to download executable: Unable to create directory\n") } f, err := os.OpenFile(filepath.Join(binDir, mcVersion, "client.jar"), os.O_CREATE|os.O_RDWR, 0755) if err != nil { return fmt.Errorf("unable to download executable: Unable to open file\n") } defer f.Close() f.Write(data) wruntime.EventsEmit(a.Ctx, "download_complete") return nil } func GetBaseLaunchArgs(mcVersion string, libDir string, binDir string, assetDir string, gameDir string) ([]string, error) { args := []string{} metadata, err := GetVersionMetadata(mcVersion) if err != nil { return args, fmt.Errorf("GetLaunchArgs: %e\n", err) } searchArgs := []string{} if metadata.MinecraftArguments != "" { searchArgs = strings.Split(metadata.MinecraftArguments, " ") } else { searchArgs = metadata.Arguments.Game } args = append(args, "-Djava.library.path="+filepath.Join(gameDir, "natives")) args = append(args, "-Djna.tmpdir="+filepath.Join(gameDir, "natives")) args = append(args, "-Dorg.lwjgl.system.SharedLibraryExtractpath="+filepath.Join(gameDir, "natives")) args = append(args, "-Dio.netty.native.workdir="+filepath.Join(gameDir, "natives")) args = append(args, strings.ReplaceAll(metadata.Logging.Client.Argument, "${path}", filepath.Join(gameDir, "log4j2.xml"))) args = append(args, "-Xms512m") args = append(args, "-Xmx1024m") args = append(args, "-cp") arg := "" for _, lib := range metadata.Libraries { arg += filepath.Join(libDir, lib.Downloads.Artifact.Path) + ":" } arg += filepath.Join(binDir, mcVersion, "client.jar") args = append(args, arg) args = append(args, metadata.MainClass) for _, val := range searchArgs { switch val { case "${version_name}": args = append(args, mcVersion) case "${game_directory}": args = append(args, gameDir) case "${assets_root}": args = append(args, assetDir) case "${clientid}": args = append(args, client_id) case "${version_type}": args = append(args, metadata.Type) case "${user_type}": args = append(args, "mojang") case "${assets_index_name}": args = append(args, metadata.Assets) case "${user_properties}": args = append(args, "null") default: args = append(args, val) } } return args, nil } func GetOfflineLaunchArgs(mcVersion string, libDir string, binDir string, assetDir string, gameDir string, playerName string) ([]string, error) { args, err := GetBaseLaunchArgs(mcVersion, libDir, binDir, assetDir, gameDir) if err != nil { return []string{}, fmt.Errorf("GatOfflineLaunchArgs: %e\n", err) } for ind, val := range args { switch val{ case "${auth_player_name}": args[ind] = playerName case "${auth_uuid}": args[ind] = "null" case "${auth_access_token}": args[ind] = "null" case "${auth_xuid}": args[ind] = "null" default: } } return args, nil } func GetOnlineLaunchArgs(mcVersion string, libDir string, binDir string, assetDir string, gameDir string, auth LauncherAuth) ([]string, error) { args, err := GetBaseLaunchArgs(mcVersion, libDir, binDir, assetDir, gameDir) if err != nil { return []string{}, fmt.Errorf("GatOfflineLaunchArgs: %e\n", err) } for ind, val := range args { switch val{ case "${auth_player_name}": args[ind] = auth.Name case "${auth_uuid}": args[ind] = auth.Id case "${auth_access_token}": args[ind] = auth.Token case "${auth_xuid}": args[ind] = auth.Id default: } } return args, nil }