Извлечение нативных библиотек...
panic: ошибка при запуске Minecraft: lwjgl.dll не найден в директории natives: D:\JBProjects\GOLAND\MineLauncherTest\.legit\versions\1.20.1\natives

goroutine 1 [running]:
main.main()
        D:/JBProjects/GOLAND/MineLauncherTest/main.go:31 +0x259


package launcher

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

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

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,omitempty"`
		Time        string `json:"time,omitempty"`
		ReleaseTime string `json:"releaseTime,omitempty"`
	} `json:"versions"`
}

type VersionData map[string]interface{}

type Launcher struct {
	minelauncherPath string
	versionManifest  *VersionManifest
}

func NewLauncher() *Launcher {
	return &Launcher{
		minelauncherPath: ".minecraft", // Задаем путь по умолчанию
	}
}

func (l *Launcher) SetMinecraftPath(path string) {
	l.minelauncherPath = path
}

func (l *Launcher) GetMinecraftPath() string {
	return l.minelauncherPath
}

func (l *Launcher) loadCustomVersions() ([]struct {
	ID          string `json:"id"`
	Type        string `json:"type"`
	URL         string `json:"url,omitempty"`
	Time        string `json:"time,omitempty"`
	ReleaseTime string `json:"releaseTime,omitempty"`
}, error) {
	var customVersions []struct {
		ID          string `json:"id"`
		Type        string `json:"type"`
		URL         string `json:"url,omitempty"`
		Time        string `json:"time,omitempty"`
		ReleaseTime string `json:"releaseTime,omitempty"`
	}

	versionsDir := filepath.Join(l.minelauncherPath, "versions")
	entries, err := os.ReadDir(versionsDir)
	if err != nil {
		return nil, fmt.Errorf("ошибка при чтении директории версий: %v", err)
	}

	for _, entry := range entries {
		if entry.IsDir() {
			versionID := entry.Name()
			versionJsonPath := filepath.Join(versionsDir, versionID, versionID+".json")
			if _, err := os.Stat(versionJsonPath); err == nil {
				data, err := os.ReadFile(versionJsonPath)
				if err != nil {
					fmt.Printf("Предупреждение: не удалось прочитать файл версии %s: %v\n", versionJsonPath, err)
					continue
				}
				var versionData map[string]interface{}
				err = json.Unmarshal(data, &versionData)
				if err != nil {
					fmt.Printf("Предупреждение: ошибка при разборе данных версии %s: %v\n", versionJsonPath, err)
					continue
				}

				versionType, ok := versionData["type"].(string)
				if !ok {
					versionType = "custom" // Default type
				}

				// Append custom version
				customVersions = append(customVersions, struct {
					ID          string `json:"id"`
					Type        string `json:"type"`
					URL         string `json:"url,omitempty"`
					Time        string `json:"time,omitempty"`
					ReleaseTime string `json:"releaseTime,omitempty"`
				}{
					ID:   versionID,
					Type: versionType,
				})
			}
		}
	}

	return customVersions, nil
}

func (l *Launcher) getVersionManifest() (*VersionManifest, error) {
	fmt.Println("Получение манифеста версий...")

	// Fetch the official version manifest
	resp, err := http.Get(versionManifestURL)
	if err != nil {
		return nil, fmt.Errorf("не удалось получить манифест версий: %v", err)
	}
	defer resp.Body.Close()

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

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("не удалось прочитать тело ответа манифеста версий: %v", err)
	}

	var manifest VersionManifest
	err = json.Unmarshal(bodyBytes, &manifest)
	if err != nil {
		return nil, fmt.Errorf("ошибка при разборе манифеста версий: %v", err)
	}

	// Scan for custom versions in the 'versions' directory
	customVersions, err := l.loadCustomVersions()
	if err != nil {
		fmt.Printf("Предупреждение: не удалось загрузить пользовательские версии: %v\n", err)
	} else {
		manifest.Versions = append(manifest.Versions, customVersions...)
	}

	return &manifest, nil
}

func (l *Launcher) GetMinecraftVersions() ([]string, error) {
	if l.versionManifest == nil {
		manifest, err := l.getVersionManifest()
		if err != nil {
			return nil, err
		}
		l.versionManifest = manifest
	}

	versionsMap := make(map[string]string)
	for _, v := range l.versionManifest.Versions {
		if v.Type == "custom" {
			versionsMap[v.ID] = fmt.Sprintf("%s (custom)", v.ID)
		} else {
			versionsMap[v.ID] = fmt.Sprintf("%s (%s)", v.ID, v.Type)
		}
	}

	// Собираем версии в слайс
	versions := []string{}
	for _, v := range versionsMap {
		versions = append(versions, v)
	}
	return versions, nil
}

