-
-
Save fifty-six/ceba4fdb4fe962c732b753c90748ff6b to your computer and use it in GitHub Desktop.
LiveSplit plugin for Sekiro: Shadows Die Twice
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Sekiro Speerunning Plugin by B3 | |
v1.2 | |
Features: | |
- Fixed timer implementation. | |
- Auto start | |
- No logo mod. | |
- No tutorial mod. | |
Patches: | |
23/04/19 v1.1 | |
- Compatibility for game version 1.03 | |
- fixed timer flickering. (thanks CapitaineToinon) | |
29/10/20 v1.2 | |
- Made IGT work for game version 1.06 (contributed by 56#1363) | |
If you have issues or questions message me (B3LYP#2159) | |
on the Sekiro Speedrunning Discord (https://discord.gg/DVXvRPu) | |
Technical: | |
This plugin modifies the in-game timer of Sekiro to make it run at the correct | |
speed independent of frame rate. The fix as C++ code for reference: | |
''' | |
void updateTimerOriginal(float frame_time){ | |
igt += static_cast<unsigned int>(frame_time); | |
} | |
void updateTimerNew(float frame_time){ | |
static float frac = 0.f; | |
frac += frame_time - static_cast<unsigned int>(frame_time); | |
if(frac >= 1.f){ | |
frame_time++; | |
frac--; | |
} | |
igt += static_cast<unsigned int>(frame_time); | |
} | |
''' | |
*/ | |
state("Sekiro", "1.02") | |
{ | |
int igt : 0x3B47CF0, 0x9C; | |
} | |
state("Sekiro", "1.03") | |
{ | |
int igt : 0x3B48D30, 0x9C; | |
} | |
state("Sekiro", "1.06") { | |
int igt : 0x3D5AAC0, 0x9C; | |
} | |
init | |
{ | |
/*We have to wait a little for SteamDRM to decrypt the exe. | |
This could potentially cause issues. Replace with code that | |
detects decryption if necessary | |
*/ | |
System.Threading.Thread.Sleep(3000); | |
vars.game_is_patched = false; | |
//variable used to cache igt | |
vars.internal_time = 0; | |
switch(modules.First().ModuleMemorySize){ | |
case 67727360: | |
version = "1.02"; | |
break; | |
case 67731456: | |
version = "1.03"; | |
break; | |
case 70066176: | |
version = "1.06"; | |
break; | |
default: | |
print("Unknown version detected"); | |
return false; | |
} | |
//TutorialMsgDialog (Tutorial popups that pause the game) | |
IntPtr TutorialMsgDialog = (IntPtr)0; | |
//CSTutorialDialog (Little noise making tutorial popups that come up on the left) | |
IntPtr CSTutorialDialog = (IntPtr)0; | |
//CSMenuTutorialDialog (Tutorial messages in the pause and warp menus) | |
IntPtr CSMenuTutorialDialog = (IntPtr)0; | |
//no logo | |
IntPtr noLogo = (IntPtr)0; //no logo mod | |
//timer mod | |
IntPtr igtFixEntryPoint = (IntPtr)0; | |
//cvttss2si rax, xmm0 <--- | |
//add [rcx+9Ch], eax ; timer_update | |
//mov rax, cs:qword_143B47CF0 | |
//cmp dword ptr [rax+9Ch], 0D693A018h | |
//jbe short loc_1407A8D41 | |
IntPtr igtFixCodeLoc = (IntPtr)0; //Start of TutorialMsgDialog constructor. This is dead code after applying the no-tut mod so the timer mod can be injected here | |
switch(version){ | |
case "1.02": | |
TutorialMsgDialog = (IntPtr)0x140DC7A8E; | |
CSTutorialDialog = (IntPtr)0x140D6EB98; | |
CSMenuTutorialDialog = (IntPtr)0x140D6D51C; | |
noLogo = (IntPtr)0x140DEBF2B; | |
igtFixEntryPoint = (IntPtr)0x1407A8D19; | |
igtFixCodeLoc = (IntPtr)0x140DBE2D0; | |
break; | |
case "1.03": | |
TutorialMsgDialog = (IntPtr)0x140DC83BE; | |
CSTutorialDialog = (IntPtr)0x140D6F4C8; | |
CSMenuTutorialDialog = (IntPtr)0x140D6DE4C; | |
noLogo = (IntPtr)0x140DEC85B; | |
igtFixEntryPoint = (IntPtr)0x1407A8D99; | |
igtFixCodeLoc = (IntPtr)0x140DBEC00; | |
break; | |
case "1.06": | |
igtFixEntryPoint = (IntPtr) 0x1407B1C89; | |
// I'm not sure how to find the rest of this, so it's as-is from 1.03 with the same offset between the igt entry point | |
// As a result, the tutorial skipping doesn't rn | |
TutorialMsgDialog = (IntPtr)0x140DC83BE + 0x8EF0; | |
CSTutorialDialog = (IntPtr)0x140D6F4C8 + 0x8EF0; | |
CSMenuTutorialDialog = (IntPtr)0x140D6DE4C + 0x8EF0; | |
noLogo = (IntPtr)0x140DEC85B + 0x8EF0; | |
igtFixCodeLoc = (IntPtr)0x140DBEC00 + 0x8EF0; | |
break; | |
default: | |
throw new NotImplementedException(); | |
} | |
//fix detour | |
var igtFixDetourCode = new List<byte>(){0xE9}; | |
int detourTarget = (int) (igtFixCodeLoc.ToInt64()-(igtFixEntryPoint.ToInt64()+5)); | |
igtFixDetourCode.AddRange(BitConverter.GetBytes(detourTarget)); | |
//fix body | |
var frac = game.AllocateMemory(sizeof(double)); | |
var igtFixCode = new List<byte>(){ | |
0x53, //push rbx | |
0x48, 0xBB //mov rbx, fracAddress | |
}; | |
igtFixCode.AddRange(BitConverter.GetBytes((long)frac)); | |
igtFixCode.AddRange(new byte[]{ | |
0x44, 0x0F, 0x10, 0xF0, //movups xmm14, xmm0 | |
0xF3, 0x45, 0x0F, 0x5A, 0xF6, //cvtss2sd xmm14, xmm14 | |
0xF2, 0x49, 0x0F, 0x2C, 0xC6, //cvttsd2si rax, xmm14 | |
0xF2, 0x4C, 0x0F, 0x2A, 0xF8, //cvtsi2sd xmm15, rax | |
0xF2, 0x45, 0x0F, 0x5C, 0xF7, //subsd xmm14, xmm15 | |
0x66, 0x44, 0x0F, 0x10, 0x3B, //movupd xmm15, [rbx] | |
0xF2, 0x45, 0x0F, 0x58, 0xFE, //addsd xmm15, xmm14 | |
0x66, 0x44, 0x0F, 0x11, 0x3B, //movupd [rbx], xmm15 | |
0xF2, 0x49, 0x0F, 0x2C, 0xC7, //cvttsd2si rax, xmm15 | |
0x48, 0x85, 0xC0, //test rax, rax | |
0x74, 0x1D, //jz +1D | |
0x90, 0x90, 0x90, 0x90, //nop | |
0xF2, 0x4C, 0x0F, 0x2A, 0xF0, //cvtsi2sd xmm14, rax | |
0xF2, 0x45, 0x0F, 0x5C, 0xFE, //subsd xmm15, xmm14 | |
0x66, 0x44, 0x0F, 0x11, 0x3B, //movupd [rbx], xmm15 | |
0xF2, 0x45, 0x0F, 0x5A, 0xF6, //cvtsd2ss xmm14, xmm14 | |
0xF3, 0x41, 0x0F, 0x58, 0xC6, //addss xmm0, xmm14 | |
0x45, 0x0F, 0x57, 0xF6, //xorps xmm14, xmm14 | |
0x45, 0x0F, 0x57, 0xFF, //xorps xmm15, xmm15 | |
0x5B, //pop rbx | |
0xF3, 0x48, 0x0F, 0x2C, 0xC0, //cvttss2si rax,xmm0 | |
0xE9 //jmp return igtFixEntryPoint +5 | |
}); | |
int jmpTarget = (int)((igtFixEntryPoint.ToInt64()+5)-(igtFixCodeLoc.ToInt64()+103+5)); | |
igtFixCode.AddRange(BitConverter.GetBytes(jmpTarget)); | |
//Write fixes to game memory | |
game.Suspend(); | |
//No Tutorials | |
vars.game_is_patched = true; | |
vars.game_is_patched &= game.WriteBytes(TutorialMsgDialog,new byte[] {0x75}); | |
vars.game_is_patched &= game.WriteBytes(CSTutorialDialog, new byte[] {0x75}); | |
vars.game_is_patched &= game.WriteBytes(CSMenuTutorialDialog, new byte[] {0x75}); | |
//No logo | |
vars.game_is_patched &= game.WriteBytes(noLogo, new byte[] {0x75}); | |
//No broken timer | |
vars.game_is_patched &= game.WriteBytes(igtFixCodeLoc, igtFixCode.ToArray()); | |
vars.game_is_patched &= game.WriteBytes(igtFixEntryPoint, igtFixDetourCode.ToArray()); | |
game.Resume(); | |
if(vars.game_is_patched != true){ | |
print("Error encountered while writing patch to game memory"); | |
return false; | |
} | |
return true; | |
} | |
update{ | |
//only update if the game was succesfully patched | |
return vars.game_is_patched; | |
} | |
start{ | |
if(old.igt == 0 && current.igt > 0){ | |
vars.internal_time = 0; | |
return true; | |
} | |
} | |
isLoading{ | |
return true; | |
} | |
gameTime | |
{ | |
if(/*current.igt < old.igt || */current.igt == 0){ | |
return TimeSpan.FromMilliseconds(vars.internal_time); | |
}else{ | |
vars.internal_time = current.igt; | |
return TimeSpan.FromMilliseconds(current.igt); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment