Compare commits

...

4 Commits

Author SHA1 Message Date
10651d294a docs: README, CLAUDE.md 작성
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 00:20:43 +09:00
f77a7e0e23 feat: 게임 가제 'One of the plans' UI 텍스트 적용
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 00:18:13 +09:00
28c1b377df feat: 다운로드 창 다크 테마 적용
- 배경 #2E2C2F, 타이틀 #BACDB0(강조색), 상태 텍스트 밝은 회색
- WM_CTLCOLORSTATIC으로 STATIC 컨트롤 색상 제어
- SetWindowTheme + PBM_SETBARCOLOR로 진행 막대 색상 변경
- "A301" 타이틀 레이블(13pt bold) + 상태 레이블(9pt) 분리
- 웹사이트 색상 팔레트와 통일

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 00:14:39 +09:00
22b5efdaab feat: DPI 인식 및 진행 막대 UI 추가
- SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2)로 고DPI 디스플레이 지원
- GetDpiForSystem()으로 시스템 DPI 조회 후 모든 크기 동적 스케일링
- 다운로드 진행창에 msctls_progress32 진행 막대 추가 (0~100%)
- Segoe UI 9pt 폰트를 DPI에 맞게 CreateFontIndirectW로 생성
- 4K(200% / 192 DPI)와 FHD(100% / 96 DPI) 모두 동일한 시각적 크기 보장

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 00:08:06 +09:00
3 changed files with 353 additions and 57 deletions

51
CLAUDE.md Normal file
View File

@@ -0,0 +1,51 @@
# CLAUDE.md
## Build Command
```bash
C:\Users\98kim\sdk\go1.25.1\bin\go.exe build -ldflags="-H windowsgui -s -w" -o launcher.exe .
```
`-H windowsgui` 필수 — 없으면 실행 시 콘솔 창이 함께 열림.
## Tech Stack
- **Go** 단일 파일 (`main.go`)
- **Win32 API** — `user32.dll`, `gdi32.dll`, `comctl32.dll`, `uxtheme.dll`, `shell32.dll`
- `golang.org/x/sys/windows` + `windows/registry`
## Project Purpose
"One of the plans" Windows 런처.
`a301://` 커스텀 URI 프로토콜 등록 → 게임 자동 다운로드/업데이트/실행.
## Architecture
모든 로직이 `main.go` 단일 파일에 있음:
- **`enableDPIAwareness()`** — `SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2)` 호출. `main()` 첫 줄에서 실행.
- **`downloadWithProgress()`** — Win32 메시지 루프 직접 운영. 반드시 메인 고루틴에서 호출 (`runtime.LockOSThread`).
- **`progressWndProc()`** — `WM_CTLCOLORSTATIC`으로 다크 테마 적용. `hBrushBg` 전역 변수 참조.
- **`setProgress(text, pct)`** — 다운로드 고루틴에서 호출해 레이블 텍스트와 진행 막대 동시 업데이트.
- **`fetchServerInfo()`** — `https://a301.api.tolelom.xyz/api/download/info` 조회.
- **`ensureGame()`** — `A301.exe` SHA256 해시 비교 후 불일치 시 재다운로드.
## UI Details
- DPI: `GetDpiForSystem()``dpiScale(px, dpi)` 함수로 모든 크기 동적 계산
- 진행 막대 색: `SetWindowTheme("", "")` 비주얼 스타일 비활성화 후 `PBM_SETBARCOLOR` 적용
- 다크 배경: `WM_CTLCOLORSTATIC` 핸들러에서 `hBrushBg` 반환 + `SetBkMode(TRANSPARENT)`
- 타이틀 레이블(`titleLabelHwnd`)만 강조색, 나머지 STATIC은 밝은 회색
## Key Constants
```go
serverInfoURL = "https://a301.api.tolelom.xyz/api/download/info"
gameExeName = "A301.exe" // 기술 식별자 — 게임 표기명과 별개
protocolName = "a301" // 기술 식별자
```
## Notes
- `extractZip()` — zip 내 최상위 디렉토리 1단계 제거 후 추출. `launcher.exe` 자신은 덮어쓰기 방지.
- 레지스트리는 `HKCU` (현재 사용자) 에만 쓰므로 관리자 권한 불필요.

64
README.md Normal file
View File