func (l *Launcher) getVersionDetails(versionID string) (VersionData, error) {
	fmt.Printf("Получение данных для версии: %s\n", versionID)

	// Проверяем локальный JSON-файл версии
	versionJsonPath := filepath.Join(l.minelauncherPath, "versions", versionID, versionID+".json")
	if _, err := os.Stat(versionJsonPath); err == nil {
		// Локальный JSON существует, загружаем его
		data, err := os.ReadFile(versionJsonPath)
		if err != nil {
			return nil, fmt.Errorf("не удалось прочитать локальный файл версии %s: %v", versionJsonPath, err)
		}
		var versionData VersionData
		err = json.Unmarshal(data, &versionData)
		if err != nil {
			return nil, fmt.Errorf("ошибка при разборе локальных данных версии %s: %v", versionJsonPath, err)
		}
		return versionData, nil
	}

	// Иначе получаем данные версии из официального манифеста
	var versionURL string
	for _, version := range l.versionManifest.Versions {
		if version.ID == versionID {
			versionURL = version.URL
			break
		}
	}
	if versionURL == "" {
		return nil, fmt.Errorf("версия %s не найдена в манифесте", versionID)
	}

	resp, err := http.Get(versionURL)
	if err != nil {
		return nil, fmt.Errorf("не удалось получить данные версии: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("не удалось получить данные версии, статус код: %d", resp.StatusCode)
	}

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("не удалось прочитать тело ответа версии: %v", err)
	}

	var versionData VersionData
	err = json.Unmarshal(bodyBytes, &versionData)
	if err != nil {
		return nil, fmt.Errorf("ошибка при разборе данных версии: %v", err)
	}
	return versionData, nil
}

func (l *Launcher) mergeVersionData(versionData VersionData) (VersionData, error) {
	if parentID, ok := versionData["inheritsFrom"].(string); ok {
		parentData, err := l.getVersionDetails(parentID)
		if err != nil {
			return nil, fmt.Errorf("ошибка при получении родительской версии %s: %v", parentID, err)
		}
		// Рекурсивно объединяем данные родительской версии
		parentData, err = l.mergeVersionData(parentData)
		if err != nil {
			return nil, err
		}
		// Объединяем данные parentData в versionData
		mergedData := l.deepMergeVersionData(parentData, versionData)
		return mergedData, nil
	}
	return versionData, nil
}

func (l *Launcher) deepMergeVersionData(parent, child VersionData) VersionData {
	merged := make(VersionData)
	// Копируем данные родительской версии
	for k, v := range parent {
		merged[k] = v
	}
	// Переопределяем данными дочерней версии
	for k, v := range child {
		if k == "libraries" {
			// Объединяем массивы библиотек
			parentLibs, _ := parent["libraries"].([]interface{})
			childLibs, _ := v.([]interface{})
			mergedLibs := append(parentLibs, childLibs...)
			merged["libraries"] = mergedLibs
		} else if k == "arguments" {
			// Объединяем аргументы
			parentArgs, _ := parent["arguments"].(map[string]interface{})
			childArgs, _ := v.(map[string]interface{})
			mergedArgs := make(map[string]interface{})
			for ak, av := range parentArgs {
				if childArgValue, hasChildArg := childArgs[ak]; hasChildArg {
					if isNonEmpty(childArgValue) {
						// Если дочерний аргумент не пустой, используем его
						mergedArgs[ak] = childArgValue
					} else {
						// Если дочерний аргумент пустой, сохраняем родительский
						mergedArgs[ak] = av
					}
				} else {
					// Если в дочерней версии нет этого аргумента, сохраняем родительский
					mergedArgs[ak] = av
				}
			}
			// Добавляем новые аргументы из дочерней версии, которых нет в родительской
			for ak, av := range childArgs {
				if _, exists := parentArgs[ak]; !exists {
					mergedArgs[ak] = av
				}
			}
			merged["arguments"] = mergedArgs
		} else if k == "logging" {
			// Переопределяем logging
			merged["logging"] = v
		} else {
			merged[k] = v
		}
	}
	return merged
}

func isNonEmpty(value interface{}) bool {
	if value == nil {
		return false
	}
	switch v := value.(type) {
	case []interface{}:
		return len(v) > 0
	case map[string]interface{}:
		return len(v) > 0
	default:
		return true
	}
}

