package main import ( "encoding/json" "errors" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "golang.org/x/sys/windows/registry" ) const ( protocolName = "a301" gameExeName = "A301.exe" serverInfoURL = "https://a301.api.tolelom.xyz/api/download/info" redeemTicketURL = "https://a301.api.tolelom.xyz/api/auth/redeem-ticket" ) // errNoRetry wraps errors that should not be retried (e.g. 4xx responses). type errNoRetry struct { err error } func (e *errNoRetry) Error() string { return e.err.Error() } func (e *errNoRetry) Unwrap() error { return e.err } type downloadInfo struct { FileHash string `json:"fileHash"` URL string `json:"url"` LauncherURL string `json:"launcherUrl"` LauncherHash string `json:"launcherHash"` } // installDir returns the fixed install directory: %LOCALAPPDATA%\A301 func installDir() (string, error) { localAppData := os.Getenv("LOCALAPPDATA") if localAppData == "" { return "", fmt.Errorf("LOCALAPPDATA 환경변수를 찾을 수 없습니다") } return filepath.Join(localAppData, "A301"), nil } // launcherPath returns the current executable's absolute path. func launcherPath() (string, error) { exe, err := os.Executable() if err != nil { return "", err } return filepath.Abs(exe) } func install() error { srcPath, err := launcherPath() if err != nil { return fmt.Errorf("실행 파일 경로를 찾을 수 없습니다: %w", err) } dir, err := installDir() if err != nil { return err } if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("설치 디렉토리 생성 실패: %w", err) } dstPath := filepath.Join(dir, "launcher.exe") if !strings.EqualFold(srcPath, dstPath) { if err := copyFile(srcPath, dstPath); err != nil { return fmt.Errorf("런처 설치 실패: %w", err) } } key, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\`+protocolName, registry.SET_VALUE) if err != nil { return fmt.Errorf("레지스트리 키 생성 실패: %w", err) } defer key.Close() if err := key.SetStringValue("", "URL:One of the plans Protocol"); err != nil { return fmt.Errorf("프로토콜 값 설정 실패: %w", err) } if err := key.SetStringValue("URL Protocol", ""); err != nil { return fmt.Errorf("URL Protocol 값 설정 실패: %w", err) } cmdKey, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\`+protocolName+`\shell\open\command`, registry.SET_VALUE) if err != nil { return fmt.Errorf("command 키 생성 실패: %w", err) } defer cmdKey.Close() return cmdKey.SetStringValue("", fmt.Sprintf(`"%s" "%%1"`, dstPath)) } func uninstall() error { paths := []string{ `Software\Classes\` + protocolName + `\shell\open\command`, `Software\Classes\` + protocolName + `\shell\open`, `Software\Classes\` + protocolName + `\shell`, `Software\Classes\` + protocolName, } for _, p := range paths { if err := registry.DeleteKey(registry.CURRENT_USER, p); err != nil && err != registry.ErrNotExist { return err } } return nil } func fetchServerInfoOnce() (*downloadInfo, error) { resp, err := apiClient.Get(serverInfoURL) if err != nil { return nil, fmt.Errorf("서버 연결 실패: %w", err) } defer resp.Body.Close() if resp.StatusCode == 404 { return nil, &errNoRetry{fmt.Errorf("게임이 아직 준비되지 않았습니다")} } if resp.StatusCode >= 400 { return nil, &errNoRetry{fmt.Errorf("서버 오류 (HTTP %d)", resp.StatusCode)} } var info downloadInfo if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&info); err != nil { return nil, fmt.Errorf("서버 응답 파싱 실패: %w", err) } return &info, nil } func fetchServerInfo() (*downloadInfo, error) { const maxRetries = 3 var lastErr error for i := range maxRetries { info, err := fetchServerInfoOnce() if err == nil { return info, nil } lastErr = err var noRetry *errNoRetry if errors.As(err, &noRetry) { return nil, err } time.Sleep(time.Duration(1<