FransBouma / InjectableGenericCameraSystem

This is a generic camera system to be used as the base for cameras for taking screenshots within games. The main purpose of the system is to hijack the in-game 3D camera by overwriting values in its camera structure with our own values so we can control where the camera is located, it's pitch/yaw/roll values, its FoV and the camera's look vector.
BSD 2-Clause "Simplified" License
715 stars 249 forks source link

A better camera for dishonored 2 and potentially dishonored doto #83

Closed fakuivan closed 5 years ago

fakuivan commented 5 years ago

Camera properties: I've seen this take two rcx addresses on the menu screen.

Dishonored2.exe+A0F8D0 - 40 53                 - push rbx
Dishonored2.exe+A0F8D2 - 48 83 EC 70           - sub rsp,70 { 112 }
Dishonored2.exe+A0F8D6 - 8B 02                 - mov eax,[rdx]
Dishonored2.exe+A0F8D8 - 0F57 C9               - xorps xmm1,xmm1
Dishonored2.exe+A0F8DB - 48 8B D9              - mov rbx,rcx
Dishonored2.exe+A0F8DE - 89 81 A8000000        - mov [rcx+000000A8],eax { Camera x write }
Dishonored2.exe+A0F8E4 - 8B 42 04              - mov eax,[rdx+04]
Dishonored2.exe+A0F8E7 - 4C 8D 4C 24 40        - lea r9,[rsp+40]
Dishonored2.exe+A0F8EC - 89 81 AC000000        - mov [rcx+000000AC],eax { Camera y write }
Dishonored2.exe+A0F8F2 - 8B 42 08              - mov eax,[rdx+08]
Dishonored2.exe+A0F8F5 - 89 81 B0000000        - mov [rcx+000000B0],eax { Camera vertical position write }
Dishonored2.exe+A0F8FB - 8B 02                 - mov eax,[rdx]
Dishonored2.exe+A0F8FD - 89 81 C0000000        - mov [rcx+000000C0],eax { Duplicated coords }
Dishonored2.exe+A0F903 - 8B 42 04              - mov eax,[rdx+04]
Dishonored2.exe+A0F906 - 89 81 C4000000        - mov [rcx+000000C4],eax { ... }
Dishonored2.exe+A0F90C - 8B 42 08              - mov eax,[rdx+08]
Dishonored2.exe+A0F90F - 89 81 C8000000        - mov [rcx+000000C8],eax { ... }
Dishonored2.exe+A0F915 - 41 8B 00              - mov eax,[r8]
Dishonored2.exe+A0F918 - 89 81 CC000000        - mov [rcx+000000CC],eax { mat 0,0 write }
Dishonored2.exe+A0F91E - 41 8B 40 04           - mov eax,[r8+04]
Dishonored2.exe+A0F922 - 89 81 D0000000        - mov [rcx+000000D0],eax { ... }
Dishonored2.exe+A0F928 - 41 8B 40 08           - mov eax,[r8+08]
Dishonored2.exe+A0F92C - 89 81 D4000000        - mov [rcx+000000D4],eax
Dishonored2.exe+A0F932 - 41 8B 40 0C           - mov eax,[r8+0C]
Dishonored2.exe+A0F936 - 89 81 D8000000        - mov [rcx+000000D8],eax
Dishonored2.exe+A0F93C - 41 8B 40 10           - mov eax,[r8+10]
Dishonored2.exe+A0F940 - 89 81 DC000000        - mov [rcx+000000DC],eax
Dishonored2.exe+A0F946 - 41 8B 40 14           - mov eax,[r8+14]
Dishonored2.exe+A0F94A - 89 81 E0000000        - mov [rcx+000000E0],eax
Dishonored2.exe+A0F950 - 41 8B 40 18           - mov eax,[r8+18]
Dishonored2.exe+A0F954 - 89 81 E4000000        - mov [rcx+000000E4],eax
Dishonored2.exe+A0F95A - 41 8B 40 1C           - mov eax,[r8+1C]
Dishonored2.exe+A0F95E - 89 81 E8000000        - mov [rcx+000000E8],eax
Dishonored2.exe+A0F964 - 41 8B 40 20           - mov eax,[r8+20]
Dishonored2.exe+A0F968 - 89 81 EC000000        - mov [rcx+000000EC],eax { mat 3,3 write }
Dishonored2.exe+A0F96E - C6 81 C0390000 01     - mov byte ptr [rcx+000039C0],01 { 1 }
Dishonored2.exe+A0F975 - 48 8B 0D 3CE6C801     - mov rcx,[Dishonored2.exe+269DFB8] { [2985DB55BB0] }
Dishonored2.exe+A0F97C - F3 41 0F10 40 08      - movss xmm0,[r8+08]
Dishonored2.exe+A0F982 - F3 41 0F10 50 04      - movss xmm2,[r8+04]
Dishonored2.exe+A0F988 - F3 41 0F10 20         - movss xmm4,[r8]
Dishonored2.exe+A0F98D - 0F14 D1               - unpcklps xmm2,xmm1
Dishonored2.exe+A0F990 - 0F14 E0               - unpcklps xmm4,xmm0
Dishonored2.exe+A0F993 - 48 8D 05 D6454C01     - lea rax,[Dishonored2.exe+1ED3F70] { ["player"] }
Dishonored2.exe+A0F99A - F3 41 0F10 40 20      - movss xmm0,[r8+20]
Dishonored2.exe+A0F9A0 - F3 41 0F10 58 18      - movss xmm3,[r8+18]
Dishonored2.exe+A0F9A6 - 0F14 E2               - unpcklps xmm4,xmm2