func (l *Launcher) downloadFile(url, filepath string) error {
	fmt.Printf("Скачивание файла: %s\n", url)
	client := http.Client{
		Timeout: 30 * time.Second,
	}

	resp, err := client.Get(url)
	if err != nil {
		return fmt.Errorf("ошибка при выполнении GET-запроса: %v", 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 fmt.Errorf("не удалось создать файл %s: %v", filepath, err)
	}
	defer out.Close()

	_, err = io.Copy(out, resp.Body)
	if err != nil {
		return fmt.Errorf("ошибка при записи файла %s: %v", filepath, err)
	}

	fmt.Printf("Файл скачан и сохранён: %s\n", filepath)
	return nil
}

func (l *Launcher) downloadFileWithRetry(url, filepath string, retries int) error {
	for i := 0; i <= retries; i++ {
		err := l.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 (l *Launcher) verifyFileChecksum(filePath, expectedHash string) bool {
	file, err := os.Open(filePath)
	if err != nil {
		fmt.Printf("Не удалось открыть файл для проверки хэша: %s, ошибка: %v\n", filePath, err)
		return false
	}
	defer file.Close()

	hash := sha1.New()
	if _, err := io.Copy(hash, file); err != nil {
		fmt.Printf("Не удалось вычислить хэш для файла: %s, ошибка: %v\n", filePath, err)
		return false
	}
	calculatedHash := hex.EncodeToString(hash.Sum(nil))
	if expectedHash != calculatedHash {
		fmt.Printf("Хэш не совпадает для файла: %s (ожидалось: %s, получено: %s)\n", filePath, expectedHash, calculatedHash)
		return false
	}
	return true
}

func (l *Launcher) shouldIncludeLibrary(libInfo map[string]interface{}) bool {
	allowed := true
	if rules, ok := libInfo["rules"].([]interface{}); ok {
		for _, ruleInterface := range rules {
			rule, ok := ruleInterface.(map[string]interface{})
			if !ok {
				continue
			}
			action, ok := rule["action"].(string)
			if !ok {
				continue
			}

			applies := 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 {
					applies = false
				}
			}

			if applies {
				if action == "allow" {
					allowed = true
				} else if action == "disallow" {
					allowed = false
				}
			}
		}
	}
	return allowed
}

func (l *Launcher) getNatives(libInfo map[string]interface{}) string {
	archType := "64"
	if runtime.GOARCH == "386" || runtime.GOARCH == "arm" {
		archType = "32"
	}

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

	var classifierName string
	switch runtime.GOOS {
	case "windows":
		if val, exists := natives["windows"].(string); exists {
			classifierName = strings.ReplaceAll(val, "${arch}", archType)
		}
	case "darwin":
		if val, exists := natives["osx"].(string); exists {
			classifierName = strings.ReplaceAll(val, "${arch}", archType)
		}
	case "linux":
		if val, exists := natives["linux"].(string); exists {
			classifierName = strings.ReplaceAll(val, "${arch}", archType)
		}
	default:
		return ""
	}

	return classifierName
}

func (l *Launcher) unzip(src, dest string) error {
	fmt.Printf("Распаковка архива: %s в %s\n", src, dest)
	r, err := zip.OpenReader(src)
	if err != nil {
		return fmt.Errorf("не удалось открыть ZIP-архив %s: %v", src, err)
	}
	defer r.Close()

	for _, f := range r.File {
		if strings.HasPrefix(f.Name, "META-INF") {
			continue
		}

		fpath := filepath.Join(dest, f.Name)

		// Проверка на безопасность пути
		if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
			return fmt.Errorf("недопустимый путь в архиве: %s", fpath)
		}

		if f.FileInfo().IsDir() {
			err := os.MkdirAll(fpath, os.ModePerm)
			if err != nil {
				return fmt.Errorf("не удалось создать директорию %s: %v", fpath, err)
			}
			continue
		}

		if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
			return fmt.Errorf("не удалось создать директорию %s: %v", filepath.Dir(fpath), err)
		}

		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			return fmt.Errorf("не удалось открыть файл для записи %s: %v", fpath, err)
		}

		rc, err := f.Open()
		if err != nil {
			outFile.Close()
			return fmt.Errorf("не удалось открыть файл внутри архива %s: %v", f.Name, err)
		}

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

		if err != nil {
			return fmt.Errorf("ошибка при копировании файла %s: %v", fpath, err)
		}
	}
	return nil
}

func (l *Launcher) verifyMainClass(clientJarPath string) bool {
	r, err := zip.OpenReader(clientJarPath)
	if err != nil {
		fmt.Printf("Не удалось открыть client.jar для проверки main class: %v\n", err)
		return false
	}
	defer r.Close()

	target := "net/minecraft/client/main/Main.class"
	for _, f := range r.File {
		if f.Name == target {
			return true
		}
	}
	fmt.Println("Main class не найден в client.jar")
	return false
}

func (l *Launcher) downloadClientJar(versionData VersionData, versionID string) error {
	fmt.Println("Скачивание клиентского JAR-файла...")

	dirPath := filepath.Join(l.minelauncherPath, "versions", versionID)
	os.MkdirAll(dirPath, os.ModePerm)
	filePath := filepath.Join(dirPath, versionID+".jar")

	// Проверяем, существует ли файл client.jar
	if _, err := os.Stat(filePath); err == nil {
		fmt.Println("Клиентский JAR-файл уже существует, пропускаем скачивание.")
		return nil
	}

	// Проверяем, есть ли указание на загрузку клиентского JAR-файла
	downloads, ok := versionData["downloads"].(map[string]interface{})
	if ok {
		client, ok := downloads["client"].(map[string]interface{})
		if ok {
			url, ok := client["url"].(string)
			if !ok {
				return fmt.Errorf("не удалось получить URL клиента")
			}
			expectedHash, _ := client["sha1"].(string)

			absFilePath, err := filepath.Abs(filePath)
			if err != nil {
				return fmt.Errorf("не удалось получить абсолютный путь к файлу %s: %v", filePath, err)
			}

			err = l.downloadFileWithRetry(url, absFilePath, 4)
			if err != nil {
				return fmt.Errorf("ошибка при скачивании клиентского JAR-файла: %v", err)
			}

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

			if !l.verifyMainClass(absFilePath) {
				return fmt.Errorf("файл client.jar не содержит необходимого основного класса")
			}

			return nil
		}
	}

	// Если не удалось найти информацию о загрузке, проверяем наличие "inheritsFrom"
	if parentID, ok := versionData["inheritsFrom"].(string); ok {
		// Пытаемся использовать client.jar родительской версии
		parentClientJar := filepath.Join(l.minelauncherPath, "versions", parentID, parentID+".jar")
		if _, err := os.Stat(parentClientJar); err != nil {
			// Если файл не существует, скачиваем родительскую версию
			parentData, err := l.getVersionDetails(parentID)
			if err != nil {
				return fmt.Errorf("не удалось получить данные родительской версии %s: %v", parentID, err)
			}
			err = l.downloadClientJar(parentData, parentID)
			if err != nil {
				return err
			}
		}
		// Копируем client.jar из родительской версии
		err := copyFile(parentClientJar, filePath)
		if err != nil {
			return fmt.Errorf("не удалось скопировать client.jar из родительской версии: %v", err)
		}
		return nil
	}

	return fmt.Errorf("не удалось скачать или найти client.jar для версии %s", versionID)
}

