Fix GUI instance counts and launcher tmp paths

This commit is contained in:
every_holiday
2026-06-25 02:36:55 +09:00
parent 23d5fcfcda
commit 75f2d8b9c2
24 changed files with 2479 additions and 20 deletions

View File

@@ -4,18 +4,39 @@ import (
"flag"
"log"
"os"
"os/exec"
"path/filepath"
"vrc_osc_go/internal/app"
)
func main() {
var configPath string
var mode string
var noGUI bool
flag.StringVar(&configPath, "config", "config/config.toml", "path to config.toml")
flag.StringVar(&mode, "mode", "", "generate-json or extract-log")
flag.BoolVar(&noGUI, "no-gui", false, "do not launch the GUI companion")
flag.Parse()
if err := app.Run(configPath); err != nil {
if mode == "" && !noGUI {
launchGUI()
}
if err := app.Run(configPath, mode); err != nil {
log.SetOutput(os.Stderr)
log.Fatalf("vrc_osc_go: %v", err)
}
}
func launchGUI() {
exe, err := os.Executable()
if err != nil {
return
}
gui := filepath.Join(filepath.Dir(exe), "vrc_osc_gui.exe")
if _, err := os.Stat(gui); err != nil {
return
}
_ = exec.Command(gui).Start()
}

14
cmd/vrc_osc_gui/main.go Normal file
View File

@@ -0,0 +1,14 @@
//go:build windows
package main
import (
"log"
"vrc_osc_go/internal/app"
)
func main() {
_ = app.AppendRuntimeLog("GUI", "starting vrc_osc_gui.exe")
log.Fatal(runNativeGUI())
}

View File

@@ -0,0 +1,216 @@
//go:build windows
package main
import (
"fmt"
"log"
"os/exec"
"syscall"
"unsafe"
)
const (
wmUser = 0x0400
wmTray = wmUser + 1
wmCommand = 0x0111
wmDestroy = 0x0002
wmClose = 0x0010
wmRButtonUp = 0x0205
wmLButtonUp = 0x0202
wmCreate = 0x0001
wmQuit = 0x0012
niAdd = 0x00000000
niModify = 0x00000001
niDelete = 0x00000002
nifMessage = 0x00000001
nifIcon = 0x00000002
nifTip = 0x00000004
nifInfo = 0x00000010
nimPopup = 0x00000000
miString = 0x00000000
miSeparator = 0x00000800
miDefault = 0x00001000
swHide = 0
idOpen = 1001
idExit = 1002
maxTip = 128
maxClassName = 64
)
type trayApp struct {
uiURL string
open func()
}
func newTray(uiURL string, open func()) *trayApp {
return &trayApp{uiURL: uiURL, open: open}
}
func (t *trayApp) Run() error {
return runTray(t.uiURL, t.open)
}
func openBrowser(url string) {
_ = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
}
func runTray(uiURL string, open func()) error {
user32 := syscall.NewLazyDLL("user32.dll")
shell32 := syscall.NewLazyDLL("shell32.dll")
kernel32 := syscall.NewLazyDLL("kernel32.dll")
registerClass := user32.NewProc("RegisterClassW")
createWindowEx := user32.NewProc("CreateWindowExW")
defWindowProc := user32.NewProc("DefWindowProcW")
getMessage := user32.NewProc("GetMessageW")
translateMessage := user32.NewProc("TranslateMessage")
dispatchMessage := user32.NewProc("DispatchMessageW")
postQuitMessage := user32.NewProc("PostQuitMessage")
createPopupMenu := user32.NewProc("CreatePopupMenu")
appendMenu := user32.NewProc("AppendMenuW")
setForegroundWindow := user32.NewProc("SetForegroundWindow")
trackPopupMenu := user32.NewProc("TrackPopupMenu")
showWindow := user32.NewProc("ShowWindow")
loadIcon := user32.NewProc("LoadIconW")
shellNotifyIcon := shell32.NewProc("Shell_NotifyIconW")
getModuleHandle := kernel32.NewProc("GetModuleHandleW")
type wndClassEx 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 point struct{ X, Y int32 }
type msg struct {
hwnd uintptr
message uint32
wParam uintptr
lParam uintptr
time uint32
pt point
}
type notifyIconData struct {
cbSize uint32
hWnd uintptr
uID uint32
uFlags uint32
uCallbackMessage uint32
hIcon uintptr
szTip [maxTip]uint16
dwState uint32
dwStateMask uint32
szInfo [256]uint16
uTimeoutVersion uint32
szInfoTitle [64]uint16
dwInfoFlags uint32
guidItem [16]byte
hBalloonIcon uintptr
}
className, _ := syscall.UTF16PtrFromString("VRC_OSC_TRAY")
windowTitle, _ := syscall.UTF16PtrFromString("VRC OSC Tray")
icon, _, _ := loadIcon.Call(0, 32512) // IDI_APPLICATION
var hwnd uintptr
var menu uintptr
var nid notifyIconData
wndProc := syscall.NewCallback(func(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case wmCreate:
return 0
case wmTray:
switch lParam {
case wmLButtonUp:
if open != nil {
open()
}
case wmRButtonUp:
if menu == 0 {
m, _, _ := createPopupMenu.Call()
menu = m
openText, _ := syscall.UTF16PtrFromString("Open UI")
exitText, _ := syscall.UTF16PtrFromString("Exit")
appendMenu.Call(menu, miString|miDefault, idOpen, uintptr(unsafe.Pointer(openText)))
appendMenu.Call(menu, miString, idExit, uintptr(unsafe.Pointer(exitText)))
}
setForegroundWindow.Call(hwnd)
var p point
user32.NewProc("GetCursorPos").Call(uintptr(unsafe.Pointer(&p)))
trackPopupMenu.Call(menu, nimPopup, uintptr(p.X), uintptr(p.Y), 0, hwnd, 0)
}
return 0
case wmCommand:
switch uint16(wParam & 0xffff) {
case idOpen:
if open != nil {
open()
}
case idExit:
user32.NewProc("DestroyWindow").Call(hwnd)
}
return 0
case wmDestroy:
nid.cbSize = uint32(unsafe.Sizeof(nid))
nid.hWnd = hwnd
nid.uID = 1
shellNotifyIcon.Call(niDelete, uintptr(unsafe.Pointer(&nid)))
postQuitMessage.Call(0)
return 0
}
ret, _, _ := defWindowProc.Call(hwnd, uintptr(msg), wParam, lParam)
return ret
})
hInstance, _, _ := getModuleHandle.Call(0)
_, _, regErr := registerClass.Call(uintptr(unsafe.Pointer(&wndClassEx{
cbSize: uint32(unsafe.Sizeof(wndClassEx{})),
lpfnWndProc: wndProc,
hInstance: hInstance,
hIcon: icon,
lpszClassName: className,
})))
if regErr != nil {
return fmt.Errorf("RegisterClassW failed: %v", regErr)
}
hwnd, _, err := createWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(windowTitle)), 0, 0, 0, 0, 0, 0, 0, hInstance, 0)
if hwnd == 0 {
return fmt.Errorf("CreateWindowExW failed: %v", err)
}
showWindow.Call(hwnd, swHide)
nid.cbSize = uint32(unsafe.Sizeof(nid))
nid.hWnd = hwnd
nid.uID = 1
nid.uFlags = nifMessage | nifIcon | nifTip
nid.uCallbackMessage = wmTray
nid.hIcon = icon
tip := syscall.StringToUTF16("VRC OSC")
copy(nid.szTip[:], tip)
if ok, _, _ := shellNotifyIcon.Call(niAdd, uintptr(unsafe.Pointer(&nid))); ok == 0 {
return fmt.Errorf("Shell_NotifyIconW add failed")
}
log.Printf("tray started")
var m msg
for {
r, _, _ := getMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
if int32(r) <= 0 {
break
}
translateMessage.Call(uintptr(unsafe.Pointer(&m)))
dispatchMessage.Call(uintptr(unsafe.Pointer(&m)))
}
return nil
}

