diff --git a/SwiftSplit/CelesteScanner.swift b/SwiftSplit/CelesteScanner.swift index 7869534..989ecfc 100644 --- a/SwiftSplit/CelesteScanner.swift +++ b/SwiftSplit/CelesteScanner.swift @@ -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)) } @@ -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 } diff --git a/SwiftSplit/CelesteSplitter.swift b/SwiftSplit/CelesteSplitter.swift index d827a98..40aa1fa 100644 --- a/SwiftSplit/CelesteSplitter.swift +++ b/SwiftSplit/CelesteSplitter.swift @@ -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() @@ -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 @@ -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 @@ -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 } diff --git a/SwiftSplit/Core/MemoryScanner.swift b/SwiftSplit/Core/MemoryScanner.swift index 24feb95..b56a4bb 100644 --- a/SwiftSplit/Core/MemoryScanner.swift +++ b/SwiftSplit/Core/MemoryScanner.swift @@ -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 { @@ -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 @@ -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 } diff --git a/SwiftSplit/Core/Mono.swift b/SwiftSplit/Core/Mono.swift index 2cad225..0115d47 100644 --- a/SwiftSplit/Core/Mono.swift +++ b/SwiftSplit/Core/Mono.swift @@ -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 @@ -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(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.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)))") diff --git a/SwiftSplit/Core/RemoteMemoryAccess.swift b/SwiftSplit/Core/RemoteMemoryAccess.swift index 4565255..9cc4c2d 100644 --- a/SwiftSplit/Core/RemoteMemoryAccess.swift +++ b/SwiftSplit/Core/RemoteMemoryAccess.swift @@ -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) { diff --git a/authoring_routes.md b/authoring_routes.md index 916dd35..9f33a75 100644 --- a/authoring_routes.md +++ b/authoring_routes.md @@ -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" ] } @@ -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 ` - Triggered when leaving chapter `` - `enter chapter` - Triggered when any chapter is entered -- `enter chapter ` - Triggered when chapter `` is entered +- `leave chapter` - Triggered when leaving any chapter - `complete chapter` - Triggered when any chapter is completed +- `enter chapter ` - Triggered when chapter `` is entered +- `leave chapter ` - Triggered when leaving chapter `` - `complete chapter ` - Triggered when chapter `` 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 ` - Triggered when chapter ``'s A-side is entered - `leave a-side ` - Triggered when leaving chapter ``'s A-side - `complete a-side ` - Triggered when chapter ``'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 ` - Triggered when chapter ``'s B-side is entered - `leave b-side ` - Triggered when leaving chapter ``'s B-side - `complete b-side ` - Triggered when chapter ``'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 ` - Triggered when chapter ``'s C-side is entered - `leave c-side ` - Triggered when leaving chapter ``'s C-side - `complete c-side ` - Triggered when chapter ``'s C-side is completed @@ -86,38 +87,53 @@ Bringing this together, `" ! d8 > d3# other stuff "` is a valid event, and tra - ` chapter strawberries` - Triggered when a total of `` strawberries are collected in a chapter - ` file strawberries` - Triggered when a total of `` 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 ''` - Triggered when the given chapter is entered +- `leave chapter ''` - Triggered when leaving the given chapter +- `complete chapter ''` - Triggered when the given chapter is completed +- **A-side specific:** + - `enter a-side ''` - Triggered when the given chapter's A-side is entered + - `leave a-side ''` - Triggered when leaving the given chapter's A-side + - `complete a-side ''` - Triggered when the given chapter's A-side is completed +- **B-side specific:** + - `enter b-side ''` - Triggered when the given chapter's B-side is entered + - `leave b-side ''` - Triggered when leaving the given chapter's B-side + - `complete b-side ''` - Triggered when the given chapter's B-side is completed +- **C-side specific:** + - `enter c-side ''` - Triggered when the given chapter's C-side is entered + - `leave c-side ''` - Triggered when leaving the given chapter's C-side + - `complete c-side ''` - Triggered when the given chapter's C-side is completed +- **Collectables** + - `collect chapter '' cassette` - Triggered when the cassette in the specified chapter is collected + - `collect chapter '' 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. diff --git a/example/Celeste.zip b/example/Celeste.zip index 638aada..52c320f 100644 Binary files a/example/Celeste.zip and b/example/Celeste.zip differ diff --git a/example/Into The Jungle/Any%/Into The Jungle - Ancient Ruins Any%.json b/example/Into The Jungle/Any%/Into The Jungle - Ancient Ruins Any%.json new file mode 100644 index 0000000..4c1d03f --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Ancient Ruins Any%.json @@ -0,0 +1,12 @@ +{ + "useFileTime": false, + "reset": "leave chapter", + "route": [ + "enter chapter 15 ## Start", + "a_15 > b_01 ## Skyward", + "b_14 > c_01 ## Elevation", + "c_13 > d_01 ## Remnants", + "complete chapter" + ] +} + diff --git a/example/Into The Jungle/Any%/Into The Jungle - Ancient Ruins Any%.lss b/example/Into The Jungle/Any%/Into The Jungle - Ancient Ruins Any%.lss new file mode 100644 index 0000000..7eddfee --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Ancient Ruins Any%.lss @@ -0,0 +1 @@ +Celeste: Into The JungleAncient Ruins Any%00:00:00.00000000StartSkywardElevationRemnants diff --git a/example/Into The Jungle/Any%/Into The Jungle - Any%.json b/example/Into The Jungle/Any%/Into The Jungle - Any%.json new file mode 100644 index 0000000..b18189e --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Any%.json @@ -0,0 +1,36 @@ +{ + "useFileTime": true, + "reset": "leave chapter", + "route": [ + "enter chapter 13 ## Verdant Trail", + "!a_11 > b_01 ## Undergrowth", + "!b_12 > c_01 ## Canopy", + "!c_12 > d_01 ## Emergent", + "complete chapter", + + "!enter chapter 14 ## Spoopy Thicket", + "!a_13 > b_01 ## The Veil", + "!b_15 > c_00 ## Skittering", + "!c_12 > d_01 ## The Nest", + "complete chapter", + + "!enter chapter 15 ## Ancient Ruins", + "!a_15 > b_01 ## Skyward", + "!b_14 > c_01 ## Elevation", + "!c_13 > d_01 ## Remnants", + "complete chapter", + + "!enter chapter 16 ## Shifting Temple", + "!a_14 > b_01 ## Tunnels", + "!b_12 > c_01 ## Labyrinth", + "!c_13 > d_01 ## Collapse", + "complete chapter", + + "!enter chapter 16 ## The Depths", + "!a_01 > b_01 ## Reclamation", + "!b_12 > c_01 ## Vision", + "!c_13 > d_01 ## Finale", + "complete chapter" + ] +} + diff --git a/example/Into The Jungle/Any%/Into The Jungle - Any%.lss b/example/Into The Jungle/Any%/Into The Jungle - Any%.lss new file mode 100644 index 0000000..c4791cb --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Any%.lss @@ -0,0 +1 @@ +Celeste - Into The JungleAny%00:00:00.00000000PrologueVerdant TrailSpoopy ThicketAncient RuinsShifting TempleThe Depths \ No newline at end of file diff --git a/example/Into The Jungle/Any%/Into The Jungle - Shifting Temple Any%.json b/example/Into The Jungle/Any%/Into The Jungle - Shifting Temple Any%.json new file mode 100644 index 0000000..d25bece --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Shifting Temple Any%.json @@ -0,0 +1,12 @@ +{ + "useFileTime": false, + "reset": "leave chapter", + "route": [ + "enter chapter 16 ## Start", + "a_14 > b_01 ## Tunnels", + "b_12 > c_01 ## Labyrinth", + "c_13 > d_01 ## Collapse", + "complete chapter" + ] +} + diff --git a/example/Into The Jungle/Any%/Into The Jungle - Shifting Temple Any%.lss b/example/Into The Jungle/Any%/Into The Jungle - Shifting Temple Any%.lss new file mode 100644 index 0000000..10df32d --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Shifting Temple Any%.lss @@ -0,0 +1 @@ +Celeste: Into The JungleShifting Temple Any%00:00:00.00000000StartTunnelsLabyrinthCollapse diff --git a/example/Into The Jungle/Any%/Into The Jungle - Spoopy Thicket Any%.json b/example/Into The Jungle/Any%/Into The Jungle - Spoopy Thicket Any%.json new file mode 100644 index 0000000..9924940 --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Spoopy Thicket Any%.json @@ -0,0 +1,12 @@ +{ + "useFileTime": false, + "reset": "leave chapter", + "route": [ + "enter chapter 14 ## Start", + "a_13 > b_01 ## The Veil", + "b_15 > c_00 ## Skittering", + "c_12 > d_01 ## The Nest", + "complete chapter" + ] +} + diff --git a/example/Into The Jungle/Any%/Into The Jungle - Spoopy Thicket Any%.lss b/example/Into The Jungle/Any%/Into The Jungle - Spoopy Thicket Any%.lss new file mode 100644 index 0000000..cc08bc0 --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Spoopy Thicket Any%.lss @@ -0,0 +1 @@ +Celeste: Into The JungleSpoopy Thicket Any%00:00:00.00000000StartThe VeilSkitteringThe Nest diff --git a/example/Into The Jungle/Any%/Into The Jungle - The Depths Any%.json b/example/Into The Jungle/Any%/Into The Jungle - The Depths Any%.json new file mode 100644 index 0000000..75bffcf --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - The Depths Any%.json @@ -0,0 +1,12 @@ +{ + "useFileTime": false, + "reset": "leave chapter", + "route": [ + "enter chapter 17 ## Start", + "a_01 > b_01 ## Reclamation", + "b_12 > c_01 ## Vision", + "c_13 > d_01 ## Finale", + "complete chapter" + ] +} + diff --git a/example/Into The Jungle/Any%/Into The Jungle - The Depths Any%.lss b/example/Into The Jungle/Any%/Into The Jungle - The Depths Any%.lss new file mode 100644 index 0000000..6e0e304 --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - The Depths Any%.lss @@ -0,0 +1 @@ +Celeste: Into The JungleThe Depths Any%00:00:00.00000000StartReclamationVisionFinale diff --git a/example/Into The Jungle/Any%/Into The Jungle - Verdant Trail Any%.json b/example/Into The Jungle/Any%/Into The Jungle - Verdant Trail Any%.json new file mode 100644 index 0000000..564c150 --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Verdant Trail Any%.json @@ -0,0 +1,12 @@ +{ + "useFileTime": false, + "reset": "leave chapter", + "route": [ + "enter chapter 13 ## Start", + "a_11 > b_01 ## Undergrowth", + "b_12 > c_01 ## Canopy", + "c_12 > d_01 ## Emergent", + "complete chapter" + ] +} + diff --git a/example/Into The Jungle/Any%/Into The Jungle - Verdant Trail Any%.lss b/example/Into The Jungle/Any%/Into The Jungle - Verdant Trail Any%.lss new file mode 100644 index 0000000..b8e89fc --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle - Verdant Trail Any%.lss @@ -0,0 +1 @@ +Celeste: Into The JungleVerdant Trail Any%00:00:00.00000000StartUndergrowthCanopyEmergent diff --git a/example/Into The Jungle/Any%/Into The Jungle Any%.json b/example/Into The Jungle/Any%/Into The Jungle Any%.json new file mode 100644 index 0000000..5a42afb --- /dev/null +++ b/example/Into The Jungle/Any%/Into The Jungle Any%.json @@ -0,0 +1,39 @@ +{ + "useFileTime": true, + "reset": "leave chapter", + "route": [ + "enter chapter 12 ## Prologue", + "complete chapter", + + "!enter chapter 13 ## Verdant Trail", + "!a_11 > b_01 ## Undergrowth", + "!b_12 > c_01 ## Canopy", + "!c_12 > d_01 ## Emergent", + "complete chapter", + + "!enter chapter 14 ## Spoopy Thicket", + "!a_13 > b_01 ## The Veil", + "!b_15 > c_00 ## Skittering", + "!c_12 > d_01 ## The Nest", + "complete chapter", + + "!enter chapter 15 ## Ancient Ruins", + "!a_15 > b_01 ## Skyward", + "!b_14 > c_01 ## Elevation", + "!c_13 > d_01 ## Remnants", + "complete chapter", + + "!enter chapter 16 ## Shifting Temple", + "!a_14 > b_01 ## Tunnels", + "!b_12 > c_01 ## Labyrinth", + "!c_13 > d_01 ## Collapse", + "complete chapter", + + "!enter chapter 16 ## The Depths", + "!a_01 > b_01 ## Reclamation", + "!b_12 > c_01 ## Vision", + "!c_13 > d_01 ## Finale", + "complete chapter" + ] +} + diff --git a/example/Into The Jungle/Icons/Ancient Ruins.png b/example/Into The Jungle/Icons/Ancient Ruins.png new file mode 100755 index 0000000..034650b Binary files /dev/null and b/example/Into The Jungle/Icons/Ancient Ruins.png differ diff --git a/example/Into The Jungle/Icons/Into The Jungle.png b/example/Into The Jungle/Icons/Into The Jungle.png new file mode 100644 index 0000000..d1c79f1 Binary files /dev/null and b/example/Into The Jungle/Icons/Into The Jungle.png differ diff --git a/example/Into The Jungle/Icons/Prologue.png b/example/Into The Jungle/Icons/Prologue.png new file mode 100755 index 0000000..af7cd16 Binary files /dev/null and b/example/Into The Jungle/Icons/Prologue.png differ diff --git a/example/Into The Jungle/Icons/Shifting Temple.png b/example/Into The Jungle/Icons/Shifting Temple.png new file mode 100755 index 0000000..f8109dc Binary files /dev/null and b/example/Into The Jungle/Icons/Shifting Temple.png differ diff --git a/example/Into The Jungle/Icons/Spoopy Thicket.png b/example/Into The Jungle/Icons/Spoopy Thicket.png new file mode 100755 index 0000000..9676366 Binary files /dev/null and b/example/Into The Jungle/Icons/Spoopy Thicket.png differ diff --git a/example/Into The Jungle/Icons/The Depths.png b/example/Into The Jungle/Icons/The Depths.png new file mode 100755 index 0000000..926b045 Binary files /dev/null and b/example/Into The Jungle/Icons/The Depths.png differ diff --git a/example/Into The Jungle/Icons/Verdant Trail.png b/example/Into The Jungle/Icons/Verdant Trail.png new file mode 100755 index 0000000..7d5a689 Binary files /dev/null and b/example/Into The Jungle/Icons/Verdant Trail.png differ diff --git a/example/Into The Jungle/credits.txt b/example/Into The Jungle/credits.txt new file mode 100644 index 0000000..8f60f85 --- /dev/null +++ b/example/Into The Jungle/credits.txt @@ -0,0 +1 @@ +Beautiful chapter icons by SpoopySoup