diff --git a/hooks.asm b/hooks.asm new file mode 100644 index 0000000..310db64 --- /dev/null +++ b/hooks.asm @@ -0,0 +1,701 @@ +INCLUDE include\master.inc + +; Local offset supplements +APLAYER_CurrentNetSpeed EQU 038h +UNETCONN_InternalAck EQU 0C8h ; bool — estimated, verify against dump +AFPCA_OverriddenBackpackSize EQU 0720h + +.data? + +pFn_PlayButton QWORD ? ; cached UFunction* for ReadyToStartMatch +pFn_ServerCheat QWORD ? ; unused in non-CHEATS build; kept for ABI compat + +.const + +szPlayButtonFn DB "Function FortniteGame.FortGameModeAthena.ReadyToStartMatch", 0 + +.code + +; Hooks_NetDebug(UObject* self) -> UObject* (always nullptr) +; Replaces the debug logging function; returns null to suppress output. +Hooks_NetDebug PROC + xor eax, eax + ret +Hooks_NetDebug ENDP + +; Hooks_CollectGarbage(__int64 a1) -> __int64 +; Suppresses GC during match to prevent actor deletion bugs. +Hooks_CollectGarbage PROC + xor eax, eax + ret +Hooks_CollectGarbage ENDP + +; Hooks_GetNetMode(UWorld* World) -> int (ENetMode) +; Returns NM_ListenServer (2) so the engine treats this as a listen server. +Hooks_GetNetMode PROC + mov eax, ENetMode_ListenServer ; 2 + ret +Hooks_GetNetMode ENDP + +; Hooks_KickPlayer(__int64 Session, __int64 PC, __int64 Reason) -> char +; Blocks all kick requests (returns 0 = no-op). +Hooks_KickPlayer PROC + xor eax, eax + ret +Hooks_KickPlayer ENDP + +; Hooks_LocalPlayerSpawnPlayActor — tail-call or early return +; void* Hooks_LocalPlayerSpawnPlayActor(ULocalPlayer*, FString*, FString*, UWorld*) +; If not yet traveled: tail-call original (all regs untouched). +; If already traveled: return 1 (suppress duplicate actor spawns). +; +; Stack: no frame — either ret-1 or jmp to original (tail call). +Hooks_LocalPlayerSpawnPlayActor PROC + movzx eax, BYTE PTR [bTraveled] + test al, al + jnz @@already_traveled + + ; Not traveled yet — forward all args unchanged via tail call + jmp QWORD PTR [Native_LocalPlayer_SpawnPlayActor] + +@@already_traveled: + mov eax, 1 + ret +Hooks_LocalPlayerSpawnPlayActor ENDP + +; Hooks_WelcomePlayer(UWorld*, UNetConnection*) +; Redirects to GetWorld() + Native call. +; Args: RCX=UWorld* (ignored), RDX=UNetConnection* +; +; Stack: push rbp + push rbx = 2 pushes (RSP=8->RSP=8 after 2×8); sub 40(=8) -> 0 +Hooks_WelcomePlayer PROC + push rbp + push rbx + sub rsp, 40 ; 2 pushes: RSP=8; sub40(=8) -> 0 + + mov rbx, rdx ; rbx = Connection + + call SDK_GetWorld + test rax, rax + jz @@done + + mov rcx, rax ; World + mov rdx, rbx ; Connection + call QWORD PTR [Native_World_WelcomePlayer] + +@@done: + add rsp, 40 + pop rbx + pop rbp + ret +Hooks_WelcomePlayer ENDP + +; Hooks_World_NotifyControlMessage(UWorld*, UNetConnection*, uint8, void*) +; Replaces world arg with GetWorld(); passes remaining args through. +; Args: RCX=UWorld*, RDX=Connection, R8B=MsgType, R9=Bunch +; +; Stack: push rbp,rbx,rsi,rdi = 4 pushes (RSP=8->RSP=8after4x8); +; sub 40(=8) -> 0 +Hooks_World_NotifyControlMessage PROC + push rbp + push rbx + push rsi + push rdi + sub rsp, 40 ; 4 pushes: RSP=8; sub40 -> 0 + + mov rbx, rdx ; Connection + movzx esi, r8b ; MsgType (zero-extended) + mov rdi, r9 ; Bunch + + call SDK_GetWorld + test rax, rax + jz @@done + + mov rcx, rax + mov rdx, rbx + mov r8d, esi + mov r9, rdi + call QWORD PTR [Native_World_NotifyControlMessage] + +@@done: + add rsp, 40 + pop rdi + pop rsi + pop rbx + pop rbp + ret +Hooks_World_NotifyControlMessage ENDP + +; Hooks_Beacon_NotifyAcceptingConnection(AOnlineBeacon*) -> uint8 +; Beacon is accepting a new peer — forward to World's handler. +; Args: RCX = AOnlineBeacon* +; +; Stack: push rbp + push rbx = 2 pushes; sub 40 -> 0 +Hooks_Beacon_NotifyAcceptingConnection PROC + push rbp + push rbx + sub rsp, 40 ; 2 pushes: RSP=8; sub40 -> 0 + + call SDK_GetWorld + test rax, rax + jz @@ret_zero + + mov rcx, rax + call QWORD PTR [Native_World_NotifyAcceptingConnection] + jmp @@done + +@@ret_zero: + xor eax, eax + +@@done: + add rsp, 40 + pop rbx + pop rbp + ret +Hooks_Beacon_NotifyAcceptingConnection ENDP + +; Hooks_SeamlessTravelHandlerForWorld(UEngine*, UWorld*) +; Args: RCX=Engine, RDX=UWorld* (ignored — use GetWorld()) +; +; Stack: push rbp + push rbx = 2 pushes; sub 40 -> 0 +Hooks_SeamlessTravelHandlerForWorld PROC + push rbp + push rbx + sub rsp, 40 ; 2 pushes; sub 40 -> 0 + + mov rbx, rcx ; Engine + + call SDK_GetWorld + test rax, rax + jz @@done + + mov rcx, rbx + mov rdx, rax + call QWORD PTR [Native_Engine_SeamlessTravelHandlerForWorld] + +@@done: + add rsp, 40 + pop rbx + pop rbp + ret +Hooks_SeamlessTravelHandlerForWorld ENDP + +; Hooks_PostRender(UGameViewportClient*, UCanvas*) +; Runs GUI overlay, then calls original PostRender. +; Args: RCX=GVC, RDX=Canvas +; +; Stack: push rbp,rbx,rsi = 3 pushes (RSP=0 after 3×8); +; sub 32(=0) -> 0 +Hooks_PostRender PROC + push rbp + push rbx + push rsi + sub rsp, 32 ; 3 pushes: RSP=0; sub32(=0) -> 0 + + mov rbx, rcx ; GVC + mov rsi, rdx ; Canvas + + ; ZeroGUI_SetupCanvas(Canvas) + mov rcx, rsi + call ZeroGUI_SetupCanvas + + ; GUI_Tick() + call GUI_Tick + + ; Original PostRender(GVC, Canvas) + mov rcx, rbx + mov rdx, rsi + call QWORD PTR [Native_GameViewportClient_PostRender] + + add rsp, 32 + pop rsi + pop rbx + pop rbp + ret +Hooks_PostRender ENDP + +; Hooks_TickFlush(UNetDriver*, float DeltaSeconds) +; If the server has active client connections and a replication driver, +; calls ServerReplicateActors before the original TickFlush. +; +; Args: RCX=NetDriver, XMM1=DeltaSeconds (float) +; +; Stack: push rbp,rbx = 2 pushes (RSP=8 after 2×8); sub 40(=8) -> 0 +; [rsp+32] — spill slot for DeltaSeconds (REAL4) +Hooks_TickFlush PROC + push rbp + push rbx + sub rsp, 40 ; 2 pushes: RSP=8; sub 40 -> 0 + + mov rbx, rcx ; NetDriver* + movd DWORD PTR [rsp + 32], xmm1 ; spill DeltaSeconds + + ; NetDriver null check + test rbx, rbx + jz @@call_orig + + ; ClientConnections.Num() (TArray at +UNETDRIVER_ClientConns) + ; TArray layout: [Data QWORD][Num DWORD][Max DWORD] + ; Num is at base+8 + mov eax, DWORD PTR [rbx + UNETDRIVER_ClientConns + 8] + test eax, eax + jle @@call_orig + + ; ClientConnections[0] — dereference Data ptr + mov rax, QWORD PTR [rbx + UNETDRIVER_ClientConns] + mov rax, QWORD PTR [rax] ; first element + test rax, rax + jz @@call_orig + + ; InternalAck == false means a real player (not a loopback) + movzx ecx, BYTE PTR [rax + UNETCONN_InternalAck] + test cl, cl + jnz @@call_orig + + ; ReplicationDriver present? + mov rax, QWORD PTR [rbx + UNETDRIVER_ReplDriver] + test rax, rax + jz @@call_orig + + ; ServerReplicateActors(ReplicationDriver) + mov rcx, rax + call QWORD PTR [Native_ReplicationDriver_ServerReplicateActors] + +@@call_orig: + mov rcx, rbx + movd xmm1, DWORD PTR [rsp + 32] + call QWORD PTR [Native_NetDriver_TickFlush] + + add rsp, 40 + pop rbx + pop rbp + ret +Hooks_TickFlush ENDP + +; Hooks_GetPlayerViewPoint(APlayerController*, FVector*, FRotator*) +; ------------------------------------------------------------ +; On listen-server with active clients: copy pawn's location/rotation +; to the output pointers so the server-side camera tracks the pawn. +; Falls back to the original in all other cases. +; +; Args: RCX=PC, RDX=FVector* OutLoc, R8=FRotator* OutRot +; +; Stack: push rbp,rbx,rsi,rdi = 4 pushes (RSP=8 after 4×8); sub 40(=8) -> 0 +Hooks_GetPlayerViewPoint PROC + push rbp + push rbx + push rsi + push rdi + sub rsp, 40 ; 4 pushes: RSP=8; sub 40 -> 0 + + mov rbx, rcx ; PC + mov rsi, rdx ; OutLoc + mov rdi, r8 ; OutRot + + ; PC must be valid + test rbx, rbx + jz @@call_orig + + ; HostBeacon must be set (server is live) + cmp QWORD PTR [HostBeacon], 0 + je @@call_orig + + ; ClientConnections.Num() > 0 on World's NetDriver + +@@call_orig: + mov rcx, rbx + mov rdx, rsi + mov r8, rdi + call QWORD PTR [Native_PlayerController_GetPlayerViewPoint] + + add rsp, 40 + pop rdi + pop rsi + pop rbx + pop rbp + ret +Hooks_GetPlayerViewPoint ENDP + +; Hooks_SpawnPlayActor — spawn and set up new player controller +; Original: APlayerController* (*)(UWorld*, UPlayer*, ENetRole, FURL&, void*, FString&, uint8) +; We replace UWorld* with GetWorld() and wire up the resulting PC. +; +; Arg layout at hook entry (before any pushes): +; RCX = UWorld* (replaced with GetWorld()) +; RDX = UPlayer* NewPlayer +; R8 = ENetRole +; R9 = FURL* +; [RSP+40] = void* UniqueId (5th arg — see note) +; [RSP+48] = FString* Error (6th arg) +; [RSP+56] = uint8 NetIdx (7th arg) +; +; Note on stack-arg offsets: at entry RSP points to return addr. +; Caller placed arg5 at [caller_RSP+32] = [RSP+8+32] = [RSP+40]. +; +; Stack: push rbp,rbx,rsi,rdi,r12,r13,r14 = 7 pushes (RSP=0 after 7×8). +; sub 64(=0) -> 0 +; [rsp+0..31] = shadow for callees +; [rsp+32] = arg5 (UniqueId) for SpawnPlayActor call +; [rsp+40] = arg6 (Error) +; [rsp+48] = arg7 (NetIdx byte) +Hooks_SpawnPlayActor PROC + push rbp + push rbx + push rsi + push rdi + push r12 + push r13 + push r14 + sub rsp, 64 ; 7 pushes: RSP=0; sub64(=0) -> 0 + + ; Save register args (RCX=world ignored) + mov rbx, rdx ; NewPlayer (UPlayer*) + mov r12d, r8d ; ENetRole (32-bit enum) + mov r13, r9 ; FURL* + + ; At this point: return addr at [RSP + 7*8 + 64] = [RSP+120] + ; Caller's arg5 at [RSP + 120 + 8 + 32] = [RSP + 160] + ; Caller's arg6 at [RSP + 168] + ; Caller's arg7 at [RSP + 176] + mov rax, QWORD PTR [rsp + 160] ; UniqueId (void*) + mov QWORD PTR [rsp + 32], rax + mov rax, QWORD PTR [rsp + 168] ; Error (FString*) + mov QWORD PTR [rsp + 40], rax + movzx eax, BYTE PTR [rsp + 176] ; NetPlayerIndex (uint8) + mov BYTE PTR [rsp + 48], al + + ; Get the authoritative world + call SDK_GetWorld + test rax, rax + jz @@done + + mov r14, rax ; World + + ; Call Native_World_SpawnPlayActor(World, NewPlayer, NetRole, FURL, UniqueId, Error, Idx) + mov rcx, r14 + mov rdx, rbx + mov r8d, r12d + mov r9, r13 + ; [rsp+32..48] already set up above + call QWORD PTR [Native_World_SpawnPlayActor] + + test rax, rax + jz @@done + + mov rdi, rax ; rdi = resulting APlayerController* + + ; NewPlayer->PlayerController = result (UPlayer::PlayerController at +0x030) + mov QWORD PTR [rbx + 030h], rdi + + ; Game_Mode->LoadJoiningPlayer(PlayerController) + mov rcx, rdi + call GameModeBase_LoadPlayer + + ; PlayerController->OverriddenBackpackSize = 100 + mov DWORD PTR [rdi + AFPCA_OverriddenBackpackSize], BACKPACK_SIZE + + mov rax, rdi ; return PC + +@@done: + add rsp, 64 + pop r14 + pop r13 + pop r12 + pop rdi + pop rsi + pop rbx + pop rbp + ret +Hooks_SpawnPlayActor ENDP + +; Hooks_Beacon_NotifyControlMessage +; AOnlineBeaconHost* beacon, UNetConnection* Connection, +; uint8 MessageType, FBitReader* Bunch +; +; Dispatch by MessageType: +; 4 (NMT_Netspeed) — set Connection->CurrentNetSpeed = 30000 +; 5 (NMT_Login) — receive FStrings + UniqueId, then WelcomePlayer +; 15 (NMT_PCSwap) — call original Beacon_NotifyControlMessage +; default — call Native_World_NotifyControlMessage(GetWorld(), ...) +; +; Stack: push rbp,rbx,rsi = 3 pushes (RSP=0 after 3×8). +; sub 112(=0) -> 0 . +; [rsp+0..31] = shadow for callees +; [rsp+32..47] = FString OnlinePlatformName (16 bytes) +; [rsp+48..63] = FString ClientResponse (16 bytes) +; [rsp+64..103] = FUniqueNetIdRepl UniqueId (40 bytes) +; [rsp+104..111]= spare / align +Hooks_Beacon_NotifyControlMessage PROC + push rbp + push rbx + push rsi + sub rsp, 112 ; 3 pushes: RSP=0; sub112(=0) -> 0 + + ; Save args + ; RCX = Beacon (not needed after dispatch setup) + mov rbx, rdx ; Connection + movzx ebp, r8b ; MessageType (0..255) + mov rsi, r9 ; Bunch (FBitReader*) + + ; dispatch on MessageType + cmp ebp, NMT_Netspeed ; 4 + je @@case_netspeed + cmp ebp, NMT_Login ; 5 + je @@case_login + cmp ebp, NMT_PCSwap ; 15 + je @@case_pcswap + + ; Default: World::NotifyControlMessage(GetWorld(), Connection, Type, Bunch) + call SDK_GetWorld + test rax, rax + jz @@done + mov rcx, rax + mov rdx, rbx + mov r8d, ebp + mov r9, rsi + call QWORD PTR [Native_World_NotifyControlMessage] + jmp @@done + +@@case_netspeed: + ; Connection->CurrentNetSpeed = DEFAULT_NETSPEED (30000) + mov DWORD PTR [rbx + APLAYER_CurrentNetSpeed], DEFAULT_NETSPEED + jmp @@done + +@@case_login: + ; Zero-initialise the local FString and FUniqueNetIdRepl structs + ; FString str1 at [rsp+32..47] + xor eax, eax + mov QWORD PTR [rsp + 32], rax + mov QWORD PTR [rsp + 40], rax + ; FString str2 at [rsp+48..63] + mov QWORD PTR [rsp + 48], rax + mov QWORD PTR [rsp + 56], rax + ; FUniqueNetIdRepl at [rsp+64..103] — 40 bytes (5 QWORDs) + mov QWORD PTR [rsp + 64], rax + mov QWORD PTR [rsp + 72], rax + mov QWORD PTR [rsp + 80], rax + mov QWORD PTR [rsp + 88], rax + mov QWORD PTR [rsp + 96], rax + + ; ReceiveFString(Bunch, &OnlinePlatformName) + mov rcx, rsi + lea rdx, [rsp + 32] + call QWORD PTR [Native_NetConnection_ReceiveFString] + + ; ReceiveFString(Bunch, &ClientResponse) + mov rcx, rsi + lea rdx, [rsp + 48] + call QWORD PTR [Native_NetConnection_ReceiveFString] + + ; ReceiveUniqueIdRepl(Bunch, &UniqueId) + mov rcx, rsi + lea rdx, [rsp + 64] + call QWORD PTR [Native_NetConnection_ReceiveUniqueIdRepl] + + ; WelcomePlayer(GetWorld(), Connection) + call SDK_GetWorld + test rax, rax + jz @@done + mov rcx, rax + mov rdx, rbx + call QWORD PTR [Native_World_WelcomePlayer] + jmp @@done + +@@case_pcswap: + +@@done: + add rsp, 112 + pop rsi + pop rbx + pop rbp + ret +Hooks_Beacon_NotifyControlMessage ENDP + +; Hooks_ProcessEventHook(UObject*, UFunction*, void* Params) +; Intercepts all UFunction dispatches on this process. +; +; Flow: +; 1. If !bPlayButton: check if Function == ReadyToStartMatch; +; if match -> Game_Start + InitNetworkHooks. +; 2. If bTraveled: scan UFunctionHooks_ToHook_Data for a matching +; UFunction*; if found, call the corresponding ToCall handler. +; 3. Always call original ProcessEvent(Object, Function, Params). +; +; Args: RCX=Object, RDX=Function, R8=Params +; +; Stack: push rbp,rbx,rsi,rdi,r12,r13,r14 = 7 pushes (RSP=0 after 7×8). +; sub 32(=0) -> 0 . +Hooks_ProcessEventHook PROC + push rbp + push rbx + push rsi + push rdi + push r12 + push r13 + push r14 + sub rsp, 32 ; 7 pushes: RSP=0; sub32(=0) -> 0 + + mov rbx, rcx ; Object + mov rsi, rdx ; Function + mov rdi, r8 ; Params + + ; PlayButton check (once-only) + movzx eax, BYTE PTR [bPlayButton] + test al, al + jnz @@check_traveled + + ; Resolve ReadyToStartMatch UFunction (cached after first call) + mov r12, QWORD PTR [pFn_PlayButton] + test r12, r12 + jnz @@have_playbtn + + lea rcx, szPlayButtonFn + call SDK_FindObject + mov QWORD PTR [pFn_PlayButton], rax + mov r12, rax + +@@have_playbtn: + test r12, r12 + jz @@check_traveled + + cmp rsi, r12 ; Function == ReadyToStartMatch? + jne @@check_traveled + + ; Match — server is ready to start: set flag, travel, hook network + mov BYTE PTR [bPlayButton], 1 + + call SDK_GetWorld + test rax, rax + jz @@check_traveled + mov rcx, rax + call Game_Start + + call Hooks_InitNetworkHooks + + ; UFunctionHooks dispatch (once bTraveled is true) +@@check_traveled: + movzx eax, BYTE PTR [bTraveled] + test al, al + jz @@call_orig + + mov r13d, DWORD PTR [UFunctionHooks_ToHook_Num] + test r13d, r13d + jle @@call_orig + + mov r14, QWORD PTR [UFunctionHooks_ToHook_Data] + test r14, r14 + jz @@call_orig + + xor ebp, ebp ; loop index i = 0 + +@@dispatch_loop: + cmp ebp, r13d + jge @@call_orig + + mov rax, QWORD PTR [r14 + rbp * 8] ; ToHook[i] + cmp rax, rsi ; == current Function? + jne @@dispatch_next + + ; Call ToCall[i](Object, Params) + mov rax, QWORD PTR [UFunctionHooks_ToCall_Data] + test rax, rax + jz @@dispatch_next + mov rax, QWORD PTR [rax + rbp * 8] ; handler ptr + test rax, rax + jz @@dispatch_next + mov rcx, rbx ; Object + mov rdx, rdi ; Params + call rax + +@@dispatch_next: + inc ebp + jmp @@dispatch_loop + + ; Always call original ProcessEvent +@@call_orig: + mov rcx, rbx + mov rdx, rsi + mov r8, rdi + call QWORD PTR [ProcessEvent] + + add rsp, 32 + pop r14 + pop r13 + pop r12 + pop rdi + pop rsi + pop rbx + pop rbp + ret +Hooks_ProcessEventHook ENDP + +; Hooks_InitNetworkHooks() +; Called from Hooks_ProcessEventHook when bPlayButton fires. +; Installs the 8 network-phase Detour hooks using DETOUR_START/END. +; +; DetourAttach(&TargetVariable, HookProc): +; RCX = address of the QWORD holding the original fn ptr +; RDX = address of the hook procedure +; +; Stack: push rbp + push rbx = 2 pushes (RSP=8); sub 40(=8) -> 0 +Hooks_InitNetworkHooks PROC + push rbp + push rbx + sub rsp, 40 ; 2 pushes: RSP=8; sub40 -> 0 + + ; DETOUR_START + call DetourTransactionBegin + call GetCurrentThread + mov rcx, rax + call DetourUpdateThread + + ; WelcomePlayer + lea rcx, Native_World_WelcomePlayer + lea rdx, Hooks_WelcomePlayer + call DetourAttach + + ; World::NotifyControlMessage + lea rcx, Native_World_NotifyControlMessage + lea rdx, Hooks_World_NotifyControlMessage + call DetourAttach + + ; OnlineBeacon::NotifyAcceptingConnection + lea rcx, Native_OnlineBeacon_NotifyAcceptingConnection + lea rdx, Hooks_Beacon_NotifyAcceptingConnection + call DetourAttach + + ; OnlineBeaconHost::NotifyControlMessage + lea rcx, Native_OnlineBeaconHost_NotifyControlMessage + lea rdx, Hooks_Beacon_NotifyControlMessage + call DetourAttach + + ; Engine::SeamlessTravelHandlerForWorld + lea rcx, Native_Engine_SeamlessTravelHandlerForWorld + lea rdx, Hooks_SeamlessTravelHandlerForWorld + call DetourAttach + + ; OnlineSession::KickPlayer + lea rcx, Native_OnlineSession_KickPlayer + lea rdx, Hooks_KickPlayer + call DetourAttach + + ; Actor::GetNetMode + lea rcx, Native_Actor_GetNetMode + lea rdx, Hooks_GetNetMode + call DetourAttach + + ; GameViewportClient::PostRender + lea rcx, Native_GameViewportClient_PostRender + lea rdx, Hooks_PostRender + call DetourAttach + + ; DETOUR_END + call DetourTransactionCommit + + add rsp, 40 + pop rbx + pop rbp + ret +Hooks_InitNetworkHooks ENDP + +END \ No newline at end of file diff --git a/include/externs.inc b/include/externs.inc index d092120..abd2857 100644 --- a/include/externs.inc +++ b/include/externs.inc @@ -125,8 +125,9 @@ EXTERNDEF Hooks_InitNetworkHooks :PROC ; ufunctionhooks.asm — UFunction dispatch hooks EXTERNDEF UFunctionHooks_Initialize :PROC -EXTERNDEF UFunctionHooks_ToHook_Data :QWORD ; base of the hook-registration array +EXTERNDEF UFunctionHooks_ToHook_Data :QWORD ; base of UFunction* array EXTERNDEF UFunctionHooks_ToHook_Num :DWORD ; count of registered hooks +EXTERNDEF UFunctionHooks_ToCall_Data :QWORD ; base of handler QWORD* array (BYTE (*)(UObject*,void*)) ; sdk/sdk_classes.asm — UObject / GObjects accessors EXTERNDEF SDK_GetObjectByIndex :PROC ; (index:DWORD) -> QWORD (UObject*)