Skip to content

Instantly share code, notes, and snippets.

@fifty-six
Forked from pawREP/Sekiro.asl
Created November 29, 2020 21:28
Show Gist options
  • Save fifty-six/ceba4fdb4fe962c732b753c90748ff6b to your computer and use it in GitHub Desktop.
Save fifty-six/ceba4fdb4fe962c732b753c90748ff6b to your computer and use it in GitHub Desktop.
LiveSplit plugin for Sekiro: Shadows Die Twice
/*
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