diff --git a/main.go b/main.go index 822897d..b4bfe6a 100644 --- a/main.go +++ b/main.go @@ -30,20 +30,26 @@ const ( // Win32 constants const ( - wmDestroy uint32 = 0x0002 - wmClose uint32 = 0x0010 - wmSetText uint32 = 0x000C - wmAppDone uint32 = 0x8001 + wmDestroy uint32 = 0x0002 + wmClose uint32 = 0x0010 + wmSetFont uint32 = 0x0030 + wmSetText uint32 = 0x000C + wmAppDone uint32 = 0x8001 wsPopup uintptr = 0x80000000 wsCaption uintptr = 0x00C00000 + wsSysMenu uintptr = 0x00080000 wsChild uintptr = 0x40000000 wsVisible uintptr = 0x10000000 ssCenter uintptr = 0x00000001 - swShow = 5 - smCxScreen = 0 - smCyScreen = 1 + pbsSmooth uintptr = 0x01 + pbmSetRange32 uint32 = 0x0406 + pbmSetPos uint32 = 0x0402 + + swShow = 5 + smCxScreen = 0 + smCyScreen = 1 mbOK uintptr = 0x00000000 mbInfo uintptr = 0x00000040 @@ -51,32 +57,43 @@ const ( mbYesNo uintptr = 0x00000004 mbQ uintptr = 0x00000020 idYes = 6 + + iccProgressClass uint32 = 0x00000020 + logpixelsX = 88 ) var ( - user32 = windows.NewLazySystemDLL("user32.dll") + user32 = windows.NewLazySystemDLL("user32.dll") kernel32 = windows.NewLazySystemDLL("kernel32.dll") - shell32 = windows.NewLazySystemDLL("shell32.dll") + gdi32 = windows.NewLazySystemDLL("gdi32.dll") + shell32 = windows.NewLazySystemDLL("shell32.dll") + comctl32 = windows.NewLazySystemDLL("comctl32.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") - shellExecuteWProc = shell32.NewProc("ShellExecuteW") - getModuleHandleWProc = kernel32.NewProc("GetModuleHandleW") + 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") + shellExecuteWProc = shell32.NewProc("ShellExecuteW") + getModuleHandleWProc = kernel32.NewProc("GetModuleHandleW") + createFontIndirectWProc = gdi32.NewProc("CreateFontIndirectW") + deleteObjectProc = gdi32.NewProc("DeleteObject") + initCommonControlsExProc = comctl32.NewProc("InitCommonControlsEx") wndProcCb uintptr progressLabelHwnd uintptr + progressBarHwnd uintptr ) type wndClassExW struct { @@ -104,11 +121,78 @@ type msgW struct { 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) } -// ── Win32 helpers ────────────────────────────────────────────────────────── +// ── DPI helpers ────────────────────────────────────────────────────────────── + +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(px * int(dpi) / 96) +} + +// ── Font helpers ───────────────────────────────────────────────────────────── + +func createUIFont(pointSize int, dpi uint32) uintptr { + lf := logFontW{ + lfHeight: -int32(pointSize) * int32(dpi) / 72, + lfWeight: 400, // FW_NORMAL + 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 +} + +// ── Common controls ────────────────────────────────────────────────────────── + +func initCommonControls() { + icc := initCommonControlsExS{ + dwSize: uint32(unsafe.Sizeof(initCommonControlsExS{})), + dwICC: iccProgressClass, + } + initCommonControlsExProc.Call(uintptr(unsafe.Pointer(&icc))) +} + +// ── Win32 helpers ──────────────────────────────────────────────────────────── func msgBox(title, text string, flags uintptr) int { t, _ := windows.UTF16PtrFromString(title) @@ -123,7 +207,7 @@ func openBrowser(rawURL string) { shellExecuteWProc.Call(0, uintptr(unsafe.Pointer(op)), uintptr(unsafe.Pointer(u)), 0, 0, 1) } -// ── Progress window ──────────────────────────────────────────────────────── +// ── Progress window ────────────────────────────────────────────────────────── func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr { switch uint32(uMsg) { @@ -140,17 +224,26 @@ func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr { return ret } -func setProgressText(text string) { - t, _ := windows.UTF16PtrFromString(text) - sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetText), 0, uintptr(unsafe.Pointer(t))) +func setProgress(text string, pct int) { + if text != "" { + t, _ := windows.UTF16PtrFromString(text) + sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetText), 0, uintptr(unsafe.Pointer(t))) + } + if pct >= 0 { + sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetPos), uintptr(pct), 0) + } } -// downloadWithProgress shows a progress window and downloads+extracts the zip. +// downloadWithProgress shows a DPI-aware 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) } + hInstance, _, _ := getModuleHandleWProc.Call(0) className, _ := windows.UTF16PtrFromString("A301Progress") @@ -159,13 +252,14 @@ func downloadWithProgress(downloadURL, destDir string) error { lpfnWndProc: wndProcCb, hInstance: hInstance, lpszClassName: className, - hbrBackground: 16, // COLOR_BTNFACE + 1 + hbrBackground: 16, // COLOR_BTNFACE+1 } registerClassExWProc.Call(uintptr(unsafe.Pointer(&wc))) screenW, _, _ := getSystemMetricsProc.Call(smCxScreen) screenH, _, _ := getSystemMetricsProc.Call(smCyScreen) - const winW, winH = 420, 110 + winW := s(440) + winH := s(130) x := (screenW - winW) / 2 y := (screenH - winH) / 2 @@ -174,11 +268,15 @@ func downloadWithProgress(downloadURL, destDir string) error { 0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(title)), - wsPopup|wsCaption|wsVisible, + wsPopup|wsCaption|wsSysMenu|wsVisible, x, y, winW, winH, 0, 0, hInstance, 0, ) + font := createUIFont(9, dpi) + defer deleteObjectProc.Call(font) + + // 상태 레이블 (텍스트) staticClass, _ := windows.UTF16PtrFromString("STATIC") initText, _ := windows.UTF16PtrFromString("게임 파일을 다운로드하는 중...") progressLabelHwnd, _, _ = createWindowExWProc.Call( @@ -186,9 +284,22 @@ func downloadWithProgress(downloadURL, destDir string) error { uintptr(unsafe.Pointer(staticClass)), uintptr(unsafe.Pointer(initText)), wsChild|wsVisible|ssCenter, - 10, 35, 400, 30, + s(16), s(20), winW-s(32), s(22), hwnd, 0, hInstance, 0, ) + sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetFont), font, 1) + + // 진행 막대 + progressClass, _ := windows.UTF16PtrFromString("msctls_progress32") + progressBarHwnd, _, _ = createWindowExWProc.Call( + 0, + uintptr(unsafe.Pointer(progressClass)), + 0, + wsChild|wsVisible|pbsSmooth, + s(16), s(52), winW-s(32), s(22), + hwnd, 0, hInstance, 0, + ) + sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetRange32), 0, 100) showWindowProc.Call(hwnd, swShow) updateWindowProc.Call(hwnd) @@ -239,8 +350,8 @@ func doDownload(downloadURL, destDir string) error { } downloaded += int64(n) if total > 0 { - pct := downloaded * 100 / total - setProgressText(fmt.Sprintf("다운로드 중... %d%%", pct)) + pct := int(downloaded * 100 / total) + setProgress(fmt.Sprintf("다운로드 중... %d%%", pct), pct) } } if err == io.EOF { @@ -255,7 +366,7 @@ func doDownload(downloadURL, destDir string) error { tmpFile.Close() defer os.Remove(tmpPath) - setProgressText("압축을 해제하는 중...") + setProgress("압축을 해제하는 중...", -1) return extractZip(tmpPath, destDir) } @@ -317,7 +428,7 @@ func extractZip(zipPath, destDir string) error { return nil } -// ── Server info ──────────────────────────────────────────────────────────── +// ── Server info ────────────────────────────────────────────────────────────── type downloadInfo struct { FileHash string `json:"fileHash"` @@ -358,7 +469,7 @@ func hashFile(path string) (string, error) { return hex.EncodeToString(h.Sum(nil)), nil } -// ── Launcher path ────────────────────────────────────────────────────────── +// ── Launcher path ──────────────────────────────────────────────────────────── func launcherPath() (string, error) { exe, err := os.Executable() @@ -368,7 +479,7 @@ func launcherPath() (string, error) { return filepath.Abs(exe) } -// ── Protocol install / uninstall ─────────────────────────────────────────── +// ── Protocol install / uninstall ───────────────────────────────────────────── func install() error { exePath, err := launcherPath() @@ -407,7 +518,7 @@ func uninstall() error { return nil } -// ── Game update check + download ─────────────────────────────────────────── +// ── Game update check + download ───────────────────────────────────────────── func ensureGame(gameDir, gamePath string, serverInfo *downloadInfo) error { needsDownload := false @@ -436,7 +547,7 @@ func ensureGame(gameDir, gamePath string, serverInfo *downloadInfo) error { return nil } -// ── URI handler ──────────────────────────────────────────────────────────── +// ── URI handler ────────────────────────────────────────────────────────────── func handleURI(rawURI string) error { parsed, err := url.Parse(rawURI) @@ -473,9 +584,11 @@ func handleURI(rawURI string) error { return nil } -// ── Entry point ──────────────────────────────────────────────────────────── +// ── Entry point ────────────────────────────────────────────────────────────── func main() { + enableDPIAwareness() + if len(os.Args) < 2 { ret := msgBox("A301 런처", "게임 실행을 위해 프로토콜을 등록합니다.\n계속하시겠습니까?", mbYesNo|mbQ) if ret != idYes {