Refine Go runtime behavior and log handling
Some checks failed
build-windows-exe / build (push) Failing after 48s
Some checks failed
build-windows-exe / build (push) Failing after 48s
This commit is contained in:
@@ -1,16 +1,25 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"vrc_osc_go/internal/config"
|
||||
"vrc_osc_go/internal/consenttool"
|
||||
"vrc_osc_go/internal/osc"
|
||||
)
|
||||
|
||||
type discordMuteState struct {
|
||||
mu sync.Mutex
|
||||
muted bool
|
||||
buttonPressed bool
|
||||
lastAction time.Time
|
||||
}
|
||||
|
||||
func Run(configPath string, mode string) error {
|
||||
if mode != "" {
|
||||
return consenttool.Run(configPath, mode)
|
||||
@@ -20,21 +29,43 @@ func Run(configPath string, mode string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
discordState := &discordMuteState{muted: true}
|
||||
server.Map("DiscordSend", func(_ string, args []osc.Value) error {
|
||||
return fmt.Errorf("discord mute not implemented yet")
|
||||
log.Printf("received DiscordSend args=%+v", args)
|
||||
if err := handleDiscordSend(discordState, args); err != nil {
|
||||
log.Printf("discord mute failed: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
server.Map("ocrEnabled", func(_ string, args []osc.Value) error {
|
||||
log.Printf("received ocrEnabled args=%+v", args)
|
||||
if err := runPythonOcrFromScreen(); err != nil {
|
||||
log.Printf("ocr failed: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- server.Serve() }()
|
||||
go func() {
|
||||
log.Printf("osc server listening on %s:%d", cfg.OSC.Host, cfg.OSC.Port)
|
||||
errCh <- server.Serve()
|
||||
}()
|
||||
go watchVrchatLog()
|
||||
startSelfMonitor(cfg, discordState)
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||
select {
|
||||
case err := <-errCh:
|
||||
log.Printf("osc server stopped: %v", err)
|
||||
return err
|
||||
case <-sigCh:
|
||||
log.Printf("shutdown signal received")
|
||||
server.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
36
VRWT_Tool/VRC_OSC/internal/app/discord.go
Normal file
36
VRWT_Tool/VRC_OSC/internal/app/discord.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func pressDiscordMuteHotkey() error {
|
||||
script := `
|
||||
$p = Get-Process | Where-Object { $_.MainWindowTitle -match 'Discord' } | Select-Object -First 1
|
||||
if ($null -eq $p) { exit 2 }
|
||||
[void][System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
||||
$wshell = New-Object -ComObject WScript.Shell
|
||||
$null = $wshell.AppActivate($p.Id)
|
||||
Start-Sleep -Milliseconds 150
|
||||
$wshell.SendKeys('^+m')
|
||||
`
|
||||
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if len(out) > 0 {
|
||||
log.Printf("discord mute output: %s", string(out))
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("discord mute failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pressDiscordMuteHotkeyLegacy() error {
|
||||
var buf bytes.Buffer
|
||||
_ = buf
|
||||
return pressDiscordMuteHotkey()
|
||||
}
|
||||
88
VRWT_Tool/VRC_OSC/internal/app/discord_mute.go
Normal file
88
VRWT_Tool/VRC_OSC/internal/app/discord_mute.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"vrc_osc_go/internal/osc"
|
||||
)
|
||||
|
||||
const discordDebounce = 300 * time.Millisecond
|
||||
|
||||
func handleDiscordSend(state *discordMuteState, args []osc.Value) error {
|
||||
state.mu.Lock()
|
||||
defer state.mu.Unlock()
|
||||
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("OSC args is empty")
|
||||
}
|
||||
isPressed, err := toBool(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if !state.lastAction.IsZero() && now.Sub(state.lastAction) < discordDebounce {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isPressed && !state.buttonPressed {
|
||||
state.buttonPressed = true
|
||||
if state.muted {
|
||||
log.Printf("ACTION VRChat button pressed -> Discord unmute hotkey")
|
||||
if err := pressDiscordMuteHotkey(); err != nil {
|
||||
return err
|
||||
}
|
||||
state.muted = false
|
||||
state.lastAction = now
|
||||
} else {
|
||||
log.Printf("INFO already unmuted")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isPressed && state.buttonPressed {
|
||||
state.buttonPressed = false
|
||||
if !state.muted {
|
||||
log.Printf("ACTION VRChat button released -> Discord mute hotkey")
|
||||
if err := pressDiscordMuteHotkey(); err != nil {
|
||||
return err
|
||||
}
|
||||
state.muted = true
|
||||
state.lastAction = now
|
||||
} else {
|
||||
log.Printf("INFO already muted")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toBool(v osc.Value) (bool, error) {
|
||||
switch v.Type {
|
||||
case 'T':
|
||||
return true, nil
|
||||
case 'F':
|
||||
return false, nil
|
||||
case 'i':
|
||||
return v.Int != 0, nil
|
||||
case 'f':
|
||||
return v.Float != 0, nil
|
||||
case 's':
|
||||
switch strings.ToLower(strings.TrimSpace(v.Str)) {
|
||||
case "true", "1", "on", "yes":
|
||||
return true, nil
|
||||
case "false", "0", "off", "no":
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported OSC value: %q", v.Str)
|
||||
}
|
||||
case 0:
|
||||
return false, fmt.Errorf("unsupported OSC value")
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported OSC type: %q", v.Type)
|
||||
}
|
||||
}
|
||||
28
VRWT_Tool/VRC_OSC/internal/app/ocr.go
Normal file
28
VRWT_Tool/VRC_OSC/internal/app/ocr.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func runPythonOcrFromScreen() error {
|
||||
script := `
|
||||
import sys
|
||||
from pathlib import Path
|
||||
root = Path(r"C:\Users\kenny\Documents\git\messpy\VRC\VRWT_Tool\VRC_OSC")
|
||||
sys.path.insert(0, str(root / "src"))
|
||||
from ocr.ocr_actions import runOcrFromScreen
|
||||
runOcrFromScreen()
|
||||
`
|
||||
cmd := exec.Command("python", "-c", script)
|
||||
cmd.Dir = `C:\Users\kenny\Documents\git\messpy\VRC\VRWT_Tool\VRC_OSC`
|
||||
out, err := cmd.CombinedOutput()
|
||||
if len(out) > 0 {
|
||||
log.Printf("ocr output: %s", string(out))
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("ocr failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
48
VRWT_Tool/VRC_OSC/internal/app/runtime.go
Normal file
48
VRWT_Tool/VRC_OSC/internal/app/runtime.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"vrc_osc_go/internal/common"
|
||||
)
|
||||
|
||||
func appendRuntimeLog(title, text string) error {
|
||||
dir := filepath.Join(common.RootDir(), "runtime")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(dir, "runtime.log")
|
||||
body := text
|
||||
if body == "" {
|
||||
body = "(empty)"
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = fmt.Fprintf(f, "\n[%s] %s\n%s\n", time.Now().Format("2006-01-02 15:04:05"), title, body)
|
||||
return err
|
||||
}
|
||||
|
||||
func appendJoinLeaveLog(text string) error {
|
||||
dir := filepath.Join(common.RootDir(), "runtime")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(dir, "join_leave.log")
|
||||
body := text
|
||||
if body == "" {
|
||||
body = "(empty)"
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = fmt.Fprintf(f, "\n[%s] VRC JOIN/LEAVE\n%s\n", time.Now().Format("2006-01-02 15:04:05"), body)
|
||||
return err
|
||||
}
|
||||
69
VRWT_Tool/VRC_OSC/internal/app/self_monitor.go
Normal file
69
VRWT_Tool/VRC_OSC/internal/app/self_monitor.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"vrc_osc_go/internal/config"
|
||||
"vrc_osc_go/internal/osc"
|
||||
)
|
||||
|
||||
var (
|
||||
selfJoinPattern = regexp.MustCompile(`OnPlayerJoined\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)`)
|
||||
selfLeftPattern = regexp.MustCompile(`OnPlayerLeft\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)`)
|
||||
)
|
||||
|
||||
func startSelfMonitor(cfg *config.Config, state *discordMuteState) {
|
||||
if cfg.VrcLog.SelfName == "" {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
lastState := ""
|
||||
for {
|
||||
path, err := findLatestVrchatLog()
|
||||
if err != nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
text, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
nextState := extractLatestSelfLogState(string(text), cfg.VrcLog.SelfName)
|
||||
if nextState != "" && nextState != lastState {
|
||||
if nextState == "joined" {
|
||||
if err := handleDiscordSend(state, []osc.Value{{Type: 'T', Bool: true}}); err != nil {
|
||||
log.Printf("self monitor discord mute failed: %v", err)
|
||||
}
|
||||
} else if nextState == "left" {
|
||||
if err := handleDiscordSend(state, []osc.Value{{Type: 'F', Bool: false}}); err != nil {
|
||||
log.Printf("self monitor discord mute failed: %v", err)
|
||||
}
|
||||
}
|
||||
lastState = nextState
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func extractLatestSelfLogState(text, selfName string) string {
|
||||
if selfName == "" {
|
||||
return ""
|
||||
}
|
||||
state := ""
|
||||
for _, rawLine := range strings.Split(text, "\n") {
|
||||
if m := selfJoinPattern.FindStringSubmatch(rawLine); len(m) == 2 && strings.TrimSpace(m[1]) == selfName {
|
||||
state = "joined"
|
||||
continue
|
||||
}
|
||||
if m := selfLeftPattern.FindStringSubmatch(rawLine); len(m) == 2 && strings.TrimSpace(m[1]) == selfName {
|
||||
state = "left"
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
313
VRWT_Tool/VRC_OSC/internal/app/vrc_log.go
Normal file
313
VRWT_Tool/VRC_OSC/internal/app/vrc_log.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
joinPattern = regexp.MustCompile(`OnPlayerJoined\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)`)
|
||||
leftPattern = regexp.MustCompile(`OnPlayerLeft\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)`)
|
||||
worldIdPattern = regexp.MustCompile(`worldId=(wrld_[0-9a-fA-F-]+)`)
|
||||
instanceIdPattern = regexp.MustCompile(`instanceId=([^,}\s]+)`)
|
||||
worldNamePattern = regexp.MustCompile(`worldName=([^,}]+)`)
|
||||
worldLocationPattern = regexp.MustCompile(`worldId=(wrld_[0-9a-fA-F-]+):([^\s,\]\)\"']+)`)
|
||||
worldPattern = regexp.MustCompile(`(wrld_[0-9a-fA-F-]+(?::[^\s\]\)\"']+)?)`)
|
||||
enteringRoomPattern = regexp.MustCompile(`\[Behaviour\]\s+(?:Entering Room|Joining or Creating Room):\s+(.+)$`)
|
||||
)
|
||||
|
||||
type vrcLogState struct {
|
||||
location string
|
||||
worldID string
|
||||
instanceID string
|
||||
worldName string
|
||||
pendingWorldName string
|
||||
presentSet map[string]struct{}
|
||||
initialized bool
|
||||
}
|
||||
|
||||
func getVrchatLogDir() (string, error) {
|
||||
userProfile := os.Getenv("USERPROFILE")
|
||||
if userProfile == "" {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
return filepath.Join(userProfile, "AppData", "LocalLow", "VRChat", "VRChat"), nil
|
||||
}
|
||||
|
||||
func findLatestVrchatLog() (string, error) {
|
||||
dir, err := getVrchatLogDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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 watchVrchatLog() {
|
||||
lastPath := ""
|
||||
lastSize := int64(0)
|
||||
state := &vrcLogState{
|
||||
presentSet: map[string]struct{}{},
|
||||
}
|
||||
for {
|
||||
path, err := findLatestVrchatLog()
|
||||
if err != nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
if path != lastPath {
|
||||
log.Printf("vrchat log watching %s", path)
|
||||
lastPath = path
|
||||
lastSize = 0
|
||||
if err := scanExistingVrchatLog(path, state); err != nil {
|
||||
log.Printf("vrchat log initial scan failed: %v", err)
|
||||
}
|
||||
if info2, err := os.Stat(path); err == nil {
|
||||
lastSize = info2.Size()
|
||||
} else {
|
||||
lastSize = info.Size()
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
if info.Size() < lastSize {
|
||||
lastSize = 0
|
||||
}
|
||||
if info.Size() > lastSize {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
_, _ = f.Seek(lastSize, 0)
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if err := appendRuntimeLog("VRC LOG", line); err != nil {
|
||||
log.Printf("append runtime log failed: %v", err)
|
||||
}
|
||||
if changed, worldLabel := updateWorldState(state, line); changed {
|
||||
log.Printf("world changed: %s", worldLabel)
|
||||
state.presentSet = map[string]struct{}{}
|
||||
_ = appendRuntimeLog("VRC WORLD", worldLabel)
|
||||
}
|
||||
if m := joinPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
state.presentSet[name] = struct{}{}
|
||||
out := fmt.Sprintf("[join] %s (%d)", name, len(state.presentSet))
|
||||
log.Print(out)
|
||||
_ = appendJoinLeaveLog(out)
|
||||
continue
|
||||
}
|
||||
if m := leftPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
delete(state.presentSet, name)
|
||||
out := fmt.Sprintf("[leave] %s (%d)", name, len(state.presentSet))
|
||||
log.Print(out)
|
||||
_ = appendJoinLeaveLog(out)
|
||||
continue
|
||||
}
|
||||
}
|
||||
lastSize = info.Size()
|
||||
_ = f.Close()
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func scanExistingVrchatLog(path string, state *vrcLogState) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
state.presentSet = map[string]struct{}{}
|
||||
state.location = ""
|
||||
state.worldID = ""
|
||||
state.instanceID = ""
|
||||
state.worldName = ""
|
||||
state.pendingWorldName = ""
|
||||
state.initialized = false
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if changed, _ := updateWorldState(state, line); changed {
|
||||
state.presentSet = map[string]struct{}{}
|
||||
}
|
||||
if m := joinPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
state.presentSet[name] = struct{}{}
|
||||
continue
|
||||
}
|
||||
if m := leftPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
delete(state.presentSet, name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
state.initialized = true
|
||||
if state.worldName != "" {
|
||||
log.Printf("world changed: %s", state.worldName)
|
||||
_ = appendRuntimeLog("VRC WORLD", state.worldName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateWorldState(state *vrcLogState, line string) (bool, string) {
|
||||
worldID := ""
|
||||
instanceID := ""
|
||||
worldName := ""
|
||||
roomTitle := ""
|
||||
|
||||
if m := enteringRoomPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
roomTitle = strings.TrimSpace(m[1])
|
||||
state.pendingWorldName = roomTitle
|
||||
}
|
||||
if m := worldLocationPattern.FindStringSubmatch(line); len(m) == 3 {
|
||||
worldID = strings.TrimSpace(m[1])
|
||||
instanceID = strings.TrimSpace(m[2])
|
||||
}
|
||||
|
||||
if m := worldIdPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
worldID = strings.TrimSpace(m[1])
|
||||
}
|
||||
if m := instanceIdPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
instanceID = strings.TrimSpace(m[1])
|
||||
}
|
||||
if m := worldNamePattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
worldName = strings.TrimSpace(m[1])
|
||||
}
|
||||
|
||||
if m := enteringRoomPattern.FindStringSubmatch(line); len(m) == 2 && worldID == "" {
|
||||
if worldMatch := worldPattern.FindStringSubmatch(m[1]); len(worldMatch) == 2 {
|
||||
worldID = worldMatch[1]
|
||||
}
|
||||
}
|
||||
|
||||
nextLocation := ""
|
||||
if worldID != "" {
|
||||
nextLocation = worldID
|
||||
if instanceID != "" {
|
||||
nextLocation = worldID + ":" + instanceID
|
||||
}
|
||||
}
|
||||
|
||||
if nextLocation != "" && nextLocation != state.location {
|
||||
state.location = nextLocation
|
||||
state.worldID = worldID
|
||||
state.instanceID = instanceID
|
||||
if worldName != "" {
|
||||
state.worldName = worldName
|
||||
} else if shouldUseRoomTitlePreferLatest(roomTitle, state.worldName) {
|
||||
state.worldName = roomTitle
|
||||
}
|
||||
label := state.worldName
|
||||
if label == "" {
|
||||
label = state.worldID
|
||||
}
|
||||
if label == "" {
|
||||
label = nextLocation
|
||||
}
|
||||
if state.initialized {
|
||||
log.Printf("world changed: %s", label)
|
||||
_ = appendRuntimeLog("VRC WORLD", label)
|
||||
}
|
||||
return true, label
|
||||
}
|
||||
|
||||
if worldName != "" {
|
||||
state.worldName = worldName
|
||||
state.pendingWorldName = ""
|
||||
return false, ""
|
||||
}
|
||||
if shouldUseRoomTitlePreferLatest(roomTitle, state.worldName) {
|
||||
state.worldName = roomTitle
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func shouldUseRoomTitle(roomTitle, currentWorldName string) bool {
|
||||
if roomTitle == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(roomTitle, "Home Location") {
|
||||
return false
|
||||
}
|
||||
if roomTitle == "Holiday-Cottage" && currentWorldName != "" && currentWorldName != roomTitle {
|
||||
return false
|
||||
}
|
||||
if currentWorldName == "" {
|
||||
return true
|
||||
}
|
||||
if roomTitle == currentWorldName {
|
||||
return false
|
||||
}
|
||||
if len(roomTitle) > len(currentWorldName) {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(roomTitle, "「") || strings.Contains(roomTitle, "」") || strings.Contains(roomTitle, "[") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldUseRoomTitlePreferLatest(roomTitle, currentWorldName string) bool {
|
||||
if roomTitle == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(roomTitle, "Home Location") {
|
||||
return false
|
||||
}
|
||||
if currentWorldName == "" {
|
||||
return true
|
||||
}
|
||||
if roomTitle == currentWorldName {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
16
VRWT_Tool/VRC_OSC/internal/common/project_paths.go
Normal file
16
VRWT_Tool/VRC_OSC/internal/common/project_paths.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func RuntimeBaseDir() string {
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
return filepath.Dir(exe)
|
||||
}
|
||||
return "."
|
||||
}
|
||||
|
||||
func RootDir() string { return RuntimeBaseDir() }
|
||||
118
VRWT_Tool/VRC_OSC/internal/config/config.go
Normal file
118
VRWT_Tool/VRC_OSC/internal/config/config.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"vrc_osc_go/internal/common"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
OSC OSCConfig
|
||||
VrcLog VrcLogConfig
|
||||
}
|
||||
|
||||
type OSCConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type VrcLogConfig struct {
|
||||
SelfName string
|
||||
GuestNames []string
|
||||
StaffNames []string
|
||||
MissingCount int
|
||||
LogPatterns []string
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
cfg := &Config{
|
||||
OSC: OSCConfig{Host: "127.0.0.1", Port: 9001},
|
||||
}
|
||||
if path == "" {
|
||||
path = filepath.Join(common.RootDir(), "config", "config.toml")
|
||||
} else if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(common.RootDir(), path)
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return cfg, nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var section string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
line = strings.TrimPrefix(line, "\ufeff")
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
section = strings.Trim(line, "[]")
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
val = strings.Trim(val, "\"'")
|
||||
switch section {
|
||||
case "osc":
|
||||
switch key {
|
||||
case "host":
|
||||
cfg.OSC.Host = val
|
||||
case "port":
|
||||
if n, err := strconv.Atoi(val); err == nil {
|
||||
cfg.OSC.Port = n
|
||||
}
|
||||
}
|
||||
case "self":
|
||||
if key == "name" {
|
||||
cfg.VrcLog.SelfName = val
|
||||
}
|
||||
case "notice":
|
||||
if key == "missing_count" {
|
||||
if n, err := strconv.Atoi(val); err == nil {
|
||||
cfg.VrcLog.MissingCount = n
|
||||
}
|
||||
}
|
||||
case "vrc_log":
|
||||
if key == "patterns" {
|
||||
cfg.VrcLog.LogPatterns = parseList(val)
|
||||
}
|
||||
case "staff":
|
||||
if key == "names" {
|
||||
cfg.VrcLog.StaffNames = parseList(val)
|
||||
}
|
||||
case "guest":
|
||||
if key == "names" {
|
||||
cfg.VrcLog.GuestNames = parseList(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfg, scanner.Err()
|
||||
}
|
||||
|
||||
func parseList(value string) []string {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.TrimPrefix(value, "[")
|
||||
value = strings.TrimSuffix(value, "]")
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(value, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(strings.Trim(part, "\"'"))
|
||||
if part != "" {
|
||||
out = append(out, part)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
141
VRWT_Tool/VRC_OSC/internal/osc/server.go
Normal file
141
VRWT_Tool/VRC_OSC/internal/osc/server.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package osc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Value struct {
|
||||
Type byte
|
||||
Float float32
|
||||
Int int32
|
||||
Bool bool
|
||||
Str string
|
||||
}
|
||||
|
||||
type Handler func(address string, args []Value) error
|
||||
|
||||
type Server struct {
|
||||
addr string
|
||||
conn *net.UDPConn
|
||||
handlers map[string]Handler
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewServer(host string, port int) *Server {
|
||||
return &Server{
|
||||
addr: fmt.Sprintf("%s:%d", host, port),
|
||||
handlers: map[string]Handler{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Map(param string, h Handler) { s.handlers["/avatar/parameters/"+param] = h }
|
||||
func (s *Server) Close() error {
|
||||
if s.conn != nil {
|
||||
return s.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Serve() error {
|
||||
addr, err := net.ResolveUDPAddr("udp", s.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.conn = conn
|
||||
buf := make([]byte, 2048)
|
||||
for {
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a, v, err := parseMessage(buf[:n])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.mu.RLock()
|
||||
h := s.handlers[a]
|
||||
s.mu.RUnlock()
|
||||
if h != nil {
|
||||
_ = h(a, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseMessage(b []byte) (string, []Value, error) {
|
||||
address, offset, err := parsePaddedString(b, 0)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if !strings.HasPrefix(address, "/") {
|
||||
return "", nil, fmt.Errorf("invalid address")
|
||||
}
|
||||
if offset >= len(b) {
|
||||
return address, nil, nil
|
||||
}
|
||||
typetags, offset, err := parsePaddedString(b, offset)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if !strings.HasPrefix(typetags, ",") {
|
||||
return address, nil, nil
|
||||
}
|
||||
var values []Value
|
||||
for _, tag := range typetags[1:] {
|
||||
switch tag {
|
||||
case 'i':
|
||||
if offset+4 > len(b) {
|
||||
return "", nil, fmt.Errorf("invalid int")
|
||||
}
|
||||
values = append(values, Value{Type: 'i', Int: int32(binary.BigEndian.Uint32(b[offset : offset+4]))})
|
||||
offset += 4
|
||||
case 'f':
|
||||
if offset+4 > len(b) {
|
||||
return "", nil, fmt.Errorf("invalid float")
|
||||
}
|
||||
bits := binary.BigEndian.Uint32(b[offset : offset+4])
|
||||
values = append(values, Value{Type: 'f', Float: math.Float32frombits(bits)})
|
||||
offset += 4
|
||||
case 's':
|
||||
s, next, err := parsePaddedString(b, offset)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
values = append(values, Value{Type: 's', Str: s})
|
||||
offset = next
|
||||
case 'T':
|
||||
values = append(values, Value{Type: 'T', Bool: true})
|
||||
case 'F':
|
||||
values = append(values, Value{Type: 'F', Bool: false})
|
||||
}
|
||||
}
|
||||
return address, values, nil
|
||||
}
|
||||
|
||||
func parsePaddedString(b []byte, offset int) (string, int, error) {
|
||||
if offset >= len(b) {
|
||||
return "", offset, fmt.Errorf("invalid osc string")
|
||||
}
|
||||
end := bytes.IndexByte(b[offset:], 0)
|
||||
if end < 0 {
|
||||
return "", offset, fmt.Errorf("invalid osc string")
|
||||
}
|
||||
s := string(b[offset : offset+end])
|
||||
next := offset + end + 1
|
||||
for next%4 != 0 {
|
||||
next++
|
||||
}
|
||||
if next > len(b) {
|
||||
next = len(b)
|
||||
}
|
||||
return s, next, nil
|
||||
}
|
||||
Reference in New Issue
Block a user