-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathapp.go
More file actions
173 lines (146 loc) · 5.33 KB
/
Copy pathapp.go
File metadata and controls
173 lines (146 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package main
import (
"context"
"log"
"sync"
"sync/atomic"
"time"
"github.com/HarbourMasters/Sail/internal/config"
"github.com/HarbourMasters/Sail/internal/sail"
"github.com/HarbourMasters/Sail/internal/twitchapi"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App is the Wails-bound application: it owns the Sail TCP server, the Twitch
// session/EventSub connection, and the on-disk config.
type App struct {
ctx context.Context
configStore *config.Store
configMu sync.Mutex
config config.Config
cooldownMu sync.Mutex
cooldowns map[string]time.Time
// activityMu guards recentActivity, a newest-first buffer of fired
// triggers. The live "activity" event only reaches current listeners; this
// buffer repopulates the feed on reopen and catches what fired while closed.
activityMu sync.Mutex
recentActivity []ActivityEvent
sailServer *sail.Server
serverRunning bool
// actionsMu guards lastActions, the game's action catalog cached from the
// last connection. No static fallback — empty until a game connects.
actionsMu sync.Mutex
lastActions []sail.ActionInfo
// hooksMu guards lastHooks, the hook catalog the editor offers: the
// compiled-in sail.KnownHookCatalog until a game's live hook.list replaces
// it (refreshHookCatalog).
hooksMu sync.Mutex
lastHooks []sail.HookInfo
// hookSubMu guards the hook-subscription bookkeeping. Sail subscribes to a
// hook only when something needs it (a binding, or the Hooks page
// listening) — OnActorInit/OnFlagSet fire far too often to stream blindly.
// watchedHooks is what the frontend watches live; appliedHooks is what
// clients are subscribed to (bindings ∪ watched), tracked so a change sends
// only the delta and never a double-subscribe.
hookSubMu sync.Mutex
watchedHooks map[string]bool
appliedHooks map[string]bool
// feedActive mirrors len(watchedHooks) > 0 for a lock-free check on the hot
// hook path.
feedActive atomic.Bool
twitchMu sync.Mutex
session *twitchapi.StoredSession
eventSub *twitchapi.EventSubClient
lastEventSubErr string
// sentChatMu guards sentChat (posted text -> when). Twitch echoes our own
// messages back over EventSub, so this lets handleChatMessage skip them —
// else a binding posting a command-like message loops on itself.
sentChatMu sync.Mutex
sentChat map[string]time.Time
}
// NewApp returns an App ready for startup.
func NewApp() *App {
return &App{
cooldowns: make(map[string]time.Time),
lastHooks: sail.KnownHookCatalog(),
watchedHooks: make(map[string]bool),
appliedHooks: make(map[string]bool),
sentChat: make(map[string]time.Time),
}
}
// startup loads persisted config and Twitch session, then brings up the game
// server and Twitch connection. ctx is saved for runtime calls.
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
store, err := config.NewStore()
if err != nil {
log.Printf("could not open config store, using defaults: %v", err)
a.config = config.Default()
} else {
a.configStore = store
cfg, err := store.Load()
if err != nil {
log.Printf("could not load config, using defaults: %v", err)
cfg = config.Default()
}
a.config = cfg
}
a.sailServer = sail.NewServer()
a.wireSailServer()
if err := a.StartServer(); err != nil {
log.Printf("could not start sail server: %v", err)
}
// Off the startup path: validating over the network shouldn't block, and
// the frontend handles Twitch status arriving late (twitch:status).
go a.restoreTwitchSession()
}
func (a *App) shutdown(ctx context.Context) {
a.stopEventSub()
if a.sailServer != nil {
_ = a.sailServer.Stop()
}
}
func (a *App) checkCooldown(key string, seconds float64) bool {
if seconds <= 0 {
return true
}
a.cooldownMu.Lock()
defer a.cooldownMu.Unlock()
if until, ok := a.cooldowns[key]; ok && time.Now().Before(until) {
return false
}
a.cooldowns[key] = time.Now().Add(time.Duration(seconds * float64(time.Second)))
return true
}
// maxRecentActivity bounds the buffer and Dashboard feed; the frontend caps
// its live feed to match.
const maxRecentActivity = 20
// ActivityEvent is a fired trigger, pushed live on the "activity" event and
// buffered in recentActivity for GetRecentActivity.
type ActivityEvent struct {
Source string `json:"source"` // "chat" | "redeem" | "event" | "hook"
User string `json:"user"` // empty for a hook (no viewer behind it)
Trigger string `json:"trigger"`
// Error is set when the binding's script failed; it rides the feed so a
// broken script shows on the Dashboard, not just a hidden log.
Error string `json:"error,omitempty"`
At string `json:"at"`
}
func (a *App) emitActivity(event ActivityEvent) {
event.At = time.Now().Format(time.RFC3339)
// Record before emitting so the buffer has the event before any listener.
a.activityMu.Lock()
a.recentActivity = append([]ActivityEvent{event}, a.recentActivity...)
if len(a.recentActivity) > maxRecentActivity {
a.recentActivity = a.recentActivity[:maxRecentActivity]
}
a.activityMu.Unlock()
runtime.EventsEmit(a.ctx, "activity", event)
}
// GetRecentActivity returns recent firings, newest first, so the Dashboard can
// repopulate its feed when reopened. Never nil — a nil slice serializes as
// JSON null.
func (a *App) GetRecentActivity() []ActivityEvent {
a.activityMu.Lock()
defer a.activityMu.Unlock()
return nonNilCopy(a.recentActivity)
}