diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09e9082 --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +*.obj +*.tlog +*.log +*.lastbuildstate +*.idb +*.exp +*.ilk +*.unsuccessfulbuild +*.sdf +*.opensdf +*.pdb +*.suo + +# Additional stuff +Release/ +Debug/ +ipch/ + +# IDA +# ========================= +*.id0 +*.id1 +*.id2 +*.nam +*.til \ No newline at end of file diff --git a/t5exp.sln b/t5exp.sln new file mode 100644 index 0000000..78d18b8 --- /dev/null +++ b/t5exp.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "t5exp", "t5exp\t5exp.vcxproj", "{1EBA0256-19E5-48A4-821F-084890C4BFB4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1EBA0256-19E5-48A4-821F-084890C4BFB4}.Debug|Win32.ActiveCfg = Debug|Win32 + {1EBA0256-19E5-48A4-821F-084890C4BFB4}.Debug|Win32.Build.0 = Debug|Win32 + {1EBA0256-19E5-48A4-821F-084890C4BFB4}.Release|Win32.ActiveCfg = Release|Win32 + {1EBA0256-19E5-48A4-821F-084890C4BFB4}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/t5exp/Hooking.cpp b/t5exp/Hooking.cpp new file mode 100644 index 0000000..9590518 --- /dev/null +++ b/t5exp/Hooking.cpp @@ -0,0 +1,157 @@ +#include "stdinc.h" + +#pragma unmanaged +void CallHook::initialize(DWORD place, void *hookToInstall) +{ + pPlace = (PBYTE)place; + memcpy(bOriginalCode, pPlace, sizeof(bOriginalCode)); + pOriginal = pPlace + sizeof(bOriginalCode) + *(ptrdiff_t*) (bOriginalCode + 1); + hook = hookToInstall; +} + +void CallHook::installHook(void *hookToInstall) +{ + if (hookToInstall) hook = hookToInstall; + if (hook) { + *(ptrdiff_t*) (pPlace + 1) = (PBYTE) hook - pPlace - 5; + } +} + +void CallHook::releaseHook() +{ + //memcpy(pPlace + 1, bOriginalCode + 1, sizeof(bOriginalCode) - 1); + *(ptrdiff_t*) (pPlace + 1) = (PBYTE) pOriginal - pPlace - 5; +} + +/* +void PointerHook::initialize(PVOID* place) +{ + pPlace = place; + pOriginal = NULL; +} + +int PointerHook::installHook(void (*hookToInstall)(), bool unprotect) +{ + DWORD d = 0; + + if (pOriginal) + return 0; + if (unprotect && + !VirtualProtect(pPlace, sizeof(PVOID), PAGE_READWRITE, &d)) + return 0; + pOriginal = *pPlace; + *pPlace = (PVOID) hookToInstall; + if (unprotect) + VirtualProtect(pPlace, sizeof(PVOID), d, &d); + return 1; +} + +int PointerHook::releaseHook(bool unprotect) +{ + DWORD d = 0; + + if (!pOriginal) + return 0; + if (unprotect && + !VirtualProtect(pPlace, sizeof(PVOID), PAGE_READWRITE, &d)) + return 0; + *pPlace = pOriginal; + pOriginal = NULL; + if (unprotect) + VirtualProtect(pPlace, sizeof(PVOID), d, &d); + return 1; +} + +*/ +void StompHook::initialize(DWORD place, void *hookToInstall, BYTE countBytes, bool useJump) +{ + pPlace = (PBYTE)place; + bCountBytes = countBytes < sizeof(bOriginalCode) ? countBytes : sizeof(bOriginalCode); + memcpy(bOriginalCode, pPlace, bCountBytes); + hook = hookToInstall; + jump = useJump; +} + +void StompHook::installHook(void *hookToInstall) +{ + if (hookToInstall) hook = hookToInstall; + if (hook) { + memset(pPlace, NOP, bCountBytes); + pPlace[0] = jump ? JMP_NEAR32 : CALL_NEAR32; + *(ptrdiff_t*) (pPlace + 1) = (PBYTE) hook - pPlace - 5; + } +} + +void StompHook::releaseHook() +{ + memcpy(pPlace, bOriginalCode, bCountBytes); +} + +void HookInstall(DWORD address, DWORD hookToInstall, int bCountBytes) +{ + PBYTE pPlace = (PBYTE)address; + memset(pPlace, NOP, bCountBytes); + pPlace[0] = CALL_NEAR32; + *(ptrdiff_t*) (pPlace + 1) = (PBYTE) hookToInstall - pPlace - 5; +} + +void _patch(void* pAddress, DWORD data, DWORD iSize) +{ + switch(iSize) + { + case 1: *(BYTE*)pAddress = (BYTE)data; + break; + case 2: *(WORD*)pAddress = (WORD)data; + break; + case 4: *(DWORD*)pAddress = (DWORD)data; + break; + } +} + +void _nop(void* pAddress, DWORD size) +{ + memset(pAddress, 0x90, size); + return; + + DWORD dwAddress = (DWORD)pAddress; + if ( size % 2 ) + { + *(BYTE*)pAddress = 0x90; + dwAddress++; + } + if ( size - ( size % 2 ) ) + { + DWORD sizeCopy = size - ( size % 2 ); + do + { + *(WORD*)dwAddress = 0xFF8B; + dwAddress += 2; + sizeCopy -= 2; + } + while ( sizeCopy ); + } +} + +void _call(void* pAddress, DWORD data, eCallPatcher bPatchType) +{ + switch ( bPatchType ) + { + case PATCH_JUMP: + *(BYTE*)pAddress = (BYTE)0xE9; + break; + + case PATCH_CALL: + *(BYTE*)pAddress = (BYTE)0xE8; + break; + + default: + break; + } + + *(DWORD*)((DWORD)pAddress + 1) = (DWORD)data - (DWORD)pAddress - 5; +} + +void _charptr(void* pAddress, const char* pChar) +{ + *(DWORD*)pAddress = (DWORD)pChar; +} \ No newline at end of file diff --git a/t5exp/Hooking.h b/t5exp/Hooking.h new file mode 100644 index 0000000..0ec482b --- /dev/null +++ b/t5exp/Hooking.h @@ -0,0 +1,54 @@ +#pragma once + +#define CALL_NEAR32 0xE8U +#define JMP_NEAR32 0xE9U +#define NOP 0x90U + +struct CallHook { + BYTE bOriginalCode[5]; + PBYTE pPlace; + void* pOriginal; + void* hook; + + void initialize(DWORD place, void *hookToInstall = NULL); + void installHook(void* hookToInstall = NULL); + void releaseHook(); +}; +/* +struct PointerHook { + PVOID* pPlace; + PVOID pOriginal; + + void initialize(PVOID* place); + int installHook(void (*hookToInstall)(), bool unprotect); + int releaseHook(bool unprotect); +}; +*/ +struct StompHook { + BYTE bOriginalCode[15]; + BYTE bCountBytes; + PBYTE pPlace; + void* hook; + bool jump; + + void initialize(DWORD place, void *hookToInstall = NULL, BYTE countBytes = 5, bool useJump = true); + void installHook(void* hookToInstall = NULL); + void releaseHook(); +}; + +void HookInstall(DWORD address, DWORD hookToInstall, int bCountBytes=5); + +enum eCallPatcher +{ + PATCH_CALL, + PATCH_JUMP, + PATCH_NOTHING +}; + +void _patch(void* pAddress, DWORD data, DWORD iSize); +void _nop(void* pAddress, DWORD size); +void _call(void* pAddress, DWORD data, eCallPatcher bPatchType); + +#define patch(a, v, s) _patch((void*)(a), (DWORD)(v), (s)) +#define nop(a, v) _nop((void*)(a), (v)) +#define call(a, v, bAddCall) _call((void*)(a), (DWORD)(v), (bAddCall)) diff --git a/t5exp/Main.cpp b/t5exp/Main.cpp new file mode 100644 index 0000000..5f28fc5 --- /dev/null +++ b/t5exp/Main.cpp @@ -0,0 +1,84 @@ +// ========================================================== +// project 'secretSchemes' +// +// Component: clientdll +// Sub-component: steam_api +// Purpose: Manages the initialization of iw4cli. +// +// Initial author: NTAuthority +// Started: 2011-05-04 +// ========================================================== + +#include "stdinc.h" + +void Sys_RunInit(); + +static BYTE originalCode[5]; +static PBYTE originalEP = 0; + +void Main_UnprotectModule(HMODULE hModule) +{ + PIMAGE_DOS_HEADER header = (PIMAGE_DOS_HEADER)hModule; + PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD)hModule + header->e_lfanew); + + // unprotect the entire PE image + SIZE_T size = ntHeader->OptionalHeader.SizeOfImage; + DWORD oldProtect; + VirtualProtect((LPVOID)hModule, size, PAGE_EXECUTE_READWRITE, &oldProtect); +} + +void Main_DoInit() +{ + // unprotect our entire PE image + HMODULE hModule; + if (SUCCEEDED(GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCSTR)Main_DoInit, &hModule))) + { + Main_UnprotectModule(hModule); + } + + Sys_RunInit(); + + // return to the original EP + memcpy(originalEP, &originalCode, sizeof(originalCode)); + __asm jmp originalEP +} + +void Main_SetSafeInit() +{ + // find the entry point for the executable process, set page access, and replace the EP + HMODULE hModule = GetModuleHandle(NULL); // passing NULL should be safe even with the loader lock being held (according to ReactOS ldr.c) + + if (hModule) + { + PIMAGE_DOS_HEADER header = (PIMAGE_DOS_HEADER)hModule; + PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD)hModule + header->e_lfanew); + + Main_UnprotectModule(hModule); + + // back up original code + PBYTE ep = (PBYTE)((DWORD)hModule + ntHeader->OptionalHeader.AddressOfEntryPoint); + memcpy(originalCode, ep, sizeof(originalCode)); + + // patch to call our EP + int newEP = (int)Main_DoInit - ((int)ep + 5); + ep[0] = 0xE9; // for some reason this doesn't work properly when run under the debugger + memcpy(&ep[1], &newEP, 4); + + originalEP = ep; + } +} + +//extern "C" void __declspec(dllimport) DependencyFunctionCCAPI(); + +bool __stdcall DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) +{ + if (dwReason == DLL_PROCESS_ATTACH) + { + if (*(DWORD*)0x581009 == 0x10C08352) + { + Main_SetSafeInit(); + } + } + + return true; +} \ No newline at end of file diff --git a/t5exp/PatchT5.cpp b/t5exp/PatchT5.cpp new file mode 100644 index 0000000..0d7bb17 --- /dev/null +++ b/t5exp/PatchT5.cpp @@ -0,0 +1,48 @@ +#include "stdinc.h" + +typedef void(__cdecl * DB_LoadXAssets_t)(XZoneInfo *zoneInfo, unsigned int zoneCount, int sync); +DB_LoadXAssets_t DB_LoadXAssets = (DB_LoadXAssets_t)0x4359A0; + +void XModelExport(const char* name); + +void DumpStuff() +{ + XModelExport("t5_weapon_ak74u_viewmodel"); +} + +void RunStuff() +{ + static bool run = false; + + if (!run) + { + run = true; + DumpStuff(); + } +} + +void Sys_RunInit() +{ + // No improper quit popup + nop(0x533F1A, 2); + + // Nop optimal settings + nop(0x86D114, 5); + + // Fix steam start + nop(0x86CFE3, 5); + call(0x53E8D0, 0x53E8DF, PATCH_JUMP); + + // Custom command line + *(char**)0x403683 = "+set dedicated 1"; + + //nop(0x456E03, 5); + *(BYTE*)0x46EA60 = 0xC3; + + // Hook random frame function. FastFiles will all be loaded then. + call(0x86C785, RunStuff, PATCH_CALL); + + // Don't initialize network stuff + nop(0x4ED3E8, 5); + *(BYTE*)0x5D4500 = 0xC3; +} \ No newline at end of file diff --git a/t5exp/SDLLP.cpp b/t5exp/SDLLP.cpp new file mode 100644 index 0000000..42600f5 --- /dev/null +++ b/t5exp/SDLLP.cpp @@ -0,0 +1,105 @@ +// --------------------------------------+ +// System Dynamic Link Library Proxy +// by momo5502 +// --------------------------------------+ + +#define _CRT_SECURE_NO_WARNINGS +#include +#include +#include +#include + +// Macro to declare an export +// --------------------------------------+ +#define EXPORT(_export) extern "C" __declspec(naked) __declspec(dllexport) void _export() { static FARPROC function = 0; if(!function) function = SDLLP::GetExport(__FUNCTION__, LIBRARY); __asm { jmp function } } + +// Static class +// --------------------------------------+ +class SDLLP +{ +private: + static std::map mLibraries; + + static void Log(const char* message, ...); + static void LoadLibrary(const char* library); + static bool IsLibraryLoaded(const char* library); + +public: + static FARPROC GetExport(const char* function, const char* library); +}; + +// Class variable declarations +// --------------------------------------+ +std::map SDLLP::mLibraries; + +// Load necessary library +// --------------------------------------+ +void SDLLP::LoadLibrary(const char* library) +{ + Log("[SDLLP] Loading library '%s'.", library); + + CHAR mPath[MAX_PATH]; + + GetSystemDirectoryA(mPath, MAX_PATH); + strcat_s(mPath, "\\"); + strcat_s(mPath, library); + + mLibraries[library] = ::LoadLibraryA(mPath); + + if(!IsLibraryLoaded(library)) Log("[SDLLP] Unable to load library '%s'.", library); +} + +// Check if export already loaded +// --------------------------------------+ +bool SDLLP::IsLibraryLoaded(const char* library) +{ + return (mLibraries.find(library) != mLibraries.end() && mLibraries[library]); +} + +// Get export address +// --------------------------------------+ +FARPROC SDLLP::GetExport(const char* function, const char* library) +{ + Log("[SDLLP] Export '%s' requested from %s.", function, library); + + if(!IsLibraryLoaded(library)) LoadLibrary(library); + + FARPROC address = GetProcAddress(mLibraries[library], function); + + if(!address) Log("[SDLLP] Unable to load export '%s' from library '%s'.", function, library); + return address; +} + +// Write debug string +// --------------------------------------+ +void SDLLP::Log(const char* message, ...) +{ + CHAR buffer[1024]; + va_list ap; + + va_start(ap, message); + vsprintf(buffer, message, ap); + va_end(ap); + + OutputDebugStringA(buffer); +} + +// --------------------------------------+ +// Adapt export functions and library +// --------------------------------------+ + +#define LIBRARY "d3d9.dll" +EXPORT(Direct3DShaderValidatorCreate9) +EXPORT(PSGPError) +EXPORT(PSGPSampleTexture) +EXPORT(D3DPERF_BeginEvent) +EXPORT(D3DPERF_EndEvent) +EXPORT(D3DPERF_GetStatus) +EXPORT(D3DPERF_QueryRepeatFrame) +EXPORT(D3DPERF_SetMarker) +EXPORT(D3DPERF_SetOptions) +EXPORT(D3DPERF_SetRegion) +EXPORT(DebugSetLevel) +EXPORT(DebugSetMute) +EXPORT(Direct3DCreate9) +EXPORT(Direct3DCreate9Ex) \ No newline at end of file diff --git a/t5exp/Stream.cpp b/t5exp/Stream.cpp new file mode 100644 index 0000000..6deab78 --- /dev/null +++ b/t5exp/Stream.cpp @@ -0,0 +1,108 @@ +#include "stdinc.h" + +// Reserve 100MB by default. +// That's totally fine, as the dedi doesn't load images and therefore doesn't need much memory. +// That way we can be sure it won't need to reallocate memory. +// Side note: if you need a fastfile larger than 100MB, you're doing it wrong +Stream::Stream() : Stream(0x6400000) {} + +Stream::Stream(size_t size) +{ + Stream::Buffer.reserve(size); +} + +Stream::~Stream() +{ + Stream::Buffer.clear(); +}; + +size_t Stream::Length() +{ + return Stream::Buffer.length(); +} + +size_t Stream::Size() +{ + return Stream::Buffer.size(); +} + +size_t Stream::Write(const void * _str, size_t size, size_t count) +{ + Stream::Buffer.append((char*)_str, size * count); + return count; +} + +size_t Stream::Write(int value, size_t count) +{ + size_t ret = 0; + + for (size_t i = 0; i < count; i++) + { + ret += Stream::Write(&value, 4, 1); + } + + return ret; +} + +size_t Stream::WriteString(const char* string) +{ + return Stream::WriteString(string, strlen(string)); +} + +size_t Stream::WriteString(const char* string, size_t len) +{ + size_t ret = 0; + + if (string) + { + ret += Stream::Write(string, len); + } + + ret += Stream::WriteNull(); + + return ret; +} + +size_t Stream::WriteNull(size_t count) +{ + int _null = 0; + + size_t ret = 0; + + for (size_t i = 0; i < count; i++) + { + ret += Stream::Write(&_null, 1); + } + + return ret; +} + +size_t Stream::WriteMax(size_t count) +{ + int _max = -1; + + size_t ret = 0; + + for (size_t i = 0; i < count; i++) + { + ret += Stream::Write(&_max, 1); + } + + return ret; +} + +char* Stream::At() +{ + return (char*)(Stream::Data() + Stream::Length()); +} + +char* Stream::Data() +{ + return (char*)Stream::Buffer.data(); +} + +void Stream::ToBuffer(std::basic_string& outBuffer) +{ + outBuffer.clear(); + outBuffer.append((const uint8_t*)Stream::Data(), Stream::Length()); +} \ No newline at end of file diff --git a/t5exp/Stream.h b/t5exp/Stream.h new file mode 100644 index 0000000..730afeb --- /dev/null +++ b/t5exp/Stream.h @@ -0,0 +1,29 @@ +#include +#include + +class Stream +{ +private: + std::string Buffer; + +public: + + Stream(); + Stream(size_t size); + ~Stream(); + + size_t Length(); + size_t Size(); + size_t Write(const void * _str, size_t size, size_t count = 1); + size_t Write(int value, size_t count); + + size_t WriteString(const char* string); + size_t WriteString(const char* string, size_t len); + size_t WriteNull(size_t count = 1); + size_t WriteMax(size_t count = 1); + + char* At(); + char* Data(); + + void ToBuffer(std::basic_string& outBuffer); +}; diff --git a/t5exp/XModel.h b/t5exp/XModel.h new file mode 100644 index 0000000..080bebd --- /dev/null +++ b/t5exp/XModel.h @@ -0,0 +1,630 @@ +#include + +struct DObjAnimMat +{ + float quat[4]; + float trans[3]; + float transWeight; +}; + +struct XSurfaceVertexInfo +{ + __int16 vertCount[4]; + unsigned __int16 *vertsBlend; + float *tensionData; +}; + +union GfxColor +{ + unsigned int packed; + char array[4]; +}; + +union PackedTexCoords +{ + unsigned int packed; +}; + +union PackedUnitVec +{ + unsigned int packed; + char array[4]; +}; + +struct GfxPackedVertex +{ + float xyz[3]; + float binormalSign; + GfxColor color; + PackedTexCoords texCoord; + PackedUnitVec normal; + PackedUnitVec tangent; +}; + +struct XSurfaceCollisionAabb +{ + unsigned __int16 mins[3]; + unsigned __int16 maxs[3]; +}; + +struct XSurfaceCollisionNode +{ + XSurfaceCollisionAabb aabb; + unsigned __int16 childBeginIndex; + unsigned __int16 childCount; +}; + +struct XSurfaceCollisionLeaf +{ + unsigned __int16 triangleBeginIndex; +}; + +struct XSurfaceCollisionTree +{ + float trans[3]; + float scale[3]; + unsigned int nodeCount; + XSurfaceCollisionNode *nodes; + unsigned int leafCount; + XSurfaceCollisionLeaf *leafs; +}; + +struct XRigidVertList +{ + unsigned __int16 boneOffset; + unsigned __int16 vertCount; + unsigned __int16 triOffset; + unsigned __int16 triCount; + XSurfaceCollisionTree *collisionTree; +}; + +struct XSurface +{ + char tileMode; + char vertListCount; + unsigned __int16 flags; + unsigned __int16 vertCount; + unsigned __int16 triCount; + unsigned __int16 baseTriIndex; + unsigned __int16 baseVertIndex; + unsigned __int16 *triIndices; + XSurfaceVertexInfo vertInfo; + GfxPackedVertex *verts0; + IDirect3DVertexBuffer9 *vb0; + XRigidVertList *vertList; + IDirect3DIndexBuffer9 *indexBuffer; + int partBits[5]; +}; + +struct GfxDrawSurfFields +{ + __int64 _bf0; +}; + +union GfxDrawSurf +{ + GfxDrawSurfFields fields; + unsigned __int64 packed; +}; + +struct MaterialInfo +{ + const char *name; + unsigned int gameFlags; + char pad; + char sortKey; + char textureAtlasRowCount; + char textureAtlasColumnCount; + GfxDrawSurf drawSurf; + unsigned int surfaceTypeBits; + unsigned int layeredSurfaceTypes; + unsigned __int16 hashIndex; +}; + +struct MaterialStreamRouting +{ + char source; + char dest; +}; + +struct MaterialVertexStreamRouting +{ + MaterialStreamRouting data[16]; + IDirect3DVertexDeclaration9 *decl[18]; +}; + +struct MaterialVertexDeclaration +{ + char streamCount; + bool hasOptionalSource; + bool isLoaded; + MaterialVertexStreamRouting routing; +}; + +struct GfxVertexShaderLoadDef +{ + unsigned int *program; + unsigned __int16 programSize; +}; + +struct MaterialVertexShaderProgram +{ + IDirect3DVertexShader9 *vs; + GfxVertexShaderLoadDef loadDef; +}; + +struct MaterialVertexShader +{ + const char *name; + MaterialVertexShaderProgram prog; +}; + +struct GfxPixelShaderLoadDef +{ + unsigned int *program; + unsigned __int16 programSize; +}; + +struct MaterialPixelShaderProgram +{ + IDirect3DPixelShader9 *ps; + GfxPixelShaderLoadDef loadDef; +}; + +struct MaterialPixelShader +{ + const char *name; + MaterialPixelShaderProgram prog; +}; + +struct MaterialArgumentCodeConst +{ + unsigned __int16 index; + char firstRow; + char rowCount; +}; + +union MaterialArgumentDef +{ + const float *literalConst; + MaterialArgumentCodeConst codeConst; + unsigned int codeSampler; + unsigned int nameHash; +}; + +struct MaterialShaderArgument +{ + unsigned __int16 type; + unsigned __int16 dest; + MaterialArgumentDef u; +}; + +struct MaterialPass +{ + MaterialVertexDeclaration *vertexDecl; + MaterialVertexShader *vertexShader; + MaterialPixelShader *pixelShader; + char perPrimArgCount; + char perObjArgCount; + char stableArgCount; + char customSamplerFlags; + MaterialShaderArgument *args; +}; + +struct MaterialTechnique +{ + const char *name; + unsigned __int16 flags; + unsigned __int16 passCount; + MaterialPass passArray[1]; +}; + +struct MaterialTechniqueSet +{ + const char *name; + char worldVertFormat; + char unused[1]; + unsigned __int16 techsetFlags; + MaterialTechnique *techniques[130]; +}; + +struct GfxImageLoadDef +{ + char levelCount; + char flags; + int format; + int resourceSize; + char data[1]; +}; + +union GfxTexture +{ + IDirect3DBaseTexture9 *basemap; + IDirect3DTexture9 *map; + IDirect3DVolumeTexture9 *volmap; + IDirect3DCubeTexture9 *cubemap; + GfxImageLoadDef *loadDef; +}; + +struct Picmip +{ + char platform[2]; +}; + +struct CardMemory +{ + int platform[2]; +}; + +struct GfxImage +{ + GfxTexture texture; + char mapType; + char semantic; + char category; + bool delayLoadPixels; + Picmip picmip; + bool noPicmip; + char track; + CardMemory cardMemory; + unsigned __int16 width; + unsigned __int16 height; + unsigned __int16 depth; + char levelCount; + char streaming; + unsigned int baseSize; + char *pixels; + unsigned int loadedSize; + char skippedMipLevels; + const char *name; + unsigned int hash; +}; + +struct WaterWritable +{ + float floatTime; +}; + +struct complex_s +{ + float real; + float imag; +}; + +struct water_t +{ + WaterWritable writable; + complex_s *H0; + float *wTerm; + int M; + int N; + float Lx; + float Lz; + float gravity; + float windvel; + float winddir[2]; + float amplitude; + float codeConstant[4]; + GfxImage *image; +}; + +union MaterialTextureDefInfo +{ + GfxImage *image; + water_t *water; +}; + +struct MaterialTextureDef +{ + unsigned int nameHash; + char nameStart; + char nameEnd; + char samplerState; + char semantic; + char isMatureContent; + char pad[3]; + MaterialTextureDefInfo u; +}; + +struct MaterialConstantDef +{ + unsigned int nameHash; + char name[12]; + float literal[4]; +}; + +struct GfxStateBits +{ + unsigned int loadBits[2]; +}; + +struct Material +{ + MaterialInfo info; + char stateBitsEntry[130]; + char textureCount; + char constantCount; + char stateBitsCount; + char stateFlags; + char cameraRegion; + char maxStreamedMips; + MaterialTechniqueSet *techniqueSet; + MaterialTextureDef *textureTable; + MaterialConstantDef *constantTable; + GfxStateBits *stateBitsTable; +}; + +struct XModelLodInfo +{ + float dist; + unsigned __int16 numsurfs; + unsigned __int16 surfIndex; + int partBits[5]; + char lod; + char smcIndexPlusOne; + char smcAllocBits; + char unused; +}; + +struct XModelCollTri_s +{ + float plane[4]; + float svec[4]; + float tvec[4]; +}; + +struct XModelCollSurf_s +{ + XModelCollTri_s *collTris; + int numCollTris; + float mins[3]; + float maxs[3]; + int boneIdx; + int contents; + int surfFlags; +}; + +struct XBoneInfo +{ + float bounds[2][3]; + float offset[3]; + float radiusSquared; + char collmap; +}; + +struct XModelHighMipBounds +{ + float center[3]; + float himipRadiusSq; +}; + +struct XModelStreamInfo +{ + XModelHighMipBounds *highMipBounds; +}; + +struct PhysPreset +{ + const char *name; + int flags; + float mass; + float bounce; + float friction; + float bulletForceScale; + float explosiveForceScale; + const char *sndAliasPrefix; + float piecesSpreadFraction; + float piecesUpwardVelocity; + int canFloat; + float gravityScale; + float centerOfMassOffset[3]; + float buoyancyBoxMin[3]; + float buoyancyBoxMax[3]; +}; + +struct cplane_s +{ + float normal[3]; + float dist; + char type; + char signbits; + char pad[2]; +}; + +struct cbrushside_t +{ + cplane_s *plane; + int cflags; + int sflags; +}; + +struct BrushWrapper +{ + float mins[3]; + int contents; + float maxs[3]; + unsigned int numsides; + cbrushside_t *sides; + int axial_cflags[2][3]; + int axial_sflags[2][3]; + unsigned int numverts; + float(*verts)[3]; + cplane_s *planes; +}; + +struct PhysGeomInfo +{ + BrushWrapper *brush; + int type; + float orientation[3][3]; + float offset[3]; + float halfLengths[3]; +}; + +struct PhysGeomList +{ + unsigned int count; + PhysGeomInfo *geoms; + int contents; +}; + +struct Collmap +{ + PhysGeomList *geomList; +}; + +enum ConstraintType +{ + CONSTRAINT_NONE = 0x0, + CONSTRAINT_POINT = 0x1, + CONSTRAINT_DISTANCE = 0x2, + CONSTRAINT_HINGE = 0x3, + CONSTRAINT_JOINT = 0x4, + CONSTRAINT_ACTUATOR = 0x5, + CONSTRAINT_FAKE_SHAKE = 0x6, + CONSTRAINT_LAUNCH = 0x7, + CONSTRAINT_ROPE = 0x8, + CONSTRAINT_LIGHT = 0x9, + NUM_CONSTRAINT_TYPES = 0xA, +}; + +enum AttachPointType +{ + ATTACH_POINT_WORLD = 0x0, + ATTACH_POINT_DYNENT = 0x1, + ATTACH_POINT_ENT = 0x2, + ATTACH_POINT_BONE = 0x3, +}; + +struct PhysConstraint +{ + unsigned __int16 targetname; + ConstraintType type; + AttachPointType attach_point_type1; + int target_index1; + unsigned __int16 target_ent1; + const char *target_bone1; + AttachPointType attach_point_type2; + int target_index2; + unsigned __int16 target_ent2; + const char *target_bone2; + float offset[3]; + float pos[3]; + float pos2[3]; + float dir[3]; + int flags; + int timeout; + int min_health; + int max_health; + float distance; + float damp; + float power; + float scale[3]; + float spin_scale; + float minAngle; + float maxAngle; + Material *material; + int constraintHandle; + int rope_index; + int centity_num[4]; +}; + +struct PhysConstraints +{ + const char *name; + unsigned int count; + PhysConstraint data[16]; +}; + +struct XModel +{ + const char *name; + char numBones; + char numRootBones; + char numsurfs; + char lodRampType; + unsigned __int16 *boneNames; + char *parentList; + __int16 *quats; + float *trans; + char *partClassification; + DObjAnimMat *baseMat; + XSurface *surfs; + Material **materialHandles; + XModelLodInfo lodInfo[4]; + char lodDistAutoGenerated; + XModelCollSurf_s *collSurfs; + int numCollSurfs; + int contents; + XBoneInfo *boneInfo; + float radius; + float mins[3]; + float maxs[3]; + __int16 numLods; + __int16 collLod; + XModelStreamInfo streamInfo; + int memUsage; + int flags; + bool bad; + PhysPreset *physPreset; + char numCollmaps; + Collmap *collmaps; + PhysConstraints *physConstraints; +}; + +enum XAssetType +{ + ASSET_TYPE_XMODELPIECES = 0x0, + ASSET_TYPE_PHYSPRESET = 0x1, + ASSET_TYPE_PHYSCONSTRAINTS = 0x2, + ASSET_TYPE_DESTRUCTIBLEDEF = 0x3, + ASSET_TYPE_XANIMPARTS = 0x4, + ASSET_TYPE_XMODEL = 0x5, + ASSET_TYPE_MATERIAL = 0x6, + ASSET_TYPE_TECHNIQUE_SET = 0x7, + ASSET_TYPE_IMAGE = 0x8, + ASSET_TYPE_SOUND = 0x9, + ASSET_TYPE_SOUND_PATCH = 0xA, + ASSET_TYPE_CLIPMAP = 0xB, + ASSET_TYPE_CLIPMAP_PVS = 0xC, + ASSET_TYPE_COMWORLD = 0xD, + ASSET_TYPE_GAMEWORLD_SP = 0xE, + ASSET_TYPE_GAMEWORLD_MP = 0xF, + ASSET_TYPE_MAP_ENTS = 0x10, + ASSET_TYPE_GFXWORLD = 0x11, + ASSET_TYPE_LIGHT_DEF = 0x12, + ASSET_TYPE_UI_MAP = 0x13, + ASSET_TYPE_FONT = 0x14, + ASSET_TYPE_MENULIST = 0x15, + ASSET_TYPE_MENU = 0x16, + ASSET_TYPE_LOCALIZE_ENTRY = 0x17, + ASSET_TYPE_WEAPON = 0x18, + ASSET_TYPE_WEAPONDEF = 0x19, + ASSET_TYPE_WEAPON_VARIANT = 0x1A, + ASSET_TYPE_SNDDRIVER_GLOBALS = 0x1B, + ASSET_TYPE_FX = 0x1C, + ASSET_TYPE_IMPACT_FX = 0x1D, + ASSET_TYPE_AITYPE = 0x1E, + ASSET_TYPE_MPTYPE = 0x1F, + ASSET_TYPE_MPBODY = 0x20, + ASSET_TYPE_MPHEAD = 0x21, + ASSET_TYPE_CHARACTER = 0x22, + ASSET_TYPE_XMODELALIAS = 0x23, + ASSET_TYPE_RAWFILE = 0x24, + ASSET_TYPE_STRINGTABLE = 0x25, + ASSET_TYPE_PACK_INDEX = 0x26, + ASSET_TYPE_XGLOBALS = 0x27, + ASSET_TYPE_DDL = 0x28, + ASSET_TYPE_GLASSES = 0x29, + ASSET_TYPE_EMBLEMSET = 0x2A, + ASSET_TYPE_COUNT = 0x2B, + ASSET_TYPE_STRING = 0x2B, + ASSET_TYPE_ASSETLIST = 0x2C, +}; + +struct XZoneInfo +{ + const char *name; + int allocFlags; + int freeFlags; +}; diff --git a/t5exp/XModelExport.cpp b/t5exp/XModelExport.cpp new file mode 100644 index 0000000..8b25f8c --- /dev/null +++ b/t5exp/XModelExport.cpp @@ -0,0 +1,351 @@ +#include "stdinc.h" +#include + +typedef float vec3_t[3]; + +enum scriptInstance_t +{ + SCRIPTINSTANCE_SERVER = 0x0, + SCRIPTINSTANCE_CLIENT = 0x1, + SCRIPT_INSTANCE_MAX = 0x2, +}; + +typedef void *(__cdecl * DB_FindXAssetHeader_t)(XAssetType type, const char *name, bool errorIfMissing, int waitTime); +DB_FindXAssetHeader_t DB_FindXAssetHeader = (DB_FindXAssetHeader_t)0x493A60; + +typedef const char *(__cdecl * SL_ConvertToString_t)(unsigned int stringValue, scriptInstance_t inst); +SL_ConvertToString_t SL_ConvertToString = (SL_ConvertToString_t)0x624C70; + +Stream* Buffer = 0; + +// Stuff copied from T6, might be missing some data, but who cares :P + +void Write_XSurfaceVertexInfo(XSurfaceVertexInfo* vertInfo, XSurfaceVertexInfo* destVertInfo) +{ + if (vertInfo->vertsBlend) + { + Buffer->Write(vertInfo->vertsBlend, sizeof(short), vertInfo->vertCount[0] + 3 * vertInfo->vertCount[1] + 5 * vertInfo->vertCount[2] + 7 * vertInfo->vertCount[3]); + destVertInfo->vertsBlend = (unsigned short *)-1; + } + + if (vertInfo->tensionData) + { + Buffer->Write(vertInfo->tensionData, sizeof(float), vertInfo->vertCount[0] + vertInfo->vertCount[1] + vertInfo->vertCount[2] + vertInfo->vertCount[3]); + destVertInfo->tensionData = (float *)-1; + } +} + +void Write_XSurfaceCollisionTree(XSurfaceCollisionTree* collisionTree) +{ + XSurfaceCollisionTree* destCollisionTree = (XSurfaceCollisionTree*)Buffer->At(); + Buffer->Write(collisionTree, sizeof(XSurfaceCollisionTree)); + + if (collisionTree->nodes) + { + Buffer->Write(collisionTree->nodes, sizeof(XSurfaceCollisionNode), collisionTree->nodeCount); + destCollisionTree->nodes = (XSurfaceCollisionNode *)-1; + } + + if (collisionTree->leafs) + { + Buffer->Write(collisionTree->leafs, sizeof(XSurfaceCollisionLeaf), collisionTree->leafCount); + destCollisionTree->leafs = (XSurfaceCollisionLeaf *)-1; + } +} + +void Write_XRigidVertListArray(XRigidVertList* vertList, char vertListCount) +{ + XRigidVertList* destVertList = (XRigidVertList*)Buffer->At(); + Buffer->Write(vertList, sizeof(XRigidVertList), vertListCount); + + for (char i = 0; i < vertListCount; i++) + { + XRigidVertList* destVertListEntry = &destVertList[i]; + XRigidVertList* vertListEntry = &vertList[i]; + + if (vertListEntry->collisionTree) + { + Write_XSurfaceCollisionTree(vertListEntry->collisionTree); + destVertListEntry->collisionTree = (XSurfaceCollisionTree *)-1; + } + } +} + +void Write_XSurfaceArray(XSurface* surfs, char numsurfs) +{ + XSurface* destSurfs = (XSurface*)Buffer->At(); + Buffer->Write(surfs, sizeof(XSurface), numsurfs); + + for (char i = 0; i < numsurfs; i++) + { + XSurface* destSurf = &destSurfs[i]; + XSurface* surf = &surfs[i]; + + Write_XSurfaceVertexInfo(&surf->vertInfo, &destSurf->vertInfo); + + if (!(surf->flags & 1) && surf->verts0) + { + Buffer->Write(surf->verts0, sizeof(GfxPackedVertex), surf->vertCount); + destSurf->verts0 = (GfxPackedVertex *)-1; + } + + // DirectX buffers are handled by the game. + //Write_VertexBuffer(); + + if (surf->vertList) + { + Write_XRigidVertListArray(surf->vertList, surf->vertListCount); + destSurf->vertList = (XRigidVertList *)-1; + } + + if (surf->triIndices) + { + Buffer->Write(surf->triIndices, 6, surf->triCount); + destSurf->triIndices = (unsigned __int16 *)-1; + } + + // DirectX buffers are handled by the game. + //Write_IndexBuffer(); + } +} + +void Write_XModelCollSurfArray(XModelCollSurf_s* collSurfs, int numCollSurfs) +{ + XModelCollSurf_s* destCollSurfs = (XModelCollSurf_s*)Buffer->At(); + Buffer->Write(collSurfs, sizeof(XModelCollSurf_s), numCollSurfs); + + for (int i = 0; i < numCollSurfs; i++) + { + XModelCollSurf_s* destCollSurf = &destCollSurfs[i]; + XModelCollSurf_s* collSurf = &collSurfs[i]; + + if (collSurf->collTris) + { + Buffer->Write(collSurf->collTris, sizeof(XModelCollTri_s), collSurf->numCollTris); + destCollSurfs->collTris = (XModelCollTri_s *)-1; + } + } +} + +void Write_cbrushside_tArray(cbrushside_t* sides, unsigned int numsides) +{ + cbrushside_t* destSides = (cbrushside_t*)Buffer->At(); + Buffer->Write(sides, sizeof(cbrushside_t), numsides); + + for (unsigned int i = 0; i < numsides; i++) + { + cbrushside_t* destSide = &destSides[i]; + cbrushside_t* side = &sides[i]; + + if (side->plane) + { + Buffer->Write(side->plane, sizeof(cplane_s)); + destSide->plane = (cplane_s *)-1; + } + } +} + +void Write_BrushWrapper(BrushWrapper* brush) +{ + BrushWrapper* destBrush = (BrushWrapper*)Buffer->At(); + Buffer->Write(brush, sizeof(BrushWrapper)); + + if (brush->sides) + { + Write_cbrushside_tArray(brush->sides, brush->numsides); + destBrush->sides = (cbrushside_t *)-1; + } + + if (brush->verts) + { + Buffer->Write(brush->verts, sizeof(vec3_t), brush->numverts); + destBrush->verts = (vec3_t *)-1; + } + + if (brush->planes) + { + Buffer->Write(brush->planes, sizeof(cplane_s), brush->numsides); + destBrush->planes = (cplane_s *)-1; + } +} + +void Write_PhysGeomInfoArray(PhysGeomInfo* geoms, unsigned int count) +{ + PhysGeomInfo* destGeoms = (PhysGeomInfo*)Buffer->At(); + Buffer->Write(geoms, sizeof(PhysGeomInfo), count); + + for (unsigned int i = 0; i < count; i++) + { + PhysGeomInfo* destGeom = &destGeoms[i]; + PhysGeomInfo* geom = &geoms[i]; + + if (geom->brush) + { + Write_BrushWrapper(geom->brush); + destGeom->brush = (BrushWrapper *)-1; + } + } +} + +void Write_PhysGeomList(PhysGeomList* geomList) +{ + PhysGeomList* destGeomList = (PhysGeomList*)Buffer->At(); + Buffer->Write(geomList, sizeof(PhysGeomList)); + + if (geomList->geoms) + { + Write_PhysGeomInfoArray(geomList->geoms, geomList->count); + destGeomList->geoms = (PhysGeomInfo *)-1; + } +} + +void Write_CollmapArray(Collmap* collmaps, char numCollmaps) +{ + Collmap* destCollmaps = (Collmap*)Buffer->At(); + Buffer->Write(collmaps, sizeof(Collmap), numCollmaps); + + for (char i = 0; i < numCollmaps; i++) + { + Collmap* destCollmap = &destCollmaps[i]; + Collmap* collmap = &collmaps[i]; + + if (collmap->geomList) + { + Write_PhysGeomList(collmap->geomList); + destCollmap->geomList = (PhysGeomList *)-1; + } + } +} + +void Write(XModel* Asset) +{ + XModel* dest = (XModel*)Buffer->At(); + Buffer->Write(Asset, sizeof(XModel)); + + if (Asset->name) + { + Buffer->WriteString(Asset->name); + dest->name = (const char*)-1; + } + + if (Asset->boneNames) + { + unsigned short* destBoneNames = (unsigned short*)Buffer->At(); + Buffer->Write(Asset->boneNames, sizeof(short), Asset->numBones); + + for (int i = 0; i < Asset->numBones; i++) + { + Buffer->WriteString(SL_ConvertToString(destBoneNames[i], SCRIPTINSTANCE_SERVER)); + } + + dest->boneNames = (unsigned __int16 *)-1; + } + + if (Asset->parentList) + { + Buffer->Write(Asset->parentList, Asset->numBones - Asset->numRootBones); + dest->parentList = (char *)-1; + } + + if (Asset->quats) + { + Buffer->Write(Asset->quats, 8, Asset->numBones - Asset->numRootBones); + dest->quats = (short *)-1; + } + + if (Asset->trans) + { + Buffer->Write(Asset->trans, 16, Asset->numBones - Asset->numRootBones); + dest->trans = (float *)-1; + } + + if (Asset->partClassification) + { + Buffer->Write(Asset->partClassification, Asset->numBones); + dest->partClassification = (char *)-1; + } + + if (Asset->baseMat) + { + Buffer->Write(Asset->baseMat, sizeof(DObjAnimMat), Asset->numBones); + dest->baseMat = (DObjAnimMat *)-1; + } + + if (Asset->surfs) + { + Write_XSurfaceArray(Asset->surfs, Asset->numsurfs); + dest->surfs = (XSurface *)-1; + } + + if (Asset->materialHandles) + { + Material** destMaterialHandles = (Material**)Buffer->At(); + Buffer->Write(Asset->materialHandles, sizeof(Material*), Asset->numsurfs); + + for (char i = 0; i < Asset->numsurfs; i++) + { + Buffer->WriteString(Asset->materialHandles[i]->info.name); + } + + dest->materialHandles = (Material **)-1; + } + + if (Asset->collSurfs) + { + Write_XModelCollSurfArray(Asset->collSurfs, Asset->numCollSurfs); + dest->collSurfs = (XModelCollSurf_s *)-1; + } + + if (Asset->boneInfo) + { + Buffer->Write(Asset->boneInfo, sizeof(XBoneInfo), Asset->numBones); + dest->boneInfo = (XBoneInfo *)-1; + } + + if (Asset->streamInfo.highMipBounds) + { + Buffer->Write(Asset->streamInfo.highMipBounds, sizeof(float), 4 * Asset->numsurfs); + dest->streamInfo.highMipBounds = (XModelHighMipBounds *)-1; + } + + if (Asset->physPreset) + { + Buffer->WriteString(Asset->physPreset->name); + } + + if (Asset->collmaps) + { + Write_CollmapArray(Asset->collmaps, Asset->numCollmaps); + dest->collmaps = (Collmap *)-1; + } + + if (Asset->physConstraints) + { + Buffer->WriteString(Asset->physConstraints->name); + } +} + +void XModelExport(const char* name) +{ + XModel* model = (XModel*)DB_FindXAssetHeader(ASSET_TYPE_XMODEL, name, true, -1); + + if (model) + { + Stream _Buffer; + Buffer = &_Buffer; + + Write(model); + + _mkdir("raw"); + std::string _name = "raw/"; + _name += name; + + FILE* fp = fopen(_name.c_str(), "wb"); + + if (fp) + { + fwrite(_Buffer.Data(), _Buffer.Size(), 1, fp); + fclose(fp); + } + } +} \ No newline at end of file diff --git a/t5exp/stdinc.h b/t5exp/stdinc.h new file mode 100644 index 0000000..acf808f --- /dev/null +++ b/t5exp/stdinc.h @@ -0,0 +1,8 @@ +#pragma once + +#define _CRT_SECURE_NO_WARNINGS +#define WIN32_LEAN_AND_MEAN +#include +#include "Hooking.h" +#include "XModel.h" +#include "Stream.h" \ No newline at end of file diff --git a/t5exp/t5exp.vcxproj b/t5exp/t5exp.vcxproj new file mode 100644 index 0000000..7b654bc --- /dev/null +++ b/t5exp/t5exp.vcxproj @@ -0,0 +1,112 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {1EBA0256-19E5-48A4-821F-084890C4BFB4} + Win32Proj + t5exp + + + + DynamicLibrary + true + v120 + MultiByte + + + DynamicLibrary + false + v120 + true + MultiByte + + + + + + + + + + + + + true + d3d9 + + + false + d3d9 + + + + NotUsing + Level3 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;T5EXP_EXPORTS;%(PreprocessorDefinitions) + true + true + + + Windows + true + + + copy /y "$(TargetPath)" "D:\Games\SteamLibrary\steamapps\common\Call of Duty Black Ops\" + + + + + Level3 + NotUsing + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;T5EXP_EXPORTS;%(PreprocessorDefinitions) + true + true + + + Windows + true + true + true + + + copy /y "$(TargetPath)" "D:\Games\SteamLibrary\steamapps\common\Call of Duty Black Ops\" + + + + + + + + + + + + false + + + false + + + + + + + + + + + + \ No newline at end of file diff --git a/t5exp/t5exp.vcxproj.filters b/t5exp/t5exp.vcxproj.filters new file mode 100644 index 0000000..cfe0bb9 --- /dev/null +++ b/t5exp/t5exp.vcxproj.filters @@ -0,0 +1,51 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Quelldateien + + + Quelldateien + + + Quelldateien + + + Quelldateien + + + Quelldateien + + + Quelldateien + + + + + Headerdateien + + + Headerdateien + + + Headerdateien + + + Headerdateien + + + \ No newline at end of file diff --git a/t5exp/t5exp.vcxproj.user b/t5exp/t5exp.vcxproj.user new file mode 100644 index 0000000..92e564b --- /dev/null +++ b/t5exp/t5exp.vcxproj.user @@ -0,0 +1,13 @@ + + + + t5exp.exe + D:\Games\SteamLibrary\steamapps\common\Call of Duty Black Ops\ + WindowsLocalDebugger + + + t5exp.exe + D:\Games\SteamLibrary\steamapps\common\Call of Duty Black Ops\ + WindowsLocalDebugger + + \ No newline at end of file