Error: Could not find or load main class net.minecraft.client.Main
Caused by: java.lang.ClassNotFoundException: net.minecraft.client.Main


package main

import (
	"archive/zip"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)

const versionManifestURL = "https://launchermeta.mojang.com/mc/game/version_manifest.json"

// Переменная для хранения пользовательского пути
var minelauncherPath string

// Структура для хранения манифеста версий
type VersionManifest struct {
	Latest struct {
		Release  string `json:"release"`
		Snapshot string `json:"snapshot"`
	} `json:"latest"`
	Versions []struct {
		ID          string `json:"id"`
		Type        string `json:"type"`
		URL         string `json:"url"`
		Time        string `json:"time"`
		ReleaseTime string `json:"releaseTime"`
	} `json:"versions"`
}

// Получение манифеста всех версий
func getVersionManifest() (*VersionManifest, error) {
	resp, err := http.Get(versionManifestURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var manifest VersionManifest
	err = json.Unmarshal(bodyBytes, &manifest)
	if err != nil {
		return nil, err
	}
	return &manifest, nil
}

// Получение данных выбранной версии
func getVersionDetails(versionURL string) (map[string]interface{}, error) {
	resp, err := http.Get(versionURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var versionData map[string]interface{}
	err = json.Unmarshal(bodyBytes, &versionData)
	if err != nil {
		return nil, err
	}
	return versionData, nil
}

// Функция для скачивания файла по URL с повторными попытками
func downloadFile(url, filepath string) error {
	client := http.Client{
		Timeout: 30 * time.Second, // Устанавливаем тайм-аут
	}

	resp, err := client.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Проверяем код ответа
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("не удалось скачать файл: %s, статус код: %d", url, resp.StatusCode)
	}

	out, err := os.Create(filepath)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, resp.Body)
	return err
}

// Скачивание файла с повторами
func downloadFileWithRetry(url, filepath string, retries int) error {
	for i := 0; i <= retries; i++ {
		err := downloadFile(url, filepath)
		if err == nil {
			return nil
		}
		fmt.Printf("Попытка %d скачать файл %s не удалась, ошибка: %v\n", i+1, url, err)
		time.Sleep(2 * time.Second) // Ждем перед повторной попыткой
	}
	return fmt.Errorf("не удалось скачать файл после %d попыток: %s", retries+1, url)
}

// Проверка хэша файла
func verifyFileChecksum(filePath, expectedHash string) bool {
	file, err := os.Open(filePath)
	if err != nil {
		return false
	}
	defer file.Close()

	hash := sha1.New()
	if _, err := io.Copy(hash, file); err != nil {
		return false
	}
	calculatedHash := hex.EncodeToString(hash.Sum(nil))
	return expectedHash == calculatedHash
}

// Скачивание клиентского JAR-файла
func downloadClientJar(versionData map[string]interface{}, versionID string) error {
	downloads := versionData["downloads"].(map[string]interface{})
	client := downloads["client"].(map[string]interface{})
	url := client["url"].(string)
	expectedHash := client["sha1"].(string)

	// Путь для сохранения JAR-файла
	dirPath := filepath.Join(minelauncherPath, "versions", versionID)
	os.MkdirAll(dirPath, os.ModePerm)
	filePath := filepath.Join(dirPath, versionID+".jar")

	// Проверяем, существует ли файл и совпадает ли хэш
	if _, err := os.Stat(filePath); err == nil {
		if verifyFileChecksum(filePath, expectedHash) {
			fmt.Println("Клиентский JAR-файл уже существует и корректен, пропускаем скачивание.")
			return nil
		} else {
			fmt.Println("Хэш клиентского JAR-файла не совпадает, перезагружаем файл.")
			os.Remove(filePath)
		}
	}

	fmt.Println("Скачивание клиентского JAR-файла...")
	err := downloadFileWithRetry(url, filePath, 3)
	if err != nil {
		return err
	}

	// Проверяем хэш после загрузки
	if !verifyFileChecksum(filePath, expectedHash) {
		return fmt.Errorf("хэш клиентского JAR-файла не совпадает после загрузки")
	}

	return nil
}