@@ -0,0 +1,64 @@
# One of the plans — Launcher
Windows 전용 게임 런처. `a301://` 커스텀 URI 프로토콜을 등록하고, 웹에서 게임 시작 버튼 클릭 시 게임을 자동 다운로드/업데이트/실행합니다.
## 빌드
```bash
# Go 1.24+ 필요
go build -ldflags="-H windowsgui -s -w" -o launcher.exe .
```
`-H windowsgui` — 콘솔 창 없이 실행 (Win32 GUI 앱)
`-s -w` — 디버그 심볼 제거 (바이너리 크기 축소)
## 사용법
| 실행 방법 | 동작 |
|-----------|------|
| `launcher.exe` (인자 없음) | `a301://` 프로토콜 등록 확인 다이얼로그 |
| `launcher.exe install` | 프로토콜 강제 등록 |
| `launcher.exe uninstall` | 프로토콜 제거 |
| `launcher.exe "a301://launch?token=<JWT>"` | 게임 실행 (웹에서 자동 호출) |
## 게임 실행 흐름
```
웹 "게임 시작" 클릭
└─ a301://launch?token=JWT
└─ launcher.exe 실행
├─ 서버에서 최신 버전 정보 조회 (fileHash, url)
├─ A301.exe 없음 또는 해시 불일치 → 다운로드 창 표시
│ ├─ game.zip 다운로드 (진행률 표시)
│ └─ 압축 해제 (launcher.exe 자신은 덮어쓰기 방지)
└─ A301.exe -token <JWT> 실행
```
## UI
다크 테마 Win32 창 (배경 `#2E2C2F`, 강조색 `#BACDB0`).
4K / FHD 해상도 모두 자연스럽게 표시 (DPI 인식 + 동적 스케일링).
## 배포 구조
유저가 받는 구성:
```
(게임 폴더)/
├── launcher.exe ← 이 파일
└── A301.exe ← 런처가 자동 다운로드
```
`launcher.exe`를 처음 실행하면 레지스트리에 `a301://` 프로토콜을 등록하고, 이후 웹에서 게임 시작 버튼을 누를 때마다 자동으로 이 런처가 실행됩니다.
## 레지스트리 등록 경로
```
HKCU\Software\Classes\a301\
HKCU\Software\Classes\a301\shell\open\command → "launcher.exe" "%1"
```
## 의존성
- `golang.org/x/sys/windows` — Win32 API
- `golang.org/x/sys/windows/registry` — 레지스트리 접근

295
main.go
View File

