package main import ( "fmt" "os" "runtime" "syscall" "unsafe" "golang.org/x/sys/windows" ) // ── 윈도우 핸들 (진행 창에서 사용) ────────────────────────── 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)) } func getSystemDPI() uint32 { dpi, _, _ := getDpiForSystemProc.Call() if dpi == 0 { return 96 } return uint32(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 { weight = 700 // FW_BOLD } lf := logFontW{ lfHeight: -int32(pointSize) * int32(dpi) / 72, lfWeight: weight, lfCharSet: 1, // DEFAULT_CHARSET lfQuality: 5, // CLEARTYPE_QUALITY } face, _ := windows.UTF16FromString("Segoe UI") copy(lf.lfFaceName[:], face) font, _, _ := createFontIndirectWProc.Call(uintptr(unsafe.Pointer(&lf))) return font } // ── 메시지 박스 ────────────────────────────────────────────── // exitWithError 오류 메시지를 표시하고 프로그램을 종료한다. func exitWithError(msg string) { msgBox("One of the plans 런처 - 오류", msg, mbOK|mbError) os.Exit(1) } func msgBox(title, text string, flags uintptr) int { t, err := windows.UTF16PtrFromString(title) if err != nil { return 0 } m, err := windows.UTF16PtrFromString(text) if err != nil { return 0 } ret, _, _ := messageBoxWProc.Call(0, uintptr(unsafe.Pointer(m)), uintptr(unsafe.Pointer(t)), flags) 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: ret := msgBox("One of the plans 런처", "다운로드를 취소하시겠습니까?", mbYesNo|mbQ) if ret == idYes { downloadCancelled.Store(true) destroyWindowProc.Call(hwnd) } return 0 case wmDestroy: postQuitMsgProc.Call(0) return 0 case wmAppDone: destroyWindowProc.Call(hwnd) return 0 case wmCtlColorStatic: hdc := wParam setBkModeProc.Call(hdc, setBkModeTransparent) if lParam == titleLabelHwnd { setTextColorProc.Call(hdc, colorAccent) } else { setTextColorProc.Call(hdc, colorText) } return hBrushBg } ret, _, _ := defWindowProcWProc.Call(hwnd, uMsg, wParam, lParam) return ret } // setProgress 진행 창의 텍스트와 퍼센트를 갱신한다. func setProgress(text string, pct int) { if text != "" { t, err := windows.UTF16PtrFromString(text) if err == nil { sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetText), 0, uintptr(unsafe.Pointer(t))) } } if pct >= 0 { sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetPos), uintptr(pct), 0) } } // createProgressWindow 다크 테마 진행 창을 생성하고 핸들을 반환한다. func createProgressWindow(dpi uint32) (hwnd uintptr, err error) { s := func(px int) uintptr { return dpiScale(px, dpi) } hBrushBg, _, _ = createSolidBrushProc.Call(colorBg) hInstance, _, _ := getModuleHandleWProc.Call(0) className, _ := windows.UTF16PtrFromString("A301Progress") wc := wndClassExW{ cbSize: uint32(unsafe.Sizeof(wndClassExW{})), lpfnWndProc: wndProcCb, hInstance: hInstance, lpszClassName: className, hbrBackground: hBrushBg, } atom, _, _ := registerClassExWProc.Call(uintptr(unsafe.Pointer(&wc))) if atom == 0 { return 0, fmt.Errorf("윈도우 클래스 등록 실패") } // 화면 중앙에 배치 screenW, _, _ := getSystemMetricsProc.Call(smCxScreen) screenH, _, _ := getSystemMetricsProc.Call(smCyScreen) winW := s(440) winH := s(152) x := (screenW - winW) / 2 y := (screenH - winH) / 2 titleStr, _ := windows.UTF16PtrFromString("One of the plans 런처") hwnd, _, _ = createWindowExWProc.Call( 0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(titleStr)), wsPopup|wsCaption|wsSysMenu|wsVisible, x, y, winW, winH, 0, 0, hInstance, 0, ) if hwnd == 0 { return 0, fmt.Errorf("다운로드 창 생성 실패") } // 폰트 titleFont := createUIFont(13, dpi, true) statusFont := createUIFont(9, dpi, false) // 타이틀 라벨 staticClass, _ := windows.UTF16PtrFromString("STATIC") titleText, _ := windows.UTF16PtrFromString("One of the plans") titleLabelHwnd, _, _ = createWindowExWProc.Call( 0, uintptr(unsafe.Pointer(staticClass)), uintptr(unsafe.Pointer(titleText)), wsChild|wsVisible|ssCenter, s(20), s(14), winW-s(40), s(28), hwnd, 0, hInstance, 0, ) sendMessageWProc.Call(titleLabelHwnd, uintptr(wmSetFont), titleFont, 1) // 상태 라벨 initText, _ := windows.UTF16PtrFromString("게임 파일을 다운로드하는 중...") progressLabelHwnd, _, _ = createWindowExWProc.Call( 0, uintptr(unsafe.Pointer(staticClass)), uintptr(unsafe.Pointer(initText)), wsChild|wsVisible|ssCenter, s(20), s(52), winW-s(40), s(20), hwnd, 0, hInstance, 0, ) sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetFont), statusFont, 1) // 프로그레스바 progressClass, _ := windows.UTF16PtrFromString("msctls_progress32") progressBarHwnd, _, _ = createWindowExWProc.Call( 0, uintptr(unsafe.Pointer(progressClass)), 0, wsChild|wsVisible|pbsSmooth, s(20), s(82), winW-s(40), s(18), hwnd, 0, hInstance, 0, ) 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) sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetBkColor), 0, colorProgressBg) 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) if ret == 0 || ret == ^uintptr(0) { break } translateMsgProc.Call(uintptr(unsafe.Pointer(&m))) dispatchMsgWProc.Call(uintptr(unsafe.Pointer(&m))) } return <-errCh }