// Функция для проверки правил библиотеки
func shouldIncludeLibrary(libInfo map[string]interface{}) bool {
	if rules, ok := libInfo["rules"].([]interface{}); ok {
		allowed := false
		for _, ruleInterface := range rules {
			rule := ruleInterface.(map[string]interface{})
			action := rule["action"].(string)
			osMatch := true
			if osDict, ok := rule["os"].(map[string]interface{}); ok {
				osName := runtime.GOOS
				if osName == "darwin" {
					osName = "osx"
				}
				if name, ok := osDict["name"].(string); ok && name != osName {
					osMatch = false
				}
			}
			if osMatch {
				if action == "allow" {
					allowed = true
				} else if action == "disallow" {
					allowed = false
				}
			}
		}
		return allowed
	}
	return true
}

// Скачивание библиотек
func downloadLibraries(versionData map[string]interface{}) error {
	fmt.Println("Скачивание библиотек...")
	libraries := versionData["libraries"].([]interface{})
	var wg sync.WaitGroup
	semaphore := make(chan struct{}, 10) // Ограничиваем количество одновременных загрузок

	for _, lib := range libraries {
		libInfo := lib.(map[string]interface{})

		// Проверяем, следует ли включать эту библиотеку
		if !shouldIncludeLibrary(libInfo) {
			continue
		}

		downloads, ok := libInfo["downloads"].(map[string]interface{})
		if !ok {
			continue
		}

		artifact, ok := downloads["artifact"].(map[string]interface{})
		if !ok {
			continue
		}

		url, ok := artifact["url"].(string)
		if !ok {
			continue
		}
		path, ok := artifact["path"].(string)
		if !ok {
			continue
		}
		expectedHash, ok := artifact["sha1"].(string)
		if !ok {
			continue
		}

		// Путь для сохранения файла
		filePath := filepath.Join(minelauncherPath, "libraries", filepath.FromSlash(path))
		dir := filepath.Dir(filePath)
		os.MkdirAll(dir, os.ModePerm)

		// Проверяем, существует ли файл и совпадает ли хэш
		if _, err := os.Stat(filePath); err == nil {
			if verifyFileChecksum(filePath, expectedHash) {
				// fmt.Println("Библиотека уже существует и корректна, пропускаем скачивание.")
				continue
			} else {
				fmt.Println("Хэш библиотеки не совпадает, перезагружаем файл:", filePath)
				os.Remove(filePath)
			}
		}

		wg.Add(1)
		semaphore <- struct{}{} // блокирует, если семафор заполнен

		go func(url, filePath, expectedHash string) {
			defer wg.Done()
			defer func() { <-semaphore }()

			err := downloadFileWithRetry(url, filePath, 3)
			if err != nil {
				fmt.Println("\nОшибка при скачивании библиотеки:", err)
				return
			}

			if !verifyFileChecksum(filePath, expectedHash) {
				fmt.Printf("\nХэш библиотеки не совпадает после загрузки: %s\n", filePath)
				os.Remove(filePath)
			}
		}(url, filePath, expectedHash)
	}
	wg.Wait()
	return nil
}

// Скачивание ассетов с параллельной загрузкой
func downloadAssets(versionData map[string]interface{}) error {
	fmt.Println("Скачивание ассетов...")
	assetIndex := versionData["assetIndex"].(map[string]interface{})
	url := assetIndex["url"].(string)
	expectedHash := assetIndex["sha1"].(string)

	// Скачиваем индекс ассетов
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	// Проверяем хэш индекса ассетов
	hash := sha1.New()
	hash.Write(bodyBytes)
	calculatedHash := hex.EncodeToString(hash.Sum(nil))
	if expectedHash != calculatedHash {
		return fmt.Errorf("хэш индекса ассетов не совпадает")
	}

	var assetIndexData map[string]interface{}
	err = json.Unmarshal(bodyBytes, &assetIndexData)
	if err != nil {
		return err
	}

	// Сохраняем индекс ассетов
	assetsDir := filepath.Join(minelauncherPath, "assets", "indexes")
	os.MkdirAll(assetsDir, os.ModePerm)
	indexFilePath := filepath.Join(assetsDir, assetIndex["id"].(string)+".json")
	err = ioutil.WriteFile(indexFilePath, bodyBytes, os.ModePerm)
	if err != nil {
		return fmt.Errorf("ошибка при сохранении индекса ассетов: %v", err)
	}

	objects := assetIndexData["objects"].(map[string]interface{})

	totalAssets := len(objects)
	fmt.Printf("Всего ассетов для загрузки: %d\n", totalAssets)

	var wg sync.WaitGroup
	semaphore := make(chan struct{}, 20) // Увеличиваем количество одновременных загрузок для ассетов

	currentAsset := 0
	mutex := &sync.Mutex{}

	for _, obj := range objects {
		wg.Add(1)
		semaphore <- struct{}{} // блокирует, если семафор заполнен

		go func(obj interface{}) {
			defer wg.Done()
			defer func() { <-semaphore }() // освобождаем место в семафоре

			objInfo := obj.(map[string]interface{})
			hash := objInfo["hash"].(string)
			size := int64(objInfo["size"].(float64))
			subHash := hash[:2]

			url := fmt.Sprintf("https://resources.download.minecraft.net/%s/%s", subHash, hash)
			filePath := filepath.Join(minelauncherPath, "assets", "objects", subHash, hash)
			dir := filepath.Dir(filePath)
			os.MkdirAll(dir, os.ModePerm)

			// Проверяем, существует ли файл и его размер
			if fi, err := os.Stat(filePath); err == nil && fi.Size() == size {
				// Файл существует и его размер соответствует, пропускаем
			} else {
				err := downloadFileWithRetry(url, filePath, 3)
				if err != nil {
					fmt.Println("\nОшибка при скачивании ассета:", err)
					return
				}
			}

			mutex.Lock()
			currentAsset++
			progress := float64(currentAsset) / float64(totalAssets) * 100
			fmt.Printf("\rСкачивание ассетов: %d/%d (%.2f%%)", currentAsset, totalAssets, progress)
			mutex.Unlock()
		}(obj)
	}

	wg.Wait()
	fmt.Println("\nЗагрузка ассетов завершена.")
	return nil
}