func copyFile(src, dst string) error {
	input, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("не удалось открыть файл %s для копирования: %v", src, err)
	}
	defer input.Close()

	output, err := os.Create(dst)
	if err != nil {
		return fmt.Errorf("не удалось создать файл %s для копирования: %v", dst, err)
	}
	defer output.Close()

	_, err = io.Copy(output, input)
	if err != nil {
		return fmt.Errorf("ошибка при копировании файла %s в %s: %v", src, dst, err)
	}

	return nil
}

func (l *Launcher) downloadLibraries(versionData VersionData) error {
	fmt.Println("Скачивание библиотек...")
	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		return fmt.Errorf("не удалось получить библиотеки из данных версии")
	}
	var wg sync.WaitGroup
	semaphore := make(chan struct{}, 10)

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

		if !l.shouldIncludeLibrary(libInfo) {
			continue
		}

		var downloadInfo map[string]interface{}
		downloads, hasDownloads := libInfo["downloads"].(map[string]interface{})
		if hasDownloads {
			artifact, hasArtifact := downloads["artifact"].(map[string]interface{})
			if hasArtifact {
				downloadInfo = artifact
			}
		} else {
			// Обработка старого формата
			name, ok := libInfo["name"].(string)
			if !ok {
				continue
			}
			var url string
			if customURL, ok := libInfo["url"].(string); ok {
				url = customURL
			} else {
				url = "https://libraries.minecraft.net/"
			}

			path := l.mavenArtifactPath(name)
			fullURL := url + path

			downloadInfo = map[string]interface{}{
				"url":  fullURL,
				"path": path,
			}
		}

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

		filePath := filepath.Join(l.minelauncherPath, "libraries", filepath.FromSlash(path))
		absFilePath, err := filepath.Abs(filePath)
		if err != nil {
			fmt.Printf("Не удалось получить абсолютный путь для файла %s: %v\n", filePath, err)
			continue
		}
		dir := filepath.Dir(absFilePath)
		os.MkdirAll(dir, os.ModePerm)

		// Проверяем, существует ли файл и совпадает ли хэш
		if _, err := os.Stat(absFilePath); err == nil {
			if expectedHash != "" && l.verifyFileChecksum(absFilePath, expectedHash) {
				continue
			} else {
				fmt.Printf("Хэш библиотеки не совпадает, перезагружаем файл: %s\n", absFilePath)
				os.Remove(absFilePath)
			}
		}

		wg.Add(1)
		semaphore <- struct{}{}

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

			err := l.downloadFileWithRetry(url, absFilePath, 4)
			if err != nil {
				fmt.Printf("Ошибка при скачивании библиотеки %s: %v\n", url, err)
				return
			}

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

func (l *Launcher) mavenArtifactPath(name string) string {
	parts := strings.Split(name, ":")
	if len(parts) < 3 {
		return ""
	}
	groupID := parts[0]
	artifactID := parts[1]
	version := parts[2]
	classifier := ""
	if len(parts) > 3 {
		classifier = "-" + parts[3]
	}
	path := fmt.Sprintf("%s/%s/%s/%s-%s%s.jar",
		strings.ReplaceAll(groupID, ".", "/"),
		artifactID,
		version,
		artifactID,
		version,
		classifier)
	return path
}