View File

@@ -0,0 +1,531 @@
//go:build windows
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"syscall"
"time"
"unsafe"
"vrc_osc_go/internal/app"
)
const (
wsOverlappedWindow = 0x00CF0000
wsVisible = 0x10000000
wsChild = 0x40000000
wsBorder = 0x00800000
esMultiline = 0x0004
esReadonly = 0x0800
esAutovscroll = 0x0040
esAutohscroll = 0x0080
wmCreateGUI = 0x0001
wmDestroyGUI = 0x0002
wmTimerGUI = 0x0113
wmCommandGUI = 0x0111
idStatus = 2001
idLog = 2002
timerRefresh = 1
)
var joinLeaveLinePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+\[(join|leave)\]\s+(.+?)\s+\((\d+)\)$`)
var runtimeTimePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]`)
type guiApp struct {
hwnd uintptr
leftHwnd uintptr
rightHwnd uintptr
tabJoin uintptr
tabDiscord uintptr
tabTranslate uintptr
activeTab string
lastInstanceCount int
}
type userState struct {
Name string
Present bool
LastJoin time.Time
LastLeave time.Time
}
func runtimeDir() string {
exe, err := os.Executable()
if err != nil {
return "runtime"
}
return filepath.Join(filepath.Dir(exe), "runtime")
}
func guiLog(text string) {
_ = app.AppendRuntimeLog("GUI", text)
}
func runNativeGUI() error {
user32 := syscall.NewLazyDLL("user32.dll")
kernel32 := syscall.NewLazyDLL("kernel32.dll")
registerClass := user32.NewProc("RegisterClassW")
createWindowEx := user32.NewProc("CreateWindowExW")
defWindowProc := user32.NewProc("DefWindowProcW")
showWindow := user32.NewProc("ShowWindow")
updateWindow := user32.NewProc("UpdateWindow")
getMessage := user32.NewProc("GetMessageW")
translateMessage := user32.NewProc("TranslateMessage")
dispatchMessage := user32.NewProc("DispatchMessageW")
postQuitMessage := user32.NewProc("PostQuitMessage")
setTimer := user32.NewProc("SetTimer")
loadCursor := user32.NewProc("LoadCursorW")
setWindowText := user32.NewProc("SetWindowTextW")
getModuleHandle := kernel32.NewProc("GetModuleHandleW")
loadIcon := user32.NewProc("LoadIconW")
type wndClass struct {
style uint32
lpfnWndProc uintptr
cbClsExtra int32
cbWndExtra int32
hInstance uintptr
hIcon uintptr
hCursor uintptr
hbrBackground uintptr
lpszMenuName *uint16
lpszClassName *uint16
}
type msg struct {
hwnd uintptr
message uint32
wParam uintptr
lParam uintptr
time uint32
pt struct{ X, Y int32 }
}
hInstance, _, _ := getModuleHandle.Call(0)
className, _ := syscall.UTF16PtrFromString("VRC_OSC_NATIVE_GUI")
title, _ := syscall.UTF16PtrFromString("VRC OSC")
cursor, _, _ := loadCursor.Call(0, 32512)
icon, _, _ := loadIcon.Call(0, 32512)
guiLog("stage=prepare window")
var app guiApp
wndProc := syscall.NewCallback(func(hwnd uintptr, message uint32, wParam, lParam uintptr) uintptr {
switch message {
case wmCreateGUI:
buttonClass, _ := syscall.UTF16PtrFromString("BUTTON")
leftClass, _ := syscall.UTF16PtrFromString("EDIT")
rightClass, _ := syscall.UTF16PtrFromString("EDIT")
leftTitle, _ := syscall.UTF16PtrFromString("")
rightTitle, _ := syscall.UTF16PtrFromString("")
joinTitle, _ := syscall.UTF16PtrFromString("Join Log")
discordTitle, _ := syscall.UTF16PtrFromString("Discord")
translateTitle, _ := syscall.UTF16PtrFromString("Translate")
app.tabJoin, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(joinTitle)), wsVisible|wsChild, 370, 20, 90, 28, hwnd, 3001, hInstance, 0)
app.tabDiscord, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(discordTitle)), wsVisible|wsChild, 470, 20, 90, 28, hwnd, 3002, hInstance, 0)
app.tabTranslate, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(translateTitle)), wsVisible|wsChild, 570, 20, 90, 28, hwnd, 3003, hInstance, 0)
app.leftHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(leftClass)), uintptr(unsafe.Pointer(leftTitle)), wsVisible|wsChild|wsBorder|esMultiline|esReadonly|esAutovscroll|esAutohscroll, 20, 20, 330, 300, hwnd, idStatus, hInstance, 0)
app.rightHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(rightClass)), uintptr(unsafe.Pointer(rightTitle)), wsVisible|wsChild|wsBorder|esMultiline|esReadonly|esAutovscroll|esAutohscroll, 370, 60, 330, 260, hwnd, idLog, hInstance, 0)
app.activeTab = "join"
setTimer.Call(hwnd, timerRefresh, 2000, 0)
refreshGUI(setWindowText, &app)
return 0
case wmCommandGUI:
switch uint16(wParam & 0xffff) {
case 3001:
app.activeTab = "join"
case 3002:
app.activeTab = "discord"
case 3003:
app.activeTab = "translate"
}
refreshGUI(setWindowText, &app)
return 0
case wmTimerGUI:
refreshGUI(setWindowText, &app)
return 0
case wmDestroyGUI:
postQuitMessage.Call(0)
return 0
}
ret, _, _ := defWindowProc.Call(hwnd, uintptr(message), wParam, lParam)
return ret
})
guiLog("stage=register class")
atom, _, regErr := registerClass.Call(uintptr(unsafe.Pointer(&wndClass{
style: 0,
lpfnWndProc: wndProc,
hInstance: hInstance,
hIcon: icon,
hCursor: cursor,
hbrBackground: 6,
lpszClassName: className,
})))
if atom == 0 {
guiLog(fmt.Sprintf("stage=register class failed err=%v", regErr))
return fmt.Errorf("register class: %v", regErr)
}
guiLog("stage=create window")
app.hwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(title)), wsOverlappedWindow, 200, 120, 760, 380, 0, 0, hInstance, 0)
if app.hwnd == 0 {
guiLog("stage=create window failed")
return fmt.Errorf("create window failed")
}
guiLog("stage=show window")
showWindow.Call(app.hwnd, 5)
updateWindow.Call(app.hwnd)
guiLog("stage=message loop")
var m msg
for {
r, _, _ := getMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
if int32(r) <= 0 {
break
}
translateMessage.Call(uintptr(unsafe.Pointer(&m)))
dispatchMessage.Call(uintptr(unsafe.Pointer(&m)))
}
return nil
}
func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp) {
current, instanceCount := currentUsersFromJoinLeave()
if instanceCount == 0 && app.lastInstanceCount > 0 {
instanceCount = app.lastInstanceCount
}
if instanceCount > 0 {
app.lastInstanceCount = instanceCount
}
if app.leftHwnd != 0 {
t, _ := syscall.UTF16PtrFromString(formatGuestPane("Join Log", current))
setWindowText.Call(app.leftHwnd, uintptr(unsafe.Pointer(t)))
}
if app.rightHwnd != 0 {
t, _ := syscall.UTF16PtrFromString(formatRightPane(app.activeTab, readRuntimeSnapshot(), currentWorldLabel(), instanceCount))
setWindowText.Call(app.rightHwnd, uintptr(unsafe.Pointer(t)))
}
}
func currentUsersFromJoinLeave() ([]userState, int) {
since := currentWorldStartTime()
users := map[string]*userState{}
lastLoggedCount := 0
for _, s := range readGuestSnapshot() {
if !s.Present {
continue
}
cp := s
users[s.Name] = &userState{
Name: s.Name,
Present: true,
LastJoin: cp.LastJoin,
}
}
p := filepath.Join(runtimeDir(), "join_leave.log")
b, err := os.ReadFile(p)
if err != nil {
out := make([]userState, 0, len(users))
for _, u := range users {
out = append(out, *u)
}
sort.Slice(out, func(i, j int) bool {
if out[i].Present != out[j].Present {
return out[i].Present
}
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
})
return out, countPresent(out)
}
for _, raw := range strings.Split(string(b), "\n") {
line := strings.TrimSpace(raw)
if line == "" {
continue
}
if c := parseJoinLeaveCount(line); c > 0 {
lastLoggedCount = c
}
at, kind, name := parseJoinLeaveLine(line)
if name == "" {
continue
}
if !since.IsZero() && at.Before(since) {
continue
}
u := users[name]
if u == nil {
u = &userState{Name: name}
users[name] = u
}
switch kind {
case "join":
u.Present = true
if at.After(u.LastJoin) {
u.LastJoin = at
}
case "leave":
u.Present = false
if at.After(u.LastLeave) {
u.LastLeave = at
}
}
}
// Keep any snapshot-marked present users that the join/leave delta can't rebuild.
for _, snap := range readGuestSnapshot() {
if !snap.Present {
continue
}
u := users[snap.Name]
if u == nil {
users[snap.Name] = &userState{
Name: snap.Name,
Present: true,
LastJoin: snap.LastJoin,
}
continue
}
if !u.Present {
u.Present = true
if u.LastJoin.IsZero() {
u.LastJoin = snap.LastJoin
}
}
}
out := make([]userState, 0, len(users))
for _, u := range users {
out = append(out, *u)
}
sort.Slice(out, func(i, j int) bool {
if out[i].Present != out[j].Present {
return out[i].Present
}
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
})
present := countPresent(out)
if lastLoggedCount > present {
present = lastLoggedCount
}
return out, present
}
func readGuestSnapshot() []app.GuestStatus {
p := filepath.Join(runtimeDir(), "guest_snapshot.json")
b, err := os.ReadFile(p)
if err != nil {
return nil
}
var out []app.GuestStatus
if err := json.Unmarshal(b, &out); err != nil {
return nil
}
return out
}
func formatGuestPane(title string, items []userState) string {
var b strings.Builder
b.WriteString(title)
b.WriteString("\r\n")
b.WriteString(strings.Repeat("=", len(title)))
b.WriteString("\r\n")
if len(items) == 0 {
b.WriteString("Current in room: 0")
b.WriteString("\r\n")
b.WriteString("(none)")
return b.String()
}
b.WriteString("Current in room: ")
b.WriteString(strconv.Itoa(countPresent(items)))
b.WriteString("\r\n\r\n")
for _, item := range items {
b.WriteString("・ ")
b.WriteString(item.Name)
if item.Present {
b.WriteString(" [present]")
} else {
b.WriteString(" [gray]")
if !item.LastLeave.IsZero() {
b.WriteString(" ")
b.WriteString(timeAgo(item.LastLeave))
}
}
b.WriteString("\r\n")
}
return strings.TrimRight(b.String(), "\r\n")
}
func formatRightPane(tab string, state map[string]any, worldLabel string, currentUsers int) string {
var b strings.Builder
switch tab {
case "discord":
b.WriteString("Discord")
case "translate":
b.WriteString("Translate")
default:
b.WriteString("Current World")
}
b.WriteString("\r\n")
titleLine := "Current World"
switch tab {
case "discord":
titleLine = "Discord"
case "translate":
titleLine = "Translate"
}
b.WriteString(strings.Repeat("=", len(titleLine)))
b.WriteString("\r\n")
discordMuted, _ := state["discord_muted"].(bool)
ocr, _ := state["ocr"].(string)
translate, _ := state["translate"].(string)
switch tab {
case "discord":
b.WriteString("Muted: ")
b.WriteString(fmt.Sprintf("%v", discordMuted))
b.WriteString("\r\nWorld: ")
b.WriteString(worldLabel)
b.WriteString("\r\nUsers: ")
b.WriteString(fmt.Sprintf("%d", currentUsers))
case "translate":
b.WriteString("Translate: ")
b.WriteString(translate)
b.WriteString("\r\nOCR: ")
b.WriteString(ocr)
b.WriteString("\r\nWorld: ")
b.WriteString(worldLabel)
default:
b.WriteString("World: ")
b.WriteString(worldLabel)
b.WriteString("\r\nUsers: ")
b.WriteString(fmt.Sprintf("%d", currentUsers))
b.WriteString("\r\nDiscord muted: ")
b.WriteString(fmt.Sprintf("%v", discordMuted))
b.WriteString("\r\nOCR: ")
b.WriteString(ocr)
b.WriteString("\r\nTranslate: ")
b.WriteString(translate)
}
return b.String()
}
func currentWorldLabel() string {
p := filepath.Join(runtimeDir(), "runtime.log")
b, err := os.ReadFile(p)
if err != nil {
return ""
}
lines := strings.Split(string(b), "\n")
var latest string
for i := 0; i < len(lines); i++ {
if !strings.Contains(lines[i], "VRC WORLD") {
continue
}
if i+1 < len(lines) {
latest = strings.TrimSpace(lines[i+1])
}
}
return latest
}
func readRuntimeSnapshot() map[string]any {
p := filepath.Join(runtimeDir(), "state.json")
b, err := os.ReadFile(p)
if err != nil {
return map[string]any{}
}
var out map[string]any
_ = json.Unmarshal(b, &out)
return out
}
func currentWorldStartTime() time.Time {
p := filepath.Join(runtimeDir(), "runtime.log")
b, err := os.ReadFile(p)
if err != nil {
return time.Time{}
}
var latest time.Time
for _, raw := range strings.Split(string(b), "\n") {
line := strings.TrimSpace(raw)
if line == "" || !strings.Contains(line, "VRC WORLD") {
continue
}
if at, ok := parseRuntimeTime(line); ok && at.After(latest) {
latest = at
}
}
return latest
}
func parseJoinLeaveLine(line string) (time.Time, string, string) {
m := joinLeaveLinePattern.FindStringSubmatch(line)
if len(m) != 4 {
return time.Time{}, "", ""
}
at, err := time.Parse("2006-01-02 15:04:05", m[1])
if err != nil {
return time.Time{}, "", ""
}
return at, m[2], strings.TrimSpace(m[3])
}
func parseJoinLeaveCount(line string) int {
m := regexp.MustCompile(`\((\d+)\)$`).FindStringSubmatch(line)
if len(m) != 2 {
return 0
}
n, err := strconv.Atoi(m[1])
if err != nil {
return 0
}
return n
}
func parseRuntimeTime(line string) (time.Time, bool) {
m := runtimeTimePattern.FindStringSubmatch(line)
if len(m) != 2 {
return time.Time{}, false
}
at, err := time.Parse("2006-01-02 15:04:05", m[1])
if err != nil {
return time.Time{}, false
}
return at, true
}
func countPresent(items []userState) int {
n := 0
for _, item := range items {
if item.Present {
n++
}
}
return n
}
func timeAgo(at time.Time) string {
if at.IsZero() {
return ""
}
d := time.Since(at)
if d < time.Minute {
return "0分前"
}
if d < time.Hour {
return fmt.Sprintf("%d分前", int(d.Minutes()))
}
if d < 24*time.Hour {
return fmt.Sprintf("%d時間前", int(d.Hours()))
}
return fmt.Sprintf("%d日前", int(d.Hours()/24))
}