As for pausing the game, the game has a simulation timescale value that when set to 0 all entity updating is suspended (including the code that writes to the camera values). To capture this value I used an AOB injection script on cheat engine, this is what it looks like:

{ Game   : Dishonored2.exe
  Version: 
  Date   : 2018-12-26
  Author : fakui

  This script captures the "engine timings" struct
}

[ENABLE]

aobscanmodule(engine_timings_hook,Dishonored2.exe,F2 0F 11 4B 18 F2 0F 5C) // should be unique
alloc(newmem,$1000,"Dishonored2.exe"+7DFEE)

label(code)
label(return)
label(EngineTimings)
registersymbol(EngineTimings)

newmem:
  mov [EngineTimings],rbx
  jmp code

EngineTimings:
  dq 0

code:
  movsd [rbx+18],xmm1
  jmp return

engine_timings_hook:
  jmp newmem
return:
registersymbol(engine_timings_hook)

[DISABLE]

engine_timings_hook:
  db F2 0F 11 4B 18

unregistersymbol(engine_timings_hook)
unregistersymbol(EngineTimings)
dealloc(newmem)

{
// ORIGINAL CODE - INJECTION POINT: "Dishonored2.exe"+7DFEE

"Dishonored2.exe"+7DFB9: 48 89 44 24 38              -  mov [rsp+38],rax
"Dishonored2.exe"+7DFBE: FF 15 1C 66 BC 01           -  call qword ptr [Dishonored2.exe+1C445E0]
"Dishonored2.exe"+7DFC4: 48 8B 44 24 30              -  mov rax,[rsp+30]
"Dishonored2.exe"+7DFC9: 0F 57 C9                    -  xorps xmm1,xmm1
"Dishonored2.exe"+7DFCC: 0F 57 C0                    -  xorps xmm0,xmm0
"Dishonored2.exe"+7DFCF: F2 48 0F 2A 05 F0 42 64 02  -  cvtsi2sd xmm0,[Dishonored2.exe+26C22C8]
"Dishonored2.exe"+7DFD8: 48 2B 05 F1 42 64 02        -  sub rax,[Dishonored2.exe+26C22D0]
"Dishonored2.exe"+7DFDF: F2 48 0F 2A C8              -  cvtsi2sd xmm1,rax
"Dishonored2.exe"+7DFE4: F2 0F 5E C8                 -  divsd xmm1,xmm0
"Dishonored2.exe"+7DFE8: F2 0F 58 4C 24 38           -  addsd xmm1,qword ptr [rsp+38]
// ---------- INJECTING HERE ----------
"Dishonored2.exe"+7DFEE: F2 0F 11 4B 18              -  movsd [rbx+18],xmm1
// ---------- DONE INJECTING  ----------
"Dishonored2.exe"+7DFF3: F2 0F 5C 4B 08              -  subsd xmm1,[rbx+08]
"Dishonored2.exe"+7DFF8: 66 0F 5A C1                 -  cvtpd2ps xmm0,xmm1
"Dishonored2.exe"+7DFFC: F3 0F 11 44 24 30           -  movss [rsp+30],xmm0
"Dishonored2.exe"+7E002: 8B 44 24 30                 -  mov eax,[rsp+30]
"Dishonored2.exe"+7E006: C7 44 24 30 00 00 00 00     -  mov [rsp+30],00000000
"Dishonored2.exe"+7E00E: 89 43 28                    -  mov [rbx+28],eax
"Dishonored2.exe"+7E011: 8B 44 24 30                 -  mov eax,[rsp+30]
"Dishonored2.exe"+7E015: C7 44 24 30 00 00 00 00     -  mov [rsp+30],00000000
"Dishonored2.exe"+7E01D: 89 43 78                    -  mov [rbx+78],eax
"Dishonored2.exe"+7E020: 8B 44 24 30                 -  mov eax,[rsp+30]
}