func (l *Launcher) downloadAssets(versionData VersionData) error {
	fmt.Println("Скачивание ассетов...")
	assetIndex, ok := versionData["assetIndex"].(map[string]interface{})
	if !ok {
		return fmt.Errorf("не удалось найти assetIndex в данных версии")
	}
	url, ok := assetIndex["url"].(string)
	if !ok {
		return fmt.Errorf("не удалось получить URL assetIndex")
	}
	expectedHash, ok := assetIndex["sha1"].(string)
	if !ok {
		return fmt.Errorf("не удалось получить sha1 assetIndex")
	}

	// Скачиваем индекс ассетов
	fmt.Println("Скачивание индекса ассетов...")
	resp, err := http.Get(url)
	if err != nil {
		return fmt.Errorf("не удалось скачать индекс ассетов: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("не удалось скачать индекс ассетов, статус код: %d", resp.StatusCode)
	}

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("не удалось прочитать индекс ассетов: %v", err)
	}

	hash := sha1.New()
	hash.Write(bodyBytes)
	calculatedHash := hex.EncodeToString(hash.Sum(nil))
	if expectedHash != calculatedHash {
		return fmt.Errorf("хэш индекса ассетов не совпадает (ожидалось: %s, получено: %s)", expectedHash, calculatedHash)
	}

	var assetIndexData map[string]interface{}
	err = json.Unmarshal(bodyBytes, &assetIndexData)
	if err != nil {
		return fmt.Errorf("ошибка при разборе индекса ассетов: %v", err)
	}

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

	objects, ok := assetIndexData["objects"].(map[string]interface{})
	if !ok {
		return fmt.Errorf("не удалось получить объекты ассетов")
	}

	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, ok := obj.(map[string]interface{})
			if !ok {
				return
			}
			hashStr, ok := objInfo["hash"].(string)
			if !ok {
				return
			}
			sizeFloat, ok := objInfo["size"].(float64)
			if !ok {
				return
			}
			size := int64(sizeFloat)
			if len(hashStr) < 2 {
				fmt.Printf("Некорректный хэш ассета: %s\n", hashStr)
				return
			}
			subHash := hashStr[:2]

			assetURL := fmt.Sprintf("https://resources.download.minecraft.net/%s/%s", subHash, hashStr)
			filePath := filepath.Join(l.minelauncherPath, "assets", "objects", subHash, hashStr)
			absFilePath, err := filepath.Abs(filePath)
			if err != nil {
				fmt.Printf("Не удалось получить абсолютный путь для ассета %s: %v\n", filePath, err)
				return
			}
			dir := filepath.Dir(absFilePath)
			os.MkdirAll(dir, os.ModePerm)

			if fi, err := os.Stat(absFilePath); err == nil && fi.Size() == size {
			} else {
				err := l.downloadFileWithRetry(assetURL, absFilePath, 4)
				if err != nil {
					fmt.Printf("\nОшибка при скачивании ассета %s: %v\n", assetURL, 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
}

func (l *Launcher) extractNatives(versionData VersionData, nativesDir string) error {
	fmt.Println("Извлечение нативных библиотек...")
	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		return fmt.Errorf("не удалось получить библиотеки для распаковки нативов")
	}

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

		if !l.shouldIncludeLibrary(libInfo) {
			continue
		}

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

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

		classifierName := l.getNatives(libInfo)
		if classifierName == "" {
			continue
		}

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

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

		filePath := filepath.Join(l.minelauncherPath, "libraries", filepath.FromSlash(path))
		absFilePath, err := filepath.Abs(filePath)
		if err != nil {
			fmt.Printf("Не удалось получить абсолютный путь для файла %s: %v\n", filePath, err)
			continue
		}
		dir := filepath.Dir(absFilePath)
		os.MkdirAll(dir, os.ModePerm)

		if _, err := os.Stat(absFilePath); err == nil {
			if expectedHash != "" && l.verifyFileChecksum(absFilePath, expectedHash) {
				fmt.Printf("Нативная библиотека уже существует и корректна: %s\n", absFilePath)
			} else {
				fmt.Printf("Хэш нативной библиотеки не совпадает, перезагружаем файл: %s\n", absFilePath)
				os.Remove(absFilePath)
			}
		}

		if _, err := os.Stat(absFilePath); os.IsNotExist(err) {
			fmt.Printf("Скачивание нативной библиотеки: %s\n", absFilePath)
			err := l.downloadFileWithRetry(url, absFilePath, 4)
			if err != nil {
				fmt.Printf("Ошибка при скачивании нативной библиотеки %s: %v\n", url, err)
				return err
			}
		}

		if expectedHash != "" && !l.verifyFileChecksum(absFilePath, expectedHash) {
			return fmt.Errorf("хэш нативной библиотеки не совпадает после загрузки: %s", absFilePath)
		}

		fmt.Printf("Распаковка нативных библиотек из %s в %s\n", absFilePath, nativesDir)
		err = l.unzip(absFilePath, nativesDir)
		if err != nil {
			return fmt.Errorf("ошибка при распаковке нативной библиотеки %s: %v", absFilePath, err)
		}
	}

	return nil
}

func (l *Launcher) verifyNatives(nativesDir string) error {
	files, err := os.ReadDir(nativesDir)
	if err != nil {
		return fmt.Errorf("не удалось прочитать директорию natives: %v", err)
	}

	for _, file := range files {
		if file.Name() == "lwjgl.dll" {
			return nil
		}
	}

	return fmt.Errorf("lwjgl.dll не найден в директории natives: %s", nativesDir)
}

func (l *Launcher) buildClasspath(versionData VersionData, versionID string) string {
	var classpath []string

	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		fmt.Println("Не удалось получить библиотеки для classpath")
		return ""
	}

	librarySet := make(map[string]struct{}) // Для избежания дублирования

	for _, lib := range libraries {
		libInfo, ok := lib.(map[string]interface{})
		if !ok {
			continue
		}
		if !l.shouldIncludeLibrary(libInfo) {
			continue
		}
		var libPath string

		downloads, hasDownloads := libInfo["downloads"].(map[string]interface{})
		if hasDownloads {
			artifact, hasArtifact := downloads["artifact"].(map[string]interface{})
			if hasArtifact {
				path, ok := artifact["path"].(string)
				if !ok {
					continue
				}
				libPath = filepath.Join("libraries", filepath.FromSlash(path))
			}
		} else {
			// Обработка старого формата
			name, ok := libInfo["name"].(string)
			if !ok {
				continue
			}
			path := l.mavenArtifactPath(name)
			libPath = filepath.Join("libraries", filepath.FromSlash(path))
		}

		if libPath != "" {
			if _, exists := librarySet[libPath]; !exists {
				librarySet[libPath] = struct{}{}
				classpath = append(classpath, libPath)
			}
		}
	}

	clientJar := filepath.Join("versions", versionID, versionID+".jar")
	classpath = append(classpath, clientJar)

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

func (l *Launcher) replaceVariables(arg, username, versionID string, versionData VersionData, uuid, authToken string) string {
	assetIndex := l.versionDataGetAssetIndex(versionData)

	arg = strings.ReplaceAll(arg, "${auth_player_name}", username)
	arg = strings.ReplaceAll(arg, "${version_name}", versionID)
	arg = strings.ReplaceAll(arg, "${game_directory}", l.minelauncherPath)
	arg = strings.ReplaceAll(arg, "${assets_root}", filepath.Join(l.minelauncherPath, "assets"))
	arg = strings.ReplaceAll(arg, "${assets_index_name}", l.getAssetIndexName(assetIndex))
	arg = strings.ReplaceAll(arg, "${auth_uuid}", uuid)
	arg = strings.ReplaceAll(arg, "${auth_access_token}", authToken)
	arg = strings.ReplaceAll(arg, "${clientid}", "")
	arg = strings.ReplaceAll(arg, "${auth_xuid}", "")
	arg = strings.ReplaceAll(arg, "${version_type}", l.versionDataGetVersionType(versionData))
	arg = strings.ReplaceAll(arg, "${user_type}", "legacy")
	arg = strings.ReplaceAll(arg, "--demo", "")
	arg = strings.ReplaceAll(arg, "${natives_directory}", filepath.Join(l.minelauncherPath, "versions", versionID, "natives"))
	arg = strings.ReplaceAll(arg, "${launcher_name}", "MyCustomLauncher")
	arg = strings.ReplaceAll(arg, "${launcher_version}", "1.0")

	// Устанавливаем значения разрешения (ширины и высоты)
	resolutionWidth := "854"  // Значение ширины по умолчанию
	resolutionHeight := "480" // Значение высоты по умолчанию

	if resolutionWidth != "" {
		arg = strings.ReplaceAll(arg, "${resolution_width}", resolutionWidth)
	} else {
		// Удаляем опцию '--width' и ее значение из аргументов
		arg = strings.ReplaceAll(arg, "--width ${resolution_width}", "")
	}

	if resolutionHeight != "" {
		arg = strings.ReplaceAll(arg, "${resolution_height}", resolutionHeight)
	} else {
		// Удаляем опцию '--height' и ее значение из аргументов
		arg = strings.ReplaceAll(arg, "--height ${resolution_height}", "")
	}

	return arg
}

func (l *Launcher) getAssetIndexName(assetIndex map[string]interface{}) string {
	if id, ok := assetIndex["id"].(string); ok {
		return id
	}
	return "legacy"
}

func (l *Launcher) versionDataGetAssetIndex(versionData VersionData) map[string]interface{} {
	if assetIndex, ok := versionData["assetIndex"].(map[string]interface{}); ok {
		return assetIndex
	}
	return map[string]interface{}{}
}

func (l *Launcher) versionDataGetVersionType(versionData VersionData) string {
	if versionType, ok := versionData["type"].(string); ok {
		return versionType
	}
	return "release"
}

func (l *Launcher) checkRule(rules []interface{}) bool {
	allowed := false
	for _, ruleInterface := range rules {
		rule, ok := ruleInterface.(map[string]interface{})
		if !ok {
			continue
		}
		action, ok := rule["action"].(string)
		if !ok {
			continue
		}

		applies := 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 {
				applies = false
			}
		}

		if applies {
			if action == "allow" {
				allowed = true
			} else if action == "disallow" {
				allowed = false
			}
		}
	}
	return allowed
}