// Построение classpath для запуска игры
func buildClasspath(versionData map[string]interface{}, versionID string) string {
	var classpath []string

	// Добавляем библиотеки
	libraries := versionData["libraries"].([]interface{})
	for _, lib := range libraries {
		libInfo := lib.(map[string]interface{})
		if !shouldIncludeLibrary(libInfo) {
			continue
		}
		downloads, ok := libInfo["downloads"].(map[string]interface{})
		if !ok {
			continue
		}
		artifact, ok := downloads["artifact"].(map[string]interface{})
		if !ok {
			continue
		}
		path := artifact["path"].(string)
		libPath := filepath.Join(minelauncherPath, "libraries", filepath.FromSlash(path))
		classpath = append(classpath, libPath)
	}

	// Добавляем клиентский JAR
	clientJar := filepath.Join(minelauncherPath, "versions", versionID, versionID+".jar")
	classpath = append(classpath, clientJar)

	return strings.Join(classpath, string(os.PathListSeparator))
}

// Запуск игры
func launchMinecraft(versionData map[string]interface{}, authToken, username, uuid, versionID string) error {
	mainClass := versionData["mainClass"].(string)
	fmt.Println("Main class:", mainClass)
	classpath := buildClasspath(versionData, versionID)
	fmt.Println("Classpath:", classpath)

	// Путь к директории natives
	nativesDir := filepath.Join(minelauncherPath, "versions", versionID, "natives")
	os.MkdirAll(nativesDir, os.ModePerm)

	// Распаковка нативных библиотек
	err := extractNatives(versionData, nativesDir)
	if err != nil {
		return err
	}

	// Аргументы JVM
	jvmArgs := []string{
		"-Djava.library.path=" + nativesDir,
		"-cp", classpath,
	}

	// Аргументы игры
	gameArgs := buildGameArguments(versionData, authToken, username, uuid, versionID)

	// Объединяем аргументы
	args := append(jvmArgs, mainClass)
	args = append(args, gameArgs...)

	// Команда запуска
	cmd := exec.Command("java", args...)
	cmd.Dir = minelauncherPath // Устанавливаем рабочую директорию
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	// Выводим команду запуска для отладки
	fmt.Println("Команда запуска:", cmd.String())

	err = cmd.Run()
	return err
}

// Построение аргументов игры
func buildGameArguments(versionData map[string]interface{}, authToken, username, uuid, versionID string) []string {
	var args []string

	// Проверяем, есть ли поле "arguments" (для новых версий)
	if versionData["arguments"] != nil {
		arguments := versionData["arguments"].(map[string]interface{})
		gameArgsList := arguments["game"].([]interface{})
		for _, arg := range gameArgsList {
			switch arg := arg.(type) {
			case string:
				arg = replaceVariables(arg, authToken, username, uuid, versionID)
				args = append(args, arg)
			case map[string]interface{}:
				// Обработка условных аргументов
				if rules, ok := arg["rules"].([]interface{}); ok {
					if checkRule(rules) {
						if value, ok := arg["value"].(string); ok {
							value = replaceVariables(value, authToken, username, uuid, versionID)
							args = append(args, value)
						} else if values, ok := arg["value"].([]interface{}); ok {
							for _, v := range values {
								sv := v.(string)
								sv = replaceVariables(sv, authToken, username, uuid, versionID)
								args = append(args, sv)
							}
						}
					}
				}
			}
		}
	} else {
		// Для старых версий используем поле "minecraftArguments"
		minecraftArguments := versionData["minecraftArguments"].(string)
		splitArgs := strings.Split(minecraftArguments, " ")
		for _, arg := range splitArgs {
			arg = replaceVariables(arg, authToken, username, uuid, versionID)
			args = append(args, arg)
		}
	}

	return args
}

