Skip to content

Commit ff72c49

Browse files
Added event feed and into the jungle splits
1 parent c51a23e commit ff72c49

28 files changed

+313
-67
lines changed

SwiftSplit/CelesteScanner.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class CelesteScanner {
6868

6969
func getInfo() throws -> AutoSplitterInfo? {
7070
guard let header = self.headerInfo else { return nil }
71+
7172
if try autoSplitterInfo?.read(bytes: 16) != header.signatureData {
7273
autoSplitterInfo = try process.findPointer(by: MemscanSignature(from: header.signatureData))
7374
}
@@ -257,15 +258,24 @@ struct ExtendedAutoSplitterInfo {
257258
var areaName: String
258259
var areaSID: String
259260
var levelSet: String
261+
var feedIndex: Int32
262+
var feed: [RmaPointer]
260263

261264
init(from pointer: RmaPointer) throws {
262-
// offset to skip the `1100deadbeef0011`
263-
let body = try pointer.offset(by: 8).preload(size: 40)
264-
chapterDeaths = body.value(at: 0)
265-
levelDeaths = body.value(at: 4)
266-
areaName = try Mono.readString(at: body.value(at: 8)) ?? ""
267-
areaSID = try Mono.readString(at: body.value(at: 16)) ?? ""
268-
levelSet = try Mono.readString(at: body.value(at: 24)) ?? ""
265+
let body = try pointer.preload(size: ExtendedAutoSplitterInfo.FIELD_COUNT * 8)
266+
267+
// body.value(at: 0 * 8) // ignore marker at index 0
268+
chapterDeaths = body.value(at: 1 * 8)
269+
levelDeaths = body.value(at: 2 * 8)
270+
areaName = try Mono.readString(at: body.value(at: 3 * 8)) ?? ""
271+
areaSID = try Mono.readString(at: body.value(at: 4 * 8)) ?? ""
272+
levelSet = try Mono.readString(at: body.value(at: 5 * 8)) ?? ""
273+
feedIndex = body.value(at: 6 * 8)
274+
feed = try Mono.readArray(at: body.value(at: 7 * 8)) ?? []
269275
}
276+
277+
// for ease of access the C# mod puts each field in its own 8-byte word
278+
// (this count includes the marker
279+
static let FIELD_COUNT: UInt = 8
270280
}
271281

SwiftSplit/CelesteSplitter.swift

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class CelesteSplitter {
7474
private var time: Double = 0.0
7575
private var gameTimeRunning = false
7676
private(set) var nextEventIndex = 0
77+
private var feedIndex = 0
7778

7879
func reset() {
7980
server.reset()
@@ -86,7 +87,10 @@ class CelesteSplitter {
8687
return []
8788
}
8889
let extended = try scanner.getExtendedInfo()
89-
let events = getEvents(from: autoSplitterInfo, extended: extendedInfo, to: info, extended: extended)
90+
91+
var externalFeed: [String] = try readExternalFeed(from: extended)
92+
93+
let events = getEvents(from: autoSplitterInfo, extended: extendedInfo, to: info, extended: extended, feed: externalFeed)
9094
logStateChange(from: autoSplitterInfo, extended: extendedInfo, to: info, extended: extendedInfo)
9195
autoSplitterInfo = info
9296
extendedInfo = extended
@@ -144,7 +148,29 @@ class CelesteSplitter {
144148
}
145149
}
146150

147-
func getEvents(from old: AutoSplitterInfo, extended oldExtended: ExtendedAutoSplitterInfo?, to new: AutoSplitterInfo, extended newExtended: ExtendedAutoSplitterInfo?) -> [Event] {
151+
func readExternalFeed(from extended: ExtendedAutoSplitterInfo?) throws -> [String] {
152+
guard let info = extended else { return [] }
153+
154+
let remoteIndex = Int(info.feedIndex)
155+
if self.extendedInfo == nil {
156+
self.feedIndex = remoteIndex
157+
}
158+
let remoteFeed = info.feed
159+
160+
var items: [String] = []
161+
162+
self.feedIndex = max(remoteIndex - remoteFeed.count, self.feedIndex)
163+
while self.feedIndex < remoteIndex {
164+
if let feedItem = try Mono.readString(at: remoteFeed[self.feedIndex % remoteFeed.count]) {
165+
items.append(feedItem)
166+
}
167+
self.feedIndex += 1
168+
}
169+
170+
return items
171+
}
172+
173+
func getEvents(from old: AutoSplitterInfo, extended oldExtended: ExtendedAutoSplitterInfo?, to new: AutoSplitterInfo, extended newExtended: ExtendedAutoSplitterInfo?, feed externalFeed: [String]) -> [Event] {
148174
var events: [Event] = []
149175

150176
// if we don't check `new.chapterComplete`, the summit credits trigger the autosplitter
@@ -324,6 +350,14 @@ class CelesteSplitter {
324350

325351
events.append(event)
326352
}
353+
354+
if !externalFeed.isEmpty {
355+
var event = Event()
356+
for item in externalFeed {
357+
event.add(item)
358+
}
359+
events.append(event)
360+
}
327361
return events
328362
}
329363

SwiftSplit/Core/MemoryScanner.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,26 @@ import Foundation
1010

1111
enum MemscanError : Error {
1212
case errorGettingTask(machError: String)
13-
case scanError(result: memscan_error, machError: String)
14-
case readError(result: memscan_error, machError: String)
13+
case scanError(memscanError: String, machError: String)
14+
case readError(memscanError: String, machError: String)
1515
case readNullPointer
16+
17+
static func memscanErrorString(_ error: memscan_error) -> String {
18+
switch error.memscan {
19+
case MEMSCAN_SUCCESS:
20+
return "MEMSCAN_SUCCESS"
21+
case MEMSCAN_ERROR_PAGE_SIZE_FAILED:
22+
return "MEMSCAN_ERROR_PAGE_SIZE_FAILED"
23+
case MEMSCAN_ERROR_VM_REGION_INFO_FAILED:
24+
return "MEMSCAN_ERROR_VM_REGION_INFO_FAILED"
25+
case MEMSCAN_ERROR_VM_READ_MEMORY_FAILED:
26+
return "MEMSCAN_ERROR_VM_READ_MEMORY_FAILED"
27+
case MEMSCAN_ERROR_VM_WRITE_MEMORY_FAILED:
28+
return "MEMSCAN_ERROR_VM_WRITE_MEMORY_FAILED"
29+
default:
30+
return "\(error.memscan)"
31+
}
32+
}
1633
}
1734

1835
final class MemscanSignature {
@@ -91,7 +108,7 @@ final class MemscanTarget {
91108
var error: memscan_error = memscan_error()
92109
let data = memscan_read(native, pointer, count, &error)
93110
if(error.memscan != MEMSCAN_SUCCESS) {
94-
throw MemscanError.readError(result: error, machError: String(cString: mach_error_string(error.mach)))
111+
throw MemscanError.readError(memscanError: MemscanError.memscanErrorString(error), machError: String(cString: mach_error_string(error.mach)))
95112
}
96113
guard data != nil else {
97114
throw MemscanError.readNullPointer
@@ -168,7 +185,7 @@ final class MemscanScanner {
168185
return MemscanMatch(native: match)
169186
}
170187
if(error.memscan != MEMSCAN_SUCCESS) {
171-
throw MemscanError.scanError(result: error, machError: String(cString: mach_error_string(error.mach)))
188+
throw MemscanError.scanError(memscanError: MemscanError.memscanErrorString(error), machError: String(cString: mach_error_string(error.mach)))
172189
}
173190
return nil
174191
}

SwiftSplit/Core/Mono.swift

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ final class Mono {
1313

1414
static let HEADER_BYTES: Int = 16
1515

16+
static func isNull(pointer: RmaPointer) -> Bool {
17+
return pointer.address == 0
18+
}
19+
1620
/**
1721
Parse a C# string object
1822

@@ -21,20 +25,43 @@ final class Mono {
2125
"NUM:0"
2226
```
2327
```
24-
header length "N U M : 0 "
25-
a8cd0054c57f0000 0000000000000000 05000000 4e00 5500 4d00 3a00 3000
26-
0 8 16 20
28+
MonoVTable* MonoThreadSync* length N U M : 0
29+
a8cd0054c57f0000 0000000000000000 05000000 4e00 5500 4d00 3a00 3000
30+
0 8 16 20
2731
```
2832
*/
2933
static func readString(at pointer: RmaPointer) throws -> String? {
30-
if pointer.address == 0 {
31-
return nil
32-
}
34+
if isNull(pointer: pointer) { return nil }
35+
3336
let length: Int32 = try pointer.value(at: 16)
3437
let stringData = try pointer.raw(at: 20, count: vm_offset_t(length) * 2)
3538
return String(utf16CodeUnits: stringData.buffer.bindMemory(to: unichar.self).baseAddress!, count: Int(length))
3639
}
3740

41+
/**
42+
```
43+
new int[] { 1, 2, 3, 4 }
44+
```
45+
```
46+
MonoVTable* MonoThreadSync* MonoArrayBounds* length align 1 2 3 4
47+
18140698947F0000 0000000000000000 0000000000000000 3F000000 00000000 01000000 02000000 03000000 04000000
48+
0 8 16 24 32
49+
```
50+
*/
51+
static func readArray<T>(at pointer: RmaPointer, as type: T.Type = T.self) throws -> [T]? {
52+
if isNull(pointer: pointer) { return nil }
53+
let length: Int32 = try pointer.value(at: 24)
54+
let contents = try pointer.raw(at: 32, count: vm_offset_t(MemoryLayout<T>.size * Int(length)))
55+
56+
return Array(contents.buffer.bindMemory(to: type))
57+
}
58+
59+
static func readArray(at pointer: RmaPointer) throws -> [RmaPointer]? {
60+
if isNull(pointer: pointer) { return nil }
61+
let pointers: [vm_address_t]? = try readArray(at: pointer)
62+
return pointers?.map { RmaPointer(pointer.target, at: $0) }
63+
}
64+
3865
static func debugMemory(around pointer: RmaPointer, before: vm_offset_t, after: vm_offset_t) throws {
3966
let data = try pointer.raw(at: -Int(before), count: before + after)
4067
print(" Forward: \(data.debugString(withCursor: Int(before)))")

SwiftSplit/Core/RemoteMemoryAccess.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class RmaProcess {
4343
}
4444

4545
struct RmaPointer {
46-
private let target: MemscanTarget
46+
let target: MemscanTarget
4747
let address: vm_address_t
4848

4949
init(_ target: MemscanTarget, at address: vm_address_t) {

authoring_routes.md

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
# Authoring routes
2-
3-
## Route JSON
1+
# Route JSON
42
Routes are configured using a JSON file and use "events" generated by SwiftSplit.
53

64
Here's an example for Old Site Any%:
75
```json
86
{
97
"useFileTime": false,
10-
"reset": "leave chapter",
8+
"reset": "reset chapter",
119
"route": [
12-
"enter chapter 2 # Start",
13-
"d8 > d3 # - Mirror",
14-
"3x > 3 # Intervention",
15-
"10 > 2 # - Escape",
16-
"13 > end_0 # Awake",
10+
"enter chapter 2 ## Start",
11+
"d8 > d3 ## - Mirror",
12+
"3x > 3 ## Intervention",
13+
"10 > 2 ## - Escape",
14+
"13 > end_0 ## Awake",
1715
"complete chapter 2"
1816
]
1917
}
@@ -24,44 +22,47 @@ and triggers splits on those events. The reset event can trigger at any point du
2422
LiveSplit to reset the run. There are mechanisms in place to allow leaving a chapter mid-run (either via Save and Quit
2523
or Return to Map). See the [Expected Resets](#expected-resets) section for more on that.
2624

27-
## Events
2825
Events are triggered when SwiftSplit observes a change in the game state, which is checked 30 times every second. A
2926
single event may have multiple variants, generally with differing levels of specificity (e.g. `leave chapter`,
3027
`leave chapter 1`, and `leave a-side 1`).
3128

32-
SwiftSplit has an "Event Stream" panel that displays events as they are triggered, which can be useful when creating
33-
route files. (You can copy the text out of the panel to paste directly into the route file).
34-
3529
Note that the *exact* text of an event is important. Spaces and capitalization have to match, with a couple additions:
36-
37-
- Whitespace *around* an event is ignored. (e.g. `"leave chapter"` is equivalent to `" leave chapter "`)
3830
- Inserting an exclamation point (`!`) at the beginning of an event will cause that event to be "silent" and not trigger
39-
a split. This can be useful for situations like [expected resets](#expected-resets).
40-
- Anything after `#` will be trimmed off. This can be useful for explaining events.
41-
- If after applying the previous rules the event is empty, it's simply ignored. (e.g. `! # comment` will be ignored)
31+
a split. This can be useful when your route passes between two screens multiple times but you only want one split.
32+
- Anything after `##` will be trimmed off. This can be useful for explaining events.
33+
- Any event entries that start with `#` will be ignored, allowing you to "comment out" events.
4234

43-
Bringing this together, `" ! d8 > d3# other stuff "` is a valid event, and translates to a silent transition from
44-
`d8` to `d3`.
35+
SwiftSplit has an "Event Stream" panel that displays events as they are triggered, which can be useful when creating
36+
route files. (You can copy the text out of the panel to paste directly into the route file too).
4537

46-
## Event list
38+
## Events
39+
Leave events are triggered by restarting the chapter, returning to the map, or using "Save and Quit"
4740

4841
### Chapter start/end events
49-
- `leave chapter` - Triggered when leaving any chapter (either by restarting the chapter, returning to the map, or
50-
using "Save and Quit")
51-
- `leave chapter <n>` - Triggered when leaving chapter `<n>`
5242
- `enter chapter` - Triggered when any chapter is entered
53-
- `enter chapter <n>` - Triggered when chapter `<n>` is entered
43+
- `leave chapter` - Triggered when leaving any chapter
5444
- `complete chapter` - Triggered when any chapter is completed
45+
- `enter chapter <n>` - Triggered when chapter `<n>` is entered
46+
- `leave chapter <n>` - Triggered when leaving chapter `<n>`
5547
- `complete chapter <n>` - Triggered when chapter `<n>` is completed
5648
- **A-side specific:**
49+
- `enter a-side` - Triggered when any A-side is entered
50+
- `leave a-side` - Triggered when leaving any A-side
51+
- `complete a-side` - Triggered when completing any A-side
5752
- `enter a-side <n>` - Triggered when chapter `<n>`'s A-side is entered
5853
- `leave a-side <n>` - Triggered when leaving chapter `<n>`'s A-side
5954
- `complete a-side <n>` - Triggered when chapter `<n>`'s A-side is completed
6055
- **B-side specific:**
56+
- `enter b-side` - Triggered when any B-side is entered
57+
- `leave b-side` - Triggered when leaving any B-side
58+
- `complete b-side` - Triggered when completing any B-side
6159
- `enter b-side <n>` - Triggered when chapter `<n>`'s B-side is entered
6260
- `leave b-side <n>` - Triggered when leaving chapter `<n>`'s B-side
6361
- `complete b-side <n>` - Triggered when chapter `<n>`'s B-side is completed
6462
- **C-side specific:**
63+
- `enter c-side` - Triggered when any C-side is entered
64+
- `leave c-side` - Triggered when leaving any C-side
65+
- `complete c-side` - Triggered when completing any C-side
6566
- `enter c-side <n>` - Triggered when chapter `<n>`'s C-side is entered
6667
- `leave c-side <n>` - Triggered when leaving chapter `<n>`'s C-side
6768
- `complete c-side <n>` - Triggered when chapter `<n>`'s C-side is completed
@@ -86,38 +87,53 @@ Bringing this together, `" ! d8 > d3# other stuff "` is a valid event, and tra
8687
- `<n> chapter strawberries` - Triggered when a total of `<n>` strawberries are collected in a chapter
8788
- `<n> file strawberries` - Triggered when a total of `<n>` strawberries are collected in the file
8889

90+
## Extended Events (Everest)
91+
Everest supplies additional split data
92+
93+
### SID Events
94+
Chapter numbers for custom maps are dynamically allocated by Everest, so when the Everest autosplitter data is present,
95+
SwiftSplit emits variants of all the relevant events using the Area SID
96+
97+
- `enter chapter '<sid>'` - Triggered when the given chapter is entered
98+
- `leave chapter '<sid>'` - Triggered when leaving the given chapter
99+
- `complete chapter '<sid>'` - Triggered when the given chapter is completed
100+
- **A-side specific:**
101+
- `enter a-side '<sid>'` - Triggered when the given chapter's A-side is entered
102+
- `leave a-side '<sid>'` - Triggered when leaving the given chapter's A-side
103+
- `complete a-side '<sid>'` - Triggered when the given chapter's A-side is completed
104+
- **B-side specific:**
105+
- `enter b-side '<sid>'` - Triggered when the given chapter's B-side is entered
106+
- `leave b-side '<sid>'` - Triggered when leaving the given chapter's B-side
107+
- `complete b-side '<sid>'` - Triggered when the given chapter's B-side is completed
108+
- **C-side specific:**
109+
- `enter c-side '<sid>'` - Triggered when the given chapter's C-side is entered
110+
- `leave c-side '<sid>'` - Triggered when leaving the given chapter's C-side
111+
- `complete c-side '<sid>'` - Triggered when the given chapter's C-side is completed
112+
- **Collectables**
113+
- `collect chapter '<sid>' cassette` - Triggered when the cassette in the specified chapter is collected
114+
- `collect chapter '<sid>' heart` - Triggered when the heart gem in the specified chapter is collected
115+
89116
## Return to Map & Save and Quit
90117

91118
Without the proper route file, both of these count as resetting a chapter. It's impossible for SwiftSplit to tell the
92-
difference between a restart, return to map, or save and quit. To get around this, you can define in your route where
93-
leaving the chapter is *expected.* Unless you're triggering a split there, the event should be marked as silent.
119+
difference between a reset, return to map, or save and quit. To get around this, you can define in your route where
120+
leaving the chapter is *expected.* Generally you'll want to define an event that happens right before you leave, then
121+
the leave event. This ensures that resetting any time outside that window will trigger a proper reset.
94122

95-
Here's what the reset for the 1A heart might look like:
123+
Here's what the reset for the 1A might look like:
96124
```json
97125
"route": [
98-
"enter chapter 1 # Start",
99-
"5 > 6 # Crossing",
126+
"enter chapter 1 ## Start",
127+
"5 > 6 ## Crossing",
100128
"!collect heart",
101129
"!leave chapter",
102-
"9 > 9b # Chasm",
130+
"9 > 9b ## Chasm",
103131
"complete chapter 1"
104132
]
105133
```
106134

107135
The reason we put `!collect heart` before `!leave chapter` is because any time that SwiftSplit is waiting for you to
108-
leave the chapter *you can not automatically reset the run.* Any attempt to restart the run will just result in
109-
progressing through the route. By putting the collect heart event before the leave chapter event we make sure that
110-
SwiftSplit only starts waiting for the leave event right before we do it.
111-
112-
For restarting after collecting berries you'll probably want to have two events: one when you enter the room you'll
113-
save and quit in, and the next when you collect the berry
114-
115-
```json
116-
"route": [
117-
"...",
118-
"!a00 > a03",
119-
"!collect strawberry",
120-
"!leave chapter",
121-
"..."
122-
]
123-
```
136+
leave the chapter *you can not reset the run.* Any attempt to reset the run will just result in progressing through
137+
the route. By putting the collect heart event before the leave chapter event we make sure that SwiftSplit only starts
138+
waiting for the leave event right before we do it. You could stand two pixels away from the heart and restart the
139+
chapter, and SwiftSplit would *still* recognize it as a reset.

example/Celeste.zip

260 KB
Binary file not shown.

0 commit comments

Comments
 (0)