Clean project layout and fix release workflow
Some checks failed
build-windows-exe / build (push) Failing after 1m13s
Some checks failed
build-windows-exe / build (push) Failing after 1m13s
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@@ -24,6 +25,8 @@ var (
|
||||
enteringRoomPattern = regexp.MustCompile(`\[Behaviour\]\s+(?:Entering Room|Joining or Creating Room):\s+(.+)$`)
|
||||
)
|
||||
|
||||
const vrchatLogTailBytes = 2 * 1024 * 1024
|
||||
|
||||
type vrcLogState struct {
|
||||
location string
|
||||
worldID string
|
||||
@@ -94,13 +97,13 @@ func watchVrchatLog() {
|
||||
path, err := findLatestVrchatLog()
|
||||
if err != nil {
|
||||
log.Printf("vrchat log not found: %v", err)
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(currentGUIRefreshInterval())
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
log.Printf("vrchat log stat failed: %v", err)
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(currentGUIRefreshInterval())
|
||||
continue
|
||||
}
|
||||
log.Printf("vrchat log loop path=%s size=%d last=%d", path, info.Size(), lastSize)
|
||||
@@ -119,7 +122,7 @@ func watchVrchatLog() {
|
||||
} else {
|
||||
lastSize = info.Size()
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(currentGUIRefreshInterval())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -128,10 +131,10 @@ func watchVrchatLog() {
|
||||
}
|
||||
if info.Size() > lastSize {
|
||||
log.Printf("vrchat log updated path=%s old=%d new=%d", path, lastSize, info.Size())
|
||||
b, err := os.ReadFile(path)
|
||||
b, err := readFileFrom(path, lastSize)
|
||||
if err != nil {
|
||||
log.Printf("vrchat log read failed: %v", err)
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(currentGUIRefreshInterval())
|
||||
continue
|
||||
}
|
||||
text := decodeVRChatLog(b)
|
||||
@@ -146,16 +149,13 @@ func watchVrchatLog() {
|
||||
if err := appendRuntimeLog("VRC LOG", line); err != nil {
|
||||
log.Printf("append runtime log failed: %v", err)
|
||||
}
|
||||
at := extractLineTime(line)
|
||||
if changed, worldLabel := updateWorldState(state, line); changed {
|
||||
log.Printf("world changed: %s", worldLabel)
|
||||
state.presentSet = map[string]struct{}{}
|
||||
SetCurrentWorld(worldLabel)
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.SetCurrentInstance(worldLabel)
|
||||
}
|
||||
handleWorldChange(state, worldLabel, at)
|
||||
_ = appendRuntimeLog("VRC WORLD", worldLabel)
|
||||
}
|
||||
at := extractLineTime(line)
|
||||
if m := joinPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
log.Printf("join detected: %s", name)
|
||||
@@ -164,6 +164,9 @@ func watchVrchatLog() {
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.MarkJoin(name, at)
|
||||
}
|
||||
if tracker := GetJoinLeaveEventTracker(); tracker != nil {
|
||||
tracker.Record("join", name, len(state.presentSet), at)
|
||||
}
|
||||
out := fmt.Sprintf("[join] %s (%d)", name, len(state.presentSet))
|
||||
log.Print(out)
|
||||
if err := appendJoinLeaveLog(out); err != nil {
|
||||
@@ -179,6 +182,9 @@ func watchVrchatLog() {
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.MarkLeave(name, at)
|
||||
}
|
||||
if tracker := GetJoinLeaveEventTracker(); tracker != nil {
|
||||
tracker.Record("leave", name, len(state.presentSet), at)
|
||||
}
|
||||
out := fmt.Sprintf("[leave] %s (%d)", name, len(state.presentSet))
|
||||
log.Print(out)
|
||||
if err := appendJoinLeaveLog(out); err != nil {
|
||||
@@ -190,10 +196,55 @@ func watchVrchatLog() {
|
||||
log.Printf("vrchat log batch done join=%d leave=%d", joinHits, leaveHits)
|
||||
lastSize = info.Size()
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(currentGUIRefreshInterval())
|
||||
}
|
||||
}
|
||||
|
||||
func readFileFrom(path string, offset int64) ([]byte, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
if offset > 0 {
|
||||
if _, err := f.Seek(offset, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
func readFileTail(path string, maxBytes int64) ([]byte, error) {
|
||||
if maxBytes <= 0 {
|
||||
return os.ReadFile(path)
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Size() <= maxBytes {
|
||||
return os.ReadFile(path)
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.Seek(info.Size()-maxBytes, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, c := range b {
|
||||
if c == '\n' && i+1 < len(b) {
|
||||
return b[i+1:], nil
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func scanExistingVrchatLog(path string, state *vrcLogState) error {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -208,13 +259,18 @@ func scanExistingVrchatLog(path string, state *vrcLogState) error {
|
||||
state.initialized = false
|
||||
|
||||
text := decodeVRChatLog(b)
|
||||
var lastWorldAt time.Time
|
||||
sawWorldChange := false
|
||||
for _, raw := range strings.Split(text, "\n") {
|
||||
line := strings.TrimRight(raw, "\r")
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if changed, _ := updateWorldState(state, line); changed {
|
||||
sawWorldChange = true
|
||||
lastWorldAt = extractLineTime(line)
|
||||
state.presentSet = map[string]struct{}{}
|
||||
handleWorldChange(state, state.worldName, lastWorldAt)
|
||||
}
|
||||
if m := joinPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
@@ -235,10 +291,12 @@ func scanExistingVrchatLog(path string, state *vrcLogState) error {
|
||||
}
|
||||
state.initialized = true
|
||||
if state.worldName != "" {
|
||||
log.Printf("world changed: %s", state.worldName)
|
||||
SetCurrentWorld(state.worldName)
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.SetCurrentInstance(state.worldName)
|
||||
if !sawWorldChange {
|
||||
log.Printf("world changed: %s", state.worldName)
|
||||
if lastWorldAt.IsZero() {
|
||||
lastWorldAt = time.Now()
|
||||
}
|
||||
handleWorldChange(state, state.worldName, lastWorldAt)
|
||||
}
|
||||
if err := appendRuntimeLog("VRC WORLD", state.worldName); err != nil {
|
||||
log.Printf("append world log failed: %v", err)
|
||||
@@ -251,9 +309,6 @@ func decodeVRChatLog(b []byte) string {
|
||||
if len(b) == 0 {
|
||||
return ""
|
||||
}
|
||||
if utf8.Valid(b) {
|
||||
return strings.TrimPrefix(string(b), "\ufeff")
|
||||
}
|
||||
if len(b) >= 2 {
|
||||
if b[0] == 0xff && b[1] == 0xfe {
|
||||
u16 := make([]uint16, 0, (len(b)-2)/2)
|
||||
@@ -270,12 +325,23 @@ func decodeVRChatLog(b []byte) string {
|
||||
return strings.TrimPrefix(fixMojibake(syscall.UTF16ToString(u16)), "\ufeff")
|
||||
}
|
||||
}
|
||||
for _, cp := range []uint32{932, 1252, 65001} {
|
||||
if decoded := decodeWithCodePage(b, cp); decoded != "" {
|
||||
return strings.TrimPrefix(fixMojibake(decoded), "\ufeff")
|
||||
lines := bytes.Split(b, []byte{'\n'})
|
||||
var out strings.Builder
|
||||
for i, raw := range lines {
|
||||
if i > 0 {
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
out.WriteString(decodeVRChatLogLine(raw))
|
||||
}
|
||||
return strings.TrimPrefix(fixMojibake(string(b)), "\ufeff")
|
||||
return strings.TrimPrefix(out.String(), "\ufeff")
|
||||
}
|
||||
|
||||
func decodeVRChatLogLine(b []byte) string {
|
||||
line := bytes.TrimRight(b, "\r")
|
||||
if len(line) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimPrefix(string(line), "\ufeff")
|
||||
}
|
||||
|
||||
func DecodeVRChatLog(b []byte) string {
|
||||
@@ -402,6 +468,9 @@ func updateWorldState(state *vrcLogState, line string) (bool, string) {
|
||||
nextLocation = worldID + ":" + instanceID
|
||||
}
|
||||
}
|
||||
if nextLocation == "" && roomTitle != "" {
|
||||
nextLocation = roomTitle
|
||||
}
|
||||
|
||||
if nextLocation != "" && nextLocation != state.location {
|
||||
state.location = nextLocation
|
||||
@@ -421,8 +490,8 @@ func updateWorldState(state *vrcLogState, line string) (bool, string) {
|
||||
}
|
||||
if state.initialized {
|
||||
log.Printf("world changed: %s", label)
|
||||
_ = appendRuntimeLog("VRC WORLD", label)
|
||||
}
|
||||
state.initialized = true
|
||||
return true, label
|
||||
}
|
||||
|
||||
@@ -489,3 +558,16 @@ func extractLineTime(line string) time.Time {
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func handleWorldChange(state *vrcLogState, worldLabel string, at time.Time) {
|
||||
if state == nil {
|
||||
return
|
||||
}
|
||||
SetCurrentWorldVisit(worldLabel, at)
|
||||
if tracker := GetWorldHistoryTracker(); tracker != nil {
|
||||
tracker.ObserveWorld(worldLabel, state.worldID, state.instanceID, at)
|
||||
}
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.ResetCurrentInstance(worldLabel)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user