func (l *Launcher) buildGameArguments(versionData VersionData, username, versionID, uuid, authToken string) []string {
	var args []string

	if arguments, ok := versionData["arguments"].(map[string]interface{}); ok {
		if gameArgsList, ok := arguments["game"].([]interface{}); ok {
			for _, arg := range gameArgsList {
				switch arg := arg.(type) {
				case string:
					if strings.Contains(strings.ToLower(arg), "quickplay") {
						continue
					}
					arg = l.replaceVariables(arg, username, versionID, versionData, uuid, authToken)
					args = append(args, arg)
				case map[string]interface{}:
					if rules, ok := arg["rules"].([]interface{}); ok {
						if l.checkRule(rules) {
							if value, ok := arg["value"].(interface{}); ok {
								args = l.appendArgumentValueWithoutQuickPlay(args, value, username, versionID, versionData, uuid, authToken)
							}
						}
					} else {
						if value, ok := arg["value"].(interface{}); ok {
							args = l.appendArgumentValueWithoutQuickPlay(args, value, username, versionID, versionData, uuid, authToken)
						}
					}
				}
			}
		}
	} else if minecraftArguments, ok := versionData["minecraftArguments"].(string); ok {
		splitArgs := strings.Split(minecraftArguments, " ")
		for _, arg := range splitArgs {
			if strings.Contains(strings.ToLower(arg), "quickplay") {
				continue
			}
			arg = l.replaceVariables(arg, username, versionID, versionData, uuid, authToken)
			args = append(args, arg)
		}
	}

	return args
}

