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>
This commit is contained in:
192
ui.go
192
ui.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
@@ -9,28 +10,41 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Win32 constants
|
||||
// ── Win32 메시지 상수 ────────────────────────────────────────
|
||||
|
||||
const (
|
||||
wmDestroy uint32 = 0x0002
|
||||
wmClose uint32 = 0x0010
|
||||
wmSetFont uint32 = 0x0030
|
||||
wmSetText uint32 = 0x000C
|
||||
wmCtlColorStatic uint32 = 0x0138
|
||||
wmAppDone uint32 = 0x8001
|
||||
wmAppDone uint32 = 0x8001 // 다운로드 완료 시 사용하는 커스텀 메시지
|
||||
)
|
||||
|
||||
// ── 윈도우 스타일 상수 ───────────────────────────────────────
|
||||
|
||||
const (
|
||||
wsPopup uintptr = 0x80000000
|
||||
wsCaption uintptr = 0x00C00000
|
||||
wsSysMenu uintptr = 0x00080000
|
||||
wsChild uintptr = 0x40000000
|
||||
wsVisible uintptr = 0x10000000
|
||||
ssCenter uintptr = 0x00000001
|
||||
)
|
||||
|
||||
// ── 프로그레스바 상수 ────────────────────────────────────────
|
||||
|
||||
const (
|
||||
pbsSmooth uintptr = 0x01
|
||||
pbmSetRange32 uint32 = 0x0406
|
||||
pbmSetPos uint32 = 0x0402
|
||||
pbmSetBarColor uint32 = 0x0409
|
||||
pbmSetBkColor uint32 = 0x2001
|
||||
)
|
||||
|
||||
// ── 기타 Win32 상수 ──────────────────────────────────────────
|
||||
|
||||
const (
|
||||
setBkModeTransparent = 1
|
||||
|
||||
swShow = 5
|
||||
@@ -47,7 +61,9 @@ const (
|
||||
iccProgressClass uint32 = 0x00000020
|
||||
)
|
||||
|
||||
// rgb builds a COLORREF from R, G, B components.
|
||||
// ── 색상 ─────────────────────────────────────────────────────
|
||||
|
||||
// rgb COLORREF(0x00BBGGRR) 값을 생성한다.
|
||||
func rgb(r, g, b uint8) uintptr {
|
||||
return uintptr(r) | (uintptr(g) << 8) | (uintptr(b) << 16)
|
||||
}
|
||||
@@ -60,13 +76,18 @@ var (
|
||||
colorProgressBg = rgb(65, 63, 67) // bg보다 약간 밝은 색
|
||||
)
|
||||
|
||||
// ── DLL 및 프로시저 ──────────────────────────────────────────
|
||||
|
||||
var (
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
gdi32 = windows.NewLazySystemDLL("gdi32.dll")
|
||||
comctl32 = windows.NewLazySystemDLL("comctl32.dll")
|
||||
uxtheme = windows.NewLazySystemDLL("uxtheme.dll")
|
||||
)
|
||||
|
||||
// user32.dll
|
||||
var (
|
||||
messageBoxWProc = user32.NewProc("MessageBoxW")
|
||||
registerClassExWProc = user32.NewProc("RegisterClassExW")
|
||||
createWindowExWProc = user32.NewProc("CreateWindowExW")
|
||||
@@ -85,23 +106,31 @@ var (
|
||||
setProcessDpiAwarenessContextProc = user32.NewProc("SetProcessDpiAwarenessContext")
|
||||
findWindowWProc = user32.NewProc("FindWindowW")
|
||||
setForegroundWindowProc = user32.NewProc("SetForegroundWindow")
|
||||
createMutexWProc = kernel32.NewProc("CreateMutexW")
|
||||
getModuleHandleWProc = kernel32.NewProc("GetModuleHandleW")
|
||||
createFontIndirectWProc = gdi32.NewProc("CreateFontIndirectW")
|
||||
createSolidBrushProc = gdi32.NewProc("CreateSolidBrush")
|
||||
setTextColorProc = gdi32.NewProc("SetTextColor")
|
||||
setBkModeProc = gdi32.NewProc("SetBkMode")
|
||||
deleteObjectProc = gdi32.NewProc("DeleteObject")
|
||||
initCommonControlsExProc = comctl32.NewProc("InitCommonControlsEx")
|
||||
setWindowThemeProc = uxtheme.NewProc("SetWindowTheme")
|
||||
|
||||
wndProcCb uintptr
|
||||
titleLabelHwnd uintptr
|
||||
progressLabelHwnd uintptr
|
||||
progressBarHwnd uintptr
|
||||
hBrushBg uintptr
|
||||
)
|
||||
|
||||
// kernel32.dll
|
||||
var (
|
||||
createMutexWProc = kernel32.NewProc("CreateMutexW")
|
||||
getModuleHandleWProc = kernel32.NewProc("GetModuleHandleW")
|
||||
)
|
||||
|
||||
// gdi32.dll
|
||||
var (
|
||||
createFontIndirectWProc = gdi32.NewProc("CreateFontIndirectW")
|
||||
createSolidBrushProc = gdi32.NewProc("CreateSolidBrush")
|
||||
setTextColorProc = gdi32.NewProc("SetTextColor")
|
||||
setBkModeProc = gdi32.NewProc("SetBkMode")
|
||||
deleteObjectProc = gdi32.NewProc("DeleteObject")
|
||||
)
|
||||
|
||||
// comctl32.dll / uxtheme.dll
|
||||
var (
|
||||
initCommonControlsExProc = comctl32.NewProc("InitCommonControlsEx")
|
||||
setWindowThemeProc = uxtheme.NewProc("SetWindowTheme")
|
||||
)
|
||||
|
||||
// ── Win32 구조체 ─────────────────────────────────────────────
|
||||
|
||||
type wndClassExW struct {
|
||||
cbSize uint32
|
||||
style uint32
|
||||
@@ -149,10 +178,23 @@ type logFontW struct {
|
||||
lfFaceName [32]uint16
|
||||
}
|
||||
|
||||
// ── 윈도우 핸들 (진행 창에서 사용) ──────────────────────────
|
||||
|
||||
var (
|
||||
wndProcCb uintptr
|
||||
titleLabelHwnd uintptr
|
||||
progressLabelHwnd uintptr
|
||||
progressBarHwnd uintptr
|
||||
hBrushBg uintptr
|
||||
)
|
||||
|
||||
func init() {
|
||||
wndProcCb = syscall.NewCallback(progressWndProc)
|
||||
}
|
||||
|
||||
// ── DPI ──────────────────────────────────────────────────────
|
||||
|
||||
// enableDPIAwareness Per-Monitor V2 DPI 인식을 선언한다.
|
||||
func enableDPIAwareness() {
|
||||
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 (Windows 10 1703+)
|
||||
setProcessDpiAwarenessContextProc.Call(^uintptr(3))
|
||||
@@ -166,11 +208,13 @@ func getSystemDPI() uint32 {
|
||||
return uint32(dpi)
|
||||
}
|
||||
|
||||
// dpiScale scales a base-96-DPI pixel value to the system DPI.
|
||||
// dpiScale 96 DPI 기준 픽셀 값을 현재 DPI에 맞게 변환한다.
|
||||
func dpiScale(px int, dpi uint32) uintptr {
|
||||
return uintptr(int(float64(px)*float64(dpi)/96.0 + 0.5))
|
||||
}
|
||||
|
||||
// ── 폰트 ─────────────────────────────────────────────────────
|
||||
|
||||
func createUIFont(pointSize int, dpi uint32, bold bool) uintptr {
|
||||
weight := int32(400) // FW_NORMAL
|
||||
if bold {
|
||||
@@ -185,16 +229,15 @@ func createUIFont(pointSize int, dpi uint32, bold bool) uintptr {
|
||||
face, _ := windows.UTF16FromString("Segoe UI")
|
||||
copy(lf.lfFaceName[:], face)
|
||||
font, _, _ := createFontIndirectWProc.Call(uintptr(unsafe.Pointer(&lf)))
|
||||
// font가 0이면 시스템 기본 폰트가 사용됨 (WM_SETFONT에 0 전달 시 기본값)
|
||||
return font
|
||||
}
|
||||
|
||||
func initCommonControls() {
|
||||
icc := initCommonControlsExS{
|
||||
dwSize: uint32(unsafe.Sizeof(initCommonControlsExS{})),
|
||||
dwICC: iccProgressClass,
|
||||
}
|
||||
initCommonControlsExProc.Call(uintptr(unsafe.Pointer(&icc)))
|
||||
// ── 메시지 박스 ──────────────────────────────────────────────
|
||||
|
||||
// exitWithError 오류 메시지를 표시하고 프로그램을 종료한다.
|
||||
func exitWithError(msg string) {
|
||||
msgBox("One of the plans 런처 - 오류", msg, mbOK|mbError)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func msgBox(title, text string, flags uintptr) int {
|
||||
@@ -210,6 +253,37 @@ func msgBox(title, text string, flags uintptr) int {
|
||||
return int(ret)
|
||||
}
|
||||
|
||||
// ── 단일 인스턴스 ────────────────────────────────────────────
|
||||
|
||||
func acquireSingleInstance() bool {
|
||||
name, _ := windows.UTF16PtrFromString("Global\\A301LauncherMutex")
|
||||
_, _, err := createMutexWProc.Call(0, 0, uintptr(unsafe.Pointer(name)))
|
||||
// ERROR_ALREADY_EXISTS = 183
|
||||
if errno, ok := err.(syscall.Errno); ok && errno == 183 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func activateExistingWindow() {
|
||||
className, _ := windows.UTF16PtrFromString("A301Progress")
|
||||
hwnd, _, _ := findWindowWProc.Call(uintptr(unsafe.Pointer(className)), 0)
|
||||
if hwnd != 0 {
|
||||
setForegroundWindowProc.Call(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 진행 창 (프로그레스 윈도우) ──────────────────────────────
|
||||
|
||||
func initCommonControls() {
|
||||
icc := initCommonControlsExS{
|
||||
dwSize: uint32(unsafe.Sizeof(initCommonControlsExS{})),
|
||||
dwICC: iccProgressClass,
|
||||
}
|
||||
initCommonControlsExProc.Call(uintptr(unsafe.Pointer(&icc)))
|
||||
}
|
||||
|
||||
// progressWndProc 진행 창의 메시지 처리 프로시저.
|
||||
func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr {
|
||||
switch uint32(uMsg) {
|
||||
case wmClose:
|
||||
@@ -239,6 +313,7 @@ func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr {
|
||||
return ret
|
||||
}
|
||||
|
||||
// setProgress 진행 창의 텍스트와 퍼센트를 갱신한다.
|
||||
func setProgress(text string, pct int) {
|
||||
if text != "" {
|
||||
t, err := windows.UTF16PtrFromString(text)
|
||||
@@ -251,19 +326,11 @@ func setProgress(text string, pct int) {
|
||||
}
|
||||
}
|
||||
|
||||
// downloadWithProgress shows a DPI-aware dark-themed progress window and downloads+extracts the zip.
|
||||
// Must be called from the main goroutine (Win32 message loop requirement).
|
||||
func downloadWithProgress(downloadURL, destDir string) error {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
initCommonControls()
|
||||
dpi := getSystemDPI()
|
||||
// createProgressWindow 다크 테마 진행 창을 생성하고 핸들을 반환한다.
|
||||
func createProgressWindow(dpi uint32) (hwnd uintptr, err error) {
|
||||
s := func(px int) uintptr { return dpiScale(px, dpi) }
|
||||
|
||||
hBrushBg, _, _ = createSolidBrushProc.Call(colorBg)
|
||||
defer deleteObjectProc.Call(hBrushBg)
|
||||
|
||||
hInstance, _, _ := getModuleHandleWProc.Call(0)
|
||||
|
||||
className, _ := windows.UTF16PtrFromString("A301Progress")
|
||||
@@ -276,9 +343,10 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
||||
}
|
||||
atom, _, _ := registerClassExWProc.Call(uintptr(unsafe.Pointer(&wc)))
|
||||
if atom == 0 {
|
||||
return fmt.Errorf("윈도우 클래스 등록 실패")
|
||||
return 0, fmt.Errorf("윈도우 클래스 등록 실패")
|
||||
}
|
||||
|
||||
// 화면 중앙에 배치
|
||||
screenW, _, _ := getSystemMetricsProc.Call(smCxScreen)
|
||||
screenH, _, _ := getSystemMetricsProc.Call(smCyScreen)
|
||||
winW := s(440)
|
||||
@@ -287,7 +355,7 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
||||
y := (screenH - winH) / 2
|
||||
|
||||
titleStr, _ := windows.UTF16PtrFromString("One of the plans 런처")
|
||||
hwnd, _, _ := createWindowExWProc.Call(
|
||||
hwnd, _, _ = createWindowExWProc.Call(
|
||||
0,
|
||||
uintptr(unsafe.Pointer(className)),
|
||||
uintptr(unsafe.Pointer(titleStr)),
|
||||
@@ -296,16 +364,15 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
||||
0, 0, hInstance, 0,
|
||||
)
|
||||
if hwnd == 0 {
|
||||
return fmt.Errorf("다운로드 창 생성 실패")
|
||||
return 0, fmt.Errorf("다운로드 창 생성 실패")
|
||||
}
|
||||
|
||||
// 폰트
|
||||
titleFont := createUIFont(13, dpi, true)
|
||||
defer deleteObjectProc.Call(titleFont)
|
||||
statusFont := createUIFont(9, dpi, false)
|
||||
defer deleteObjectProc.Call(statusFont)
|
||||
|
||||
// 타이틀 라벨
|
||||
staticClass, _ := windows.UTF16PtrFromString("STATIC")
|
||||
|
||||
titleText, _ := windows.UTF16PtrFromString("One of the plans")
|
||||
titleLabelHwnd, _, _ = createWindowExWProc.Call(
|
||||
0,
|
||||
@@ -317,6 +384,7 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
||||
)
|
||||
sendMessageWProc.Call(titleLabelHwnd, uintptr(wmSetFont), titleFont, 1)
|
||||
|
||||
// 상태 라벨
|
||||
initText, _ := windows.UTF16PtrFromString("게임 파일을 다운로드하는 중...")
|
||||
progressLabelHwnd, _, _ = createWindowExWProc.Call(
|
||||
0,
|
||||
@@ -328,6 +396,7 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
||||
)
|
||||
sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetFont), statusFont, 1)
|
||||
|
||||
// 프로그레스바
|
||||
progressClass, _ := windows.UTF16PtrFromString("msctls_progress32")
|
||||
progressBarHwnd, _, _ = createWindowExWProc.Call(
|
||||
0,
|
||||
@@ -339,6 +408,7 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
||||
)
|
||||
sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetRange32), 0, 100)
|
||||
|
||||
// 비주얼 스타일 비활성화 후 커스텀 색상 적용
|
||||
empty, _ := windows.UTF16PtrFromString("")
|
||||
setWindowThemeProc.Call(progressBarHwnd, uintptr(unsafe.Pointer(empty)), uintptr(unsafe.Pointer(empty)))
|
||||
sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetBarColor), 0, colorAccent)
|
||||
@@ -347,14 +417,34 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
||||
showWindowProc.Call(hwnd, swShow)
|
||||
updateWindowProc.Call(hwnd)
|
||||
|
||||
return hwnd, nil
|
||||
}
|
||||
|
||||
// downloadWithProgress 진행 창을 표시하면서 파일을 다운로드하고 추출한다.
|
||||
// Win32 메시지 루프를 사용하므로 반드시 메인 고루틴에서 호출해야 한다.
|
||||
func downloadWithProgress(downloadURL, destDir string) error {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
initCommonControls()
|
||||
dpi := getSystemDPI()
|
||||
|
||||
hwnd, err := createProgressWindow(dpi)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer deleteObjectProc.Call(hBrushBg)
|
||||
|
||||
downloadCancelled.Store(false)
|
||||
|
||||
// 별도 고루틴에서 다운로드, 완료 시 wmAppDone 메시지로 창 닫기
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- doDownload(downloadURL, destDir)
|
||||
postMessageWProc.Call(hwnd, uintptr(wmAppDone), 0, 0)
|
||||
}()
|
||||
|
||||
// Win32 메시지 루프
|
||||
var m msgW
|
||||
for {
|
||||
ret, _, _ := getMessageWProc.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
|
||||
@@ -367,21 +457,3 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
||||
|
||||
return <-errCh
|
||||
}
|
||||
|
||||
func activateExistingWindow() {
|
||||
className, _ := windows.UTF16PtrFromString("A301Progress")
|
||||
hwnd, _, _ := findWindowWProc.Call(uintptr(unsafe.Pointer(className)), 0)
|
||||
if hwnd != 0 {
|
||||
setForegroundWindowProc.Call(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
func acquireSingleInstance() bool {
|
||||
name, _ := windows.UTF16PtrFromString("Global\\A301LauncherMutex")
|
||||
_, _, err := createMutexWProc.Call(0, 0, uintptr(unsafe.Pointer(name)))
|
||||
// ERROR_ALREADY_EXISTS = 183
|
||||
if errno, ok := err.(syscall.Errno); ok && errno == 183 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user