package main import ( "fmt" "runtime" "syscall" "unsafe" "golang.org/x/sys/windows" ) // Win32 constants const ( wmDestroy uint32 = 0x0002 wmClose uint32 = 0x0010 wmSetFont uint32 = 0x0030 wmSetText uint32 = 0x000C wmCtlColorStatic uint32 = 0x0138 wmAppDone uint32 = 0x8001 wsPopup uintptr = 0x80000000 wsCaption uintptr = 0x00C00000 wsSysMenu uintptr = 0x00080000 wsChild uintptr = 0x40000000 wsVisible uintptr = 0x10000000 ssCenter uintptr = 0x00000001 pbsSmooth uintptr = 0x01 pbmSetRange32 uint32 = 0x0406 pbmSetPos uint32 = 0x0402 pbmSetBarColor uint32 = 0x0409 pbmSetBkColor uint32 = 0x2001 setBkModeTransparent = 1 swShow = 5 smCxScreen = 0 smCyScreen = 1 mbOK uintptr = 0x00000000 mbInfo uintptr = 0x00000040 mbError uintptr = 0x00000010 mbYesNo uintptr = 0x00000004 mbQ uintptr = 0x00000020 idYes = 6 iccProgressClass uint32 = 0x00000020 ) // rgb builds a COLORREF from R, G, B components. func rgb(r, g, b uint8) uintptr { return uintptr(r) | (uintptr(g) << 8) | (uintptr(b) << 16) } // 웹사이트 색상과 동일한 팔레트 var ( colorBg = rgb(46, 44, 47) // #2E2C2F colorText = rgb(200, 200, 200) // 밝은 회색 colorAccent = rgb(186, 205, 176) // #BACDB0 colorProgressBg = rgb(65, 63, 67) // bg보다 약간 밝은 색 ) 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") messageBoxWProc = user32.NewProc("MessageBoxW") registerClassExWProc = user32.NewProc("RegisterClassExW") createWindowExWProc = user32.NewProc("CreateWindowExW") showWindowProc = user32.NewProc("ShowWindow") updateWindowProc = user32.NewProc("UpdateWindow") getMessageWProc = user32.NewProc("GetMessageW") translateMsgProc = user32.NewProc("TranslateMessage") dispatchMsgWProc = user32.NewProc("DispatchMessageW") sendMessageWProc = user32.NewProc("SendMessageW") postMessageWProc = user32.NewProc("PostMessageW") defWindowProcWProc = user32.NewProc("DefWindowProcW") destroyWindowProc = user32.NewProc("DestroyWindow") postQuitMsgProc = user32.NewProc("PostQuitMessage") getSystemMetricsProc = user32.NewProc("GetSystemMetrics") getDpiForSystemProc = user32.NewProc("GetDpiForSystem") 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 ) type wndClassExW struct { cbSize uint32 style uint32 lpfnWndProc uintptr cbClsExtra int32 cbWndExtra int32 hInstance uintptr hIcon uintptr hCursor uintptr hbrBackground uintptr lpszMenuName *uint16 lpszClassName *uint16 hIconSm uintptr } type msgW struct { hwnd uintptr message uint32 wParam uintptr lParam uintptr time uint32 ptX int32 ptY int32 } type initCommonControlsExS struct { dwSize uint32 dwICC uint32 } type logFontW struct { lfHeight int32 lfWidth int32 lfEscapement int32 lfOrientation int32 lfWeight int32 lfItalic byte lfUnderline byte lfStrikeOut byte lfCharSet byte lfOutPrecision byte lfClipPrecision byte lfQuality byte lfPitchAndFamily byte lfFaceName [32]uint16 } func init() { wndProcCb = syscall.NewCallback(progressWndProc) } 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 scales a base-96-DPI pixel value to the system 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))) // font가 0이면 시스템 기본 폰트가 사용됨 (WM_SETFONT에 0 전달 시 기본값) return font } func initCommonControls() { icc := initCommonControlsExS{ dwSize: uint32(unsafe.Sizeof(initCommonControlsExS{})), dwICC: iccProgressClass, } initCommonControlsExProc.Call(uintptr(unsafe.Pointer(&icc))) } 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 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 } 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) } } // 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() 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") 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 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 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, 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) downloadCancelled.Store(false) errCh := make(chan error, 1) go func() { errCh <- doDownload(downloadURL, destDir) postMessageWProc.Call(hwnd, uintptr(wmAppDone), 0, 0) }() 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 } 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 }