feat: 자동 다운로드/설치 기능 추가
- 게임 파일 없거나 해시 불일치 시 서버에서 zip 자동 다운로드 - Win32 진행률 창으로 다운로드 진행률 표시 (X% 업데이트) - zip 압축 해제 후 게임 실행 (런처 자신은 덮어쓰기 방지) - 더블클릭 시 프로토콜만 등록 (토큰 없으므로 다운로드 불필요) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
535
main.go
535
main.go
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -11,7 +12,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
@@ -19,153 +22,303 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
protocolName = "a301"
|
protocolName = "a301"
|
||||||
gameExeName = "A301.exe"
|
gameExeName = "A301.exe"
|
||||||
serverInfoURL = "https://a301.tolelom.xyz/api/download/info"
|
serverInfoURL = "https://a301.tolelom.xyz/api/download/info"
|
||||||
webDownloadURL = "https://a301.tolelom.xyz"
|
webURL = "https://a301.tolelom.xyz"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Win32 constants
|
||||||
|
const (
|
||||||
|
wmDestroy uint32 = 0x0002
|
||||||
|
wmClose uint32 = 0x0010
|
||||||
|
wmSetText uint32 = 0x000C
|
||||||
|
wmAppDone uint32 = 0x8001
|
||||||
|
|
||||||
|
wsPopup uintptr = 0x80000000
|
||||||
|
wsCaption uintptr = 0x00C00000
|
||||||
|
wsChild uintptr = 0x40000000
|
||||||
|
wsVisible uintptr = 0x10000000
|
||||||
|
ssCenter uintptr = 0x00000001
|
||||||
|
|
||||||
|
swShow = 5
|
||||||
|
smCxScreen = 0
|
||||||
|
smCyScreen = 1
|
||||||
|
|
||||||
|
mbOK uintptr = 0x00000000
|
||||||
|
mbInfo uintptr = 0x00000040
|
||||||
|
mbError uintptr = 0x00000010
|
||||||
|
mbYesNo uintptr = 0x00000004
|
||||||
|
mbQ uintptr = 0x00000020
|
||||||
|
idYes = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||||
messageBoxW = user32.NewProc("MessageBoxW")
|
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||||
shellExecuteW = windows.NewLazySystemDLL("shell32.dll").NewProc("ShellExecuteW")
|
shell32 = windows.NewLazySystemDLL("shell32.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")
|
||||||
|
|
||||||
|
wndProcCb uintptr
|
||||||
|
progressLabelHwnd uintptr
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type wndClassExW struct {
|
||||||
mbOK = 0x00000000
|
cbSize uint32
|
||||||
mbIconInfo = 0x00000040
|
style uint32
|
||||||
mbIconError = 0x00000010
|
lpfnWndProc uintptr
|
||||||
mbYesNo = 0x00000004
|
cbClsExtra int32
|
||||||
mbIconQuestion = 0x00000020
|
cbWndExtra int32
|
||||||
idYes = 6
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
wndProcCb = syscall.NewCallback(progressWndProc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Win32 helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func msgBox(title, text string, flags uintptr) int {
|
func msgBox(title, text string, flags uintptr) int {
|
||||||
t, _ := windows.UTF16PtrFromString(title)
|
t, _ := windows.UTF16PtrFromString(title)
|
||||||
m, _ := windows.UTF16PtrFromString(text)
|
m, _ := windows.UTF16PtrFromString(text)
|
||||||
ret, _, _ := messageBoxW.Call(
|
ret, _, _ := messageBoxWProc.Call(0, uintptr(unsafe.Pointer(m)), uintptr(unsafe.Pointer(t)), flags)
|
||||||
0,
|
|
||||||
uintptr(unsafe.Pointer(m)),
|
|
||||||
uintptr(unsafe.Pointer(t)),
|
|
||||||
flags,
|
|
||||||
)
|
|
||||||
return int(ret)
|
return int(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openBrowser(rawURL string) {
|
func openBrowser(rawURL string) {
|
||||||
u, _ := windows.UTF16PtrFromString(rawURL)
|
u, _ := windows.UTF16PtrFromString(rawURL)
|
||||||
op, _ := windows.UTF16PtrFromString("open")
|
op, _ := windows.UTF16PtrFromString("open")
|
||||||
shellExecuteW.Call(0, uintptr(unsafe.Pointer(op)), uintptr(unsafe.Pointer(u)), 0, 0, 1)
|
shellExecuteWProc.Call(0, uintptr(unsafe.Pointer(op)), uintptr(unsafe.Pointer(u)), 0, 0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
// ── Progress window ────────────────────────────────────────────────────────
|
||||||
if len(os.Args) < 2 {
|
|
||||||
ret := msgBox(
|
func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr {
|
||||||
"A301 런처",
|
switch uint32(uMsg) {
|
||||||
"게임 실행을 위해 프로토콜을 등록합니다.\n계속하시겠습니까?",
|
case wmClose:
|
||||||
mbYesNo|mbIconQuestion,
|
return 0 // 닫기 방지
|
||||||
)
|
case wmDestroy:
|
||||||
if ret != idYes {
|
postQuitMsgProc.Call(0)
|
||||||
return
|
return 0
|
||||||
}
|
case wmAppDone:
|
||||||
if err := install(); err != nil {
|
destroyWindowProc.Call(hwnd)
|
||||||
msgBox("A301 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbIconError)
|
return 0
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
msgBox("A301 런처", "a301:// 프로토콜이 등록되었습니다.\n이제 웹에서 게임 시작 버튼을 사용할 수 있습니다.", mbOK|mbIconInfo)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
arg := os.Args[1]
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case arg == "install":
|
|
||||||
if err := install(); err != nil {
|
|
||||||
msgBox("A301 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbIconError)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
msgBox("A301 런처", "a301:// 프로토콜이 등록되었습니다.", mbOK|mbIconInfo)
|
|
||||||
|
|
||||||
case arg == "uninstall":
|
|
||||||
if err := uninstall(); err != nil {
|
|
||||||
msgBox("A301 런처 - 오류", fmt.Sprintf("제거 실패:\n%v", err), mbOK|mbIconError)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
msgBox("A301 런처", "a301:// 프로토콜이 제거되었습니다.", mbOK|mbIconInfo)
|
|
||||||
|
|
||||||
case strings.HasPrefix(arg, protocolName+"://"):
|
|
||||||
if err := handleURI(arg); err != nil {
|
|
||||||
msgBox("A301 런처 - 오류", fmt.Sprintf("실행 실패:\n%v", err), mbOK|mbIconError)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
msgBox("A301 런처 - 오류", fmt.Sprintf("알 수 없는 명령: %s", arg), mbOK|mbIconError)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
ret, _, _ := defWindowProcWProc.Call(hwnd, uMsg, wParam, lParam)
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func launcherPath() (string, error) {
|
func setProgressText(text string) {
|
||||||
exe, err := os.Executable()
|
t, _ := windows.UTF16PtrFromString(text)
|
||||||
if err != nil {
|
sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetText), 0, uintptr(unsafe.Pointer(t)))
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Abs(exe)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func install() error {
|
// downloadWithProgress shows a progress window and downloads+extracts the zip.
|
||||||
exePath, err := launcherPath()
|
// Must be called from the main goroutine (Win32 message loop requirement).
|
||||||
if err != nil {
|
func downloadWithProgress(downloadURL, destDir string) error {
|
||||||
return fmt.Errorf("실행 파일 경로를 찾을 수 없습니다: %w", err)
|
runtime.LockOSThread()
|
||||||
}
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
key, _, err := registry.CreateKey(
|
hInstance, _, _ := getModuleHandleWProc.Call(0)
|
||||||
registry.CURRENT_USER,
|
|
||||||
`Software\Classes\`+protocolName,
|
className, _ := windows.UTF16PtrFromString("A301Progress")
|
||||||
registry.SET_VALUE,
|
wc := wndClassExW{
|
||||||
|
cbSize: uint32(unsafe.Sizeof(wndClassExW{})),
|
||||||
|
lpfnWndProc: wndProcCb,
|
||||||
|
hInstance: hInstance,
|
||||||
|
lpszClassName: className,
|
||||||
|
hbrBackground: 16, // COLOR_BTNFACE + 1
|
||||||
|
}
|
||||||
|
registerClassExWProc.Call(uintptr(unsafe.Pointer(&wc)))
|
||||||
|
|
||||||
|
screenW, _, _ := getSystemMetricsProc.Call(smCxScreen)
|
||||||
|
screenH, _, _ := getSystemMetricsProc.Call(smCyScreen)
|
||||||
|
const winW, winH = 420, 110
|
||||||
|
x := (screenW - winW) / 2
|
||||||
|
y := (screenH - winH) / 2
|
||||||
|
|
||||||
|
title, _ := windows.UTF16PtrFromString("A301 - 게임 설치")
|
||||||
|
hwnd, _, _ := createWindowExWProc.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(className)),
|
||||||
|
uintptr(unsafe.Pointer(title)),
|
||||||
|
wsPopup|wsCaption|wsVisible,
|
||||||
|
x, y, winW, winH,
|
||||||
|
0, 0, hInstance, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("레지스트리 키 생성 실패: %w", err)
|
|
||||||
}
|
|
||||||
defer key.Close()
|
|
||||||
|
|
||||||
if err := key.SetStringValue("", "URL:A301 Protocol"); err != nil {
|
staticClass, _ := windows.UTF16PtrFromString("STATIC")
|
||||||
return err
|
initText, _ := windows.UTF16PtrFromString("게임 파일을 다운로드하는 중...")
|
||||||
}
|
progressLabelHwnd, _, _ = createWindowExWProc.Call(
|
||||||
if err := key.SetStringValue("URL Protocol", ""); err != nil {
|
0,
|
||||||
return err
|
uintptr(unsafe.Pointer(staticClass)),
|
||||||
}
|
uintptr(unsafe.Pointer(initText)),
|
||||||
|
wsChild|wsVisible|ssCenter,
|
||||||
cmdKey, _, err := registry.CreateKey(
|
10, 35, 400, 30,
|
||||||
registry.CURRENT_USER,
|
hwnd, 0, hInstance, 0,
|
||||||
`Software\Classes\`+protocolName+`\shell\open\command`,
|
|
||||||
registry.SET_VALUE,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("command 키 생성 실패: %w", err)
|
|
||||||
}
|
|
||||||
defer cmdKey.Close()
|
|
||||||
|
|
||||||
cmdValue := fmt.Sprintf(`"%s" "%%1"`, exePath)
|
showWindowProc.Call(hwnd, swShow)
|
||||||
return cmdKey.SetStringValue("", cmdValue)
|
updateWindowProc.Call(hwnd)
|
||||||
|
|
||||||
|
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 uninstall() error {
|
func doDownload(downloadURL, destDir string) error {
|
||||||
paths := []string{
|
resp, err := http.Get(downloadURL)
|
||||||
`Software\Classes\` + protocolName + `\shell\open\command`,
|
if err != nil {
|
||||||
`Software\Classes\` + protocolName + `\shell\open`,
|
return fmt.Errorf("다운로드 연결 실패: %w", err)
|
||||||
`Software\Classes\` + protocolName + `\shell`,
|
|
||||||
`Software\Classes\` + protocolName,
|
|
||||||
}
|
}
|
||||||
for _, p := range paths {
|
defer resp.Body.Close()
|
||||||
err := registry.DeleteKey(registry.CURRENT_USER, p)
|
|
||||||
if err != nil && err != registry.ErrNotExist {
|
tmpPath := filepath.Join(os.TempDir(), "a301_game.zip")
|
||||||
|
tmpFile, err := os.Create(tmpPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("임시 파일 생성 실패: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
total := resp.ContentLength
|
||||||
|
var downloaded int64
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err := resp.Body.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
if _, werr := tmpFile.Write(buf[:n]); werr != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return fmt.Errorf("파일 쓰기 실패: %w", werr)
|
||||||
|
}
|
||||||
|
downloaded += int64(n)
|
||||||
|
if total > 0 {
|
||||||
|
pct := downloaded * 100 / total
|
||||||
|
setProgressText(fmt.Sprintf("다운로드 중... %d%%", pct))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return fmt.Errorf("다운로드 중 오류: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
defer os.Remove(tmpPath)
|
||||||
|
|
||||||
|
setProgressText("압축을 해제하는 중...")
|
||||||
|
return extractZip(tmpPath, destDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractZip(zipPath, destDir string) error {
|
||||||
|
r, err := zip.OpenReader(zipPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("zip 열기 실패: %w", err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
// 런처 자신의 파일명 (덮어쓰기 방지)
|
||||||
|
selfName := strings.ToLower(filepath.Base(os.Args[0]))
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
// zip 내 최상위 디렉토리 제거 (A301/A301.exe → A301.exe)
|
||||||
|
clean := filepath.ToSlash(f.Name)
|
||||||
|
parts := strings.SplitN(clean, "/", 2)
|
||||||
|
var rel string
|
||||||
|
if len(parts) == 2 && parts[1] != "" {
|
||||||
|
rel = parts[1]
|
||||||
|
} else if len(parts) == 1 && parts[0] != "" {
|
||||||
|
rel = parts[0]
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 런처 파일 건너뜀
|
||||||
|
if strings.ToLower(filepath.Base(rel)) == selfName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := filepath.Join(destDir, filepath.FromSlash(rel))
|
||||||
|
|
||||||
|
if f.FileInfo().IsDir() {
|
||||||
|
os.MkdirAll(dest, 0755)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out, err := os.Create(dest)
|
||||||
|
if err != nil {
|
||||||
|
rc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(out, rc)
|
||||||
|
out.Close()
|
||||||
|
rc.Close()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Server info ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type downloadInfo struct {
|
type downloadInfo struct {
|
||||||
FileHash string `json:"fileHash"`
|
FileHash string `json:"fileHash"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
@@ -191,7 +344,6 @@ func hashFile(path string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
if _, err := io.Copy(h, f); err != nil {
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -199,6 +351,86 @@ func hashFile(path string) (string, error) {
|
|||||||
return hex.EncodeToString(h.Sum(nil)), nil
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Launcher path ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func launcherPath() (string, error) {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Abs(exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Protocol install / uninstall ───────────────────────────────────────────
|
||||||
|
|
||||||
|
func install() error {
|
||||||
|
exePath, err := launcherPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("실행 파일 경로를 찾을 수 없습니다: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\`+protocolName, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("레지스트리 키 생성 실패: %w", err)
|
||||||
|
}
|
||||||
|
defer key.Close()
|
||||||
|
key.SetStringValue("", "URL:A301 Protocol")
|
||||||
|
key.SetStringValue("URL Protocol", "")
|
||||||
|
|
||||||
|
cmdKey, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\`+protocolName+`\shell\open\command`, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("command 키 생성 실패: %w", err)
|
||||||
|
}
|
||||||
|
defer cmdKey.Close()
|
||||||
|
return cmdKey.SetStringValue("", fmt.Sprintf(`"%s" "%%1"`, exePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func uninstall() error {
|
||||||
|
paths := []string{
|
||||||
|
`Software\Classes\` + protocolName + `\shell\open\command`,
|
||||||
|
`Software\Classes\` + protocolName + `\shell\open`,
|
||||||
|
`Software\Classes\` + protocolName + `\shell`,
|
||||||
|
`Software\Classes\` + protocolName,
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
if err := registry.DeleteKey(registry.CURRENT_USER, p); err != nil && err != registry.ErrNotExist {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Game update check + download ───────────────────────────────────────────
|
||||||
|
|
||||||
|
func ensureGame(gameDir, gamePath string, serverInfo *downloadInfo) error {
|
||||||
|
needsDownload := false
|
||||||
|
|
||||||
|
if _, err := os.Stat(gamePath); os.IsNotExist(err) {
|
||||||
|
needsDownload = true
|
||||||
|
} else if serverInfo.FileHash != "" {
|
||||||
|
localHash, err := hashFile(gamePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("파일 검증 실패: %w", err)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(localHash, serverInfo.FileHash) {
|
||||||
|
needsDownload = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsDownload {
|
||||||
|
if serverInfo.URL == "" {
|
||||||
|
return fmt.Errorf("다운로드 URL이 없습니다")
|
||||||
|
}
|
||||||
|
if err := downloadWithProgress(serverInfo.URL, gameDir); err != nil {
|
||||||
|
return fmt.Errorf("게임 설치 실패: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── URI handler ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func handleURI(rawURI string) error {
|
func handleURI(rawURI string) error {
|
||||||
parsed, err := url.Parse(rawURI)
|
parsed, err := url.Parse(rawURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -217,34 +449,13 @@ func handleURI(rawURI string) error {
|
|||||||
gameDir := filepath.Dir(exePath)
|
gameDir := filepath.Dir(exePath)
|
||||||
gamePath := filepath.Join(gameDir, gameExeName)
|
gamePath := filepath.Join(gameDir, gameExeName)
|
||||||
|
|
||||||
if _, err := os.Stat(gamePath); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("게임 파일을 찾을 수 없습니다:\n%s", gamePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 서버에서 최신 해시 조회
|
|
||||||
serverInfo, err := fetchServerInfo()
|
serverInfo, err := fetchServerInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("버전 확인 실패:\n%w", err)
|
return fmt.Errorf("버전 확인 실패: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 서버에 해시가 등록된 경우에만 검증
|
if err := ensureGame(gameDir, gamePath, serverInfo); err != nil {
|
||||||
if serverInfo.FileHash != "" {
|
return err
|
||||||
localHash, err := hashFile(gamePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("게임 파일 검증 실패:\n%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.EqualFold(localHash, serverInfo.FileHash) {
|
|
||||||
ret := msgBox(
|
|
||||||
"A301 - 업데이트 필요",
|
|
||||||
"새로운 버전의 게임이 있습니다.\n최신 버전을 다운로드해주세요.\n\n확인을 누르면 다운로드 페이지로 이동합니다.",
|
|
||||||
mbOK|mbIconInfo,
|
|
||||||
)
|
|
||||||
if ret > 0 {
|
|
||||||
openBrowser(webDownloadURL)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("버전이 최신이 아닙니다")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(gamePath, "-token", token)
|
cmd := exec.Command(gamePath, "-token", token)
|
||||||
@@ -252,6 +463,56 @@ func handleURI(rawURI string) error {
|
|||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("게임 실행 실패: %w", err)
|
return fmt.Errorf("게임 실행 실패: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Entry point ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
ret := msgBox("A301 런처", "게임 실행을 위해 프로토콜을 등록합니다.\n계속하시겠습니까?", mbYesNo|mbQ)
|
||||||
|
if ret != idYes {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := install(); err != nil {
|
||||||
|
msgBox("A301 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbError)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
msgBox("A301 런처", "a301:// 프로토콜이 등록되었습니다.\n이제 웹에서 게임 시작 버튼을 사용할 수 있습니다.", mbOK|mbInfo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
arg := os.Args[1]
|
||||||
|
switch {
|
||||||
|
case arg == "install":
|
||||||
|
if err := install(); err != nil {
|
||||||
|
msgBox("A301 런처 - 오류", fmt.Sprintf("등록 실패:\n%v", err), mbOK|mbError)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
msgBox("A301 런처", "a301:// 프로토콜이 등록되었습니다.", mbOK|mbInfo)
|
||||||
|
|
||||||
|
case arg == "uninstall":
|
||||||
|
if err := uninstall(); err != nil {
|
||||||
|
msgBox("A301 런처 - 오류", fmt.Sprintf("제거 실패:\n%v", err), mbOK|mbError)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
msgBox("A301 런처", "a301:// 프로토콜이 제거되었습니다.", mbOK|mbInfo)
|
||||||
|
|
||||||
|
case strings.HasPrefix(arg, protocolName+"://"):
|
||||||
|
if err := handleURI(arg); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "버전이 최신이 아닙니다") {
|
||||||
|
ret := msgBox("A301 - 업데이트 필요", "새로운 버전이 있습니다. 다운로드 페이지로 이동할까요?", mbYesNo|mbInfo)
|
||||||
|
if ret == idYes {
|
||||||
|
openBrowser(webURL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
msgBox("A301 런처 - 오류", fmt.Sprintf("실행 실패:\n%v", err), mbOK|mbError)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
msgBox("A301 런처 - 오류", fmt.Sprintf("알 수 없는 명령: %s", arg), mbOK|mbError)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user