@@ -11,18 +11,38 @@ import Foundation
11
11
enum SplitterError : Error {
12
12
case noHeader
13
13
}
14
+
15
+ struct Event {
16
+ var variants : Set < String >
17
+
18
+ init ( ) {
19
+ self . variants = Set ( )
20
+ }
21
+
22
+ mutating func add( variant: String ) {
23
+ self . variants. insert ( variant)
24
+ }
25
+ }
26
+
27
+ extension Event : ExpressibleByArrayLiteral {
28
+ init ( arrayLiteral elements: String ... ) {
29
+ self . init ( )
30
+ for element in elements {
31
+ self . add ( variant: element)
32
+ }
33
+ }
34
+
35
+ typealias ArrayLiteralElement = String
36
+ }
37
+
14
38
/**
15
39
Handles sending split commands based on state changes
16
40
*/
17
41
class CelesteSplitter {
18
42
let scanner : CelesteScanner
19
43
let server : LiveSplitServer
20
44
var autoSplitterInfo : AutoSplitterInfo = AutoSplitterInfo ( )
21
- var routeConfig = RouteConfig (
22
- useFileTime: false ,
23
- reset: " " ,
24
- route: [ ]
25
- )
45
+ var routeConfig = RouteConfig ( )
26
46
27
47
init ( pid: pid_t , server: LiveSplitServer ) throws {
28
48
self . scanner = try CelesteScanner ( pid: pid)
@@ -35,19 +55,20 @@ class CelesteSplitter {
35
55
}
36
56
37
57
private var gameTimeRunning = false
38
- private( set) var routeIndex = 0
58
+ private( set) var nextEventIndex = 0
39
59
40
60
func reset( ) {
41
61
server. reset ( )
42
- routeIndex = 0
62
+ nextEventIndex = 0
43
63
}
44
64
45
- func update( ) throws -> [ String ] {
65
+ func update( ) throws -> [ Event ] {
46
66
guard let info = try scanner. getInfo ( ) else {
47
67
autoSplitterInfo = AutoSplitterInfo ( )
48
68
return [ ]
49
69
}
50
70
let events = getEvents ( from: autoSplitterInfo, to: info)
71
+ // logStateChange(from: autoSplitterInfo, to: info)
51
72
autoSplitterInfo = info
52
73
53
74
let time = routeConfig. useFileTime ? autoSplitterInfo. fileTime : autoSplitterInfo. chapterTime
@@ -65,106 +86,170 @@ class CelesteSplitter {
65
86
return events
66
87
}
67
88
68
- func getEvents( from old: AutoSplitterInfo , to new: AutoSplitterInfo ) -> [ String ] {
69
- var events : [ String ] = [ ]
89
+ var lastStateTime = DispatchTime . now ( )
90
+
91
+ func logStateChange( from old: AutoSplitterInfo , to new: AutoSplitterInfo ) {
92
+ if !old. stableStateEquals ( other: new) {
93
+ let currentTime = DispatchTime . now ( )
94
+ let delta = Double ( currentTime. uptimeNanoseconds - lastStateTime. uptimeNanoseconds) / 1_000_000_000
95
+ print (
96
+ " [ \( delta) ] " +
97
+ " chapter: \( new. chapter) , mode: \( new. mode) , level: \( new. level) , timerActive: \( new. timerActive) , " +
98
+ " chapterStarted: \( new. chapterStarted) , chapterComplete: \( new. chapterComplete) , chapterTime: \( new. chapterTime) , " +
99
+ " chapterStrawberries: \( new. chapterStrawberries) , chapterCassette: \( new. chapterCassette) , chapterHeart: \( new. chapterHeart) , fileTime: \( new. fileTime) , " +
100
+ " fileStrawberries: \( new. fileStrawberries) , fileCassettes: \( new. fileCassettes) , fileHearts: \( new. fileHearts) "
101
+ )
102
+ lastStateTime = currentTime
103
+ }
104
+ }
105
+
106
+ func getEvents( from old: AutoSplitterInfo , to new: AutoSplitterInfo ) -> [ Event ] {
107
+ var events : [ Event ] = [ ]
70
108
71
109
// if we don't check `new.chapterComplete`, the summit credits trigger the autosplitter
72
110
if new. chapterStarted && !old. chapterStarted && !new. chapterComplete {
73
- events . append ( " start chapter \( new. chapter) " )
111
+ var event : Event = [ " start chapter " , " start chapter \( new. chapter) " ]
74
112
switch new. mode {
75
- case . Normal: events . append ( " start a-side \( new. chapter) " )
76
- case . BSide: events . append ( " start b-side \( new. chapter) " )
77
- case . CSide: events . append ( " start c-side \( new. chapter) " )
113
+ case . Normal: event . add ( variant : " start a-side \( new. chapter) " )
114
+ case . BSide: event . add ( variant : " start b-side \( new. chapter) " )
115
+ case . CSide: event . add ( variant : " start c-side \( new. chapter) " )
78
116
default : break
79
117
}
118
+ events. append ( event)
80
119
}
81
120
if !new. chapterStarted && old. chapterStarted && !old. chapterComplete {
82
- events. append ( " reset chapter " )
83
- events. append ( " reset chapter \( old. chapter) " )
121
+ var event : Event = [ " return to map " , " reset chapter " , " reset chapter \( old. chapter) " ]
84
122
switch new. mode {
85
- case . Normal: events . append ( " reset a-side \( old. chapter) " )
86
- case . BSide: events . append ( " reset b-side \( old. chapter) " )
87
- case . CSide: events . append ( " reset c-side \( old. chapter) " )
123
+ case . Normal: event . add ( variant : " reset a-side \( old. chapter) " )
124
+ case . BSide: event . add ( variant : " reset b-side \( old. chapter) " )
125
+ case . CSide: event . add ( variant : " reset c-side \( old. chapter) " )
88
126
default : break
89
127
}
128
+ events. append ( event)
90
129
}
91
130
if new. chapterComplete && !old. chapterComplete {
92
- events . append ( " complete chapter \( old. chapter) " )
131
+ var event : Event = [ " complete chapter " , " complete chapter \( old. chapter) " ]
93
132
switch new. mode {
94
- case . Normal: events . append ( " complete a-side \( old. chapter) " )
95
- case . BSide: events . append ( " complete b-side \( old. chapter) " )
96
- case . CSide: events . append ( " complete c-side \( old. chapter) " )
133
+ case . Normal: event . add ( variant : " complete a-side \( old. chapter) " )
134
+ case . BSide: event . add ( variant : " complete b-side \( old. chapter) " )
135
+ case . CSide: event . add ( variant : " complete c-side \( old. chapter) " )
97
136
default : break
98
137
}
138
+ events. append ( event)
99
139
}
100
140
101
141
if new. level != old. level && old. level != " " && new. level != " " {
102
- events. append ( " \( old. level) > \( new. level) " )
142
+ events. append ( [ " \( old. level) > \( new. level) " ] )
103
143
}
104
144
if new. chapterCassette && !old. chapterCassette {
105
- events. append ( " cassette " )
106
- events. append ( " chapter \( new. chapter) cassette " )
107
- events. append ( " \( new. fileCassettes) total cassettes " )
145
+ events. append ( [ " cassette " , " chapter \( new. chapter) cassette " , " \( new. fileCassettes) total cassettes " ] )
108
146
}
109
147
if new. chapterHeart && !old. chapterHeart {
110
- events. append ( " heart " )
111
- events. append ( " chapter \( new. chapter) heart " )
112
- events. append ( " \( new. fileHearts) total hearts " )
148
+ events. append ( [ " heart " , " chapter \( new. chapter) heart " , " \( new. fileHearts) total hearts " ] )
113
149
}
114
150
if new. chapterStrawberries > old. chapterStrawberries {
115
- events. append ( " strawberry " )
116
- events. append ( " \( new. chapterStrawberries) chapter strawberries " )
117
- events. append ( " \( new. fileStrawberries) file strawberries " )
151
+ events. append ( [ " strawberry " , " \( new. chapterStrawberries) chapter strawberries " , " \( new. fileStrawberries) file strawberries " ] )
118
152
}
119
153
return events
120
154
}
121
155
122
- func processEvents( _ events: [ String ] ) {
123
- for event in events {
124
- print ( " Event: ` \( event) ` " )
125
- if event == routeConfig. reset {
126
- server. reset ( )
127
- routeIndex = 0
128
- } else if routeIndex < routeConfig. route. count {
129
- let nextEvent = routeConfig. route [ routeIndex]
130
- if event == nextEvent || " ! \( event) " == nextEvent {
131
- if routeIndex == 0 {
156
+ func processEvents( _ events: [ Event ] ) {
157
+ if events. count == 0 {
158
+ return
159
+ }
160
+ var events = events
161
+ // go through the route, gobbling up events as they match
162
+ route: for routeEvent in routeConfig. route [ nextEventIndex... ] {
163
+ for i in events. indices {
164
+ if routeEvent. matches ( event: events [ i] ) {
165
+ print ( " Matched against: ` \( routeEvent. event) ` " )
166
+ events. remove ( at: i)
167
+
168
+ if nextEventIndex == 0 {
132
169
server. reset ( )
133
170
server. start ( )
134
171
gameTimeRunning = true
135
- } else if !nextEvent . starts ( with : " ! " ) {
172
+ } else if !routeEvent . silent {
136
173
server. split ( )
137
174
}
138
- routeIndex += 1
139
- if routeIndex == routeConfig. route. count {
140
- routeIndex = 0
141
- }
175
+
176
+ nextEventIndex += 1
177
+ continue route
142
178
}
143
179
}
180
+ break
181
+ }
182
+
183
+ if events. contains ( where: { routeConfig. reset. matches ( event: $0) } ) {
184
+ server. reset ( )
185
+ nextEventIndex = 0
144
186
}
145
187
}
146
188
}
147
189
148
190
struct RouteConfig {
149
191
let useFileTime : Bool
150
- let reset : String
151
- let route : [ String ]
192
+ let reset : RouteEvent
193
+ let route : [ RouteEvent ]
152
194
}
153
195
154
196
extension RouteConfig {
155
197
init ? ( json: [ String : Any ] ) {
156
198
guard let useFileTime = json [ " useFileTime " ] as? Bool ,
157
199
let reset = json [ " reset " ] as? String ,
200
+ let resetEvent = RouteEvent ( from: reset) ,
158
201
let route = json [ " route " ] as? [ String ]
159
202
else {
160
203
return nil
161
204
}
205
+ let routeEvents = route. compactMap ( { RouteEvent ( from: $0) } )
206
+ if ( routeEvents. count != route. count) {
207
+ return nil
208
+ }
162
209
self . useFileTime = useFileTime
163
- self . reset = reset . components ( separatedBy : " ## " ) [ 0 ]
164
- self . route = route . map { $0 . components ( separatedBy : " ## " ) [ 0 ] }
210
+ self . reset = resetEvent
211
+ self . route = routeEvents
165
212
}
166
213
167
214
init ( ) {
168
- self . init ( useFileTime: false , reset: " " , route: [ ] )
215
+ self . init ( useFileTime: false , reset: RouteEvent ( silent: false , event: " " ) , route: [ ] )
216
+ }
217
+ }
218
+
219
+ class RouteEvent {
220
+ var silent : Bool
221
+ var event : String
222
+
223
+ init ? ( from jsonString: String ) {
224
+ guard let match = RouteEvent . pattern. firstMatch ( in: jsonString, options: [ ] , range: NSRange ( jsonString. startIndex..< jsonString. endIndex, in: jsonString) ) else {
225
+ return nil
226
+ }
227
+ silent = match. range ( at: 1 ) . location != NSNotFound
228
+ if let eventRange = Range ( match. range ( at: 2 ) , in: jsonString) {
229
+ event = String ( jsonString [ eventRange] ) . lowercased ( with: nil )
230
+ } else {
231
+ return nil
232
+ }
233
+ }
234
+
235
+ init ( silent: Bool , event: String ) {
236
+ self . silent = silent
237
+ self . event = event
238
+ }
239
+
240
+ func matches( event: Event ) -> Bool {
241
+ return event. variants. contains ( self . event)
242
+ }
243
+
244
+ private static let pattern = try ! NSRegularExpression (
245
+ pattern: #"^(!)?\s*(.*?)\s*(##.*)?$"#
246
+ )
247
+ }
248
+
249
+ extension RouteEvent : CustomStringConvertible {
250
+ var description : String {
251
+ ( silent ? " ! " : " " ) + event
169
252
}
253
+
254
+
170
255
}
0 commit comments