Skip to content

Commit 5241b06

Browse files
Initial mid-run reset support
1 parent dd3b3d8 commit 5241b06

File tree

4 files changed

+162
-58
lines changed

4 files changed

+162
-58
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ The `.lss` files can be imported directly into LiveSplit One and the correspondi
6060
# Route JSON
6161
Routes are configured using a JSON file and use "events" generated by SwiftSplit. They consist of a reset event and a
6262
list of route events. SwiftSplit expects route events in a specific order and triggers splits on those events. The reset
63-
event can trigger at any point during the route and will reset the run.
63+
event can trigger at any point during the route and will reset the run. If the next event in the route is itself the
64+
reset event, that will take priority and the run won't be reset. This can be used to implement returning to map into
65+
your route.
6466

6567
Here's an example for Old Site Any%:
6668
```json

SwiftSplit/CelesteScanner.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ struct AutoSplitterData: Equatable {
279279
}
280280
}
281281

282-
enum ChapterMode {
282+
enum ChapterMode : Equatable {
283283
case Normal
284284
case BSide
285285
case CSide
@@ -353,5 +353,22 @@ class AutoSplitterInfo {
353353
self.fileCassettes = Int(data.fileCassettes)
354354
self.fileHearts = Int(data.fileHearts)
355355
}
356+
357+
func stableStateEquals(other: AutoSplitterInfo) -> Bool {
358+
return self.chapter == other.chapter &&
359+
self.mode == other.mode &&
360+
self.level == other.level &&
361+
self.timerActive == other.timerActive &&
362+
self.chapterStarted == other.chapterStarted &&
363+
self.chapterComplete == other.chapterComplete &&
364+
// self.chapterTime == other.chapterTime &&
365+
self.chapterStrawberries == other.chapterStrawberries &&
366+
self.chapterCassette == other.chapterCassette &&
367+
self.chapterHeart == other.chapterHeart &&
368+
// self.fileTime == other.fileTime &&
369+
self.fileStrawberries == other.fileStrawberries &&
370+
self.fileCassettes == other.fileCassettes &&
371+
self.fileHearts == other.fileHearts
372+
}
356373

357374
}

SwiftSplit/CelesteSplitter.swift

Lines changed: 138 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,38 @@ import Foundation
1111
enum SplitterError: Error {
1212
case noHeader
1313
}
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+
1438
/**
1539
Handles sending split commands based on state changes
1640
*/
1741
class CelesteSplitter {
1842
let scanner: CelesteScanner
1943
let server: LiveSplitServer
2044
var autoSplitterInfo: AutoSplitterInfo = AutoSplitterInfo()
21-
var routeConfig = RouteConfig(
22-
useFileTime: false,
23-
reset: "",
24-
route: []
25-
)
45+
var routeConfig = RouteConfig()
2646

2747
init(pid: pid_t, server: LiveSplitServer) throws {
2848
self.scanner = try CelesteScanner(pid: pid)
@@ -35,19 +55,20 @@ class CelesteSplitter {
3555
}
3656

3757
private var gameTimeRunning = false
38-
private(set) var routeIndex = 0
58+
private(set) var nextEventIndex = 0
3959

4060
func reset() {
4161
server.reset()
42-
routeIndex = 0
62+
nextEventIndex = 0
4363
}
4464

45-
func update() throws -> [String] {
65+
func update() throws -> [Event] {
4666
guard let info = try scanner.getInfo() else {
4767
autoSplitterInfo = AutoSplitterInfo()
4868
return []
4969
}
5070
let events = getEvents(from: autoSplitterInfo, to: info)
71+
// logStateChange(from: autoSplitterInfo, to: info)
5172
autoSplitterInfo = info
5273

5374
let time = routeConfig.useFileTime ? autoSplitterInfo.fileTime : autoSplitterInfo.chapterTime
@@ -65,106 +86,170 @@ class CelesteSplitter {
6586
return events
6687
}
6788

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] = []
70108

71109
// if we don't check `new.chapterComplete`, the summit credits trigger the autosplitter
72110
if new.chapterStarted && !old.chapterStarted && !new.chapterComplete {
73-
events.append("start chapter \(new.chapter)")
111+
var event: Event = ["start chapter", "start chapter \(new.chapter)"]
74112
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)")
78116
default: break
79117
}
118+
events.append(event)
80119
}
81120
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)"]
84122
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)")
88126
default: break
89127
}
128+
events.append(event)
90129
}
91130
if new.chapterComplete && !old.chapterComplete {
92-
events.append("complete chapter \(old.chapter)")
131+
var event: Event = ["complete chapter", "complete chapter \(old.chapter)"]
93132
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)")
97136
default: break
98137
}
138+
events.append(event)
99139
}
100140

101141
if new.level != old.level && old.level != "" && new.level != "" {
102-
events.append("\(old.level) > \(new.level)")
142+
events.append(["\(old.level) > \(new.level)"])
103143
}
104144
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"])
108146
}
109147
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"])
113149
}
114150
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"])
118152
}
119153
return events
120154
}
121155

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 {
132169
server.reset()
133170
server.start()
134171
gameTimeRunning = true
135-
} else if !nextEvent.starts(with: "!") {
172+
} else if !routeEvent.silent {
136173
server.split()
137174
}
138-
routeIndex += 1
139-
if routeIndex == routeConfig.route.count {
140-
routeIndex = 0
141-
}
175+
176+
nextEventIndex += 1
177+
continue route
142178
}
143179
}
180+
break
181+
}
182+
183+
if events.contains(where: { routeConfig.reset.matches(event: $0) }) {
184+
server.reset()
185+
nextEventIndex = 0
144186
}
145187
}
146188
}
147189

148190
struct RouteConfig {
149191
let useFileTime: Bool
150-
let reset: String
151-
let route: [String]
192+
let reset: RouteEvent
193+
let route: [RouteEvent]
152194
}
153195

154196
extension RouteConfig {
155197
init?(json: [String: Any]) {
156198
guard let useFileTime = json["useFileTime"] as? Bool,
157199
let reset = json["reset"] as? String,
200+
let resetEvent = RouteEvent(from: reset),
158201
let route = json["route"] as? [String]
159202
else {
160203
return nil
161204
}
205+
let routeEvents = route.compactMap({ RouteEvent(from: $0) })
206+
if(routeEvents.count != route.count) {
207+
return nil
208+
}
162209
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
165212
}
166213

167214
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
169252
}
253+
254+
170255
}

SwiftSplit/ViewController.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class ViewController: NSViewController, RouteBoxDelegate {
7171

7272
do {
7373
let events = try splitter.update()
74-
eventStream = (eventStream + events.map { "\"\($0)\"" }).suffix(eventStreamLength)
74+
eventStream = (eventStream + events.flatMap { $0.variants }).suffix(eventStreamLength)
7575
} catch {
7676
self.splitter = nil
7777
eventStream = Array(repeating: "", count: eventStreamLength)
@@ -215,8 +215,8 @@ class ViewController: NSViewController, RouteBoxDelegate {
215215

216216
gameTimeLabel.stringValue = timeString
217217

218-
nextEventLabel.stringValue = splitter.routeIndex < routeConfig.route.count ?
219-
"\"\(routeConfig.route[splitter.routeIndex])\" (\(splitter.routeIndex + 1)/\(routeConfig.route.count))"
218+
nextEventLabel.stringValue = splitter.nextEventIndex < routeConfig.route.count ?
219+
"\"\(routeConfig.route[splitter.nextEventIndex])\" (\(splitter.nextEventIndex + 1)/\(routeConfig.route.count))"
220220
: "<none>"
221221
}
222222

0 commit comments

Comments
 (0)