Add coke's JoinScriptUrl implementation and add output logging
This commit is contained in:
parent
144bbdd96e
commit
9a26837dc3
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project>
|
||||||
|
<ProjectOutputs>
|
||||||
|
<ProjectOutput>
|
||||||
|
<FullPath>C:\polygon-git\PolygonDLLUtilities\Debug\PolygonClientUtilities.dll</FullPath>
|
||||||
|
</ProjectOutput>
|
||||||
|
</ProjectOutputs>
|
||||||
|
<ContentFiles />
|
||||||
|
<SatelliteDlls />
|
||||||
|
<NonRecipeFileRefs />
|
||||||
|
</Project>
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,14 @@
|
||||||
|
#include "pch.h"
|
||||||
|
#include "Patches.h"
|
||||||
|
|
||||||
|
#include <detours.h>
|
||||||
|
|
||||||
|
LONG Patches::Apply()
|
||||||
|
{
|
||||||
|
DetourTransactionBegin();
|
||||||
|
|
||||||
|
for (Patch patch : patchList)
|
||||||
|
DetourAttach(&(PVOID&)*patch.first, patch.second);
|
||||||
|
|
||||||
|
return DetourTransactionCommit();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Patches
|
||||||
|
{
|
||||||
|
typedef std::pair<void**, void*> Patch;
|
||||||
|
|
||||||
|
extern std::vector<Patch> patchList;
|
||||||
|
|
||||||
|
LONG Apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
#define START_PATCH_LIST() std::vector<Patches::Patch> Patches::patchList = {
|
||||||
|
#define ADD_PATCH(a, b) { (void**)&(a), (b) },
|
||||||
|
#define END_PATCH_LIST() };
|
||||||
|
|
@ -90,11 +90,14 @@
|
||||||
<ConformanceMode>true</ConformanceMode>
|
<ConformanceMode>true</ConformanceMode>
|
||||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||||
|
<AdditionalIncludeDirectories>$(SolutionDir)Detours\include</AdditionalIncludeDirectories>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<EnableUAC>false</EnableUAC>
|
<EnableUAC>false</EnableUAC>
|
||||||
|
<AdditionalLibraryDirectories>$(SolutionDir)Detours\lib.X86;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||||
|
<AdditionalDependencies>detours.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||||
|
|
@ -107,6 +110,7 @@
|
||||||
<ConformanceMode>true</ConformanceMode>
|
<ConformanceMode>true</ConformanceMode>
|
||||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||||
|
<AdditionalIncludeDirectories>$(SolutionDir)Detours\include</AdditionalIncludeDirectories>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
|
|
@ -114,6 +118,8 @@
|
||||||
<OptimizeReferences>true</OptimizeReferences>
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<EnableUAC>false</EnableUAC>
|
<EnableUAC>false</EnableUAC>
|
||||||
|
<AdditionalLibraryDirectories>$(SolutionDir)Detours\lib.X86;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||||
|
<AdditionalDependencies>detours.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
|
|
@ -151,17 +157,21 @@
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="framework.h" />
|
<ClInclude Include="Patches.h" />
|
||||||
<ClInclude Include="pch.h" />
|
<ClInclude Include="pch.h" />
|
||||||
|
<ClInclude Include="RobloxMFCClasses.h" />
|
||||||
|
<ClInclude Include="RobloxMFCHooks.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="dllmain.cpp" />
|
<ClCompile Include="dllmain.cpp" />
|
||||||
|
<ClCompile Include="Patches.cpp" />
|
||||||
<ClCompile Include="pch.cpp">
|
<ClCompile Include="pch.cpp">
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="RobloxMFCHooks.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
<ImportGroup Label="ExtensionTargets">
|
<ImportGroup Label="ExtensionTargets">
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,16 @@
|
||||||
</Filter>
|
</Filter>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="framework.h">
|
<ClInclude Include="pch.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="pch.h">
|
<ClInclude Include="RobloxMFCHooks.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="RobloxMFCClasses.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="Patches.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
@ -29,5 +35,11 @@
|
||||||
<ClCompile Include="pch.cpp">
|
<ClCompile Include="pch.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="RobloxMFCHooks.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="Patches.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <oaidl.h>
|
||||||
|
|
||||||
|
// CWorkspace
|
||||||
|
// 2010: 0x0047EC10
|
||||||
|
// 2011: 0x0049FC90
|
||||||
|
|
||||||
|
class CWorkspace;
|
||||||
|
|
||||||
|
const auto CWorkspace__ExecUrlScript = (HRESULT(__stdcall*)(CWorkspace * workspace, LPCWSTR, VARIANTARG, VARIANTARG, VARIANTARG, VARIANTARG, LPVOID))0x0047EC10;
|
||||||
|
|
||||||
|
// CRobloxDoc
|
||||||
|
|
||||||
|
class CRobloxDoc
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
void* padding1[40];
|
||||||
|
public:
|
||||||
|
CWorkspace* workspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
// CRobloxApp
|
||||||
|
// 2010: 0x0044F6E0
|
||||||
|
// 2011: 0x0045D030
|
||||||
|
|
||||||
|
class CRobloxApp;
|
||||||
|
|
||||||
|
const auto CRobloxApp__CreateDocument = (CRobloxDoc * (__thiscall*)(CRobloxApp * _this))0x0044F6E0;
|
||||||
|
// const auto CRobloxApp__CreateGame = (CWorkspace * (__thiscall*)(CRobloxApp * _this, LPCWSTR))0x00405D20; // is CApp the same thing as CRobloxApp??
|
||||||
|
|
||||||
|
// CRobloxCommandLineInfo
|
||||||
|
// 2010: 0x007A80A0
|
||||||
|
// 2011: 0x0081354A
|
||||||
|
|
||||||
|
class CCommandLineInfo
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
void* padding1[3];
|
||||||
|
public:
|
||||||
|
BOOL m_bRunAutomated;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CRobloxCommandLineInfo : public CCommandLineInfo {};
|
||||||
|
|
||||||
|
const auto CCommandLineInfo__ParseLast = (void(__thiscall*)(CCommandLineInfo * _this, BOOL bLast))0x007A80A0;
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
#include "pch.h"
|
||||||
|
#include "RobloxMFCHooks.h"
|
||||||
|
|
||||||
|
static HANDLE handle;
|
||||||
|
static std::ofstream jobLog;
|
||||||
|
|
||||||
|
static bool hasJoinArg = false;
|
||||||
|
static bool hasJobId = false;
|
||||||
|
|
||||||
|
static std::wstring joinScriptUrl;
|
||||||
|
static std::string jobId;
|
||||||
|
|
||||||
|
// 2010: 0x00452900;
|
||||||
|
// 2011: 0x004613C0;
|
||||||
|
|
||||||
|
CRobloxApp__InitInstance_t CRobloxApp__InitInstance = (CRobloxApp__InitInstance_t)0x00452900;
|
||||||
|
|
||||||
|
BOOL __fastcall CRobloxApp__InitInstance_hook(CRobloxApp* _this)
|
||||||
|
{
|
||||||
|
if (!CRobloxApp__InitInstance(_this))
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (hasJoinArg && !joinScriptUrl.empty())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CRobloxDoc* document = CRobloxApp__CreateDocument(_this);
|
||||||
|
CWorkspace__ExecUrlScript(document->workspace, joinScriptUrl.c_str(), VARIANTARG(), VARIANTARG(), VARIANTARG(), VARIANTARG(), nullptr);
|
||||||
|
}
|
||||||
|
catch (std::runtime_error& exception)
|
||||||
|
{
|
||||||
|
MessageBoxA(nullptr, exception.what(), nullptr, MB_ICONERROR);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2010: 0x00450AC0;
|
||||||
|
// 2011: 0x0045EE50;
|
||||||
|
|
||||||
|
CRobloxCommandLineInfo__ParseParam_t CRobloxCommandLineInfo__ParseParam = (CRobloxCommandLineInfo__ParseParam_t)0x00450AC0;
|
||||||
|
|
||||||
|
void __fastcall CRobloxCommandLineInfo__ParseParam_hook(CRobloxCommandLineInfo* _this, void*, const char* pszParam, BOOL bFlag, BOOL bLast)
|
||||||
|
{
|
||||||
|
if (hasJoinArg && joinScriptUrl.empty())
|
||||||
|
{
|
||||||
|
int size = MultiByteToWideChar(CP_ACP, 0, pszParam, strlen(pszParam), nullptr, 0);
|
||||||
|
joinScriptUrl.resize(size);
|
||||||
|
MultiByteToWideChar(CP_ACP, 0, pszParam, strlen(pszParam), &joinScriptUrl[0], size);
|
||||||
|
|
||||||
|
_this->m_bRunAutomated = TRUE;
|
||||||
|
|
||||||
|
CCommandLineInfo__ParseLast(_this, bLast);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasJobId && jobId.empty())
|
||||||
|
{
|
||||||
|
jobId = std::string(pszParam);
|
||||||
|
jobLog = std::ofstream(jobId + std::string(".txt"));
|
||||||
|
|
||||||
|
AllocConsole();
|
||||||
|
freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);
|
||||||
|
handle = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||||
|
SetStdHandle(STD_OUTPUT_HANDLE, handle);
|
||||||
|
|
||||||
|
CCommandLineInfo__ParseLast(_this, bLast);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bFlag && _stricmp(pszParam, "j") == 0)
|
||||||
|
{
|
||||||
|
hasJoinArg = true;
|
||||||
|
CCommandLineInfo__ParseLast(_this, bLast);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bFlag && _stricmp(pszParam, "jobId") == 0)
|
||||||
|
{
|
||||||
|
hasJobId = true;
|
||||||
|
CCommandLineInfo__ParseLast(_this, bLast);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CRobloxCommandLineInfo__ParseParam(_this, pszParam, bFlag, bLast);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2010: 0x0059F340;
|
||||||
|
// 2011: 0x005B25E0;
|
||||||
|
|
||||||
|
StandardOut__print_t StandardOut__print = (StandardOut__print_t)0x0059F340;
|
||||||
|
|
||||||
|
void __fastcall StandardOut__print_hook(void* _this, void*, int type, const std::string& message)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case 1: // RBX::MESSAGE_OUTPUT:
|
||||||
|
jobLog << "[RBX::MESSAGE_OUTPUT] " << message.c_str() << std::endl;
|
||||||
|
SetConsoleTextAttribute(handle, FOREGROUND_BLUE | FOREGROUND_INTENSITY);
|
||||||
|
break;
|
||||||
|
case 0: // RBX::MESSAGE_INFO:
|
||||||
|
jobLog << "[RBX::MESSAGE_INFO] " << message.c_str() << std::endl;
|
||||||
|
SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
|
||||||
|
break;
|
||||||
|
case 2: // RBX::MESSAGE_WARNING:
|
||||||
|
jobLog << "[RBX::MESSAGE_WARNING] " << message.c_str() << std::endl;
|
||||||
|
SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_GREEN);
|
||||||
|
break;
|
||||||
|
case 3: // RBX::MESSAGE_ERROR:
|
||||||
|
jobLog << "[RBX::MESSAGE_ERROR] " << message.c_str() << std::endl;
|
||||||
|
SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_INTENSITY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
printf("%s\n", message.c_str());
|
||||||
|
SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
|
||||||
|
|
||||||
|
StandardOut__print(_this, type, message);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RobloxMFCClasses.h"
|
||||||
|
|
||||||
|
// CRobloxApp
|
||||||
|
|
||||||
|
typedef BOOL(__thiscall* CRobloxApp__InitInstance_t)(CRobloxApp* _this);
|
||||||
|
extern CRobloxApp__InitInstance_t CRobloxApp__InitInstance;
|
||||||
|
|
||||||
|
BOOL __fastcall CRobloxApp__InitInstance_hook(CRobloxApp* _this);
|
||||||
|
|
||||||
|
// CRobloxCommandLineInfo
|
||||||
|
|
||||||
|
typedef void(__thiscall* CRobloxCommandLineInfo__ParseParam_t)(CRobloxCommandLineInfo* _this, const char* pszParam, BOOL bFlag, BOOL bLast);
|
||||||
|
extern CRobloxCommandLineInfo__ParseParam_t CRobloxCommandLineInfo__ParseParam;
|
||||||
|
|
||||||
|
void __fastcall CRobloxCommandLineInfo__ParseParam_hook(CRobloxCommandLineInfo* _this, void*, const char* pszParam, BOOL bFlag, BOOL bLast);
|
||||||
|
|
||||||
|
// StandardOut
|
||||||
|
|
||||||
|
typedef void(__thiscall* StandardOut__print_t)(void* _this, int type, const std::string& message);
|
||||||
|
extern StandardOut__print_t StandardOut__print;
|
||||||
|
|
||||||
|
void __fastcall StandardOut__print_hook(void* _this, void*, int type, const std::string& message);
|
||||||
|
|
@ -1,19 +1,31 @@
|
||||||
// dllmain.cpp : Defines the entry point for the DLL application.
|
|
||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
|
#include "Patches.h"
|
||||||
|
#include "RobloxMFCHooks.h"
|
||||||
|
|
||||||
BOOL APIENTRY DllMain( HMODULE hModule,
|
START_PATCH_LIST()
|
||||||
DWORD ul_reason_for_call,
|
ADD_PATCH(CRobloxApp__InitInstance, CRobloxApp__InitInstance_hook)
|
||||||
LPVOID lpReserved
|
ADD_PATCH(CRobloxCommandLineInfo__ParseParam, CRobloxCommandLineInfo__ParseParam_hook)
|
||||||
)
|
ADD_PATCH(StandardOut__print, StandardOut__print_hook)
|
||||||
|
END_PATCH_LIST()
|
||||||
|
|
||||||
|
// To be added to game imports
|
||||||
|
void __declspec(dllexport) doNothing() {}
|
||||||
|
|
||||||
|
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||||
{
|
{
|
||||||
switch (ul_reason_for_call)
|
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
|
||||||
{
|
{
|
||||||
case DLL_PROCESS_ATTACH:
|
LONG error = Patches::Apply();
|
||||||
case DLL_THREAD_ATTACH:
|
if (error != NO_ERROR)
|
||||||
case DLL_THREAD_DETACH:
|
{
|
||||||
case DLL_PROCESS_DETACH:
|
#ifdef _DEBUG
|
||||||
break;
|
std::string message = "Patches::Apply returned " + std::to_string(error);
|
||||||
|
MessageBoxA(nullptr, message.c_str(), nullptr, MB_ICONERROR);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ExitProcess(EXIT_FAILURE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1 @@
|
||||||
// pch.cpp: source file corresponding to the pre-compiled header
|
|
||||||
|
|
||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
|
|
||||||
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
// pch.h: This is a precompiled header file.
|
#pragma once
|
||||||
// Files listed below are compiled only once, improving build performance for future builds.
|
|
||||||
// This also affects IntelliSense performance, including code completion and many code browsing features.
|
|
||||||
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
|
|
||||||
// Do not add files here that you will be updating frequently as this negates the performance advantage.
|
|
||||||
|
|
||||||
#ifndef PCH_H
|
#include <Windows.h>
|
||||||
#define PCH_H
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
// add headers that you want to pre-compile here
|
#include <string>
|
||||||
#include "framework.h"
|
#include <vector>
|
||||||
|
#include <stdexcept>
|
||||||
#endif //PCH_H
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
# PolygonDLLUtilities
|
# PolygonDLLUtilities
|
||||||
Manages DLLs for extending game client/server functionality
|
Manages DLLs for extending game client/server functionality
|
||||||
|
Based off [ndoesstuff/JoinScriptUrlImpl](https://github.com/ndoesstuff/JoinScriptUrlImpl) as per the MIT license
|
||||||
Loading…
Reference in New Issue