View File

@@ -4,8 +4,10 @@ import (
"flag"
"log"
"os"
"os/exec"
"path/filepath"
"syscall"
"unsafe"
"vrc_osc_go/internal/buildinfo"
"vrc_osc_go/internal/common"
@@ -31,12 +33,18 @@ func main() {
if _, err := mgr.CheckAndUpdate(); err != nil {
log.Printf("auto update skipped: %v", err)
}
exe := filepath.Join(common.RootDir(), "vrc_osc.exe")
base := runtimeBaseDir()
exe := filepath.Join(base, "vrc_osc.exe")
gui := filepath.Join(base, "vrc_osc_gui.exe")
if _, err := os.Stat(exe); err != nil {
showError("vrc_osc_launcher", "missing exe: "+err.Error())
log.Fatalf("missing exe: %v", err)
}
if err := update.Launch(exe, os.Args[1:]); err != nil {
if _, err := os.Stat(gui); err == nil {
_ = exec.Command(gui).Start()
}
args := append([]string{"--no-gui"}, os.Args[1:]...)
if err := update.Launch(exe, args); err != nil {
showError("vrc_osc_launcher", "launch failed: "+err.Error())
log.Fatalf("launch failed: %v", err)
}
@@ -49,8 +57,19 @@ func showError(title, message string) {
const mbIconError = 0x00000010
_, _, _ = msgBox.Call(
0,
uintptr(syscall.StringToUTF16Ptr(message)),
uintptr(syscall.StringToUTF16Ptr(title)),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(message))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
uintptr(mbOK|mbIconError),
)
}
func runtimeBaseDir() string {
root := common.RootDir()
if _, err := os.Stat(filepath.Join(root, "vrc_osc.exe")); err == nil {
return root
}
if _, err := os.Stat(filepath.Join(root, "tmp", "vrc_osc.exe")); err == nil {
return filepath.Join(root, "tmp")
}
return root
}

33
cmd/vrwt_tool/main.go Normal file
View File

@@ -0,0 +1,33 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"vrc_osc_go/internal/consenttool"
)
func main() {
var cfgPath string
var mode string
flag.StringVar(&cfgPath, "config", "config/consent.toml", "path to consent tool config")
flag.StringVar(&mode, "mode", "", "generate-json or extract-log")
flag.Parse()
if mode == "" && flag.NArg() > 0 {
mode = flag.Arg(0)
}
if mode == "" {
fmt.Fprintln(os.Stderr, "missing mode: generate-json or extract-log")
os.Exit(2)
}
if err := consenttool.Run(cfgPath, mode); err != nil {
log.SetOutput(os.Stderr)
log.Fatalf("vrwt_tool: %v", err)
}
}