Skip to content

Instantly share code, notes, and snippets.

@pawREP
Last active January 8, 2021 01:28
Show Gist options
  • Save pawREP/98c2828e7550caeb521d65c581f43a69 to your computer and use it in GitHub Desktop.
Save pawREP/98c2828e7550caeb521d65c581f43a69 to your computer and use it in GitHub Desktop.
LiveSplit plugin for Sekiro: Shadows Die Twice
/*
Sekiro Speerunning Plugin by B3
v1.1
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)
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;
}
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;
//print(modules.First().ModuleMemorySize.ToString());
switch(modules.First().ModuleMemorySize){
case 67727360:
version = "1.02";
break;
case 67731456:
version = "1.03";
break;
default:
print("Unkown 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;
}
//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);
}
}
@pawREP
Copy link
Author

pawREP commented Jan 8, 2021

Great, go ahead and change the URL in the XML to your fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment