Stabilize GUI settings controls
This commit is contained in:
@@ -16,12 +16,15 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"vrc_osc_go/internal/app"
|
||||
"vrc_osc_go/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
wsOverlappedWindow = 0x00CF0000
|
||||
wsVisible = 0x10000000
|
||||
wsChild = 0x40000000
|
||||
wsPopup = 0x80000000
|
||||
wsClipChildren = 0x02000000
|
||||
wsBorder = 0x00800000
|
||||
esMultiline = 0x0004
|
||||
esReadonly = 0x0800
|
||||
@@ -31,6 +34,7 @@ const (
|
||||
wmDestroyGUI = 0x0002
|
||||
wmTimerGUI = 0x0113
|
||||
wmCommandGUI = 0x0111
|
||||
wmSetFont = 0x0030
|
||||
idStatus = 2001
|
||||
idLog = 2002
|
||||
timerRefresh = 1
|
||||
@@ -38,15 +42,50 @@ const (
|
||||
|
||||
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})\]`)
|
||||
var vrcLogTimePattern = regexp.MustCompile(`^(\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2})`)
|
||||
var vrcJoinPattern = regexp.MustCompile(`OnPlayerJoined\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)`)
|
||||
var vrcLeftPattern = regexp.MustCompile(`OnPlayerLeft\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)`)
|
||||
var hwndTopMostFlag uintptr = ^uintptr(0) - 1
|
||||
var hwndNoMove uintptr = 0x0002
|
||||
var hwndNoSize uintptr = 0x0001
|
||||
var hwndNoActivate uintptr = 0x0010
|
||||
var hwndShowWindow uintptr = 0x0040
|
||||
var swRestore uintptr = 9
|
||||
var swHideControl uintptr = 0
|
||||
var swShowControl uintptr = 5
|
||||
var swpVisibleFlags uintptr = hwndNoActivate | hwndShowWindow
|
||||
var swpFlags uintptr = hwndNoMove | hwndNoSize | hwndNoActivate
|
||||
var swpShowFlags uintptr = hwndNoMove | hwndNoSize | hwndShowWindow
|
||||
var smXVirtualScreen int32 = 76
|
||||
var smYVirtualScreen int32 = 77
|
||||
var smCXVirtualScreen int32 = 78
|
||||
var smCYVirtualScreen int32 = 79
|
||||
var setWindowPosProc *syscall.LazyProc
|
||||
var sendMessageProc *syscall.LazyProc
|
||||
var createFontProc *syscall.LazyProc
|
||||
var deleteObjectProc *syscall.LazyProc
|
||||
var showWindowProc *syscall.LazyProc
|
||||
|
||||
type guiApp struct {
|
||||
hwnd uintptr
|
||||
leftHwnd uintptr
|
||||
rightHwnd uintptr
|
||||
settingsPaneHwnd uintptr
|
||||
tabJoin uintptr
|
||||
tabDiscord uintptr
|
||||
tabTranslate uintptr
|
||||
tabSettings uintptr
|
||||
btnActiveWindow uintptr
|
||||
btnTopMost uintptr
|
||||
btnFontDown uintptr
|
||||
btnFontUp uintptr
|
||||
activeTab string
|
||||
activeWindow bool
|
||||
lastActiveWindow bool
|
||||
topMost bool
|
||||
lastTopMost bool
|
||||
fontSize int
|
||||
hFont uintptr
|
||||
lastSettingsAction time.Time
|
||||
lastInstanceCount int
|
||||
}
|
||||
|
||||
@@ -72,18 +111,30 @@ func guiLog(text string) {
|
||||
func runNativeGUI() error {
|
||||
user32 := syscall.NewLazyDLL("user32.dll")
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
gdi32 := syscall.NewLazyDLL("gdi32.dll")
|
||||
registerClass := user32.NewProc("RegisterClassW")
|
||||
createWindowEx := user32.NewProc("CreateWindowExW")
|
||||
defWindowProc := user32.NewProc("DefWindowProcW")
|
||||
showWindow := user32.NewProc("ShowWindow")
|
||||
showWindowProc = user32.NewProc("ShowWindow")
|
||||
updateWindow := user32.NewProc("UpdateWindow")
|
||||
setWindowPosProc = user32.NewProc("SetWindowPos")
|
||||
sendMessageProc = user32.NewProc("SendMessageW")
|
||||
createFontProc = gdi32.NewProc("CreateFontW")
|
||||
deleteObjectProc = gdi32.NewProc("DeleteObject")
|
||||
getMessage := user32.NewProc("GetMessageW")
|
||||
translateMessage := user32.NewProc("TranslateMessage")
|
||||
dispatchMessage := user32.NewProc("DispatchMessageW")
|
||||
postQuitMessage := user32.NewProc("PostQuitMessage")
|
||||
setForegroundWindow := user32.NewProc("SetForegroundWindow")
|
||||
bringWindowToTop := user32.NewProc("BringWindowToTop")
|
||||
setTimer := user32.NewProc("SetTimer")
|
||||
loadCursor := user32.NewProc("LoadCursorW")
|
||||
setWindowText := user32.NewProc("SetWindowTextW")
|
||||
getWindowRect := user32.NewProc("GetWindowRect")
|
||||
isWindowVisible := user32.NewProc("IsWindowVisible")
|
||||
isIconic := user32.NewProc("IsIconic")
|
||||
getForegroundWindow := user32.NewProc("GetForegroundWindow")
|
||||
getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
getModuleHandle := kernel32.NewProc("GetModuleHandleW")
|
||||
loadIcon := user32.NewProc("LoadIconW")
|
||||
|
||||
@@ -116,39 +167,108 @@ func runNativeGUI() error {
|
||||
guiLog("stage=prepare window")
|
||||
|
||||
var app guiApp
|
||||
guiCfg := loadGUISettings()
|
||||
app.activeWindow = guiCfg.ActiveWindow
|
||||
app.topMost = guiCfg.TopMost
|
||||
if guiCfg.FontSize > 0 {
|
||||
app.fontSize = guiCfg.FontSize
|
||||
} else {
|
||||
app.fontSize = 18
|
||||
}
|
||||
app.hFont = createAppFont(app.fontSize)
|
||||
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")
|
||||
leftClass, _ := syscall.UTF16PtrFromString("STATIC")
|
||||
rightClass, _ := syscall.UTF16PtrFromString("STATIC")
|
||||
leftTitle, _ := syscall.UTF16PtrFromString("")
|
||||
rightTitle, _ := syscall.UTF16PtrFromString("")
|
||||
joinTitle, _ := syscall.UTF16PtrFromString("Join Log")
|
||||
discordTitle, _ := syscall.UTF16PtrFromString("Discord")
|
||||
translateTitle, _ := syscall.UTF16PtrFromString("Translate")
|
||||
settingsTitle, _ := syscall.UTF16PtrFromString("Settings")
|
||||
activeWindowTitle, _ := syscall.UTF16PtrFromString("Active window")
|
||||
topMostTitle, _ := syscall.UTF16PtrFromString("Top most")
|
||||
fontDownTitle, _ := syscall.UTF16PtrFromString("A-")
|
||||
fontUpTitle, _ := syscall.UTF16PtrFromString("A+")
|
||||
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.tabTranslate, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(translateTitle)), wsVisible|wsChild, 470, 20, 90, 28, hwnd, 3002, hInstance, 0)
|
||||
app.tabSettings, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(settingsTitle)), 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, 20, 20, 330, 300, hwnd, idStatus, hInstance, 0)
|
||||
app.rightHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(rightClass)), uintptr(unsafe.Pointer(rightTitle)), wsVisible|wsChild|wsClipChildren, 370, 60, 330, 260, hwnd, idLog, hInstance, 0)
|
||||
settingsPaneTitle, _ := syscall.UTF16PtrFromString("")
|
||||
app.settingsPaneHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(rightClass)), uintptr(unsafe.Pointer(settingsPaneTitle)), wsVisible|wsChild|wsClipChildren, 370, 60, 330, 260, hwnd, idLog+1, hInstance, 0)
|
||||
app.btnActiveWindow, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(activeWindowTitle)), wsVisible|wsChild, 390, 90, 130, 30, hwnd, 3004, hInstance, 0)
|
||||
app.btnTopMost, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(topMostTitle)), wsVisible|wsChild, 390, 130, 120, 30, hwnd, 3005, hInstance, 0)
|
||||
app.btnFontDown, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontDownTitle)), wsVisible|wsChild, 390, 175, 50, 30, hwnd, 3006, hInstance, 0)
|
||||
app.btnFontUp, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontUpTitle)), wsVisible|wsChild, 445, 175, 50, 30, hwnd, 3007, hInstance, 0)
|
||||
app.activeTab = "join"
|
||||
app.activeWindow = true
|
||||
app.lastActiveWindow = false
|
||||
setTimer.Call(hwnd, timerRefresh, 2000, 0)
|
||||
applyFont(&app)
|
||||
refreshGUI(setWindowText, &app)
|
||||
applyActiveWindow(hwnd, &app, showWindowProc, setForegroundWindow, bringWindowToTop, setWindowPosProc, getForegroundWindow, getSystemMetrics, getWindowRect, isWindowVisible, isIconic)
|
||||
ensureWindowVisible("startup", hwnd, showWindowProc, setWindowPosProc, getWindowRect, isWindowVisible, isIconic, getSystemMetrics)
|
||||
logWindowState("after-ensure", hwnd, getWindowRect, isWindowVisible, isIconic)
|
||||
return 0
|
||||
case wmCommandGUI:
|
||||
switch uint16(wParam & 0xffff) {
|
||||
case 3001:
|
||||
app.activeTab = "join"
|
||||
case 3002:
|
||||
app.activeTab = "discord"
|
||||
case 3003:
|
||||
app.activeTab = "translate"
|
||||
case 3003:
|
||||
app.activeTab = "settings"
|
||||
case 3004:
|
||||
if !allowRapidSettingsAction(&app) {
|
||||
return 0
|
||||
}
|
||||
app.activeWindow = !app.activeWindow
|
||||
saveGUISettings(&app)
|
||||
applyActiveWindow(hwnd, &app, showWindowProc, setForegroundWindow, bringWindowToTop, setWindowPosProc, getForegroundWindow, getSystemMetrics, getWindowRect, isWindowVisible, isIconic)
|
||||
case 3005:
|
||||
if !allowRapidSettingsAction(&app) {
|
||||
return 0
|
||||
}
|
||||
app.topMost = !app.topMost
|
||||
saveGUISettings(&app)
|
||||
applyTopMost(hwnd, &app)
|
||||
case 3006:
|
||||
if !allowRapidSettingsAction(&app) {
|
||||
return 0
|
||||
}
|
||||
if app.fontSize > 10 {
|
||||
app.fontSize -= 2
|
||||
oldFont := app.hFont
|
||||
app.hFont = createAppFont(app.fontSize)
|
||||
applyFont(&app)
|
||||
if oldFont != 0 && deleteObjectProc != nil {
|
||||
deleteObjectProc.Call(oldFont)
|
||||
}
|
||||
saveGUISettings(&app)
|
||||
}
|
||||
case 3007:
|
||||
if !allowRapidSettingsAction(&app) {
|
||||
return 0
|
||||
}
|
||||
if app.fontSize < 30 {
|
||||
app.fontSize += 2
|
||||
oldFont := app.hFont
|
||||
app.hFont = createAppFont(app.fontSize)
|
||||
applyFont(&app)
|
||||
if oldFont != 0 && deleteObjectProc != nil {
|
||||
deleteObjectProc.Call(oldFont)
|
||||
}
|
||||
saveGUISettings(&app)
|
||||
}
|
||||
}
|
||||
refreshGUI(setWindowText, &app)
|
||||
refreshSettingsControls(showWindowProc, setWindowPosProc, &app)
|
||||
return 0
|
||||
case wmTimerGUI:
|
||||
refreshGUI(setWindowText, &app)
|
||||
refreshSettingsControls(showWindowProc, setWindowPosProc, &app)
|
||||
return 0
|
||||
case wmDestroyGUI:
|
||||
postQuitMessage.Call(0)
|
||||
@@ -174,15 +294,20 @@ func runNativeGUI() error {
|
||||
}
|
||||
|
||||
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)
|
||||
app.hwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(title)), wsOverlappedWindow|wsVisible, 200, 120, 760, 380, 0, 0, hInstance, 0)
|
||||
if app.hwnd == 0 {
|
||||
guiLog("stage=create window failed")
|
||||
return fmt.Errorf("create window failed")
|
||||
}
|
||||
guiLog(fmt.Sprintf("stage=create window ok hwnd=%d", app.hwnd))
|
||||
logWindowState("after-create", app.hwnd, getWindowRect, isWindowVisible, isIconic)
|
||||
|
||||
guiLog("stage=show window")
|
||||
showWindow.Call(app.hwnd, 5)
|
||||
prevVisible, _, _ := showWindowProc.Call(app.hwnd, 1)
|
||||
guiLog(fmt.Sprintf("stage=show window ret=%d", prevVisible))
|
||||
updateWindow.Call(app.hwnd)
|
||||
ensureWindowVisible("startup", app.hwnd, showWindowProc, setWindowPosProc, getWindowRect, isWindowVisible, isIconic, getSystemMetrics)
|
||||
logWindowState("after-show", app.hwnd, getWindowRect, isWindowVisible, isIconic)
|
||||
guiLog("stage=message loop")
|
||||
|
||||
var m msg
|
||||
@@ -197,8 +322,38 @@ func runNativeGUI() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func logWindowState(stage string, hwnd uintptr, getWindowRect, isWindowVisible, isIconic *syscall.LazyProc) {
|
||||
if hwnd == 0 {
|
||||
guiLog(stage + " hwnd=0")
|
||||
return
|
||||
}
|
||||
var rect struct {
|
||||
Left int32
|
||||
Top int32
|
||||
Right int32
|
||||
Bottom int32
|
||||
}
|
||||
getWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&rect)))
|
||||
visible, _, _ := isWindowVisible.Call(hwnd)
|
||||
minimized, _, _ := isIconic.Call(hwnd)
|
||||
guiLog(fmt.Sprintf(
|
||||
"%s hwnd=%d visible=%t minimized=%t rect=(%d,%d)-(%d,%d) size=%dx%d",
|
||||
stage,
|
||||
hwnd,
|
||||
visible != 0,
|
||||
minimized != 0,
|
||||
rect.Left,
|
||||
rect.Top,
|
||||
rect.Right,
|
||||
rect.Bottom,
|
||||
rect.Right-rect.Left,
|
||||
rect.Bottom-rect.Top,
|
||||
))
|
||||
}
|
||||
|
||||
func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp) {
|
||||
current, instanceCount := currentUsersFromJoinLeave()
|
||||
guiLog(fmt.Sprintf("refreshGUI activeTab=%s current=%d", app.activeTab, instanceCount))
|
||||
if instanceCount == 0 && app.lastInstanceCount > 0 {
|
||||
instanceCount = app.lastInstanceCount
|
||||
}
|
||||
@@ -209,59 +364,264 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp) {
|
||||
t, _ := syscall.UTF16PtrFromString(formatGuestPane("Join Log", current))
|
||||
setWindowText.Call(app.leftHwnd, uintptr(unsafe.Pointer(t)))
|
||||
}
|
||||
if app.btnActiveWindow != 0 {
|
||||
label := "Active window: OFF"
|
||||
if app.activeWindow {
|
||||
label = "Active window: ON"
|
||||
}
|
||||
t, _ := syscall.UTF16PtrFromString(label)
|
||||
setWindowText.Call(app.btnActiveWindow, uintptr(unsafe.Pointer(t)))
|
||||
}
|
||||
if app.btnTopMost != 0 {
|
||||
label := "Top most: OFF"
|
||||
if app.topMost {
|
||||
label = "Top most: ON"
|
||||
}
|
||||
t, _ := syscall.UTF16PtrFromString(label)
|
||||
setWindowText.Call(app.btnTopMost, uintptr(unsafe.Pointer(t)))
|
||||
}
|
||||
if app.activeTab == "settings" {
|
||||
if app.rightHwnd != 0 {
|
||||
t, _ := syscall.UTF16PtrFromString(formatRightPane(app.activeTab, readRuntimeSnapshot(), currentWorldLabel(), instanceCount))
|
||||
showWindowProc.Call(app.rightHwnd, swHideControl)
|
||||
}
|
||||
if app.settingsPaneHwnd != 0 {
|
||||
showWindowProc.Call(app.settingsPaneHwnd, swShowControl)
|
||||
setWindowPosProc.Call(app.settingsPaneHwnd, 0, 370, 60, 330, 260, swpVisibleFlags)
|
||||
}
|
||||
for _, hwnd := range []uintptr{app.btnActiveWindow, app.btnTopMost, app.btnFontDown, app.btnFontUp} {
|
||||
if hwnd != 0 {
|
||||
showWindowProc.Call(hwnd, swShowControl)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if app.rightHwnd != 0 {
|
||||
state := readRuntimeSnapshot()
|
||||
state["active_window"] = app.activeWindow
|
||||
state["top_most"] = app.topMost
|
||||
state["font_size"] = app.fontSize
|
||||
rightText := formatRightPane(app.activeTab, state, currentWorldLabel(), instanceCount)
|
||||
showWindowProc.Call(app.rightHwnd, swShowControl)
|
||||
t, _ := syscall.UTF16PtrFromString(rightText)
|
||||
setWindowText.Call(app.rightHwnd, uintptr(unsafe.Pointer(t)))
|
||||
}
|
||||
if app.settingsPaneHwnd != 0 {
|
||||
showWindowProc.Call(app.settingsPaneHwnd, swHideControl)
|
||||
}
|
||||
for _, hwnd := range []uintptr{app.btnActiveWindow, app.btnTopMost, app.btnFontDown, app.btnFontUp} {
|
||||
if hwnd != 0 {
|
||||
showWindowProc.Call(hwnd, swHideControl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func currentUsersFromJoinLeave() ([]userState, int) {
|
||||
since := currentWorldStartTime()
|
||||
users := map[string]*userState{}
|
||||
lastLoggedCount := 0
|
||||
for _, s := range readGuestSnapshot() {
|
||||
if !s.Present {
|
||||
func refreshSettingsControls(showWindowProc, setWindowPosProc *syscall.LazyProc, app *guiApp) {
|
||||
if showWindowProc == nil || setWindowPosProc == nil || app == nil {
|
||||
return
|
||||
}
|
||||
if app.activeTab == "settings" {
|
||||
if app.settingsPaneHwnd != 0 {
|
||||
showWindowProc.Call(app.settingsPaneHwnd, swShowControl)
|
||||
setWindowPosProc.Call(app.settingsPaneHwnd, 0, 370, 60, 330, 260, swpVisibleFlags)
|
||||
}
|
||||
for _, hwnd := range []uintptr{app.btnActiveWindow, app.btnTopMost, app.btnFontDown, app.btnFontUp} {
|
||||
if hwnd != 0 {
|
||||
showWindowProc.Call(hwnd, swShowControl)
|
||||
}
|
||||
}
|
||||
guiLog("refreshSettingsControls visible")
|
||||
return
|
||||
}
|
||||
if app.settingsPaneHwnd != 0 {
|
||||
showWindowProc.Call(app.settingsPaneHwnd, swHideControl)
|
||||
}
|
||||
for _, hwnd := range []uintptr{app.btnActiveWindow, app.btnTopMost, app.btnFontDown, app.btnFontUp} {
|
||||
if hwnd != 0 {
|
||||
showWindowProc.Call(hwnd, swHideControl)
|
||||
}
|
||||
}
|
||||
guiLog("refreshSettingsControls hidden")
|
||||
}
|
||||
|
||||
func applyActiveWindow(hwnd uintptr, app *guiApp, showWindowProc, setForegroundWindow, bringWindowToTop, setWindowPosProc, getForegroundWindow, getSystemMetrics, getWindowRect, isWindowVisible, isIconic *syscall.LazyProc) {
|
||||
if hwnd == 0 || app.activeWindow == app.lastActiveWindow {
|
||||
return
|
||||
}
|
||||
if app.activeWindow {
|
||||
showWindowProc.Call(hwnd, swRestore)
|
||||
setWindowPosProc.Call(hwnd, 0, 0, 0, 0, 0, swpShowFlags)
|
||||
bringWindowToTop.Call(hwnd)
|
||||
setForegroundWindow.Call(hwnd)
|
||||
foreground, _, _ := getForegroundWindow.Call()
|
||||
guiLog(fmt.Sprintf("applyActiveWindow on foreground=%d target=%d", foreground, hwnd))
|
||||
ensureWindowVisible("active-window", hwnd, showWindowProc, setWindowPosProc, getWindowRect, isWindowVisible, isIconic, getSystemMetrics)
|
||||
} else {
|
||||
guiLog("applyActiveWindow off")
|
||||
}
|
||||
app.lastActiveWindow = app.activeWindow
|
||||
}
|
||||
|
||||
func ensureWindowVisible(stage string, hwnd uintptr, showWindowProc, setWindowPosProc, getWindowRect, isWindowVisible, isIconic, getSystemMetrics *syscall.LazyProc) {
|
||||
if hwnd == 0 || getSystemMetrics == nil {
|
||||
return
|
||||
}
|
||||
var rect struct {
|
||||
Left int32
|
||||
Top int32
|
||||
Right int32
|
||||
Bottom int32
|
||||
}
|
||||
if getWindowRect != nil {
|
||||
getWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&rect)))
|
||||
}
|
||||
minimized := false
|
||||
visible := true
|
||||
if isIconic != nil {
|
||||
v, _, _ := isIconic.Call(hwnd)
|
||||
minimized = v != 0
|
||||
}
|
||||
if isWindowVisible != nil {
|
||||
v, _, _ := isWindowVisible.Call(hwnd)
|
||||
visible = v != 0
|
||||
}
|
||||
if minimized && showWindowProc != nil {
|
||||
showWindowProc.Call(hwnd, swRestore)
|
||||
guiLog(stage + " restored from minimized")
|
||||
}
|
||||
vx, _, _ := getSystemMetrics.Call(uintptr(smXVirtualScreen))
|
||||
vy, _, _ := getSystemMetrics.Call(uintptr(smYVirtualScreen))
|
||||
vw, _, _ := getSystemMetrics.Call(uintptr(smCXVirtualScreen))
|
||||
vh, _, _ := getSystemMetrics.Call(uintptr(smCYVirtualScreen))
|
||||
left := int32(vx)
|
||||
top := int32(vy)
|
||||
width := int32(vw)
|
||||
height := int32(vh)
|
||||
w := rect.Right - rect.Left
|
||||
h := rect.Bottom - rect.Top
|
||||
if w <= 0 {
|
||||
w = 760
|
||||
}
|
||||
if h <= 0 {
|
||||
h = 380
|
||||
}
|
||||
offscreen := rect.Right <= left || rect.Bottom <= top || rect.Left >= left+width || rect.Top >= top+height
|
||||
if offscreen && setWindowPosProc != nil {
|
||||
nx := left + (width-w)/2
|
||||
ny := top + (height-h)/2
|
||||
setWindowPosProc.Call(hwnd, 0, uintptr(nx), uintptr(ny), 0, 0, swpShowFlags)
|
||||
guiLog(fmt.Sprintf("%s recentered visible=%t minimized=%t old=(%d,%d)-(%d,%d) new=(%d,%d) screen=(%d,%d %dx%d)",
|
||||
stage, visible, minimized, rect.Left, rect.Top, rect.Right, rect.Bottom, nx, ny, left, top, width, height))
|
||||
return
|
||||
}
|
||||
guiLog(fmt.Sprintf("%s visible=%t minimized=%t rect=(%d,%d)-(%d,%d) screen=(%d,%d %dx%d)",
|
||||
stage, visible, minimized, rect.Left, rect.Top, rect.Right, rect.Bottom, left, top, width, height))
|
||||
}
|
||||
|
||||
func applyTopMost(hwnd uintptr, app *guiApp) {
|
||||
if hwnd == 0 || app.topMost == app.lastTopMost {
|
||||
return
|
||||
}
|
||||
if app.topMost {
|
||||
setWindowPosProc.Call(hwnd, hwndTopMostFlag, 0, 0, 0, 0, swpFlags)
|
||||
} else {
|
||||
setWindowPosProc.Call(hwnd, 0, 0, 0, 0, 0, swpFlags)
|
||||
}
|
||||
app.lastTopMost = app.topMost
|
||||
}
|
||||
|
||||
func createAppFont(size int) uintptr {
|
||||
if createFontProc == nil {
|
||||
return 0
|
||||
}
|
||||
h, _, _ := createFontProc.Call(
|
||||
uintptr(-size), 0, 0, 0, 400, 0, 0, 0,
|
||||
128, 0, 0, 0, 0,
|
||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Yu Gothic UI"))),
|
||||
)
|
||||
return h
|
||||
}
|
||||
|
||||
func applyFont(app *guiApp) {
|
||||
if sendMessageProc == nil || app.hFont == 0 {
|
||||
return
|
||||
}
|
||||
targets := []uintptr{app.leftHwnd, app.rightHwnd, app.tabJoin, app.tabTranslate, app.tabSettings}
|
||||
for _, hwnd := range targets {
|
||||
if hwnd == 0 {
|
||||
continue
|
||||
}
|
||||
cp := s
|
||||
users[s.Name] = &userState{
|
||||
Name: s.Name,
|
||||
Present: true,
|
||||
LastJoin: cp.LastJoin,
|
||||
}
|
||||
sendMessageProc.Call(hwnd, wmSetFont, app.hFont, 1)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
func allowRapidSettingsAction(app *guiApp) bool {
|
||||
if app == nil {
|
||||
return false
|
||||
}
|
||||
now := time.Now()
|
||||
if !app.lastSettingsAction.IsZero() && now.Sub(app.lastSettingsAction) < 150*time.Millisecond {
|
||||
guiLog("settings action skipped: debounce")
|
||||
return false
|
||||
}
|
||||
app.lastSettingsAction = now
|
||||
return true
|
||||
}
|
||||
|
||||
func currentUsersFromJoinLeave() ([]userState, int) {
|
||||
if snap := readGuestSnapshot(); len(snap) > 0 {
|
||||
out := make([]userState, 0, len(snap))
|
||||
for _, s := range snap {
|
||||
out = append(out, userState{
|
||||
Name: s.Name,
|
||||
Present: s.Present,
|
||||
LastJoin: s.LastJoin,
|
||||
LastLeave: s.LastLeave,
|
||||
})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].Present != out[j].Present {
|
||||
return out[i].Present
|
||||
}
|
||||
if out[i].LastLeave.Equal(out[j].LastLeave) {
|
||||
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
||||
}
|
||||
return out[i].LastLeave.After(out[j].LastLeave)
|
||||
})
|
||||
guiLog(fmt.Sprintf("currentUsers source=snapshot count=%d total=%d", countPresent(out), len(out)))
|
||||
return out, countPresent(out)
|
||||
}
|
||||
|
||||
for _, raw := range strings.Split(string(b), "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" {
|
||||
continue
|
||||
if out, ok := currentUsersFromJoinLeaveLog(); ok {
|
||||
guiLog(fmt.Sprintf("currentUsers source=join_leave count=%d total=%d", countPresent(out), len(out)))
|
||||
return out, countPresent(out)
|
||||
}
|
||||
if c := parseJoinLeaveCount(line); c > 0 {
|
||||
lastLoggedCount = c
|
||||
|
||||
if out, ok := currentUsersFromVRChatLog(); ok {
|
||||
guiLog(fmt.Sprintf("currentUsers source=vrc_log count=%d total=%d", countPresent(out), len(out)))
|
||||
return out, countPresent(out)
|
||||
}
|
||||
|
||||
guiLog("currentUsers source=none")
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
func currentUsersFromJoinLeaveLog() ([]userState, bool) {
|
||||
p := filepath.Join(runtimeDir(), "join_leave.log")
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil || len(b) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
users := map[string]*userState{}
|
||||
lines := strings.Split(string(b), "\n")
|
||||
for _, raw := range lines {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" || strings.HasPrefix(line, "VRC JOIN/LEAVE") {
|
||||
continue
|
||||
}
|
||||
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}
|
||||
@@ -270,39 +630,74 @@ func currentUsersFromJoinLeave() ([]userState, int) {
|
||||
switch kind {
|
||||
case "join":
|
||||
u.Present = true
|
||||
if at.After(u.LastJoin) {
|
||||
if !at.IsZero() {
|
||||
u.LastJoin = at
|
||||
}
|
||||
case "leave":
|
||||
u.Present = false
|
||||
if at.After(u.LastLeave) {
|
||||
if !at.IsZero() {
|
||||
u.LastLeave = at
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortUserStates(users), len(users) > 0
|
||||
}
|
||||
|
||||
// Keep any snapshot-marked present users that the join/leave delta can't rebuild.
|
||||
for _, snap := range readGuestSnapshot() {
|
||||
if !snap.Present {
|
||||
func currentUsersFromVRChatLog() ([]userState, bool) {
|
||||
logPath, err := findLatestVRChatLog()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
b, err := os.ReadFile(logPath)
|
||||
if err != nil || len(b) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
text := app.DecodeVRChatLog(b)
|
||||
if strings.TrimSpace(text) == "" {
|
||||
return nil, false
|
||||
}
|
||||
users := map[string]*userState{}
|
||||
lines := strings.Split(text, "\n")
|
||||
for _, raw := range lines {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
u := users[snap.Name]
|
||||
if strings.Contains(line, "Entering Room") || strings.Contains(line, "Joining or Creating Room") {
|
||||
users = map[string]*userState{}
|
||||
continue
|
||||
}
|
||||
at := parseLogTime(line)
|
||||
if m := vrcJoinPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
u := users[name]
|
||||
if u == nil {
|
||||
users[snap.Name] = &userState{
|
||||
Name: snap.Name,
|
||||
Present: true,
|
||||
LastJoin: snap.LastJoin,
|
||||
u = &userState{Name: name}
|
||||
users[name] = u
|
||||
}
|
||||
u.Present = true
|
||||
if !at.IsZero() {
|
||||
u.LastJoin = at
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !u.Present {
|
||||
u.Present = true
|
||||
if u.LastJoin.IsZero() {
|
||||
u.LastJoin = snap.LastJoin
|
||||
if m := vrcLeftPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
u := users[name]
|
||||
if u == nil {
|
||||
u = &userState{Name: name}
|
||||
users[name] = u
|
||||
}
|
||||
u.Present = false
|
||||
if !at.IsZero() {
|
||||
u.LastLeave = at
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortUserStates(users), len(users) > 0
|
||||
}
|
||||
|
||||
func sortUserStates(users map[string]*userState) []userState {
|
||||
out := make([]userState, 0, len(users))
|
||||
for _, u := range users {
|
||||
out = append(out, *u)
|
||||
@@ -311,13 +706,55 @@ func currentUsersFromJoinLeave() ([]userState, int) {
|
||||
if out[i].Present != out[j].Present {
|
||||
return out[i].Present
|
||||
}
|
||||
if out[i].LastLeave.Equal(out[j].LastLeave) {
|
||||
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
||||
})
|
||||
present := countPresent(out)
|
||||
if lastLoggedCount > present {
|
||||
present = lastLoggedCount
|
||||
}
|
||||
return out, present
|
||||
return out[i].LastLeave.After(out[j].LastLeave)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func parseLogTime(line string) time.Time {
|
||||
m := vrcLogTimePattern.FindStringSubmatch(line)
|
||||
if len(m) != 2 {
|
||||
return time.Time{}
|
||||
}
|
||||
at, err := time.Parse("2006.01.02 15:04:05", m[1])
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return at
|
||||
}
|
||||
|
||||
func findLatestVRChatLog() (string, error) {
|
||||
dir := filepath.Join(os.Getenv("USERPROFILE"), "AppData", "LocalLow", "VRChat", "VRChat")
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var latest string
|
||||
var latestMod time.Time
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if !strings.HasSuffix(strings.ToLower(name), ".log") && !strings.HasSuffix(strings.ToLower(name), ".txt") {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().After(latestMod) {
|
||||
latestMod = info.ModTime()
|
||||
latest = filepath.Join(dir, name)
|
||||
}
|
||||
}
|
||||
if latest == "" {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func readGuestSnapshot() []app.GuestStatus {
|
||||
@@ -335,6 +772,9 @@ func readGuestSnapshot() []app.GuestStatus {
|
||||
|
||||
func formatGuestPane(title string, items []userState) string {
|
||||
var b strings.Builder
|
||||
selfName := currentSelfName()
|
||||
var selfJoin string
|
||||
guiLog("formatGuestPane start")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\r\n")
|
||||
b.WriteString(strings.Repeat("=", len(title)))
|
||||
@@ -348,21 +788,113 @@ func formatGuestPane(title string, items []userState) string {
|
||||
b.WriteString("Current in room: ")
|
||||
b.WriteString(strconv.Itoa(countPresent(items)))
|
||||
b.WriteString("\r\n\r\n")
|
||||
present := make([]userState, 0, len(items))
|
||||
left := make([]userState, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Present {
|
||||
present = append(present, item)
|
||||
} else {
|
||||
left = append(left, item)
|
||||
}
|
||||
if selfName != "" && strings.EqualFold(strings.TrimSpace(item.Name), selfName) && !item.LastJoin.IsZero() {
|
||||
selfJoin = item.LastJoin.Format("15:04:05")
|
||||
}
|
||||
}
|
||||
if selfJoin != "" {
|
||||
b.WriteString("You joined at ")
|
||||
b.WriteString(selfJoin)
|
||||
b.WriteString(" (self=")
|
||||
b.WriteString(selfName)
|
||||
b.WriteString(")")
|
||||
b.WriteString("\r\n\r\n")
|
||||
}
|
||||
if len(present) > 0 {
|
||||
b.WriteString("Present now")
|
||||
b.WriteString("\r\n")
|
||||
for _, item := range present {
|
||||
b.WriteString("・ ")
|
||||
b.WriteString(item.Name)
|
||||
if item.Present {
|
||||
b.WriteString(" [present]")
|
||||
} else {
|
||||
if !item.LastJoin.IsZero() {
|
||||
b.WriteString(" ")
|
||||
b.WriteString(item.LastJoin.Format("15:04:05"))
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
if len(left) > 0 {
|
||||
b.WriteString("Left already")
|
||||
b.WriteString("\r\n")
|
||||
sort.SliceStable(left, func(i, j int) bool {
|
||||
if left[i].LastLeave.Equal(left[j].LastLeave) {
|
||||
return strings.ToLower(left[i].Name) < strings.ToLower(left[j].Name)
|
||||
}
|
||||
return left[i].LastLeave.After(left[j].LastLeave)
|
||||
})
|
||||
for _, item := range left {
|
||||
b.WriteString("・ ")
|
||||
b.WriteString(item.Name)
|
||||
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")
|
||||
}
|
||||
text := strings.TrimRight(b.String(), "\r\n")
|
||||
guiLog("formatGuestPane done")
|
||||
if err := app.AppendDesktopJoinLog(title, currentWorldLabel(), text); err != nil {
|
||||
_ = app.AppendRuntimeLog("GUI JOIN LOG WRITE FAIL", err.Error())
|
||||
} else {
|
||||
_ = app.AppendRuntimeLog("GUI JOIN LOG WRITE OK", "written desktop join.txt")
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func currentSelfName() string {
|
||||
if cfg, err := config.Load(""); err == nil && cfg != nil {
|
||||
if name := strings.TrimSpace(cfg.VrcLog.SelfName); name != "" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
cfg, err := config.Load(filepath.Join(cwd, "config", "config.toml"))
|
||||
if err != nil || cfg == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(cfg.VrcLog.SelfName)
|
||||
}
|
||||
|
||||
func loadGUISettings() config.GUIConfig {
|
||||
cfg, err := config.Load("")
|
||||
if err != nil || cfg == nil {
|
||||
return config.GUIConfig{ActiveWindow: true, FontSize: 18}
|
||||
}
|
||||
if cfg.GUI.FontSize <= 0 {
|
||||
cfg.GUI.FontSize = 18
|
||||
}
|
||||
return cfg.GUI
|
||||
}
|
||||
|
||||
func saveGUISettings(app *guiApp) {
|
||||
if app == nil {
|
||||
return
|
||||
}
|
||||
guiCfg := config.GUIConfig{
|
||||
ActiveWindow: app.activeWindow,
|
||||
TopMost: app.topMost,
|
||||
FontSize: app.fontSize,
|
||||
}
|
||||
if err := config.SaveGUI("", guiCfg); err != nil {
|
||||
guiLog("saveGUISettings failed: " + err.Error())
|
||||
return
|
||||
}
|
||||
guiLog("saveGUISettings ok")
|
||||
}
|
||||
|
||||
func formatRightPane(tab string, state map[string]any, worldLabel string, currentUsers int) string {
|
||||
@@ -372,52 +904,86 @@ func formatRightPane(tab string, state map[string]any, worldLabel string, curren
|
||||
b.WriteString("Discord")
|
||||
case "translate":
|
||||
b.WriteString("Translate")
|
||||
case "settings":
|
||||
b.WriteString("Settings")
|
||||
default:
|
||||
b.WriteString("Current World")
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
titleLine := "Current World"
|
||||
switch tab {
|
||||
case "discord":
|
||||
titleLine = "Discord"
|
||||
case "translate":
|
||||
titleLine = "Translate"
|
||||
case "settings":
|
||||
titleLine = "Settings"
|
||||
case "discord":
|
||||
titleLine = "Discord"
|
||||
}
|
||||
b.WriteString(strings.Repeat("=", len(titleLine)))
|
||||
b.WriteString("\r\n")
|
||||
discordMuted, _ := state["discord_muted"].(bool)
|
||||
ocr, _ := state["ocr"].(string)
|
||||
translate, _ := state["translate"].(string)
|
||||
fontSize, _ := state["font_size"].(int)
|
||||
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)
|
||||
if strings.TrimSpace(translate) == "" {
|
||||
b.WriteString("OFF")
|
||||
} else {
|
||||
b.WriteString("ON")
|
||||
}
|
||||
b.WriteString("\r\nOCR: ")
|
||||
b.WriteString(ocr)
|
||||
if strings.TrimSpace(ocr) == "" {
|
||||
b.WriteString("OFF")
|
||||
} else {
|
||||
b.WriteString("ON")
|
||||
}
|
||||
b.WriteString("\r\nWorld: ")
|
||||
b.WriteString(worldLabel)
|
||||
case "settings":
|
||||
b.WriteString("Active window: ")
|
||||
if stateBool(state, "active_window") {
|
||||
b.WriteString("ON")
|
||||
} else {
|
||||
b.WriteString("OFF")
|
||||
}
|
||||
b.WriteString("\r\nTop most: ")
|
||||
if stateBool(state, "top_most") {
|
||||
b.WriteString("ON")
|
||||
} else {
|
||||
b.WriteString("OFF")
|
||||
}
|
||||
b.WriteString("\r\nFont size: ")
|
||||
if fontSize == 0 {
|
||||
fontSize = 18
|
||||
}
|
||||
b.WriteString(strconv.Itoa(fontSize))
|
||||
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)
|
||||
if strings.TrimSpace(ocr) == "" {
|
||||
b.WriteString("OFF")
|
||||
} else {
|
||||
b.WriteString("ON")
|
||||
}
|
||||
b.WriteString("\r\nTranslate: ")
|
||||
b.WriteString(translate)
|
||||
if strings.TrimSpace(translate) == "" {
|
||||
b.WriteString("OFF")
|
||||
} else {
|
||||
b.WriteString("ON")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func stateBool(state map[string]any, key string) bool {
|
||||
v, _ := state[key].(bool)
|
||||
return v
|
||||
}
|
||||
|
||||
func currentWorldLabel() string {
|
||||
p := filepath.Join(runtimeDir(), "runtime.log")
|
||||
b, err := os.ReadFile(p)
|
||||
@@ -457,7 +1023,13 @@ func currentWorldStartTime() 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") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if !(strings.Contains(line, "VRC WORLD") ||
|
||||
strings.Contains(line, "Joining or Creating Room") ||
|
||||
strings.Contains(line, "world loaded") ||
|
||||
strings.Contains(line, "Successfully joined room")) {
|
||||
continue
|
||||
}
|
||||
if at, ok := parseRuntimeTime(line); ok && at.After(latest) {
|
||||
|
||||
Reference in New Issue
Block a user