func (l *Launcher) appendArgumentValueWithoutQuickPlay(args []string, value interface{}, username, versionID string, versionData VersionData, uuid, authToken string) []string {
	switch v := value.(type) {
	case string:
		if strings.Contains(strings.ToLower(v), "quickplay") {
			return args
		}
		v = l.replaceVariables(v, username, versionID, versionData, uuid, authToken)
		args = append(args, v)
	case []interface{}:
		for _, item := range v {
			if sv, ok := item.(string); ok {
				if strings.Contains(strings.ToLower(sv), "quickplay") {
					continue
				}
				sv = l.replaceVariables(sv, username, versionID, versionData, uuid, authToken)
				args = append(args, sv)
			}
		}
	}
	return args
}

func (l *Launcher) buildJvmArguments(versionData VersionData, nativesDir string) []string {
	var args []string

	// Добавляем стандартные JVM аргументы
	args = append(args, "-Djava.library.path="+nativesDir)
	args = append(args, "-Xmx1G")   // Максимальный объем памяти
	args = append(args, "-Xms512M") // Минимальный объем памяти

	if arguments, ok := versionData["arguments"].(map[string]interface{}); ok {
		if jvmArgsList, ok := arguments["jvm"].([]interface{}); ok {
			for _, arg := range jvmArgsList {
				switch arg := arg.(type) {
				case string:
					arg = l.replaceVariables(arg, "", "", versionData, "", "")
					args = append(args, arg)
				case map[string]interface{}:
					if rules, ok := arg["rules"].([]interface{}); ok {
						if l.checkRule(rules) {
							if value, ok := arg["value"].(interface{}); ok {
								args = l.appendJvmArgumentValue(args, value, versionData)
							}
						}
					} else {
						if value, ok := arg["value"].(interface{}); ok {
							args = l.appendJvmArgumentValue(args, value, versionData)
						}
					}
				}
			}
		}
	}

	return args
}

func (l *Launcher) appendJvmArgumentValue(args []string, value interface{}, versionData VersionData) []string {
	switch v := value.(type) {
	case string:
		v = l.replaceVariables(v, "", "", versionData, "", "")
		args = append(args, v)
	case []interface{}:
		for _, item := range v {
			if sv, ok := item.(string); ok {
				sv = l.replaceVariables(sv, "", "", versionData, "", "")
				args = append(args, sv)
			}
		}
	}
	return args
}

func (l *Launcher) launchMinecraft(versionData VersionData, authToken, username, uuid, versionID string) error {
	fmt.Println("Запуск Minecraft...")
	mainClass, ok := versionData["mainClass"].(string)
	if !ok {
		return fmt.Errorf("не удалось найти mainClass в данных версии")
	}
	fmt.Println("Main class:", mainClass)
	classpath := l.buildClasspath(versionData, versionID)
	fmt.Println("Classpath длина:", len(classpath))

	if classpath == "" {
		return fmt.Errorf("classpath пустой")
	}

	nativesDir := filepath.Join(l.minelauncherPath, "versions", versionID, "natives")
	absNativesDir, err := filepath.Abs(nativesDir)
	if err != nil {
		return fmt.Errorf("не удалось получить абсолютный путь для nativesDir %s: %v", nativesDir, err)
	}
	os.MkdirAll(absNativesDir, os.ModePerm)

	// Распаковка нативных библиотек
	err = l.extractNatives(versionData, absNativesDir)
	if err != nil {
		return fmt.Errorf("ошибка при извлечении нативных библиотек: %v", err)
	}

	err = l.verifyNatives(absNativesDir)
	if err != nil {
		return err
	}

	// Build JVM and game arguments
	jvmArgs := l.buildJvmArguments(versionData, absNativesDir)
	gameArgs := l.buildGameArguments(versionData, username, versionID, uuid, authToken)

	if len(gameArgs) == 0 {
		return fmt.Errorf("аргументы игры пустые")
	}

	// Combine all arguments
	args := append(jvmArgs, "-cp", classpath, mainClass)
	args = append(args, gameArgs...)

	// Write arguments to a temporary file
	argsFilePath := filepath.Join(os.TempDir(), "minecraft_args.txt")
	err = l.writeArgsToFile(argsFilePath, args)
	if err != nil {
		return fmt.Errorf("ошибка при создании файла аргументов: %v", err)
	}

	// Prepare the command to use the argument file
	cmd := exec.Command("java", "@"+argsFilePath)
	cmd.Dir = l.minelauncherPath
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	fmt.Println("Команда запуска: java @" + argsFilePath)

	err = cmd.Run()
	if err != nil {
		return fmt.Errorf("ошибка при запуске Minecraft: %v", err)
	}

	return nil
}

func (l *Launcher) writeArgsToFile(filePath string, args []string) error {
	var contentBuilder strings.Builder
	for _, arg := range args {
		arg = strings.ReplaceAll(arg, "\\", "\\\\")
		arg = strings.ReplaceAll(arg, `"`, `\"`)

		if strings.ContainsAny(arg, " \t\n\"") {
			arg = `"` + arg + `"`
		}

		contentBuilder.WriteString(arg)
		contentBuilder.WriteString(" ")
	}

	content := contentBuilder.String()
	return os.WriteFile(filePath, []byte(content), 0644)
}

