Restore VRTW_Tool title; note discord mute regression
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"vrc_osc_go/internal/app"
|
"vrc_osc_go/internal/app"
|
||||||
)
|
)
|
||||||
@@ -11,12 +13,30 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
var configPath string
|
var configPath string
|
||||||
var mode string
|
var mode string
|
||||||
|
var noGUI bool
|
||||||
flag.StringVar(&configPath, "config", "config/config.toml", "path to config.toml")
|
flag.StringVar(&configPath, "config", "config/config.toml", "path to config.toml")
|
||||||
flag.StringVar(&mode, "mode", "", "generate-json or extract-log")
|
flag.StringVar(&mode, "mode", "", "generate-json or extract-log")
|
||||||
|
flag.BoolVar(&noGUI, "no-gui", false, "do not launch the GUI companion")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if mode == "" && !noGUI {
|
||||||
|
launchGUI()
|
||||||
|
}
|
||||||
|
|
||||||
if err := app.Run(configPath, mode); err != nil {
|
if err := app.Run(configPath, mode); err != nil {
|
||||||
log.SetOutput(os.Stderr)
|
log.SetOutput(os.Stderr)
|
||||||
log.Fatalf("vrc_osc_go: %v", err)
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,20 +5,21 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
|
||||||
|
|
||||||
"vrc_osc_go/internal/app"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func runtimeDir() string {
|
||||||
go func() {
|
exe, err := os.Executable()
|
||||||
if err := app.Run("config/config.toml", ""); err != nil {
|
if err != nil {
|
||||||
log.Printf("backend stopped: %v", err)
|
return "runtime"
|
||||||
|
}
|
||||||
|
return filepath.Join(filepath.Dir(exe), "runtime")
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
|
func main() {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", handleIndex)
|
mux.HandleFunc("/", handleIndex)
|
||||||
mux.HandleFunc("/api/state", handleState)
|
mux.HandleFunc("/api/state", handleState)
|
||||||
@@ -36,7 +37,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="refresh" content="3">
|
<meta http-equiv="refresh" content="3">
|
||||||
<title>VRC_OSC GUI</title>
|
<title>VRTW_Tool</title>
|
||||||
<style>
|
<style>
|
||||||
body{font-family:sans-serif;background:#111;color:#eee;margin:20px}
|
body{font-family:sans-serif;background:#111;color:#eee;margin:20px}
|
||||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||||
@@ -49,7 +50,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>VRC_OSC GUI</h1>
|
<h1>VRTW_Tool</h1>
|
||||||
<div id="root">loading...</div>
|
<div id="root">loading...</div>
|
||||||
<script>
|
<script>
|
||||||
const sortKey = 'vrc_osc_gui_sort';
|
const sortKey = 'vrc_osc_gui_sort';
|
||||||
@@ -149,19 +150,42 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleState(w http.ResponseWriter, r *http.Request) {
|
func handleState(w http.ResponseWriter, r *http.Request) {
|
||||||
s := app.SnapshotRuntime()
|
statePath := filepath.Join(runtimeDir(), "state.json")
|
||||||
guests := []app.GuestStatus{}
|
body, err := os.ReadFile(statePath)
|
||||||
if t := app.GetGuestTracker(); t != nil {
|
if err != nil {
|
||||||
guests = t.Snapshot(time.Now())
|
|
||||||
}
|
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"world": s.CurrentWorld,
|
"world": "",
|
||||||
"discord_muted": s.DiscordMuted,
|
"discord_muted": false,
|
||||||
"ocr": s.LastOCRText,
|
"ocr": "",
|
||||||
"translate": s.LastTranslate,
|
"translate": "",
|
||||||
"guests": guests,
|
"guests": []any{},
|
||||||
"guest_count": len(guests),
|
"guest_count": 0,
|
||||||
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var state map[string]any
|
||||||
|
if err := json.Unmarshal(body, &state); err != nil {
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guests := loadGuestSnapshot()
|
||||||
|
state["guests"] = guests
|
||||||
|
state["guest_count"] = len(guests)
|
||||||
|
_ = json.NewEncoder(w).Encode(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadGuestSnapshot() []map[string]any {
|
||||||
|
statePath := filepath.Join(runtimeDir(), "guest_snapshot.json")
|
||||||
|
body, err := os.ReadFile(statePath)
|
||||||
|
if err != nil {
|
||||||
|
return []map[string]any{}
|
||||||
|
}
|
||||||
|
var guests []map[string]any
|
||||||
|
if err := json.Unmarshal(body, &guests); err != nil {
|
||||||
|
return []map[string]any{}
|
||||||
|
}
|
||||||
|
return guests
|
||||||
}
|
}
|
||||||
|
|
||||||
func openBrowser(url string) {
|
func openBrowser(url string) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func Run(configPath string, mode string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
SetGuestTracker(NewGuestTracker(cfg.VrcLog.GuestNames))
|
InitGuestTracker(NewGuestTracker(cfg.VrcLog.GuestNames))
|
||||||
log.Printf("vrc_osc_go starting osc=%s:%d config=%s", cfg.OSC.Host, cfg.OSC.Port, configPath)
|
log.Printf("vrc_osc_go starting osc=%s:%d config=%s", cfg.OSC.Host, cfg.OSC.Port, configPath)
|
||||||
|
|
||||||
server := osc.NewServer(cfg.OSC.Host, cfg.OSC.Port)
|
server := osc.NewServer(cfg.OSC.Host, cfg.OSC.Port)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -9,13 +8,68 @@ import (
|
|||||||
|
|
||||||
func pressDiscordMuteHotkey() error {
|
func pressDiscordMuteHotkey() error {
|
||||||
script := `
|
script := `
|
||||||
$p = Get-Process | Where-Object { $_.MainWindowTitle -match 'Discord' } | Select-Object -First 1
|
$ErrorActionPreference = "Stop"
|
||||||
if ($null -eq $p) { exit 2 }
|
|
||||||
[void][System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
function Get-DiscordWindows {
|
||||||
|
$windows = foreach ($p in Get-Process) {
|
||||||
|
$title = ($p.MainWindowTitle | Out-String).Trim()
|
||||||
|
if (-not $title) { continue }
|
||||||
|
if (-not $title.ToLower().Contains('discord')) { continue }
|
||||||
|
$p
|
||||||
|
}
|
||||||
|
if (-not $windows) { return @() }
|
||||||
|
return $windows
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-BestDiscordWindow {
|
||||||
|
$windows = Get-DiscordWindows
|
||||||
|
if (-not $windows -or $windows.Count -eq 0) { return $null }
|
||||||
|
|
||||||
|
$browserKeywords = @('google chrome', 'microsoft edge', 'mozilla firefox', 'brave', 'opera')
|
||||||
|
$ranked = $windows | Sort-Object {
|
||||||
|
$title = ($_.MainWindowTitle | Out-String).Trim().ToLower()
|
||||||
|
$score = 0
|
||||||
|
foreach ($keyword in $browserKeywords) {
|
||||||
|
if ($title.Contains($keyword)) { $score += 10 }
|
||||||
|
}
|
||||||
|
if ($title -match '^\(\d+\)\s*discord') { $score += 20 }
|
||||||
|
if ($title.Contains('discord')) { $score += 5 }
|
||||||
|
$score
|
||||||
|
} -Descending
|
||||||
|
|
||||||
|
return $ranked | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-VRChatWindow {
|
||||||
|
$windows = Get-Process | Where-Object {
|
||||||
|
$_.MainWindowTitle -and (
|
||||||
|
$_.MainWindowTitle -eq 'VRChat' -or $_.MainWindowTitle.ToLower().StartsWith('vrchat ')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (-not $windows) { return $null }
|
||||||
|
return $windows | Sort-Object { $_.MainWindowHandle } -Descending | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Activate-Window($p) {
|
||||||
|
if ($null -eq $p) { return }
|
||||||
$wshell = New-Object -ComObject WScript.Shell
|
$wshell = New-Object -ComObject WScript.Shell
|
||||||
$null = $wshell.AppActivate($p.Id)
|
try { $null = $wshell.AppActivate($p.Id) } catch {}
|
||||||
Start-Sleep -Milliseconds 150
|
Start-Sleep -Milliseconds 200
|
||||||
|
}
|
||||||
|
|
||||||
|
$discord = Get-BestDiscordWindow
|
||||||
|
if ($null -eq $discord) { exit 2 }
|
||||||
|
|
||||||
|
Activate-Window $discord
|
||||||
|
$wshell = New-Object -ComObject WScript.Shell
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
$wshell.SendKeys('^+m')
|
$wshell.SendKeys('^+m')
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
|
||||||
|
$vrchat = Get-VRChatWindow
|
||||||
|
if ($null -ne $vrchat) {
|
||||||
|
Activate-Window $vrchat
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
|
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||||
@@ -28,9 +82,3 @@ $wshell.SendKeys('^+m')
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pressDiscordMuteHotkeyLegacy() error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = buf
|
|
||||||
return pressDiscordMuteHotkey()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ func handleDiscordSend(state *discordMuteState, args []osc.Value) error {
|
|||||||
} else {
|
} else {
|
||||||
log.Printf("INFO already muted")
|
log.Printf("INFO already muted")
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"vrc_osc_go/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GuestStatus struct {
|
type GuestStatus struct {
|
||||||
@@ -48,6 +53,7 @@ func (t *GuestTracker) MarkJoin(name string, at time.Time) {
|
|||||||
s.Present = true
|
s.Present = true
|
||||||
s.LastJoin = at
|
s.LastJoin = at
|
||||||
s.AbsentFor = ""
|
s.AbsentFor = ""
|
||||||
|
_ = t.persistLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *GuestTracker) MarkLeave(name string, at time.Time) {
|
func (t *GuestTracker) MarkLeave(name string, at time.Time) {
|
||||||
@@ -59,6 +65,7 @@ func (t *GuestTracker) MarkLeave(name string, at time.Time) {
|
|||||||
if !s.LastJoin.IsZero() {
|
if !s.LastJoin.IsZero() {
|
||||||
s.AbsentFor = humanSince(at)
|
s.AbsentFor = humanSince(at)
|
||||||
}
|
}
|
||||||
|
_ = t.persistLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *GuestTracker) Snapshot(now time.Time) []GuestStatus {
|
func (t *GuestTracker) Snapshot(now time.Time) []GuestStatus {
|
||||||
@@ -76,6 +83,26 @@ func (t *GuestTracker) Snapshot(now time.Time) []GuestStatus {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *GuestTracker) persistLocked() error {
|
||||||
|
dir := filepath.Join(common.RootDir(), "runtime")
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
snapshot := make([]GuestStatus, 0, len(t.items))
|
||||||
|
for _, s := range t.items {
|
||||||
|
cp := *s
|
||||||
|
snapshot = append(snapshot, cp)
|
||||||
|
}
|
||||||
|
f, err := os.Create(filepath.Join(dir, "guest_snapshot.json"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
enc := json.NewEncoder(f)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *GuestTracker) ensure(name string) *GuestStatus {
|
func (t *GuestTracker) ensure(name string) *GuestStatus {
|
||||||
if s, ok := t.items[name]; ok {
|
if s, ok := t.items[name]; ok {
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import "sync"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"vrc_osc_go/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
type RuntimeState struct {
|
type RuntimeState struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -14,30 +22,42 @@ var runtimeState = &RuntimeState{}
|
|||||||
var guestTracker *GuestTracker
|
var guestTracker *GuestTracker
|
||||||
|
|
||||||
func SetGuestTracker(t *GuestTracker) { guestTracker = t }
|
func SetGuestTracker(t *GuestTracker) { guestTracker = t }
|
||||||
|
func InitGuestTracker(t *GuestTracker) {
|
||||||
|
guestTracker = t
|
||||||
|
if guestTracker != nil {
|
||||||
|
guestTracker.mu.Lock()
|
||||||
|
_ = guestTracker.persistLocked()
|
||||||
|
guestTracker.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
func GetGuestTracker() *GuestTracker { return guestTracker }
|
func GetGuestTracker() *GuestTracker { return guestTracker }
|
||||||
|
|
||||||
func SetDiscordMuted(v bool) {
|
func SetDiscordMuted(v bool) {
|
||||||
runtimeState.mu.Lock()
|
runtimeState.mu.Lock()
|
||||||
defer runtimeState.mu.Unlock()
|
|
||||||
runtimeState.DiscordMuted = v
|
runtimeState.DiscordMuted = v
|
||||||
|
runtimeState.mu.Unlock()
|
||||||
|
_ = persistRuntimeState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetOCRText(v string) {
|
func SetOCRText(v string) {
|
||||||
runtimeState.mu.Lock()
|
runtimeState.mu.Lock()
|
||||||
defer runtimeState.mu.Unlock()
|
|
||||||
runtimeState.LastOCRText = v
|
runtimeState.LastOCRText = v
|
||||||
|
runtimeState.mu.Unlock()
|
||||||
|
_ = persistRuntimeState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetTranslateText(v string) {
|
func SetTranslateText(v string) {
|
||||||
runtimeState.mu.Lock()
|
runtimeState.mu.Lock()
|
||||||
defer runtimeState.mu.Unlock()
|
|
||||||
runtimeState.LastTranslate = v
|
runtimeState.LastTranslate = v
|
||||||
|
runtimeState.mu.Unlock()
|
||||||
|
_ = persistRuntimeState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetCurrentWorld(v string) {
|
func SetCurrentWorld(v string) {
|
||||||
runtimeState.mu.Lock()
|
runtimeState.mu.Lock()
|
||||||
defer runtimeState.mu.Unlock()
|
|
||||||
runtimeState.CurrentWorld = v
|
runtimeState.CurrentWorld = v
|
||||||
|
runtimeState.mu.Unlock()
|
||||||
|
_ = persistRuntimeState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SnapshotRuntime() RuntimeState {
|
func SnapshotRuntime() RuntimeState {
|
||||||
@@ -45,3 +65,25 @@ func SnapshotRuntime() RuntimeState {
|
|||||||
defer runtimeState.mu.RUnlock()
|
defer runtimeState.mu.RUnlock()
|
||||||
return *runtimeState
|
return *runtimeState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func persistRuntimeState() error {
|
||||||
|
s := SnapshotRuntime()
|
||||||
|
dir := filepath.Join(common.RootDir(), "runtime")
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := os.Create(filepath.Join(dir, "state.json"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
enc := json.NewEncoder(f)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(map[string]any{
|
||||||
|
"discord_muted": s.DiscordMuted,
|
||||||
|
"ocr": s.LastOCRText,
|
||||||
|
"translate": s.LastTranslate,
|
||||||
|
"world": s.CurrentWorld,
|
||||||
|
"updated_at": time.Now().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user