package app import ( "fmt" "encoding/json" "os" "path/filepath" "sort" "sync" "time" "vrc_osc_go/internal/common" ) 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 = "" _ = t.persistLocked() } 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) } _ = t.persistLocked() } 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) 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 { 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) }