FCLauncher/fclauncher/minecraft.go

632 lines
18 KiB
Go
Raw Normal View History

2024-10-29 21:48:59 -06:00
package main
import (
"bytes"
2024-10-29 21:48:59 -06:00
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
2024-10-29 21:48:59 -06:00
"time"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/zhyee/zipstream"
2024-10-29 21:48:59 -06:00
)
type McVersionManifestEntry struct {
2024-11-25 19:21:20 -07:00
Id string
Type string
Url string
Time time.Time
ReleaseTime time.Time
Sha1 string
2024-10-29 21:48:59 -06:00
ComplianceLevel int
}
type McLatestEntry struct {
2024-11-25 19:21:20 -07:00
Release string
2024-10-29 21:48:59 -06:00
Snapshot string
}
type McVersionManifest struct {
2024-11-25 19:21:20 -07:00
Latest McLatestEntry
2024-10-29 21:48:59 -06:00
Versions []McVersionManifestEntry
}
type McArguments struct {
Game []string
2024-11-25 19:21:20 -07:00
Jvm []string
2024-10-29 21:48:59 -06:00
}
type McAssetIndex struct {
2024-11-25 19:21:20 -07:00
Id string
Sha1 string
Size int
2024-10-29 21:48:59 -06:00
TotalSize int
2024-11-25 19:21:20 -07:00
Url string
2024-10-29 21:48:59 -06:00
}
type McJavaVersion struct {
2024-11-25 19:21:20 -07:00
Component string
2024-10-29 21:48:59 -06:00
MajorVersion int
}
type McLibraryArtifact struct {
Path string
Sha1 string
Size int
2024-11-25 19:21:20 -07:00
Url string
2024-10-29 21:48:59 -06:00
}
type McLibraryDownload struct {
2024-11-25 19:21:20 -07:00
Artifact McLibraryArtifact
Classifiers map[string]interface{}
2024-10-29 21:48:59 -06:00
}
type McRuleOs struct {
2024-11-25 19:21:20 -07:00
Name string
2024-10-29 21:48:59 -06:00
Version string
2024-11-25 19:21:20 -07:00
ARch string
2024-10-29 21:48:59 -06:00
}
type McRule struct {
2024-11-25 19:21:20 -07:00
Action string
2024-10-29 21:48:59 -06:00
Features map[string]bool
2024-11-25 19:21:20 -07:00
Os McRuleOs
2024-10-29 21:48:59 -06:00
}
type McLibrary struct {
Downloads McLibraryDownload
2024-11-25 19:21:20 -07:00
Name string
Rules []McRule
Natives map[string]string
2024-10-29 21:48:59 -06:00
}
type McDownload struct {
Sha1 string
Size int
2024-11-25 19:21:20 -07:00
Url string
2024-10-29 21:48:59 -06:00
}
2024-10-30 15:58:05 -06:00
type McLogging struct {
Client McLoggingClient
}
type McLoggingClient struct {
Argument string
2024-11-25 19:21:20 -07:00
File McDownload
2024-10-30 15:58:05 -06:00
}
2024-10-29 21:48:59 -06:00
type McMetadata struct {
2024-11-25 19:21:20 -07:00
Arguments McArguments
AssetIndex McAssetIndex
Assets string
ComplianceLevel int
Downloads map[string]McDownload
Id string
JavaVersion McJavaVersion
Libraries []McLibrary
MainClass string
2024-10-29 21:48:59 -06:00
MinimumLauncherVersion int
2024-11-25 19:21:20 -07:00
ReleaseTime time.Time
Time time.Time
Type string
MinecraftArguments string
Logging McLogging
2024-10-29 21:48:59 -06:00
}
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 {
2024-11-25 19:21:20 -07:00
versionManifest := McVersionManifest{}
2024-10-29 21:48:59 -06:00
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)
2024-11-25 19:21:20 -07:00
if hex.EncodeToString(sha[:20]) == version.Sha1 {
2024-10-29 21:48:59 -06:00
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
2024-11-25 19:21:20 -07:00
2024-10-29 21:48:59 -06:00
}
}
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)
2024-11-25 19:21:20 -07:00
}
}
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
}
2024-11-25 19:21:20 -07:00
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)
}
}
}
}
}
}
2024-11-25 19:21:20 -07:00
func DownloadLoggingConfig(mcVersion string, gameDir string) {
2024-10-30 15:58:05 -06:00
metadata, _ := GetVersionMetadata(mcVersion)
resp, err := http.Get(metadata.Logging.Client.File.Url)
if err != nil {
fmt.Printf("Error downloading logging config: %s\n", err)
2024-10-30 19:11:24 -06:00
return
2024-10-30 15:58:05 -06:00
}
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
}
2024-10-31 16:23:38 -06:00
func GetBaseLaunchArgs(mcVersion string, instance Instance, 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"))
2024-10-30 19:11:24 -06:00
loggingArg := strings.ReplaceAll(metadata.Logging.Client.Argument, "${path}", filepath.Join(gameDir, "log4j2.xml"))
if loggingArg != "" {
args = append(args, loggingArg)
}
args = append(args, "-Xms512m")
2024-11-25 19:21:20 -07:00
args = append(args, "-Xmx4096m")
args = append(args, "-cp")
arg := ""
2024-10-30 19:11:24 -06:00
separater := ":"
if runtime.GOOS == "windows" {
separater = ";"
}
2024-10-31 16:23:38 -06:00
for _, lib := range instance.Libraries {
arg += filepath.Join(libDir, lib) + separater
2024-10-31 18:57:54 -06:00
if _, err := os.Stat(filepath.Join(libDir, lib)); err != nil {
fmt.Printf("Error: missing library: %s\n", lib)
}
}
arg += filepath.Join(binDir, mcVersion, "client.jar")
args = append(args, arg)
2024-10-31 16:23:38 -06:00
args = append(args, instance.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}":
2024-10-30 19:11:24 -06:00
args = append(args, "{}")
default:
args = append(args, val)
}
}
return args, nil
}
2024-10-31 16:23:38 -06:00
func GetOfflineLaunchArgs(mcVersion string, instance Instance, libDir string, binDir string, assetDir string, gameDir string, playerName string) ([]string, error) {
args, err := GetBaseLaunchArgs(mcVersion, instance, libDir, binDir, assetDir, gameDir)
if err != nil {
return []string{}, fmt.Errorf("GatOfflineLaunchArgs: %e\n", err)
}
for ind, val := range args {
2024-11-25 19:21:20 -07:00
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
}
2024-10-31 16:23:38 -06:00
func GetOnlineLaunchArgs(mcVersion string, instance Instance, libDir string, binDir string, assetDir string, gameDir string, auth LauncherAuth) ([]string, error) {
args, err := GetBaseLaunchArgs(mcVersion, instance, libDir, binDir, assetDir, gameDir)
if err != nil {
return []string{}, fmt.Errorf("GatOfflineLaunchArgs: %e\n", err)
}
for ind, val := range args {
2024-11-25 19:21:20 -07:00
switch val {
case "${auth_player_name}":
2024-10-30 17:16:10 -06:00
args[ind] = auth.Name
case "${auth_uuid}":
2024-10-30 17:16:10 -06:00
args[ind] = auth.Id
case "${auth_access_token}":
2024-10-30 17:16:10 -06:00
args[ind] = auth.Token
case "${auth_xuid}":
2024-10-30 17:16:10 -06:00
args[ind] = auth.Id
default:
}
}
return args, nil
}