Files
a301_launcher/game.go
tolelom 281a365952
Some checks failed
CI/CD / test (push) Has been cancelled
CI/CD / release (push) Has been cancelled
refactor: 코드 가독성 개선 및 버그 수정
- main.go를 main()만 남기고 함수 분리 (game.go, protocol.go, ui.go)
- 재시도 로직을 retryWithBackoff 공통 함수로 통합
- redeemTicketFrom 별도 HTTP 클라이언트 → apiClient 사용으로 통일
- doDownload에서 resumeOffset 이중 계산 제거
- extractZip에서 stripTopDir/extractFile 함수 분리
- downloadWithProgress에서 createProgressWindow 함수 분리
- DLL 선언을 DLL별로 그룹화, 상수를 역할별로 분리
- 전체 주석 한국어 통일 및 섹션 구분 추가

버그 수정:
- ensureLauncher가 설치 경로 대신 실행 중인 경로를 해시하던 문제 수정
- uninstall 시 실행 중인 exe 삭제 실패 → 백그라운드 cmd로 대체
- moveContents에서 os.Remove 에러를 무시하던 문제 수정
- install/uninstall 메시지 통일, exitWithError 헬퍼 추가
- .gitignore에 *.exe 통일, ANALYSIS.md 삭제
- 빌드 명령에 git 태그 기반 버전 주입 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 02:17:51 +09:00

142 lines
3.9 KiB
Go

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
}