// Функция для проверки правил аргументов
func checkRule(rules []interface{}) bool {
	// Здесь можно добавить более точную проверку правил
	return true
}

// Функция для замены переменных в аргументах
func replaceVariables(arg, authToken, username, uuid, versionID string) string {
	arg = strings.ReplaceAll(arg, "${auth_player_name}", username)
	arg = strings.ReplaceAll(arg, "${version_name}", versionID)
	arg = strings.ReplaceAll(arg, "${game_directory}", minelauncherPath)
	arg = strings.ReplaceAll(arg, "${assets_root}", filepath.Join(minelauncherPath, "assets"))
	arg = strings.ReplaceAll(arg, "${assets_index_name}", versionID)
	arg = strings.ReplaceAll(arg, "${auth_uuid}", uuid)
	arg = strings.ReplaceAll(arg, "${auth_access_token}", authToken)
	arg = strings.ReplaceAll(arg, "${user_type}", "msa")
	arg = strings.ReplaceAll(arg, "${version_type}", "release")
	return arg
}

// Распаковка нативных библиотек
func extractNatives(versionData map[string]interface{}, nativesDir string) error {
	libraries := versionData["libraries"].([]interface{})
	for _, lib := range libraries {
		libInfo := lib.(map[string]interface{})

		// Проверяем, следует ли включать эту библиотеку
		if !shouldIncludeLibrary(libInfo) {
			continue
		}

		// Проверяем наличие нативов
		downloads, ok := libInfo["downloads"].(map[string]interface{})
		if !ok {
			continue
		}
		classifiers, ok := downloads["classifiers"].(map[string]interface{})
		if !ok {
			continue
		}

		// Определяем текущую ОС
		osName := runtime.GOOS
		var classifierName string
		switch osName {
		case "windows":
			classifierName = "natives-windows"
		case "darwin":
			classifierName = "natives-macos"
		case "linux":
			classifierName = "natives-linux"
		default:
			continue
		}

		native, ok := classifiers[classifierName].(map[string]interface{})
		if !ok {
			continue
		}

		url := native["url"].(string)
		path := native["path"].(string)
		expectedHash := native["sha1"].(string)
		filePath := filepath.Join(minelauncherPath, "libraries", filepath.FromSlash(path))
		dir := filepath.Dir(filePath)
		os.MkdirAll(dir, os.ModePerm)

		fmt.Printf("Скачивание нативной библиотеки: %s\n", filePath)

		// Проверяем, существует ли файл и совпадает ли хэш
		if _, err := os.Stat(filePath); err == nil {
			if verifyFileChecksum(filePath, expectedHash) {
				// fmt.Println("Нативная библиотека уже существует и корректна, пропускаем скачивание.")
			} else {
				fmt.Println("Хэш нативной библиотеки не совпадает, перезагружаем файл:", filePath)
				os.Remove(filePath)
			}
		} else {
			err := downloadFileWithRetry(url, filePath, 3)
			if err != nil {
				fmt.Println("Ошибка при скачивании нативной библиотеки:", err)
				return err
			}
		}

		// Проверяем хэш после загрузки
		if !verifyFileChecksum(filePath, expectedHash) {
			fmt.Printf("Хэш нативной библиотеки не совпадает после загрузки: %s\n", filePath)
			return fmt.Errorf("хэш нативной библиотеки не совпадает")
		}

		// Распаковываем нативы
		err := unzip(filePath, nativesDir)
		if err != nil {
			fmt.Println("Ошибка при распаковке нативной библиотеки:", err)
			return err
		}
	}
	return nil
}