@@ -30,20 +30,31 @@ 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
wmCtlColorStatic uint32 = 0x0138
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
pbmSetBarColor uint32 = 0x0409
pbmSetBkColor uint32 = 0x2001
setBkModeTransparent = 1
swShow = 5
smCxScreen = 0
smCyScreen = 1
mbOK uintptr = 0x00000000
mbInfo uintptr = 0x00000040
@@ -51,32 +62,62 @@ const (
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")
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")
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")
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")
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 {
@@ -104,11 +145,82 @@ 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, 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
}
// ── 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 +235,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) {
@@ -135,22 +247,45 @@ func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr {
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 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 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) }
// 배경 브러시 생성 (window proc에서도 사용)
hBrushBg, _, _ = createSolidBrushProc.Call(colorBg)
defer deleteObjectProc.Call(hBrushBg)
hInstance, _, _ := getModuleHandleWProc.Call(0)
className, _ := windows.UTF16PtrFromString("A301Progress")
@@ -159,36 +294,80 @@ func downloadWithProgress(downloadURL, destDir string) error {
lpfnWndProc: wndProcCb,
hInstance: hInstance,
lpszClassName: className,
hbrBackground: 16, // COLOR_BTNFACE + 1
hbrBackground: hBrushBg,
}
registerClassExWProc.Call(uintptr(unsafe.Pointer(&wc)))
screenW, _, _ := getSystemMetricsProc.Call(smCxScreen)
screenH, _, _ := getSystemMetricsProc.Call(smCyScreen)
const winW, winH = 420, 110
winW := s(440)
winH := s(152)
x := (screenW - winW) / 2
y := (screenH - winH) / 2
title, _ := windows.UTF16PtrFromString("A301 - 게임 설치")
// 창 타이틀은 비워서 타이틀바를 최소화
titleStr, _ := windows.UTF16PtrFromString("One of the plans 런처")
hwnd, _, _ := createWindowExWProc.Call(
0,
uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(title)),
wsPopup|wsCaption|wsVisible,
uintptr(unsafe.Pointer(titleStr)),
wsPopup|wsCaption|wsSysMenu|wsVisible,
x, y, winW, winH,
0, 0, hInstance, 0,
)
titleFont := createUIFont(13, dpi, true)
defer deleteObjectProc.Call(titleFont)
statusFont := createUIFont(9, dpi, false)
defer deleteObjectProc.Call(statusFont)
staticClass, _ := windows.UTF16PtrFromString("STATIC")
// ── "A301" 타이틀 레이블 ──
// 클라이언트 영역 레이아웃 (base 96 DPI):
// y=14 h=28 → "A301" 타이틀 (13pt bold, 강조색)
// y=52 h=20 → 상태 텍스트 (9pt, 밝은 회색)
// y=82 h=18 → 진행 막대
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,
10, 35, 400, 30,
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)
// 비주얼 스타일 비활성화 → PBM_SETBARCOLOR/PBM_SETBKCOLOR 적용 가능
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)
@@ -239,8 +418,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 +434,7 @@ func doDownload(downloadURL, destDir string) error {
tmpFile.Close()
defer os.Remove(tmpPath)
setProgressText("압축을 해제하는 중...")
setProgress("압축을 해제하는 중...", -1)
return extractZip(tmpPath, destDir)
}
@@ -317,7 +496,7 @@ func extractZip(zipPath, destDir string) error {
return nil
}
// ── Server info ────────────────────────────────────────────────────────────
// ── Server info ──────────────────────────────────────────────────────────────
type downloadInfo struct {
FileHash string `json:"fileHash"`
@@ -358,7 +537,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 +547,7 @@ func launcherPath() (string, error) {
return filepath.Abs(exe)
}
// ── Protocol install / uninstall ───────────────────────────────────────────
// ── Protocol install / uninstall ─────────────────────────────────────────────
func install() error {
exePath, err := launcherPath()
@@ -381,7 +560,7 @@ func install() error {
return fmt.Errorf("레지스트리 키 생성 실패: %w", err)
}
defer key.Close()
key.SetStringValue("", "URL:A301 Protocol")
key.SetStringValue("", "URL:One of the plans Protocol")
key.SetStringValue("URL Protocol", "")
cmdKey, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\`+protocolName+`\shell\open\command`, registry.SET_VALUE)
@@ -407,7 +586,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 +615,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,19 +652,21 @@ 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)
ret := msgBox("One of the plans 런처", "게임 실행을 위해 프로토콜을 등록합니다.\n계속하시겠습니까?", mbYesNo|mbQ)
if ret != idYes {
return
}
if err := install(); err != nil {
msgBox("A301 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbError)
msgBox("One of the plans 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbError)
os.Exit(1)
}
msgBox("A301 런처", "a301:// 프로토콜이 등록되었습니다.\n이제 웹에서 게임 시작 버튼을 사용할 수 있습니다.", mbOK|mbInfo)
msgBox("One of the plans 런처", "a301:// 프로토콜이 등록되었습니다.\n이제 웹에서 게임 시작 버튼을 사용할 수 있습니다.", mbOK|mbInfo)
return
}
@@ -493,33 +674,33 @@ func main() {
switch {
case arg == "install":
if err := install(); err != nil {
msgBox("A301 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbError)
msgBox("One of the plans 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbError)
os.Exit(1)
}
msgBox("A301 런처", "a301:// 프로토콜이 등록되었습니다.", mbOK|mbInfo)
msgBox("One of the plans 런처", "a301:// 프로토콜이 등록되었습니다.", mbOK|mbInfo)
case arg == "uninstall":
if err := uninstall(); err != nil {
msgBox("A301 런처 - 오류", fmt.Sprintf("제거 실패:\n%v", err), mbOK|mbError)
msgBox("One of the plans 런처 - 오류", fmt.Sprintf("제거 실패:\n%v", err), mbOK|mbError)
os.Exit(1)
}
msgBox("A301 런처", "a301:// 프로토콜이 제거되었습니다.", mbOK|mbInfo)
msgBox("One of the plans 런처", "a301:// 프로토콜이 제거되었습니다.", mbOK|mbInfo)
case strings.HasPrefix(arg, protocolName+"://"):
if err := handleURI(arg); err != nil {
if strings.Contains(err.Error(), "버전이 최신이 아닙니다") {
ret := msgBox("A301 - 업데이트 필요", "새로운 버전이 있습니다. 다운로드 페이지로 이동할까요?", mbYesNo|mbInfo)
ret := msgBox("One of the plans - 업데이트 필요", "새로운 버전이 있습니다. 다운로드 페이지로 이동할까요?", mbYesNo|mbInfo)
if ret == idYes {
openBrowser(webURL)
}
} else {
msgBox("A301 런처 - 오류", fmt.Sprintf("실행 실패:\n%v", err), mbOK|mbError)
msgBox("One of the plans 런처 - 오류", fmt.Sprintf("실행 실패:\n%v", err), mbOK|mbError)
}
os.Exit(1)
}
default:
msgBox("A301 런처 - 오류", fmt.Sprintf("알 수 없는 명령: %s", arg), mbOK|mbError)
msgBox("One of the plans 런처 - 오류", fmt.Sprintf("알 수 없는 명령: %s", arg), mbOK|mbError)
os.Exit(1)
}
}