314 lines
7.6 KiB
Go
314 lines
7.6 KiB
Go
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
|
|
}
|