From c51a23e296cef657c39093ec12cdc856c7ada21b Mon Sep 17 00:00:00 2001 From: Pierce Corcoran Date: Fri, 9 Jul 2021 18:22:33 -0700 Subject: [PATCH] Added splits using extended chapter info --- SwiftSplit/CelesteScanner.swift | 30 ++-- SwiftSplit/CelesteSplitter.swift | 269 +++++++++++++++++++---------- SwiftSplit/EventListDelegate.swift | 4 +- SwiftSplit/ViewController.swift | 41 ++--- 4 files changed, 208 insertions(+), 136 deletions(-) diff --git a/SwiftSplit/CelesteScanner.swift b/SwiftSplit/CelesteScanner.swift index 4e5f9c2..7869534 100644 --- a/SwiftSplit/CelesteScanner.swift +++ b/SwiftSplit/CelesteScanner.swift @@ -39,7 +39,7 @@ class CelesteScanner { func findHeader() throws { print("Scanning for the AutoSplitterData object header") - extendedInfo = try process.findPointer(by: "11efbeadde11") + extendedInfo = try process.findPointer(by: "1100efbeadde0011") if let info = extendedInfo { try Mono.debugMemory(around: info, before: 64, after: 64) } @@ -59,11 +59,11 @@ class CelesteScanner { } - func readExtended() throws -> ExtendedAutoSplitterData? { + func getExtendedInfo() throws -> ExtendedAutoSplitterInfo? { guard let extendedInfo = extendedInfo else { return nil } - return try ExtendedAutoSplitterData(from: extendedInfo) + return try ExtendedAutoSplitterInfo(from: extendedInfo) } func getInfo() throws -> AutoSplitterInfo? { @@ -251,27 +251,21 @@ class AutoSplitterInfo { } } -struct ExtendedAutoSplitterData { - var madelineX: Float - var madelineY: Float - var fileDeaths: Int32 +struct ExtendedAutoSplitterInfo { + var chapterDeaths: Int32 var levelDeaths: Int32 var areaName: String var areaSID: String var levelSet: String - var completeScreenName: String init(from pointer: RmaPointer) throws { - // offset to skip the `11deadbeef11` - let body = try pointer.offset(by: 8).preload(size: 48) - madelineX = body.value(at: 0) - madelineY = body.value(at: 4) - fileDeaths = body.value(at: 8) - levelDeaths = body.value(at: 12) - areaName = try Mono.readString(at: body.value(at: 16)) ?? "" - areaSID = try Mono.readString(at: body.value(at: 24)) ?? "" - levelSet = try Mono.readString(at: body.value(at: 32)) ?? "" - completeScreenName = try Mono.readString(at: body.value(at: 40)) ?? "" + // offset to skip the `1100deadbeef0011` + let body = try pointer.offset(by: 8).preload(size: 40) + chapterDeaths = body.value(at: 0) + levelDeaths = body.value(at: 4) + areaName = try Mono.readString(at: body.value(at: 8)) ?? "" + areaSID = try Mono.readString(at: body.value(at: 16)) ?? "" + levelSet = try Mono.readString(at: body.value(at: 24)) ?? "" } } diff --git a/SwiftSplit/CelesteSplitter.swift b/SwiftSplit/CelesteSplitter.swift index d5df13b..d827a98 100644 --- a/SwiftSplit/CelesteSplitter.swift +++ b/SwiftSplit/CelesteSplitter.swift @@ -13,23 +13,10 @@ enum SplitterError: Error { } enum VariantType: Equatable { - case normal(specificity: Int) + case normal case legacy } -extension VariantType { - var sortOrder: Int { - get { - switch self { - case .normal(specificity: let spec): - return spec - case .legacy: - return Int.max - } - } - } -} - struct EventVariant { var event: String var type: VariantType @@ -38,8 +25,8 @@ struct EventVariant { return EventVariant(event: event, type: .legacy) } - static func normal(_ event: String, specificity: Int) -> EventVariant { - return EventVariant(event: event, type: .normal(specificity: specificity)) + static func normal(_ event: String) -> EventVariant { + return EventVariant(event: event, type: .normal) } } @@ -58,6 +45,10 @@ struct Event { self.variants.append(variant) } } + + mutating func add(_ event: String) { + self.variants.append(.normal(event)) + } } /** @@ -67,6 +58,7 @@ class CelesteSplitter { let scanner: CelesteScanner let server: LiveSplitServer var autoSplitterInfo: AutoSplitterInfo = AutoSplitterInfo() + var extendedInfo: ExtendedAutoSplitterInfo? = nil var routeConfig = RouteConfig() init(pid: pid_t, server: LiveSplitServer) throws { @@ -93,9 +85,11 @@ class CelesteSplitter { autoSplitterInfo = AutoSplitterInfo() return [] } - let events = getEvents(from: autoSplitterInfo, to: info) - logStateChange(from: autoSplitterInfo, to: info) + let extended = try scanner.getExtendedInfo() + let events = getEvents(from: autoSplitterInfo, extended: extendedInfo, to: info, extended: extended) + logStateChange(from: autoSplitterInfo, extended: extendedInfo, to: info, extended: extendedInfo) autoSplitterInfo = info + extendedInfo = extended if autoSplitterInfo.mode != .Menu { // save and quit resets the chapter timer to zero, but we don't want to reset it for QTM strats time = routeConfig.useFileTime ? autoSplitterInfo.fileTime : autoSplitterInfo.chapterTime @@ -114,8 +108,8 @@ class CelesteSplitter { } var lastStateTime = DispatchTime.now() - - func logStateChange(from old: AutoSplitterInfo, to new: AutoSplitterInfo) { + + func logStateChange(from old: AutoSplitterInfo, extended oldExtended: ExtendedAutoSplitterInfo?, to new: AutoSplitterInfo, extended newExtended: ExtendedAutoSplitterInfo?) { let comparisons = [ (name: "chapter", old: "\(old.chapter)", new: "\(new.chapter)"), (name: "mode", old: "\(old.mode)", new: "\(new.mode)"), @@ -131,9 +125,15 @@ class CelesteSplitter { (name: "fileStrawberries", old: "\(old.fileStrawberries)", new: "\(new.fileStrawberries)"), (name: "fileCassettes", old: "\(old.fileCassettes)", new: "\(new.fileCassettes)"), (name: "fileHearts", old: "\(old.fileHearts)", new: "\(new.fileHearts)"), + + (name: "chapterDeaths", old: "\(oldExtended?.chapterDeaths ?? -1)", new: "\(newExtended?.chapterDeaths ?? -1)"), + (name: "levelDeaths", old: "\(oldExtended?.levelDeaths ?? -1)", new: "\(newExtended?.levelDeaths ?? -1)"), + (name: "areaName", old: "\(oldExtended?.areaName ?? "")", new: "\(newExtended?.areaName ?? "")"), + (name: "areaSID", old: "\(oldExtended?.areaSID ?? "")", new: "\(newExtended?.areaSID ?? "")"), + (name: "levelSet", old: "\(oldExtended?.levelSet ?? "")", new: "\(newExtended?.levelSet ?? "")"), ] let changes = comparisons.filter { $0.old != $0.new } - + if !changes.isEmpty { let currentTime = DispatchTime.now() let delta = Double(currentTime.uptimeNanoseconds - lastStateTime.uptimeNanoseconds) / 1_000_000_000 @@ -144,104 +144,189 @@ class CelesteSplitter { } } - func getEvents(from old: AutoSplitterInfo, to new: AutoSplitterInfo) -> [Event] { + func getEvents(from old: AutoSplitterInfo, extended oldExtended: ExtendedAutoSplitterInfo?, to new: AutoSplitterInfo, extended newExtended: ExtendedAutoSplitterInfo?) -> [Event] { var events: [Event] = [] - + // if we don't check `new.chapterComplete`, the summit credits trigger the autosplitter if new.chapterStarted && !old.chapterStarted && !new.chapterComplete { - var event = Event( - .normal("enter chapter", specificity: 0), - .normal("enter chapter \(new.chapter)", specificity: 1) - ) - switch new.mode { - case .Normal: event.add(variant: .normal("enter a-side \(new.chapter)", specificity: 2)) - case .BSide: event.add(variant: .normal("enter b-side \(new.chapter)", specificity: 2)) - case .CSide: event.add(variant: .normal("enter c-side \(new.chapter)", specificity: 2)) - default: break + var event = Event() + + do { // no chapter + event.add("enter chapter") + switch new.mode { + case .Normal: event.add("enter a-side") + case .BSide: event.add("enter b-side") + case .CSide: event.add("enter c-side") + default: break + } } - event.add(variants: - .legacy("start chapter"), - .legacy("start chapter \(new.chapter)") - ) - switch new.mode { - case .Normal: event.add(variant: .legacy("start a-side \(new.chapter)")) - case .BSide: event.add(variant: .legacy("start b-side \(new.chapter)")) - case .CSide: event.add(variant: .legacy("start c-side \(new.chapter)")) - default: break + do { // chapter index + event.add("enter chapter \(new.chapter)") + switch new.mode { + case .Normal: event.add("enter a-side \(new.chapter)") + case .BSide: event.add("enter b-side \(new.chapter)") + case .CSide: event.add("enter c-side \(new.chapter)") + default: break + } + } + + do { // area SID + if let newSID = newExtended?.areaSID { + event.add("enter chapter '\(newSID)'") + switch new.mode { + case .Normal: event.add("enter a-side '\(newSID)'") + case .BSide: event.add("enter b-side '\(newSID)'") + case .CSide: event.add("enter c-side '\(newSID)'") + default: break + } + } + } + + do { // legacy + event.add(variants: .legacy("start chapter"), .legacy("start chapter \(new.chapter)")) + switch new.mode { + case .Normal: event.add(variant: .legacy("start a-side \(new.chapter)")) + case .BSide: event.add(variant: .legacy("start b-side \(new.chapter)")) + case .CSide: event.add(variant: .legacy("start c-side \(new.chapter)")) + default: break + } } events.append(event) } + if !new.chapterStarted && old.chapterStarted && !old.chapterComplete { - var event: Event = Event( - .normal("leave chapter", specificity: 0), - .normal("leave chapter \(old.chapter)", specificity: 1) - ) - switch new.mode { - case .Normal: event.add(variant: .normal("leave a-side \(old.chapter)", specificity: 2)) - case .BSide: event.add(variant: .normal("leave b-side \(old.chapter)", specificity: 2)) - case .CSide: event.add(variant: .normal("leave c-side \(old.chapter)", specificity: 2)) - default: break + var event: Event = Event() + + do { // no chapter + event.add("leave chapter") + switch old.mode { + case .Normal: event.add("leave a-side") + case .BSide: event.add("leave b-side") + case .CSide: event.add("leave c-side") + default: break + } + } + + do { // chapter index + event.add("leave chapter \(old.chapter)") + switch old.mode { + case .Normal: event.add("leave a-side \(old.chapter)") + case .BSide: event.add("leave b-side \(old.chapter)") + case .CSide: event.add("leave c-side \(old.chapter)") + default: break + } + } + + do { // area SID + if let oldSID = oldExtended?.areaSID { + event.add("leave chapter '\(oldSID)'") + switch old.mode { + case .Normal: event.add("leave a-side '\(oldSID)'") + case .BSide: event.add("leave b-side '\(oldSID)'") + case .CSide: event.add("leave c-side '\(oldSID)'") + default: break + } + } } - event.add(variants: .legacy("reset chapter"), .legacy("reset chapter \(old.chapter)")) - switch new.mode { - case .Normal: event.add(variant: .legacy("reset a-side \(old.chapter)")) - case .BSide: event.add(variant: .legacy("reset b-side \(old.chapter)")) - case .CSide: event.add(variant: .legacy("reset c-side \(old.chapter)")) - default: break + do { // legacy + event.add(variants: .legacy("reset chapter"), .legacy("reset chapter \(old.chapter)")) + switch old.mode { + case .Normal: event.add(variant: .legacy("reset a-side \(old.chapter)")) + case .BSide: event.add(variant: .legacy("reset b-side \(old.chapter)")) + case .CSide: event.add(variant: .legacy("reset c-side \(old.chapter)")) + default: break + } } events.append(event) } + if new.chapterComplete && !old.chapterComplete { - var event: Event = Event( - .normal("complete chapter", specificity: 0), - .normal("complete chapter \(old.chapter)", specificity: 1) - ) - switch new.mode { - case .Normal: event.add(variant: .normal("complete a-side \(old.chapter)", specificity: 2)) - case .BSide: event.add(variant: .normal("complete b-side \(old.chapter)", specificity: 2)) - case .CSide: event.add(variant: .normal("complete c-side \(old.chapter)", specificity: 2)) - default: break + var event: Event = Event() + + do { // no chapter + event.add("complete chapter") + switch new.mode { + case .Normal: event.add("complete a-side") + case .BSide: event.add("complete b-side") + case .CSide: event.add("complete c-side") + default: break + } + } + + do { // chapter index + event.add("complete chapter \(old.chapter)") + switch new.mode { + case .Normal: event.add("complete a-side \(old.chapter)") + case .BSide: event.add("complete b-side \(old.chapter)") + case .CSide: event.add("complete c-side \(old.chapter)") + default: break + } + } + + do { // area SID + if let oldSID = oldExtended?.areaSID { + event.add("complete chapter '\(oldSID)'") + switch new.mode { + case .Normal: event.add("complete a-side '\(oldSID)'") + case .BSide: event.add("complete b-side '\(oldSID)'") + case .CSide: event.add("complete c-side '\(oldSID)'") + default: break + } + } } + events.append(event) } if new.level != old.level && old.level != "" && new.level != "" { - events.append(Event(.normal("\(old.level) > \(new.level)", specificity: 0))) + events.append(Event(.normal("\(old.level) > \(new.level)"))) } + if new.chapterCassette && !old.chapterCassette { - events.append(Event( - .normal("collect cassette", specificity: 0), - .normal("collect chapter \(new.chapter) cassette", specificity: 1), - .normal("\(new.fileCassettes) total cassettes", specificity: 1), - // compat: - .legacy("cassette"), - .legacy("chapter \(new.chapter) cassette") - )) + var event = Event() + + event.add("collect cassette") // no chapter + event.add("collect chapter \(new.chapter) cassette") // chapter index + if let newSID = newExtended?.areaSID { // area SID + event.add("collect chapter '\(newSID)' cassette") + } + event.add("\(new.fileCassettes) total cassettes") + // legacy + event.add(variants: .legacy("cassette"), .legacy("chapter \(new.chapter) cassette")) + + events.append(event) } + if new.chapterHeart && !old.chapterHeart { - events.append(Event( - .normal("collect heart", specificity: 0), - .normal("collect chapter \(new.chapter) heart", specificity: 1), - .normal("\(new.fileHearts) total hearts", specificity: 1), - // compat: - .legacy("heart"), - .legacy("chapter \(new.chapter) heart") - )) + var event = Event() + + event.add("collect heart") // no chapter + event.add("collect chapter \(new.chapter) heart") // chapter index + if let newSID = newExtended?.areaSID { // area SID + event.add("collect chapter '\(newSID)' heart") + } + event.add("\(new.fileHearts) total hearts") + // legacy + event.add(variants: .legacy("heart"), .legacy("chapter \(new.chapter) heart")) + + events.append(event) } + if new.chapterStrawberries > old.chapterStrawberries { - events.append(Event( - .normal("collect strawberry", specificity: 0), - .normal("\(new.chapterStrawberries) chapter strawberries", specificity: 1), - .normal("\(new.fileStrawberries) file strawberries", specificity: 1), - // compat: - .legacy("strawberry") - )) + var event = Event() + + event.add("collect strawberry") + event.add("\(new.chapterStrawberries) chapter strawberries") + event.add("\(new.fileStrawberries) file strawberries") + event.add(variant: .legacy("strawberry")) + + events.append(event) } return events } - + func processEvents(_ events: [Event]) { if events.count == 0 { return @@ -308,7 +393,7 @@ extension RouteConfig { class RouteEvent { var silent: Bool var event: String - + init?(from jsonString: String) { guard let match = RouteEvent.pattern.firstMatch(in: jsonString, options: [], range: NSRange(jsonString.startIndex.. Bool { return event.variants.contains(where: { self.matches(variant: $0) }) } diff --git a/SwiftSplit/EventListDelegate.swift b/SwiftSplit/EventListDelegate.swift index 94151f1..ffe95ee 100644 --- a/SwiftSplit/EventListDelegate.swift +++ b/SwiftSplit/EventListDelegate.swift @@ -26,9 +26,7 @@ class EventListDelegate: NSObject, NSCollectionViewDelegate, NSCollectionViewDat return } - let variants = events.flatMap { event in - event.variants.sorted(by: { $0.type.sortOrder < $1.type.sortOrder }) - }.filter { $0.type != VariantType.legacy } + let variants = events.flatMap { $0.variants }.filter { $0.type != VariantType.legacy } let entries = variants.map { $0.event } let removeCount = eventEntries.count + entries.count - maxEntries diff --git a/SwiftSplit/ViewController.swift b/SwiftSplit/ViewController.swift index bc8673d..e1354b1 100644 --- a/SwiftSplit/ViewController.swift +++ b/SwiftSplit/ViewController.swift @@ -107,7 +107,7 @@ class ViewController: NSViewController, RouteBoxDelegate { } var connectionAttempts = 0 - self.connectionStatusLabel.stringValue = "Connecting…" + self.connectionStatusLabel.stringValue = "Waiting…" if CelesteScanner.canImmediatelyConnect(pid: pid) { self.connect(pid: pid) return @@ -117,6 +117,7 @@ class ViewController: NSViewController, RouteBoxDelegate { self.connectTimer?.invalidate() self.connectTimer = nil self.connectionStatusLabel.stringValue = "Not connected" + return } self.connect(pid: pid) @@ -149,7 +150,9 @@ class ViewController: NSViewController, RouteBoxDelegate { } func connect(pid: pid_t) { + self.connectionStatusLabel.stringValue = "Connecting…" guard let server = self.server else { + self.connectionStatusLabel.stringValue = "Failed to start split server" return } @@ -205,29 +208,21 @@ class ViewController: NSViewController, RouteBoxDelegate { Cassettes: \(info.fileCassettes) Hearts: \(info.fileHearts) """ - do { - let extended = try self.splitter?.scanner.readExtended() - if let extended = extended { - var address = "none" - if let p = self.splitter?.scanner.extendedInfo?.address { - address = String(format: "%llx", p) - } - dataText += """ - - Extended: - Address: \(address) - Madeline X: \(extended.madelineX) - Madeline Y: \(extended.madelineY) - File Deaths: \(extended.fileDeaths) - Level Deaths: \(extended.levelDeaths) - Area Name: \(extended.areaName) - Area SID: \(extended.areaSID) - Level Set: \(extended.levelSet) - """ - + if let extended = splitter.extendedInfo { + var address = "none" + if let p = self.splitter?.scanner.extendedInfo?.address { + address = String(format: "%llx", p) } - } catch { - print(error) + dataText += """ + + Extended: + Address: \(address) + Chapter Deaths: \(extended.chapterDeaths) + Level Deaths: \(extended.levelDeaths) + Area Name: \(extended.areaName) + Area SID: \(extended.areaSID) + Level Set: \(extended.levelSet) + """ } celesteDataLabel.stringValue = dataText } else {