Skip to content

Commit

Permalink
Added event feed and into the jungle splits
Browse files Browse the repository at this point in the history
  • Loading branch information
thecodewarrior committed Apr 9, 2022
1 parent c51a23e commit ff72c49
Show file tree
Hide file tree
Showing 28 changed files with 313 additions and 67 deletions.
24 changes: 17 additions & 7 deletions SwiftSplit/CelesteScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class CelesteScanner {

func getInfo() throws -> AutoSplitterInfo? {
guard let header = self.headerInfo else { return nil }

if try autoSplitterInfo?.read(bytes: 16) != header.signatureData {
autoSplitterInfo = try process.findPointer(by: MemscanSignature(from: header.signatureData))
}
Expand Down Expand Up @@ -257,15 +258,24 @@ struct ExtendedAutoSplitterInfo {
var areaName: String
var areaSID: String
var levelSet: String
var feedIndex: Int32
var feed: [RmaPointer]

init(from pointer: RmaPointer) throws {
// 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)) ?? ""
let body = try pointer.preload(size: ExtendedAutoSplitterInfo.FIELD_COUNT * 8)

// body.value(at: 0 * 8) // ignore marker at index 0
chapterDeaths = body.value(at: 1 * 8)
levelDeaths = body.value(at: 2 * 8)
areaName = try Mono.readString(at: body.value(at: 3 * 8)) ?? ""
areaSID = try Mono.readString(at: body.value(at: 4 * 8)) ?? ""
levelSet = try Mono.readString(at: body.value(at: 5 * 8)) ?? ""
feedIndex = body.value(at: 6 * 8)
feed = try Mono.readArray(at: body.value(at: 7 * 8)) ?? []
}

// for ease of access the C# mod puts each field in its own 8-byte word
// (this count includes the marker
static let FIELD_COUNT: UInt = 8
}

38 changes: 36 additions & 2 deletions SwiftSplit/CelesteSplitter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class CelesteSplitter {
private var time: Double = 0.0
private var gameTimeRunning = false
private(set) var nextEventIndex = 0
private var feedIndex = 0

func reset() {
server.reset()
Expand All @@ -86,7 +87,10 @@ class CelesteSplitter {
return []
}
let extended = try scanner.getExtendedInfo()
let events = getEvents(from: autoSplitterInfo, extended: extendedInfo, to: info, extended: extended)

var externalFeed: [String] = try readExternalFeed(from: extended)

let events = getEvents(from: autoSplitterInfo, extended: extendedInfo, to: info, extended: extended, feed: externalFeed)
logStateChange(from: autoSplitterInfo, extended: extendedInfo, to: info, extended: extendedInfo)
autoSplitterInfo = info
extendedInfo = extended
Expand Down Expand Up @@ -144,7 +148,29 @@ class CelesteSplitter {
}
}

func getEvents(from old: AutoSplitterInfo, extended oldExtended: ExtendedAutoSplitterInfo?, to new: AutoSplitterInfo, extended newExtended: ExtendedAutoSplitterInfo?) -> [Event] {
func readExternalFeed(from extended: ExtendedAutoSplitterInfo?) throws -> [String] {
guard let info = extended else { return [] }

let remoteIndex = Int(info.feedIndex)
if self.extendedInfo == nil {
self.feedIndex = remoteIndex
}
let remoteFeed = info.feed

var items: [String] = []

self.feedIndex = max(remoteIndex - remoteFeed.count, self.feedIndex)
while self.feedIndex < remoteIndex {
if let feedItem = try Mono.readString(at: remoteFeed[self.feedIndex % remoteFeed.count]) {
items.append(feedItem)
}
self.feedIndex += 1
}

return items
}

func getEvents(from old: AutoSplitterInfo, extended oldExtended: ExtendedAutoSplitterInfo?, to new: AutoSplitterInfo, extended newExtended: ExtendedAutoSplitterInfo?, feed externalFeed: [String]) -> [Event] {
var events: [Event] = []

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

events.append(event)
}

if !externalFeed.isEmpty {
var event = Event()
for item in externalFeed {
event.add(item)
}
events.append(event)
}
return events
}

Expand Down
25 changes: 21 additions & 4 deletions SwiftSplit/Core/MemoryScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,26 @@ import Foundation

enum MemscanError : Error {
case errorGettingTask(machError: String)
case scanError(result: memscan_error, machError: String)
case readError(result: memscan_error, machError: String)
case scanError(memscanError: String, machError: String)
case readError(memscanError: String, machError: String)
case readNullPointer

static func memscanErrorString(_ error: memscan_error) -> String {
switch error.memscan {
case MEMSCAN_SUCCESS:
return "MEMSCAN_SUCCESS"
case MEMSCAN_ERROR_PAGE_SIZE_FAILED:
return "MEMSCAN_ERROR_PAGE_SIZE_FAILED"
case MEMSCAN_ERROR_VM_REGION_INFO_FAILED:
return "MEMSCAN_ERROR_VM_REGION_INFO_FAILED"
case MEMSCAN_ERROR_VM_READ_MEMORY_FAILED:
return "MEMSCAN_ERROR_VM_READ_MEMORY_FAILED"
case MEMSCAN_ERROR_VM_WRITE_MEMORY_FAILED:
return "MEMSCAN_ERROR_VM_WRITE_MEMORY_FAILED"
default:
return "\(error.memscan)"
}
}
}

final class MemscanSignature {
Expand Down Expand Up @@ -91,7 +108,7 @@ final class MemscanTarget {
var error: memscan_error = memscan_error()
let data = memscan_read(native, pointer, count, &error)
if(error.memscan != MEMSCAN_SUCCESS) {
throw MemscanError.readError(result: error, machError: String(cString: mach_error_string(error.mach)))
throw MemscanError.readError(memscanError: MemscanError.memscanErrorString(error), machError: String(cString: mach_error_string(error.mach)))
}
guard data != nil else {
throw MemscanError.readNullPointer
Expand Down Expand Up @@ -168,7 +185,7 @@ final class MemscanScanner {
return MemscanMatch(native: match)
}
if(error.memscan != MEMSCAN_SUCCESS) {
throw MemscanError.scanError(result: error, machError: String(cString: mach_error_string(error.mach)))
throw MemscanError.scanError(memscanError: MemscanError.memscanErrorString(error), machError: String(cString: mach_error_string(error.mach)))
}
return nil
}
Expand Down
39 changes: 33 additions & 6 deletions SwiftSplit/Core/Mono.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ final class Mono {

static let HEADER_BYTES: Int = 16

static func isNull(pointer: RmaPointer) -> Bool {
return pointer.address == 0
}

/**
Parse a C# string object

Expand All @@ -21,20 +25,43 @@ final class Mono {
"NUM:0"
```
```
header length "N U M : 0 "
a8cd0054c57f0000 0000000000000000 05000000 4e00 5500 4d00 3a00 3000
0 8 16 20
MonoVTable* MonoThreadSync* length N U M : 0
a8cd0054c57f0000 0000000000000000 05000000 4e00 5500 4d00 3a00 3000
0 8 16 20
```
*/
static func readString(at pointer: RmaPointer) throws -> String? {
if pointer.address == 0 {
return nil
}
if isNull(pointer: pointer) { return nil }

let length: Int32 = try pointer.value(at: 16)
let stringData = try pointer.raw(at: 20, count: vm_offset_t(length) * 2)
return String(utf16CodeUnits: stringData.buffer.bindMemory(to: unichar.self).baseAddress!, count: Int(length))
}

/**
```
new int[] { 1, 2, 3, 4 }
```
```
MonoVTable* MonoThreadSync* MonoArrayBounds* length align 1 2 3 4
18140698947F0000 0000000000000000 0000000000000000 3F000000 00000000 01000000 02000000 03000000 04000000
0 8 16 24 32
```
*/
static func readArray<T>(at pointer: RmaPointer, as type: T.Type = T.self) throws -> [T]? {
if isNull(pointer: pointer) { return nil }
let length: Int32 = try pointer.value(at: 24)
let contents = try pointer.raw(at: 32, count: vm_offset_t(MemoryLayout<T>.size * Int(length)))

return Array(contents.buffer.bindMemory(to: type))
}

static func readArray(at pointer: RmaPointer) throws -> [RmaPointer]? {
if isNull(pointer: pointer) { return nil }
let pointers: [vm_address_t]? = try readArray(at: pointer)
return pointers?.map { RmaPointer(pointer.target, at: $0) }
}

static func debugMemory(around pointer: RmaPointer, before: vm_offset_t, after: vm_offset_t) throws {
let data = try pointer.raw(at: -Int(before), count: before + after)
print(" Forward: \(data.debugString(withCursor: Int(before)))")
Expand Down
2 changes: 1 addition & 1 deletion SwiftSplit/Core/RemoteMemoryAccess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class RmaProcess {
}

struct RmaPointer {
private let target: MemscanTarget
let target: MemscanTarget
let address: vm_address_t

init(_ target: MemscanTarget, at address: vm_address_t) {
Expand Down
110 changes: 63 additions & 47 deletions authoring_routes.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
# Authoring routes

## Route JSON
# Route JSON
Routes are configured using a JSON file and use "events" generated by SwiftSplit.

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

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

SwiftSplit has an "Event Stream" panel that displays events as they are triggered, which can be useful when creating
route files. (You can copy the text out of the panel to paste directly into the route file).

Note that the *exact* text of an event is important. Spaces and capitalization have to match, with a couple additions:

- Whitespace *around* an event is ignored. (e.g. `"leave chapter"` is equivalent to `" leave chapter "`)
- Inserting an exclamation point (`!`) at the beginning of an event will cause that event to be "silent" and not trigger
a split. This can be useful for situations like [expected resets](#expected-resets).
- Anything after `#` will be trimmed off. This can be useful for explaining events.
- If after applying the previous rules the event is empty, it's simply ignored. (e.g. `! # comment` will be ignored)
a split. This can be useful when your route passes between two screens multiple times but you only want one split.
- Anything after `##` will be trimmed off. This can be useful for explaining events.
- Any event entries that start with `#` will be ignored, allowing you to "comment out" events.

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

## Event list
## Events
Leave events are triggered by restarting the chapter, returning to the map, or using "Save and Quit"

### Chapter start/end events
- `leave chapter` - Triggered when leaving any chapter (either by restarting the chapter, returning to the map, or
using "Save and Quit")
- `leave chapter <n>` - Triggered when leaving chapter `<n>`
- `enter chapter` - Triggered when any chapter is entered
- `enter chapter <n>` - Triggered when chapter `<n>` is entered
- `leave chapter` - Triggered when leaving any chapter
- `complete chapter` - Triggered when any chapter is completed
- `enter chapter <n>` - Triggered when chapter `<n>` is entered
- `leave chapter <n>` - Triggered when leaving chapter `<n>`
- `complete chapter <n>` - Triggered when chapter `<n>` is completed
- **A-side specific:**
- `enter a-side` - Triggered when any A-side is entered
- `leave a-side` - Triggered when leaving any A-side
- `complete a-side` - Triggered when completing any A-side
- `enter a-side <n>` - Triggered when chapter `<n>`'s A-side is entered
- `leave a-side <n>` - Triggered when leaving chapter `<n>`'s A-side
- `complete a-side <n>` - Triggered when chapter `<n>`'s A-side is completed
- **B-side specific:**
- `enter b-side` - Triggered when any B-side is entered
- `leave b-side` - Triggered when leaving any B-side
- `complete b-side` - Triggered when completing any B-side
- `enter b-side <n>` - Triggered when chapter `<n>`'s B-side is entered
- `leave b-side <n>` - Triggered when leaving chapter `<n>`'s B-side
- `complete b-side <n>` - Triggered when chapter `<n>`'s B-side is completed
- **C-side specific:**
- `enter c-side` - Triggered when any C-side is entered
- `leave c-side` - Triggered when leaving any C-side
- `complete c-side` - Triggered when completing any C-side
- `enter c-side <n>` - Triggered when chapter `<n>`'s C-side is entered
- `leave c-side <n>` - Triggered when leaving chapter `<n>`'s C-side
- `complete c-side <n>` - Triggered when chapter `<n>`'s C-side is completed
Expand All @@ -86,38 +87,53 @@ Bringing this together, `" ! d8 > d3# other stuff "` is a valid event, and tra
- `<n> chapter strawberries` - Triggered when a total of `<n>` strawberries are collected in a chapter
- `<n> file strawberries` - Triggered when a total of `<n>` strawberries are collected in the file

## Extended Events (Everest)
Everest supplies additional split data

### SID Events
Chapter numbers for custom maps are dynamically allocated by Everest, so when the Everest autosplitter data is present,
SwiftSplit emits variants of all the relevant events using the Area SID

- `enter chapter '<sid>'` - Triggered when the given chapter is entered
- `leave chapter '<sid>'` - Triggered when leaving the given chapter
- `complete chapter '<sid>'` - Triggered when the given chapter is completed
- **A-side specific:**
- `enter a-side '<sid>'` - Triggered when the given chapter's A-side is entered
- `leave a-side '<sid>'` - Triggered when leaving the given chapter's A-side
- `complete a-side '<sid>'` - Triggered when the given chapter's A-side is completed
- **B-side specific:**
- `enter b-side '<sid>'` - Triggered when the given chapter's B-side is entered
- `leave b-side '<sid>'` - Triggered when leaving the given chapter's B-side
- `complete b-side '<sid>'` - Triggered when the given chapter's B-side is completed
- **C-side specific:**
- `enter c-side '<sid>'` - Triggered when the given chapter's C-side is entered
- `leave c-side '<sid>'` - Triggered when leaving the given chapter's C-side
- `complete c-side '<sid>'` - Triggered when the given chapter's C-side is completed
- **Collectables**
- `collect chapter '<sid>' cassette` - Triggered when the cassette in the specified chapter is collected
- `collect chapter '<sid>' heart` - Triggered when the heart gem in the specified chapter is collected

## Return to Map & Save and Quit

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

Here's what the reset for the 1A heart might look like:
Here's what the reset for the 1A might look like:
```json
"route": [
"enter chapter 1 # Start",
"5 > 6 # Crossing",
"enter chapter 1 ## Start",
"5 > 6 ## Crossing",
"!collect heart",
"!leave chapter",
"9 > 9b # Chasm",
"9 > 9b ## Chasm",
"complete chapter 1"
]
```

The reason we put `!collect heart` before `!leave chapter` is because any time that SwiftSplit is waiting for you to
leave the chapter *you can not automatically reset the run.* Any attempt to restart the run will just result in
progressing through the route. By putting the collect heart event before the leave chapter event we make sure that
SwiftSplit only starts waiting for the leave event right before we do it.

For restarting after collecting berries you'll probably want to have two events: one when you enter the room you'll
save and quit in, and the next when you collect the berry

```json
"route": [
"...",
"!a00 > a03",
"!collect strawberry",
"!leave chapter",
"..."
]
```
leave the chapter *you can not reset the run.* Any attempt to reset the run will just result in progressing through
the route. By putting the collect heart event before the leave chapter event we make sure that SwiftSplit only starts
waiting for the leave event right before we do it. You could stand two pixels away from the heart and restart the
chapter, and SwiftSplit would *still* recognize it as a reset.
Binary file modified example/Celeste.zip
Binary file not shown.
Loading

0 comments on commit ff72c49

Please sign in to comment.