However this code is not executed on the menu screen. We should look for a more reliable one.

The time scale variable can now be accessed at offset 0x110 from EngineTimings

fakuivan commented 5 years ago

I forgot to mention, the version of the game is 1.77.9.0, Dishonored2.exe's sha256 hash is F90068D43402DB520F7EC2AE6B95FB827140ECC7D3B846B83FBCB84EF933ADFB. The update without denuvo

fakuivan commented 5 years ago

Update: The address for the cvar g_stopTime is Dishonored2.exe+3BF9A98. Setting it to != 0 will pause the game, sadly there's no code that accesses the cvar g_freezeTime.

I was able to find and extract all of the addresses for the cvars in this game using a script that would hook the command buffer read (there's no dev console functionality on the release version), to later write arbitrary commands. The listCvars command at some point access an array with pointers to all the cvar structs on the game. I compiled a list with the cvars' name, default value, description, address of the struct and the address to the integer representation of the value here https://gist.github.com/fakuivan/8910a208fc88f831f06b047d0955e4fe . This list lacks 4-7 cvars that got trimmed because I wasn't able to determine the size of the cvar list programmatically (and I don't like counting offsets).

I hope this helps

fakuivan commented 5 years ago

These are the offsets for the focal distance (I'm not sure what it means from an optics perspective to have two different focal points, but they might be weird FOVxy translations), camera coords and rotation matrix: image These offsets are relative to the address contained in rcx at Dishonored2.exe+A0F8D0. These values won't be overwritten when g_stopTime is set to a non-zero value.

fakuivan commented 5 years ago

I found an instruction that will reliably access the camera struct (at a -0x30 offset from the first proof of concept). Even on the main menu! Also the above "different focal distances for each axis" nonsense turned out to be the FOVs in radians.

This is the injection script for cheat engine:

{ Game   : Dishonored2.exe
  Version:
  Date   : 2019-01-13
  Author : fakui

  This script does blah blah blah
}

[ENABLE]

aobscanmodule(hook_reliable_camera_struct,Dishonored2.exe,48 8B 71 10 48 89 BC 24 80 00 00 00) // should be unique
alloc(newmem,$1000,"Dishonored2.exe"+96F064)

label(code)
label(return)
label(CameraStruct)
registersymbol(CameraStruct)

newmem:
  mov [CameraStruct],rcx
  jmp code

CameraStruct:
  dq 0

code:
  mov rsi,[rcx+10]
  mov [rsp+00000080],rdi
  jmp return

hook_reliable_camera_struct:
  jmp newmem
  nop
  nop
  nop
  nop
  nop
  nop
  nop
return:
registersymbol(hook_reliable_camera_struct)

[DISABLE]

hook_reliable_camera_struct:
  db 48 8B 71 10 48 89 BC 24 80 00 00 00

unregistersymbol(hook_reliable_camera_struct)
unregistersymbol(CameraStruct)
dealloc(newmem)

{
// ORIGINAL CODE - INJECTION POINT: "Dishonored2.exe"+96F064

"Dishonored2.exe"+96F040: 48 85 C0                 -  test rax,rax
"Dishonored2.exe"+96F043: 74 06                    -  je Dishonored2.exe+96F04B
"Dishonored2.exe"+96F045: 48 83 C0 E0              -  add rax,-20
"Dishonored2.exe"+96F049: EB 03                    -  jmp Dishonored2.exe+96F04E
"Dishonored2.exe"+96F04B: 49 8B C7                 -  mov rax,r15
"Dishonored2.exe"+96F04E: 48 8D 69 30              -  lea rbp,[rcx+30]
"Dishonored2.exe"+96F052: 48 83 C0 30              -  add rax,30
"Dishonored2.exe"+96F056: 48 3B C5                 -  cmp rax,rbp
"Dishonored2.exe"+96F059: 0F 85 A0 01 00 00        -  jne Dishonored2.exe+96F1FF
"Dishonored2.exe"+96F05F: 48 89 74 24 78           -  mov [rsp+78],rsi
// ---------- INJECTING HERE ----------
"Dishonored2.exe"+96F064: 48 8B 71 10              -  mov rsi,[rcx+10]
"Dishonored2.exe"+96F068: 48 89 BC 24 80 00 00 00  -  mov [rsp+00000080],rdi
// ---------- DONE INJECTING  ----------
"Dishonored2.exe"+96F070: 48 8D B9 28 4F 02 00     -  lea rdi,[rcx+00024F28]
"Dishonored2.exe"+96F077: 48 85 F6                 -  test rsi,rsi
"Dishonored2.exe"+96F07A: 74 46                    -  je Dishonored2.exe+96F0C2
"Dishonored2.exe"+96F07C: 48 8B 06                 -  mov rax,[rsi]
"Dishonored2.exe"+96F07F: 48 8B CE                 -  mov rcx,rsi
"Dishonored2.exe"+96F082: FF 10                    -  call qword ptr [rax]
"Dishonored2.exe"+96F084: 8B 48 60                 -  mov ecx,[rax+60]
"Dishonored2.exe"+96F087: 3B 0D A3 80 2B 03        -  cmp ecx,[Dishonored2.exe+3C27130]
"Dishonored2.exe"+96F08D: 7C 33                    -  jl Dishonored2.exe+96F0C2
"Dishonored2.exe"+96F08F: 3B 0D 9F 80 2B 03        -  cmp ecx,[Dishonored2.exe+3C27134]
}

and the new offsets (nudged back 0x30):

image

FransBouma commented 5 years ago

Thanks for all the hard work! :) I don't have the game installed currently, so can't immediately jump in (moving systems as well) so it might take a while (if ever) that I pick up this info,

A remark about the FoVs: usually there are 2 indeed, one for horizontal and one for vertical viewing angle, but the game engines usually calculate these from a single FoV value + AR setting. The single FoV value is easier to work with (as you then just have to change 1 value to zoom in ;)).