func (l *Launcher) verifyAllFiles(versionData VersionData, versionID string) error {
	fmt.Println("Проверка наличия всех необходимых файлов...")
	clientJar := filepath.Join(l.minelauncherPath, "versions", versionID, versionID+".jar")
	if _, err := os.Stat(clientJar); os.IsNotExist(err) {
		return fmt.Errorf("клиентский JAR-файл отсутствует: %s", clientJar)
	}

	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		return fmt.Errorf("не удалось получить библиотеки для проверки")
	}
	for _, lib := range libraries {
		libInfo, ok := lib.(map[string]interface{})
		if !ok {
			continue
		}
		if !l.shouldIncludeLibrary(libInfo) {
			continue
		}
		var libPath string

		downloads, hasDownloads := libInfo["downloads"].(map[string]interface{})
		if hasDownloads {
			artifact, hasArtifact := downloads["artifact"].(map[string]interface{})
			if hasArtifact {
				path, ok := artifact["path"].(string)
				if !ok {
					continue
				}
				libPath = filepath.Join(l.minelauncherPath, "libraries", filepath.FromSlash(path))
			}
		} else {
			// Обработка старого формата
			name, ok := libInfo["name"].(string)
			if !ok {
				continue
			}
			path := l.mavenArtifactPath(name)
			libPath = filepath.Join(l.minelauncherPath, "libraries", filepath.FromSlash(path))
		}

		if libPath == "" {
			fmt.Printf("Пустой путь для библиотеки: %+v\n", libInfo)
			continue
		}

		if _, err := os.Stat(libPath); os.IsNotExist(err) {
			return fmt.Errorf("отсутствует библиотека: %s", libPath)
		}
	}

	fmt.Println("Все необходимые файлы присутствуют.")
	return nil
}

func (l *Launcher) generateOfflineUUID() string {
	uuid := make([]byte, 16)
	_, err := rand.Read(uuid)
	if err != nil {
		panic(err)
	}

	uuid[6] = (uuid[6] & 0x0F) | 0x40
	uuid[8] = (uuid[8] & 0x3F) | 0x80

	return fmt.Sprintf("%x-%x-%x-%x-%x",
		uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
}

func (l *Launcher) PlayMinecraftVersion(versionID string, username string) error {
	absPath, err := filepath.Abs(l.minelauncherPath)
	if err != nil {
		return fmt.Errorf("не удалось получить абсолютный путь для %s: %v", l.minelauncherPath, err)
	}
	l.minelauncherPath = absPath

	err = os.MkdirAll(l.minelauncherPath, os.ModePerm)
	if err != nil {
		return fmt.Errorf("не удалось создать директорию %s: %v", l.minelauncherPath, err)
	}

	versionData, err := l.getVersionDetails(versionID)
	if err != nil {
		return err
	}

	// Объединяем данные версий с учетом inheritsFrom
	versionData, err = l.mergeVersionData(versionData)
	if err != nil {
		return err
	}

	versionDir := filepath.Join(l.minelauncherPath, "versions", versionID)
	err = os.MkdirAll(versionDir, os.ModePerm)
	if err != nil {
		return fmt.Errorf("не удалось создать директорию %s: %v", versionDir, err)
	}
	versionJsonPath := filepath.Join(versionDir, versionID+".json")

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

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

	err = l.downloadClientJar(versionData, versionID)
	if err != nil {
		return fmt.Errorf("ошибка при скачивании клиентского JAR-файла: %v", err)
	}

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

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

	err = l.verifyAllFiles(versionData, versionID)
	if err != nil {
		return fmt.Errorf("ошибка валидации файлов: %v", err)
	}

	dirs := []string{
		filepath.Join(l.minelauncherPath, "saves"),
		filepath.Join(l.minelauncherPath, "resourcepacks"),
		filepath.Join(l.minelauncherPath, "logs"),
		filepath.Join(l.minelauncherPath, "runtime"),
	}
	for _, dir := range dirs {
		err := os.MkdirAll(dir, os.ModePerm)
		if err != nil {
			return fmt.Errorf("не удалось создать директорию %s: %v", dir, err)
		}
	}

	optionsFilePath := filepath.Join(l.minelauncherPath, "options.txt")
	if _, err := os.Stat(optionsFilePath); os.IsNotExist(err) {
		err := os.WriteFile(optionsFilePath, []byte(""), 0644)
		if err != nil {
			return fmt.Errorf("не удалось создать файл %s: %v", optionsFilePath, err)
		}
	}

	javaVersionCmd := exec.Command("java", "-version")
	javaVersionOutput, err := javaVersionCmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("ошибка при проверке версии Java: %v", err)
	}
	fmt.Println("Java версия:", string(javaVersionOutput))

	uname := username
	uuid := l.generateOfflineUUID()
	authToken := ""

	err = l.launchMinecraft(versionData, authToken, uname, uuid, versionID)
	if err != nil {
		return fmt.Errorf("ошибка при запуске Minecraft: %v", err)
	}

	fmt.Println("Minecraft запущен успешно!")
	return nil
}


Извлечение нативных библиотек...
panic: ошибка при запуске Minecraft: lwjgl.dll не найден в директории natives: D:\JBProjects\GOLAND\MineLauncherTest\.legit\versions\1.20.1\natives

goroutine 1 [running]:
main.main()
        D:/JBProjects/GOLAND/MineLauncherTest/main.go:31 +0x259


исправь теперь некоторые версии не рабоатют natives должны быть у каждой версии свои в своей папки версии versions/version/natives испрявь сделай так чтобы онп устанавливались