Improve GUI guest sorting and presence display

This commit is contained in:
every_holiday
2026-06-24 02:38:06 +09:00
parent b6421958af
commit ab9551c241
8 changed files with 398 additions and 1 deletions

View File

@@ -29,16 +29,19 @@ func Run(configPath string, mode string) error {
if err != nil {
return err
}
SetGuestTracker(NewGuestTracker(cfg.VrcLog.GuestNames))
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}
SetDiscordMuted(true)
server.Map("DiscordSend", func(_ string, args []osc.Value) error {
log.Printf("received DiscordSend args=%+v", args)
if err := handleDiscordSend(discordState, args); err != nil {
log.Printf("discord mute failed: %v", err)
return err
}
SetDiscordMuted(discordState.muted)
return nil
})
server.Map("ocrEnabled", func(_ string, args []osc.Value) error {

View File

@@ -36,6 +36,7 @@ func handleDiscordSend(state *discordMuteState, args []osc.Value) error {
return err
}
state.muted = false
SetDiscordMuted(state.muted)
state.lastAction = now
} else {
log.Printf("INFO already unmuted")
@@ -51,6 +52,7 @@ func handleDiscordSend(state *discordMuteState, args []osc.Value) error {
return err
}
state.muted = true
SetDiscordMuted(state.muted)
state.lastAction = now
} else {
log.Printf("INFO already muted")

View File

@@ -0,0 +1,109 @@
package app
import (
"fmt"
"sort"
"sync"
"time"
)
type GuestStatus struct {
Name string `json:"name"`
Present bool `json:"present"`
LastJoin time.Time `json:"last_join"`
LastLeave time.Time `json:"last_leave"`
AbsentFor string `json:"absent_for"`
}
type GuestTracker struct {
mu sync.RWMutex
current string
items map[string]*GuestStatus
}
func NewGuestTracker(names []string) *GuestTracker {
items := make(map[string]*GuestStatus, len(names))
for _, name := range names {
items[name] = &GuestStatus{Name: name}
}
return &GuestTracker{items: items}
}
func (t *GuestTracker) SetCurrentInstance(label string) {
t.mu.Lock()
defer t.mu.Unlock()
t.current = label
}
func (t *GuestTracker) CurrentInstance() string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.current
}
func (t *GuestTracker) MarkJoin(name string, at time.Time) {
t.mu.Lock()
defer t.mu.Unlock()
s := t.ensure(name)
s.Present = true
s.LastJoin = at
s.AbsentFor = ""
}
func (t *GuestTracker) MarkLeave(name string, at time.Time) {
t.mu.Lock()
defer t.mu.Unlock()
s := t.ensure(name)
s.Present = false
s.LastLeave = at
if !s.LastJoin.IsZero() {
s.AbsentFor = humanSince(at)
}
}
func (t *GuestTracker) Snapshot(now time.Time) []GuestStatus {
t.mu.RLock()
defer t.mu.RUnlock()
out := make([]GuestStatus, 0, len(t.items))
for _, s := range t.items {
cp := *s
if !cp.Present && !cp.LastLeave.IsZero() {
cp.AbsentFor = humanSince(cp.LastLeave, now)
}
out = append(out, cp)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
func (t *GuestTracker) ensure(name string) *GuestStatus {
if s, ok := t.items[name]; ok {
return s
}
s := &GuestStatus{Name: name}
t.items[name] = s
return s
}
func humanSince(at time.Time, now ...time.Time) string {
ref := time.Now()
if len(now) > 0 {
ref = now[0]
}
if at.IsZero() {
return ""
}
d := ref.Sub(at)
if d < time.Minute {
return "1分未満"
}
m := int(d.Minutes())
if m < 60 {
return fmt.Sprintf("%d分前", m)
}
h := m / 60
if h < 24 {
return fmt.Sprintf("%d時間前", h)
}
return fmt.Sprintf("%d日前", h/24)
}

View File

@@ -20,6 +20,8 @@ runOcrFromScreen()
out, err := cmd.CombinedOutput()
if len(out) > 0 {
log.Printf("ocr output: %s", string(out))
SetOCRText(string(out))
SetTranslateText("翻訳: 未実装")
}
if err != nil {
return fmt.Errorf("ocr failed: %w", err)

View File

@@ -0,0 +1,47 @@
package app
import "sync"
type RuntimeState struct {
mu sync.RWMutex
DiscordMuted bool
LastOCRText string
LastTranslate string
CurrentWorld string
}
var runtimeState = &RuntimeState{}
var guestTracker *GuestTracker
func SetGuestTracker(t *GuestTracker) { guestTracker = t }
func GetGuestTracker() *GuestTracker { return guestTracker }
func SetDiscordMuted(v bool) {
runtimeState.mu.Lock()
defer runtimeState.mu.Unlock()
runtimeState.DiscordMuted = v
}
func SetOCRText(v string) {
runtimeState.mu.Lock()
defer runtimeState.mu.Unlock()
runtimeState.LastOCRText = v
}
func SetTranslateText(v string) {
runtimeState.mu.Lock()
defer runtimeState.mu.Unlock()
runtimeState.LastTranslate = v
}
func SetCurrentWorld(v string) {
runtimeState.mu.Lock()
defer runtimeState.mu.Unlock()
runtimeState.CurrentWorld = v
}
func SnapshotRuntime() RuntimeState {
runtimeState.mu.RLock()
defer runtimeState.mu.RUnlock()
return *runtimeState
}

View File

@@ -128,11 +128,19 @@ func watchVrchatLog() {
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)
}
_ = appendRuntimeLog("VRC WORLD", worldLabel)
}
at := extractLineTime(line)
if m := joinPattern.FindStringSubmatch(line); len(m) == 2 {
name := strings.TrimSpace(m[1])
state.presentSet[name] = struct{}{}
if tracker := GetGuestTracker(); tracker != nil {
tracker.MarkJoin(name, at)
}
out := fmt.Sprintf("[join] %s (%d)", name, len(state.presentSet))
log.Print(out)
_ = appendJoinLeaveLog(out)
@@ -141,6 +149,9 @@ func watchVrchatLog() {
if m := leftPattern.FindStringSubmatch(line); len(m) == 2 {
name := strings.TrimSpace(m[1])
delete(state.presentSet, name)
if tracker := GetGuestTracker(); tracker != nil {
tracker.MarkLeave(name, at)
}
out := fmt.Sprintf("[leave] %s (%d)", name, len(state.presentSet))
log.Print(out)
_ = appendJoinLeaveLog(out)
@@ -178,11 +189,17 @@ func scanExistingVrchatLog(path string, state *vrcLogState) error {
if m := joinPattern.FindStringSubmatch(line); len(m) == 2 {
name := strings.TrimSpace(m[1])
state.presentSet[name] = struct{}{}
if tracker := GetGuestTracker(); tracker != nil {
tracker.MarkJoin(name, extractLineTime(line))
}
continue
}
if m := leftPattern.FindStringSubmatch(line); len(m) == 2 {
name := strings.TrimSpace(m[1])
delete(state.presentSet, name)
if tracker := GetGuestTracker(); tracker != nil {
tracker.MarkLeave(name, extractLineTime(line))
}
continue
}
}
@@ -192,6 +209,10 @@ 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)
}
_ = appendRuntimeLog("VRC WORLD", state.worldName)
}
return nil
@@ -311,3 +332,14 @@ func shouldUseRoomTitlePreferLatest(roomTitle, currentWorldName string) bool {
}
return true
}
func extractLineTime(line string) time.Time {
if len(line) < 19 {
return time.Now()
}
t, err := time.ParseInLocation("2006.01.02 15:04:05", line[:19], time.Local)
if err == nil {
return t
}
return time.Now()
}