fakuivan commented 5 years ago

It's ok, I'm planning on providing support myself. It'd be cool if you could point me to commits where you updated the code base for other games, in a similar way I'd need to do for this one, with the AOB scanner and RIP-relative address extractor.

FransBouma commented 5 years ago

Sorry for the late reply.

First I hope you've read my long article about this: https://weblogs.asp.net/fbouma/let-s-add-a-photo-mode-to-wolfenstein-ii-the-new-colossus-pc (it talks about how IGCS works)

What I always do is pick the latest camera source and copy that into a new folder, then remove the temp folders from VC++, rename the project in vs and go from there.

Here you have two choices: either the Assassin's Creed Odyssey camera or the Shadow of the Tombraider camera. I'd pick the Odyssey camera, as it has everything you need. The last 5-8 cameras have all the logic you need, and important bugfixes in e.g. hook setting.

Say you pick the odyssey camera, then the first thing you need to do the following:

From here, I always first disable all hook interception in InterceptorHelper (https://github.com/FransBouma/InjectableGenericCameraSystem/blob/master/Cameras/AssassinsCreedOdyssey/InjectableGenericCameraSystem/InterceptorHelper.cpp#L102 and https://github.com/FransBouma/InjectableGenericCameraSystem/blob/master/Cameras/AssassinsCreedOdyssey/InjectableGenericCameraSystem/InterceptorHelper.cpp#L108), and simply build a dll and inject it to see if the overlay works and the window title is picked up.

After that, first I'll see how many interceptions I need to add to the interceptor.asm file. All functions are defined here: https://github.com/FransBouma/InjectableGenericCameraSystem/blob/master/Cameras/AssassinsCreedOdyssey/InjectableGenericCameraSystem/InterceptorHelper.cpp#L41, all continue pointers are defined right below it. The AOB blocks are defined below that.

If you have questions, where what's done, let me know. The Dishonored2 camera I made for ansel has the right order of quaternion operations, so it should be easy to port.

FransBouma commented 5 years ago

Closed as no activity for a long time.