package main import ( "fmt" "net/url" "os" "os/exec" "path/filepath" "strings" ) // handleURI 웹에서 a301://... 링크를 통해 호출되는 핵심 게임 실행 흐름. func handleURI(rawURI string) error { // 1. URI에서 티켓 추출 → JWT 교환 token, err := authenticateFromURI(rawURI) if err != nil { return err } // 2. 게임 디렉토리 준비 + 프로토콜 갱신 + 잔여 파일 정리 gameDir, gamePath, err := prepareGameDir() if err != nil { return err } // 3. 서버 정보 조회 (실패 시 오프라인 실행 시도) serverInfo, err := fetchServerInfo() if err != nil { return tryOfflineLaunch(gamePath, gameDir, token, err) } // 4. 런처 자동 업데이트 (실패해도 게임 실행은 계속) if restartNeeded := tryLauncherUpdate(serverInfo); restartNeeded { return nil } // 5. 게임 다운로드/검증 if err := ensureGame(gameDir, gamePath, serverInfo); err != nil { return err } // 6. 게임 실행 return launchGame(gamePath, gameDir, token) } // authenticateFromURI URI를 파싱하고 일회용 티켓을 JWT로 교환한다. func authenticateFromURI(rawURI string) (string, error) { parsed, err := url.Parse(rawURI) if err != nil { return "", fmt.Errorf("URI 파싱 실패: %w", err) } if parsed.Scheme != protocolName { return "", fmt.Errorf("지원하지 않는 URI 스킴: %s", parsed.Scheme) } ticket := parsed.Query().Get("token") if ticket == "" { return "", fmt.Errorf("토큰이 없습니다") } token, err := redeemTicket(ticket) if err != nil { return "", fmt.Errorf("런처 인증에 실패했습니다: %w", err) } // JWT는 점(.)으로 구분된 3파트 형식이어야 함 if parts := strings.Split(token, "."); len(parts) != 3 { return "", fmt.Errorf("서버에서 유효하지 않은 토큰을 받았습니다") } return token, nil } // prepareGameDir 게임 디렉토리를 생성하고 프로토콜 등록을 갱신한다. func prepareGameDir() (gameDir, gamePath string, err error) { gameDir, err = installDir() if err != nil { return "", "", err } if err := os.MkdirAll(gameDir, 0755); err != nil { return "", "", fmt.Errorf("게임 디렉토리 생성 실패: %w", err) } gamePath = filepath.Join(gameDir, gameExeName) // 프로토콜 등록이 현재 런처를 가리키도록 갱신 (실패해도 진행) _ = install() // 이전 업데이트에서 남은 .old/.new 파일 정리 cleanupOldFiles(gameDir) return gameDir, gamePath, nil } // tryOfflineLaunch 서버 연결 실패 시 설치된 게임을 직접 실행한다. func tryOfflineLaunch(gamePath, gameDir, token string, serverErr error) error { if _, err := os.Stat(gamePath); err != nil { return fmt.Errorf("버전 확인 실패: %w", serverErr) } ret := msgBox("One of the plans", "서버에 연결할 수 없습니다.\n설치된 게임을 실행하시겠습니까?\n(업데이트 확인 불가)", mbYesNo|mbQ) if ret != idYes { return fmt.Errorf("사용자가 취소했습니다") } return launchGame(gamePath, gameDir, token) } // tryLauncherUpdate 런처 업데이트를 확인하고 필요 시 새 런처로 재시작한다. // 재시작이 필요하면 true를 반환한다. func tryLauncherUpdate(serverInfo *downloadInfo) bool { updated, err := ensureLauncher(serverInfo) if err != nil { return false } if !updated { return false } launcherDir, err := installDir() if err != nil { return false } cmd := exec.Command(filepath.Join(launcherDir, "launcher.exe"), os.Args[1:]...) if err := cmd.Start(); err != nil { return false } os.Exit(0) return true // unreachable, os.Exit 위에서 종료 } // launchGame 게임 프로세스를 시작한다. func launchGame(gamePath, gameDir, token string) error { cmd := exec.Command(gamePath, "-token", token) cmd.Dir = gameDir if err := cmd.Start(); err != nil { return fmt.Errorf("게임 실행 실패: %w", err) } return nil }