// Функция для распаковки ZIP-архива
func unzip(src, dest string) error {
	r, err := zip.OpenReader(src)
	if err != nil {
		return err
	}
	defer r.Close()

	for _, f := range r.File {
		fpath := filepath.Join(dest, f.Name)
		if f.FileInfo().IsDir() {
			os.MkdirAll(fpath, os.ModePerm)
			continue
		}

		if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
			return err
		}

		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			return err
		}

		rc, err := f.Open()
		if err != nil {
			return err
		}

		_, err = io.Copy(outFile, rc)

		outFile.Close()
		rc.Close()

		if err != nil {
			return err
		}
	}
	return nil
}

func main() {
	// Запрашиваем путь для хранения файлов Minecraft
	fmt.Print("Введите путь для хранения файлов Minecraft (по умолчанию './minecraft'): ")
	fmt.Scanln(&minelauncherPath)
	if minelauncherPath == "" {
		minelauncherPath = "./minecraft"
	}
	// Создаем директорию, если ее нет
	os.MkdirAll(minelauncherPath, os.ModePerm)

	// Получаем манифест версий
	manifest, err := getVersionManifest()
	if err != nil {
		fmt.Println("Ошибка при получении манифеста:", err)
		return
	}

	// Выводим список доступных версий
	fmt.Println("Доступные версии Minecraft:")
	for _, version := range manifest.Versions {
		fmt.Printf("- %s (%s)\n", version.ID, version.Type)
	}

	// Запрашиваем у пользователя выбор версии
	var selectedVersionID string
	fmt.Print("Введите ID версии для скачивания и запуска: ")
	fmt.Scanln(&selectedVersionID)

	// Находим URL выбранной версии
	var selectedVersionURL string
	for _, version := range manifest.Versions {
		if version.ID == selectedVersionID {
			selectedVersionURL = version.URL
			break
		}
	}

	if selectedVersionURL == "" {
		fmt.Println("Версия не найдена.")
		return
	}

	// Получаем данные выбранной версии
	versionData, err := getVersionDetails(selectedVersionURL)
	if err != nil {
		fmt.Println("Ошибка при получении данных версии:", err)
		return
	}

	// Сохраняем данные версии в файл version.json
	versionDir := filepath.Join(minelauncherPath, "versions", selectedVersionID)
	os.MkdirAll(versionDir, os.ModePerm)
	versionJsonPath := filepath.Join(versionDir, selectedVersionID+".json")

	versionDataBytes, err := json.MarshalIndent(versionData, "", "  ")
	if err != nil {
		fmt.Println("Ошибка при сериализации данных версии:", err)
		return
	}

	err = ioutil.WriteFile(versionJsonPath, versionDataBytes, 0644)
	if err != nil {
		fmt.Println("Ошибка при сохранении файла version.json:", err)
		return
	}

	// Скачиваем необходимые файлы
	err = downloadClientJar(versionData, selectedVersionID)
	if err != nil {
		fmt.Println("Ошибка при скачивании клиентского JAR-файла:", err)
		return
	}

	err = downloadLibraries(versionData)
	if err != nil {
		fmt.Println("Ошибка при скачивании библиотек:", err)
		return
	}

	err = downloadAssets(versionData)
	if err != nil {
		fmt.Println("Ошибка при скачивании ассетов:", err)
		return
	}

	// Создаем необходимые директории и файлы
	os.MkdirAll(filepath.Join(minelauncherPath, "saves"), os.ModePerm)
	os.MkdirAll(filepath.Join(minelauncherPath, "resourcepacks"), os.ModePerm)
	os.MkdirAll(filepath.Join(minelauncherPath, "logs"), os.ModePerm)
	os.MkdirAll(filepath.Join(minelauncherPath, "runtime"), os.ModePerm)
	optionsFilePath := filepath.Join(minelauncherPath, "options.txt")
	if _, err := os.Stat(optionsFilePath); os.IsNotExist(err) {
		ioutil.WriteFile(optionsFilePath, []byte(""), 0644)
	}

	// Проверяем наличие Java и ее версии
	javaVersionCmd := exec.Command("java", "-version")
	javaVersionOutput, err := javaVersionCmd.CombinedOutput()
	if err != nil {
		fmt.Println("Ошибка при проверке версии Java:", err)
		return
	}
	fmt.Println("Java version:", string(javaVersionOutput))

	// Здесь должна быть реализация аутентификации пользователя
	// Для простоты используем заглушки
	authToken := ``
	username := "NAMAAA"
	uuid := ``

	// Запускаем игру
	err = launchMinecraft(versionData, authToken, username, uuid, selectedVersionID)
	if err != nil {
		fmt.Println("Ошибка при запуске Minecraft:", err)
		return
	}
}