Skip to content

Instantly share code, notes, and snippets.

@jbzdarkid
Last active February 26, 2018 03:35
Show Gist options
  • Save jbzdarkid/0948c9318c6da6a7c459681f181c7ee9 to your computer and use it in GitHub Desktop.
Save jbzdarkid/0948c9318c6da6a7c459681f181c7ee9 to your computer and use it in GitHub Desktop.
Autosplitter for The Witness
state("witness64_d3d11") {}
// TODO: "Split on lasers" should actually split on lasers
// TODO: Entity_Machine_Panel::init_pattern_data_lotus
// TODO: globals, 18, BFF*8, 100/108, 0 (text) -> triple which is valid
startup {
// Environmental puzzles/patterns, the +135
settings.Add("Split on environmental patterns", false);
// Tracked via obelisks, which report their counts
vars.obelisks = new List<int> {
0x00098, // Treehouse
0x00264, // Monastery
0x0035A, // Desert
0x00368, // Mountain
0x0A16D, // Town
0x22074, // Shadows
};
settings.Add("Split on all panels (solving and non-solving)", false);
// Panels which are solved multiple times in a normal run
vars.multiPanels = new List<int>{
0x03679, 0x0367A, 0x03676, 0x03677, // Mill control panels
0x03853, 0x03859, 0x275FB, // Boathouse control panels
0x0060A, 0x17C0B, 0x17E08, 0x17E2C, 0x181F6, 0x18489, // Swamp Control Panels
0x17CE4, 0x17DB8, 0x17DD2, 0x17E53, // Treehouse R1, R2, L, Green Rotators
0x2896B, // Town Bridge Control
0x334D9, // Town RGB light control
0x09E3A, 0x09ED9, 0x09E87, // Purple, Blue, Orange Mountain Walkways
0x03554, 0x03553, 0x0354F, 0x0354A, 0x03550, 0x03546, // Cinema Panels
0x0A3B6, // Tutorial Back Left
0x0362A, // Tutorial Gate
0x09F99, // Desert Laser Redirect
0x34D97, // Boat map
0x079E0, // Town Triple Panel
0x09D9C, // Monastery Bonzai
0x0A07A, // Bunker Elevator
0x09F80, // Mountaintop Box
0x17C35, // Mountaintop Crazyhorse
0x09FCD, // Mountain Multi
0x09EEC, // Mountain Elevator
};
vars.keepWalkOns = new List<int>{
0x033EB, // Yellow
0x01BEA, // Purple
0x01CD4, // Green
0x01D40, // Blue
};
vars.multipanel = new List<int>{
0x09FCD, 0x09FCF, 0x09FD0, 0x09FD1, 0x09FD2, 0x09FD3
};
// 11 lasers which unlock the mountain-top box
settings.Add("Split on lasers", true);
vars.lasers = new List<int>{
0x032F6, // Town
0x03609, // Desert
0x0360E, // Symmetry
0x0360F, // Keep Front
0x03318, // Keep Rear
0x03613, // Quarry
0x03614, // Treehouse
0x03616, // Swamp
0x03617, // Jungle
0x09DE1, // Bunker
0x17CA5, // Monastery
0x19651, // Shadows
};
// One-off splits (usually to accompany 11 lasers splits)
settings.Add("Split on tutorial door", true);
settings.Add("Split when starting the boat", false);
settings.Add("Split on greenhouse elevator", false);
settings.Add("Split when completing the first mountain floor", false);
settings.Add("Split on mountain elevator", false);
settings.Add("Split on final elevator", true);
settings.Add("Start/split on challenge start", false);
settings.Add("Reset on challenge stop", false);
settings.Add("Split on challenge end", false);
settings.Add("Split on easter egg ending", true);
}
init {
vars.panels = null; // Used to detect if init completes properly
vars.startTime = 0.0;
vars.activePanel = 0;
var page = modules.First();
var scanner = new SignatureScanner(game, page.BaseAddress, page.ModuleMemorySize);
// get_active_panel()
IntPtr ptr = scanner.Scan(new SigScanTarget(3, // Targeting byte 3
"48 8B 05 ????????", // mov rax, [witness64_d3d11.exe + offset]
"33 C9", // xor ecx, ecx
"48 85 C0", // test rax, rax
"74 06" // je 6
));
if (ptr == IntPtr.Zero) {
throw new Exception("Could not find current puzzle!");
}
int relativePosition = (int)((long)ptr - (long)page.BaseAddress) + 4;
vars.puzzle = new MemoryWatcher<int>(new DeepPointer(
relativePosition + game.ReadValue<int>(ptr),
game.ReadValue<byte>(ptr+14),
game.ReadValue<int>(ptr+22)
));
relativePosition = (int)((long)ptr - (long)page.BaseAddress) + 50;
int basePointer = relativePosition + game.ReadValue<int>(ptr+46);
print("witness64_d3d11.globals = "+basePointer.ToString("X"));
// judge_panel()
ptr = scanner.Scan(new SigScanTarget(0,
"C7 83 ???????? 01000000", // mov [rbx+offset], 1
"48 0F45 C8" // cmovne rcx, rax
));
if (ptr == IntPtr.Zero) {
throw new Exception("Could not find solved and completed offsets!");
}
vars.solvedOffset = game.ReadValue<int>(ptr+2);
vars.completedOffset = game.ReadValue<int>(ptr+38);
// found_a_pattern()
ptr = scanner.Scan(new SigScanTarget(12,
"48 8B 7C 24 60", // mov rdi, [rsp+60]
"48 8B 74 24 58", // mov rsi, [rsp+58]
"FF 83 ????????" // inc [rbx + offset]
));
if (ptr == IntPtr.Zero) {
throw new Exception("Could not find obelisk count offset!");
}
int obeliskOffset = game.ReadValue<int>(ptr);
// Entity_Door::update_position_and_orientation()
ptr = scanner.Scan(new SigScanTarget(4,
"F3 0F11 89 ????????", // mov [rcx + offset], xmm1
"F3 0F59 89 ????????" // mulss xmm1, [rcx + ??]
));
if (ptr == IntPtr.Zero) {
throw new Exception("Could not find door offset!");
}
int doorOffset = game.ReadValue<int>(ptr);
vars.mountainDoor = new MemoryWatcher<float>(new DeepPointer(
basePointer, 0x18, 0x9E54*8, doorOffset
));
print(
"Solved offset: "+vars.solvedOffset.ToString("X")
+ " | Completed offset: "+vars.completedOffset.ToString("X")
+ " | Obelisk offset: "+obeliskOffset.ToString("X")
+ " | Door offset: "+doorOffset.ToString("X")
// + " | Panel name offset: "+vars.panelNameOffset.ToString("X")
// + " | EP name offset: "+vars.epNameOffset.ToString("X")
);
// player_is_inside_movement_hint_marker()
ptr = scanner.Scan(new SigScanTarget(4, // Targeting byte 4
"F2 0F10 15 ????????", // movsd xmm2, [Core::time_info+10]
"0F57 C9", // xorps xmm1, xmm1
"66 0F5A D2" // cvtpd2ps xmm2, xmm2
));
if (ptr == IntPtr.Zero) {
throw new Exception("Could not find time!");
}
relativePosition = (int)((long)ptr - (long)page.BaseAddress) + 4;
vars.time = new MemoryWatcher<double>(new DeepPointer(
relativePosition + game.ReadValue<int>(ptr)
));
// update_scripted_stuff()
ptr = scanner.Scan(new SigScanTarget(2, // Targeting byte 2
"FF 05 ????????", // inc [num_script_frames]
"0F28 74 24 ??", // movaps xmm6, [rsp+??]
"0F28 7C 24 ??" // movaps xmm7, [rsp+??]
));
if (ptr == IntPtr.Zero) {
throw new Exception("Could not find game frames!");
}
relativePosition = (int)((long)ptr - (long)page.BaseAddress) + 4;
vars.gameFrames = new MemoryWatcher<int>(new DeepPointer(
relativePosition + game.ReadValue<int>(ptr)
));
// update_player_control()
ptr = scanner.Scan(new SigScanTarget(9, // Targeting byte 9
"83 3D ???????? 01", // cmp dword ptr [globals+??], 01
"C7 05 ???????? 01000000", // mov [player_control_got_moving_input], 01
"75 ??" // jne ??
));
if (ptr == IntPtr.Zero) {
throw new Exception("Could not find player movement!");
}
relativePosition = (int)((long)ptr - (long)page.BaseAddress) + 8;
vars.playerMoving = new MemoryWatcher<int>(new DeepPointer(
relativePosition + game.ReadValue<int>(ptr)
));
// Entity_Record_Player::power_on()
ptr = scanner.Scan(new SigScanTarget(12, // Targeting byte 12
"C7 83 ???????? 00000000", // mov [rbx+??], 0
"C7 83 ???????? 0000803F", // mov [rbx+offset], 1.0
"48 83 C4 60" // add rsp, 60
));
if (ptr == IntPtr.Zero) {
throw new Exception("Could not find challenge start!");
}
int recordPowerOffset = game.ReadValue<int>(ptr);
if (recordPowerOffset == 0xC8) { // new version
vars.challengeActive = new MemoryWatcher<float>(new DeepPointer(
basePointer, 0x188, 0x2B8, 0x0, 0xC8
));
vars.movie = new MemoryWatcher<int>(new DeepPointer(
basePointer, 0x188, 0x358, 0x0, 0xCC
));
} else { // old version
vars.challengeActive = new MemoryWatcher<float>(new DeepPointer(
basePointer, 0x188, 0x2A8, 0x0, 0xD0
));
vars.movie = new MemoryWatcher<int>(new DeepPointer(
basePointer, 0x188, 0x338, 0x0, 0xD4
));
}
// end2_eyelid_trigger()
ptr = scanner.Scan(new SigScanTarget(8, // Targeting byte 8
"48 83 EC ??", // sub rsp, 28
"F2 0F10 05 ????????", // mov xmm0, [end2_eyelid_start_time]
"0F57 C9", // xorps xmm1, xmm1
"89 0D ????????" // mov [end2_eyelid_box_id], ecx
));
if (ptr == IntPtr.Zero) {
throw new Exception("Could not find eyelid_start_time!");
}
relativePosition = (int)((long)ptr - (long)page.BaseAddress) + 4;
vars.eyelidStart = new MemoryWatcher<double>(new DeepPointer(
relativePosition + game.ReadValue<int>(ptr)
));
// Entity_Audio_Recording::play_or_stop()
// 83 BB ???????? 00 48 8B CB 74 0A
// do_focus_mode_left_mouse_press()
// 8B 05 ???????? 85 C0 74 5B
Func<int, int, DeepPointer> createPointer = (int puzzle, int offset) => {
return new DeepPointer(basePointer, 0x18, (puzzle-1)*8, offset);
};
// First panel in the game
int panelType = createPointer(0x65, 0x8).Deref<int>(game);
if (panelType == 0) {
throw new Exception("Couldn't find panel type!");
}
print("Panel type: 0x"+panelType.ToString("X"));
vars.addPanel = (Action<int, int, int>)((int panel, int maxSolves, int offset) => {
print("<256>");
if (!vars.panels.ContainsKey(panel)) {
print("<258>");
int type = createPointer(panel, 0x8).Deref<int>(game);
print("<260>");
if (type == panelType) {
print("<262>");
vars.panels[panel] = new Tuple<int, int, DeepPointer>(
0, // Number of times solved
maxSolves, // Number of times to split
createPointer(panel, offset)
);
}
}
});
vars.panels = new Dictionary<int, Tuple<int, int, DeepPointer>>();
vars.keepWatchers = new MemoryWatcherList();
vars.multiWatchers = new MemoryWatcherList();
vars.obeliskWatchers = new MemoryWatcherList();
if (settings["Split on environmental patterns"]) {
foreach (int obelisk in vars.obelisks) {
vars.obeliskWatchers.Add(new MemoryWatcher<int>(createPointer(obelisk, obeliskOffset)));
}
}
vars.initPuzzles = (Action)(() => {
vars.epCount = 0;
foreach (var watcher in vars.obeliskWatchers) vars.epCount += watcher.Current;
print("Loaded with EP count: "+vars.epCount);
vars.panels.Clear();
if (settings["Split on all panels (solving and non-solving)"]) {
// Multi-panels use the solved offset, since they need to be solved every time you exit them
foreach (var panel in vars.multiPanels) vars.addPanel(panel, 9999, vars.solvedOffset);
foreach (var panel in vars.keepWalkOns) {
vars.keepWatchers.Add(new MemoryWatcher<int>(createPointer(panel, vars.solvedOffset)));
}
vars.keepWatchers.UpdateAll(game);
foreach (var panel in vars.multipanel) {
vars.addPanel(panel, 0, vars.solvedOffset);
vars.multiWatchers.Add(new MemoryWatcher<int>(createPointer(panel, vars.completedOffset)));
}
// Boat speed panel should never split, it's too inconsistent
vars.addPanel(0x34C80, 0, vars.completedOffset);
// Cinema input panel unsolves itself the first time
vars.addPanel(0x00816, 0, vars.solvedOffset);
} else {
// Individual panels use the completed offset, since they just need to be completed the first time you exit them
if (settings["Split on lasers"]) {
foreach (var laser in vars.lasers) {
vars.addPanel(laser, 1, vars.completedOffset);
}
}
if (settings["Split on tutorial door"]) {
vars.addPanel(0x0362A, 1, vars.completedOffset);
}
if (settings["Split when starting the boat"]) {
vars.addPanel(0x34D97, 1, vars.completedOffset);
}
if (settings["Split on greenhouse elevator"]) {
vars.addPanel(0x0A07A, 1, vars.completedOffset);
}
if (settings["Split on mountain elevator"]) {
vars.addPanel(0x09EEC, 1, vars.completedOffset);
}
if (settings["Split on final elevator"]) {
vars.addPanel(0x3D9AA, 1, vars.completedOffset);
}
}
if (settings["Split on challenge end"]) {
vars.addPanel(0x1C31A, 0, vars.solvedOffset); // Right pillar
vars.addPanel(0x1C31B, 0, vars.solvedOffset); // Left pillar
}
});
vars.initPuzzles();
}
update {
if (vars.panels == null) return false; // Init not yet done
// Don't run if the game is loading / paused
vars.time.Update(game);
if (vars.time.Current <= vars.time.Old) return false;
vars.puzzle.Update(game);
vars.gameFrames.Update(game);
// Separated out to handle manual resets
if (vars.gameFrames.Current == 0) vars.startTime = 0.0;
vars.playerMoving.Update(game);
vars.challengeActive.Update(game);
vars.mountainDoor.Update(game);
vars.movie.Update(game);
vars.eyelidStart.Update(game);
vars.keepWatchers.UpdateAll(game);
vars.obeliskWatchers.UpdateAll(game);
}
isLoading {
return true; // Disable gameTime approximation
}
gameTime {
return TimeSpan.FromSeconds(vars.time.Current - vars.startTime);
}
reset {
if (vars.gameFrames.Old != 0 && vars.gameFrames.Current == 0) {
return true;
}
if (settings["Reset on challenge stop"]) {
if (vars.challengeActive.Old == 1.0 && vars.challengeActive.Current == 0.0) {
return true;
}
}
}
start {
if (vars.startTime == 0.0) {
if (vars.playerMoving.Old == 0 && vars.playerMoving.Current == 1) {
vars.startTime = vars.time.Current;
vars.initPuzzles();
return true;
}
}
if (settings["Start/split on challenge start"]) {
if (vars.challengeActive.Old == 0.0 && vars.challengeActive.Current == 1.0) {
vars.startTime = vars.time.Current;
vars.initPuzzles();
return true;
}
}
}
split {
if (vars.puzzle.Old == 0 && vars.puzzle.Current != 0) {
int panel = vars.puzzle.Current;
vars.activePanel = panel;
print("Started panel 0x"+panel.ToString("X"));
if (!vars.panels.ContainsKey(panel) && settings["Split on all panels (solving and non-solving)"]) {
print("Encountered new panel 0x"+panel.ToString("X"));
vars.addPanel(panel, 1, vars.solvedOffset);
print(""+vars.panels[panel]);
}
}
if (vars.activePanel != 0) {
int panel = vars.activePanel;
if (!vars.panels.ContainsKey(panel)) {
vars.activePanel = 0;
return false;
}
var puzzleData = vars.panels[panel];
int state = puzzleData.Item3.Deref<int>(game);
// Valid states:
// 0: Unsolved
// 1: Solved correctly
// 2: Solved incorrectly
// 3: Exited
// 4: Pending negation
// 5: Floor Meta Subpanel error
if (state == 1 || state == 4) {
vars.panels[panel] = new Tuple<int, int, DeepPointer>(
puzzleData.Item1 + 1, // Solve count
puzzleData.Item2, // Maximum split count
puzzleData.Item3 // State pointer
);
print("Panel 0x" + panel.ToString("X") + " has been solved " + vars.panels[panel].Item1+ " of "+puzzleData.Item2 + " time(s)");
vars.activePanel = 0;
if (settings["Split on challenge end"]) {
if (vars.panels[0x1C31A].Item1 == 1 && vars.panels[0x1C31B].Item1 == 1) {
return true;
}
}
if (puzzleData.Item1 < puzzleData.Item2) { // Split fewer times than the max
return true;
}
} else if (state != 0) {
print("Panel 0x" + panel.ToString("X") + " exited in state " + state);
vars.activePanel = 0;
}
}
if (settings["Split on all panels (solving and non-solving)"]) {
// Challenge starting panel unsolves itself
if (vars.challengeActive.Old == 0.0 && vars.challengeActive.Current == 1.0) {
print("Started the challenge");
return true;
}
if (vars.movie.Old != vars.movie.Current) {
print(vars.movie.Old+" "+vars.movie.Current);
}
// Cinema starting panel unsolves itself
if (vars.movie.Old != vars.movie.Current) {
if (vars.movie.Old == 2070 || vars.movie.Current == 2070) {
return false; // Initialization
}
if (vars.movie.Current == 0) {
return false; // Movie ending
}
print("Started movie 0x"+vars.movie.Current.ToString("X"));
return true;
}
// Keep panels don't trigger nicely
for (int i=0; i<vars.keepWatchers.Count; i++) {
var panel = vars.keepWatchers[i];
if (panel.Old == 0 && panel.Current == 1) {
string color = new List<string>{"Yellow", "Purple", "Green", "Blue"}[i];
print(color + " keep panel has been solved");
return true;
}
}
// Avoid duplication for multipanel
for (int i=0; i<vars.multiWatchers.Count; i++) {
var panel = vars.multiWatchers[i];
if (panel.Old == 0 && panel.Current == 1) {
print("Completed multipanel "+i);
return true;
}
}
}
if (settings["Split when completing the first mountain floor"]) {
// Increases gradually from 0 to 1
if (vars.mountainDoor.Old == 0.0 && vars.mountainDoor.Current > 0.0) {
print("Mountain floor 1 door started opening");
return true;
}
}
if (vars.challengeActive.Old == 0.0 && vars.challengeActive.Current == 1.0) {
if (settings["Split on challenge end"]) {
vars.panels[0x1C31A] = new Tuple<int, int, DeepPointer>(
0,
vars.panels[0x1C31A].Item2,
vars.panels[0x1C31A].Item3
);
vars.panels[0x1C31B] = new Tuple<int, int, DeepPointer>(
0,
vars.panels[0x1C31B].Item2,
vars.panels[0x1C31B].Item3
);
}
if (settings["Start/split on challenge start"]) {
print("Started the challenge");
return true;
}
}
if (settings["Split on environmental patterns"]) {
int epCount = 0;
foreach (var watcher in vars.obeliskWatchers) epCount += watcher.Current;
if (epCount > vars.epCount) {
print("Solved EP #" + epCount);
vars.epCount = epCount;
return true;
}
}
if (settings["Split on easter egg ending"]) {
if (vars.eyelidStart.Old == -1 && vars.eyelidStart.Current > 0) {
return true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment