Injecting Vs. Wrapping:
What's the difference? Well there isn't much but I chose to use a wrapper and you'll see why very soon.
Wrapping:
As we saw earlier, our detour functions were in the form of wrappers. A wrapper to use was known at the time of reading, as a function with the same parameters, same return value but a different prefixed name. That was the only difference described between the wrapper and the original function.
Now we will talk about Wrapper DLL's. I know wtf right? Well it's the exact same concept.
A wrapper DLL is a copy of the original DLL. Throughout this entire tutorial, you've been writing a wrapper DLL without even noticing it! Your wrapper DLL will "wrap" specific functions within it and be loaded. Your wrapper DLL is a middle-man DLL and whenever the game wants to load OpenGL, it will load your DLL which will intern load the original OpenGL.dll
How does this happen? Well remember that .Def file I spoke of? That's the key to this. Our wrapper dll has wrapper functions for every single OpenGL32 function from the original. It's an exact copy except they're wrappers inside it. Next it redirects all calls from the original to ours. Well how is that done?
Here's an example of a .def file that does the work for us:
C++ Code:
LIBRARY OpenGL32 //We give our library the exact same name as the original!
EXPORTS //Our list of exports follow this call.
glAccum = GLHook_glAccum;
glAlphaFunc = GLHook_glAlphaFunc;
glAreTexturesResident = GLHook_glAreTexturesResident;
glArrayElement = GLHook_glArrayElement;
glBegin = GLHook_glBegin;
glBindTexture = GLHook_glBindTexture;
glBitmap = GLHook_glBitmap;
glBlendFunc = GLHook_glBlendFunc;
glCallList = GLHook_glCallList;
glCallLists = GLHook_glCallLists;
In the above, All calls to glAccum will be sent to GLHook_glAccum first. GLHook_glAccum will then use the information, log it, do whatever with it and then pass it to the original glAccum.
Lost yet? GLHook_glAccum is a middle-man between the game and the original opengl.dll. The equal sign is said to redirect calls! We do this for all 362 original functions so that all their calls go to our DLL first; which in turn passes it off to the original.
Next we place our DLL in the same folder as the game. Why? For portability reasons, there isn't any game that does:
"LoadLibrary("C:/windows/system32/opengl32.dll")"
..
No game programmer is stupid enough to do that as some systems do not have system32. Instead, they do:
"LoadLibrary("opengl32.dll")".
The system will automatically give it the closest opengl32.dll file. This is good for us! Why? Because it will look in its own folder for OpenGl32.dll first! If it isn't found then it will search the system for it! Thus the game will end up loading ours first =]
Whala we just successfully got the game to load our Wrapper/Hook DLL because it thinks that it's the original =].
Injection:
We haven't used this method but it is actually quite safe! Reason being is that if the game does decide to load specifically the system32 one, we can easily inject before that happens and force it to load ours! Yes that's right, we don't need to place it in the same folder!
First let's get to the basics of injection. How do we inject? Where do we inject? Well when injecting, you always want a nice place to start and a place that is always going to be there no matter what game you inject into.
The first is called Kernel32.dll.
Every program on Windows MUST load Kernel32.dll. Microsoft has guaranteed that and they have guaranteed that the address of it will always be the same no matter what!
Thus this is the best place to start injecting. The following code with comments explains how it's done:
C++ Code:
#include <Windows.h>#include <TlHelp32.h> //Include this file for grabbing process info.#include <iostream>using namespace std
;HANDLE hThread
; //Handle to a thread.void* pLibRemote
; //Pointer to a memory location.DWORD hLibModule
; //Handle to a module.HMODULE hKernel32
; //Handle to Kernel32.dllHANDLE hProcess
; //Process handle.PROCESSENTRY32 GetProcessInfo
(char* ProcessName
) //This function gets a snapshot of all running processes. It then searches those for the specified process and returns its info.{ HANDLE hSnap
= NULL
; PROCESSENTRY32 Proc32
= {0}; if((hSnap
= CreateToolhelp32Snapshot
(TH32CS_SNAPPROCESS
, 0)) == INVALID_HANDLE_VALUE
) return Proc32
; Proc32.
dwSize = sizeof(PROCESSENTRY32
); while(Process32Next
(hSnap
, &Proc32
)) { if(!strcmp(ProcessName
, Proc32.
szExeFile)) { CloseHandle
(hSnap
); return Proc32
; } } CloseHandle
(hSnap
); return Proc32
;}void Inject
() //This is how the injection is done.{ char szLibPath
[MAX_PATH
]; memset(szLibPath
, 0, MAX_PATH
); hKernel32
= ::GetModuleHandle("Kernel32"); //Get a handle to Kernel32.dll. PROCESSENTRY32 RSDLL
= GetProcessInfo
("JagexLauncher.exe"); //Find the process called JagexLauncher.exe. hProcess
= OpenProcess
(PROCESS_ALL_ACCESS
, false, RSDLL.
th32ProcessID); strcat(szLibPath
, "C:/Users/Brandon/Desktop/GLHook.dll"); //Open the process for writing and reading and get the path to our Wrapper/Hooking DLL. pLibRemote
= ::VirtualAllocEx(hProcess
, NULL
, sizeof(szLibPath
), MEM_COMMIT
, PAGE_READWRITE
); //Allocate space for our DLL to be injected. ::WriteProcessMemory(hProcess
, pLibRemote
, (void*)szLibPath
, sizeof(szLibPath
), NULL
); //Write the location of our DLL into the specified process. hThread
= ::CreateRemoteThread(hProcess
, NULL
, 0, (LPTHREAD_START_ROUTINE
) ::GetProcAddress(hKernel32
, "LoadLibraryA"), pLibRemote
, 0, NULL
); //When Kernel32.dll is loaded, create a thread that loads our DLL and wait until it is successful! ::WaitForSingleObject(hThread
, INFINITE
); ::GetExitCodeThread(hThread
, &hLibModule
); ::CloseHandle(hThread
); ::VirtualFreeEx(hProcess
, pLibRemote
, sizeof(szLibPath
), MEM_RELEASE
);}void Terminate
() //This is just cleanup stuff.{ hThread
= ::CreateRemoteThread(hProcess
, NULL
, 0, (LPTHREAD_START_ROUTINE
) ::GetProcAddress(hKernel32
, "FreeLibrary"), (void*)hLibModule
, 0, NULL
); ::WaitForSingleObject(hThread
, INFINITE
); ::CloseHandle(hThread
);}int main
() //Finally in our main we just inject.. We cleanup whenever the user decides to terminate the game. If the game terminates on its own then the dll cleans itself up.{ Inject
(); cin.
get(); Terminate
(); return 0;}
That's how it's done folks. Simply find the location of Kernel32.dll, write the path to your DLL into the process. The process will now load our DLL right after loading Kernel32.dll =].
Both the wrapper and the injection methods are safe! The reason why I chose the wrapper method is because it can be used both ways. You can inject your wrapper DLL and it will work exactly the same as if the game loaded it. With injection only, you'd have to re-write almost all functions to get it to load (naturally by the game) as a wrapper.
Simba & Lape Side:
So we have our OpenGL Wrapper DLL and the game loads it successfully. It grabs the data and stores it in a memory location where Simba can read it and use it! Wooohoo. Now how do we get Simba to use it?
Well first, Simba must load a DLL that can read from the memory location and interpret the data. The DLL will then give Simba all the data it needs in the form or pointers or however you decide.
GLHook.dll will thus need to have the same serialization and deserialization files. I'm not going to write it again, just copy paste from the previous section of this tutorial. It will also need to have the same structures so that it can reconstruct the data into those. Finally, it will have the same memory communication files. This is so that it knows where to grab the data from.
Thus the layout of GLHook.dll will look like the following..
SimbaPlugin.hpp:
C++ Code:
#ifndef SIMBAPLUGIN_HPP_INCLUDED
#define SIMBAPLUGIN_HPP_INCLUDED
#include <windows.h>
#include <iostream>
#include "Models.hpp" //Include the header that has all the model structs.
#include "Serialize.hpp" //Include the Serialization files.
#include "SharedMemory.hpp" //Include the Shared Memory files.
#include "MapStructures.hpp"
#define GLStatusSent 1 //You should remember these from before.
#define GLStatusReturned 2
#define GLHook_ViewPort 1
#define GLHook_Position 2
#define GLHook_Resizeable 3
#define GLHook_PanelItems 4
#define GLHook_GetText 5
#define GLHook_GetModels 6
#define GLHook_SetOverLay 7
#define GLHook_SetStride 8
#define GLHook_DrawAll 9
#define GLHook_CompassAngle 10
#define GLHook_Filter 11
#define NumberOfExports 17 //How many functions are we exporting so that Simba can use?
extern HINSTANCE hInstance;
extern char* Exports[];
GL_EXPORT int GetPluginABIVersion(); //Simba needs this.
GL_EXPORT int GetFunctionCount(); //Same as above.
GL_EXPORT int GetFunctionInfo(int Index, void*& Address, char*& Definition); //Same as above.
//Finally, these are the C++ versions of the function declarations Simba will see when the DLL is loaded!
GL_EXPORT bool GLHGetViewPort(int &X, int &Y, int &Width, int &Height);
GL_EXPORT bool GLHGetPosition(int X, int Y, float &WorldX, float &WorldY, float &WorldZ);
GL_EXPORT void GLHSetClientResizeable(bool Resizeable);
GL_EXPORT void* GLHGetPanelItems(int &VectorSize);
GL_EXPORT void* GLHGetItemsByID(int BaseID, int &VectorSize);
GL_EXPORT void* GLHGetItemsByArea(int X1, int Y1, int X2, int Y2, int& VectorSize);
GL_EXPORT void* GLHGetFonts(int& VectorSize);
GL_EXPORT void* GLGetFontsArea(int X1, int Y1, int X2, int Y2, int& VectorSize);
GL_EXPORT void* GLHGetModels(int& VectorSize);
GL_EXPORT void* GLHGetModelsByID(uint32_t ID, int &VectorSize);
GL_EXPORT void* GLHGetModelsByTCount(int TriangleCount, int &VectorSize);
GL_EXPORT void* GLHGetModelsByArea(int X1, int Y1, int X2, int Y2, int &VectorSize);
GL_EXPORT void GLHSetOverlay(bool Overlay);
GL_EXPORT void GLHSetStride(int Stride);
GL_EXPORT void GLHDrawAll(bool DrawAll);
GL_EXPORT void GLHCompassAngle(float &DegreeAngle, float& RadianAngle);
GL_EXPORT void GLHEnableFilter(bool EnableFilter, int FilterType, int ItemType, long ID, int R, int G, int B, int A, int X1, int Y1, int X2, int Y2);
//These are some structs for Simba. C++ doesn't always play nicely so I've made a couple changes to the Simba side of things.
typedef struct
{
int X, Y, Z;
} Simba3DVector;
typedef struct
{
GLuint ID;
GLubyte R, G, B, A;
GLfloat X, Y;
GLchar Symbol;
GLboolean Shadow;
GLfloat Translate[3];
GLint VX[4], VY[4];
GLint TX[4], TY[4];
} SimbaFont;
typedef struct
{
GLuint ID;
GLint SX, SY;
GLuint Stride;
GLint TriangleCount;
} SimbaModel;
extern std::vector<SimbaFont> ListOfSimbaFonts;
extern std::vector<SimbaModel> ListOfSimbaModels;
GL_EXTERN void __stdcall FontsToSimbaFonts(std::vector<FontInfo> &F, std::vector<SimbaFont> &SF);
GL_EXTERN void __stdcall ModelsToSimbaModels(std::vector<Model> &M, std::vector<SimbaModel> &SM);
#endif // SIMBAPLUGIN_HPP_INCLUDED
Ahh so that's our header! It includes everything we need to get Simba up and running and using our data. Notice that there is a function called Fonts To Simba Fonts and Models To Simba Models? This is because some structs actually don't have the same alignment as the Simba versions of them. Thus these are simple POD (Plain Old Data) structures. These will have the same alignment as the Simba/Pascal "Record" versions of them.
Now to implement the body:
C++ Code:
#include "SimbaPlugin.hpp"HINSTANCE hInstance
= NULL
;std
::vector<SimbaFont
> ListOfSimbaFonts
;std
::vector<SimbaModel
> ListOfSimbaModels
;char* Exports
[] = { //As you can see, the following is the Simba/Pascal definitions of the C++ functions. This basically converts the C++ functions to Pascal functions for use with Simba. They also act as wrappers. They have the same declarations and parameters as their C++ counterparts! (char*)"GLHGetViewPort", (char*)"Function GLHGetViewPort(var X, Y, Width, Height: Integer): Boolean;", (char*)"GLHGetPosition", (char*)"Function GLHGetPosition(X, Y: Integer; var WorldX, WorldY, WorldZ: Single): Boolean", (char*)"GLHSetClientResizeable", (char*)"Procedure GLHSetClientResizeable(Resizeable: Boolean);", (char*)"GLHGetPanelItems", (char*)"Function GLHGetPanelItems(var VectorSize: Integer): Pointer;", (char*)"GLHGetItemsByID", (char*)"Function GLHGetItemsByID(BaseID: Integer; var VectorSize: Integer): Pointer;", (char*)"GLHGetItemsByArea", (char*)"Function GLHGetItemsByArea(X1, Y1, X2, Y2: Integer; var VectorSize: Integer): Pointer;", (char*)"GLHGetFonts", (char*)"Function GLHGetFonts(var VectorSize: Integer): Pointer;", (char*)"GLGetFontsArea", (char*)"Function GLHGetFontsArea(X1, Y1, X2, Y2: Integer; var VectorSize: Integer): Pointer;", (char*)"GLHGetModels", (char*)"Function GLHGetModels(var VectorSize: Integer): Pointer;", (char*)"GLHGetModelsByID", (char*)"Function GLHGetModelsByID(ID: Cardinal; var VectorSize: Integer): Pointer;", (char*)"GLHGetModelsByTCount", (char*)"Function GLHGetModelsByTCount(TriangleCount: Integer; var VectorSize: Integer): Pointer;", (char*)"GLHGetModelsByArea", (char*)"Function GLHGetModelsByArea(X1, Y1, X2, Y2: Integer; var VectorSize: Integer): Pointer;", (char*)"GLHSetOverlay", (char*)"Procedure GLHSetOverlay(Overlay: Boolean);", (char*)"GLHSetStride", (char*)"Procedure GLHSetStride(Stride: Integer);", (char*)"GLHDrawAll", (char*)"Procedure GLHDrawAll(DrawAll: Boolean);", (char*)"GLHCompassAngle", (char*)"Procedure GLHCompassAngle(var DegreeAngle, RadianAngle: Single);", (char*)"GLHEnableFilter", (char*)"Procedure GLHEnableFilter(EnableFilter: Boolean; FilterType, ItemType: Integer; ID: LongWord; R, G, B, A, X1, Y1, X2, Y2: Integer);"};GL_EXPORT
int GetPluginABIVersion
(){ return 2;}GL_EXPORT
int GetFunctionCount
(){ return NumberOfExports
;}GL_EXPORT
int GetFunctionInfo
(int Index
, void*& Address
, char*& Definition
){ if (Index
< NumberOfExports
) { Address
= (void*)GetProcAddress
(hInstance
, Exports
[Index
* 2]); #ifdef _MSC_VER #pragma warning(disable: 4996) strcpy(Definition
, Exports
[Index
* 2 + 1]); //strcpy_s(Definition, Exports[Index * 2 + 1]); #else strcpy(Definition
, Exports
[Index
* 2 + 1]); #endif return Index
; } return -1;}GL_EXTERN
void __stdcall FontsToSimbaFonts
(std
::vector<FontInfo
> &F
, std
::vector<SimbaFont
> &SF
){ size_t I
= 0; ListOfSimbaFonts.
resize(F.
size()); for (std
::vector<FontInfo
>::iterator it
= ListOfFontData.
begin(); it
!= ListOfFontData.
end(); ++it
, ++I
) { SF
[I
].
ID = it
->ID
; SF
[I
].
R = it
->Colour.
R; SF
[I
].
G = it
->Colour.
G; SF
[I
].
B = it
->Colour.
B; SF
[I
].
A = it
->Colour.
A; SF
[I
].
X = it
->X
; SF
[I
].
Y = it
->Y
; SF
[I
].
Symbol = it
->Symbol
; SF
[I
].
Shadow = it
->Shadow
; SF
[I
].
Translate[0] = it
->Translate
[0]; SF
[I
].
Translate[1] = it
->Translate
[1]; SF
[I
].
Translate[2] = it
->Translate
[2]; for (int J
= 0; J
< 4; ++J
) { SF
[I
].
VX[J
] = it
->VX
[J
]; SF
[I
].
VY[J
] = it
->VY
[J
]; SF
[I
].
TX[J
] = it
->TX
[J
]; SF
[I
].
TY[J
] = it
->TY
[J
]; } }}GL_EXTERN
void __stdcall ModelsToSimbaModels
(std
::vector<Model
> &M
, std
::vector<SimbaModel
> &SM
){ size_t I
= 0; ListOfSimbaModels.
resize(M.
size()); for (std
::vector<Model
>::iterator it
= M.
begin(); it
!= M.
end(); ++it
, ++I
) { SM
[I
].
ID = it
->ID
; SM
[I
].
SX = it
->SX
; SM
[I
].
SY = it
->SY
; SM
[I
].
Stride = it
->Stride
; SM
[I
].
TriangleCount = it
->TriangleCount
; }}GL_EXPORT
void* GLHGetPanelItems
(int &VectorSize
) //returns a pointer to a list of all Items/Textures from the shared memory.{ double* Data
= static_cast
<double*>(RequestSharedMemory
()); //First we ask for the pointer to the shared memory if it isn't already busy. Data
[0] = GLHook_PanelItems
; //We write to the shared memory. We write that we want Items. Data
[1] = GLStatusSent
; //We also write the status. We write that we are sending a request for data! Notice we are basically writing to an array. ListOfItems.
clear(); //We clear our list and prepare for data. if (SharedDataFetched
()) //If the wrapper dll successfully sent the data to use via the shared memory.. { if (static_cast
<int
>(Data
[2]) != 0) //Data[2] lets use know if the data returned successfully. { unsigned char* SerializedData
= reinterpret_cast
<unsigned char*>(&Data
[3]); //Data[3] is a pointer to the location in the shared memory that contains the info the wrapper gave us. MemDeSerialize
(ListOfItems
, SerializedData
, size_t(Data
[2])); //We DE-serialize the bytes and reconstruct it into Models. Those models are stored in a vector. VectorSize
= int(Data
[2]); //Data[2] is the pointer to the shared memory that contained the size of the data the wrapper gave us. return ListOfItems.
data(); //Finally we give simba a pointer to the reconstructed data! } return NULL
; } return NULL
;}GL_EXPORT
void* GLHGetItemsByID
(int BaseID
, int
& VectorSize
){ GLHGetPanelItems
(VectorSize
); //We can now grab all Items from shared memory. std
::vector<PanelItem
> ItemsFound
; //We create a vector to hold all items that have a specific ID. for (std
::vector<PanelItem
>::iterator it
= ListOfItems.
begin(); it
!= ListOfItems.
end(); ++it
) { if (it
->BaseID
== BaseID
) //If the data has the same ID as the one Simba asked for, add it to the vector/list. { ItemsFound.
push_back(*it
); } } ListOfItems
= ItemsFound
; VectorSize
= ListOfItems.
size(); //Return the size of the list to simba. return ListOfItems.
data(); //Give simba a pointer to the list.}
Basically, we write to shared memory asking it for data. The wrapper dll will then look at its collection of data and write back whatever we requested. We then read the location that it wrote back to and give it to Simba.
Thus the steps are as follows:
1. Write a letter to the wrapper begging it to give us Models on array index 1.
2. Tell it that we are requesting data on array index 2, not sending.
3. Wrapper will check array index 1 to see what the letter is about.
4. Wrapper will check array index 2 to see if it's a request.
5. If it was a request, it will write to index 3 the serialized data that was requested.
6. It will write to index 2 that it is Sending data, not requesting.
7. It will write to data[4] the size of the data it sent.
8. SimbaPlugin will read data[2] to see if it was actually sent.
9. SimbaPlugin will read data[4] to see the size of the data sent.
10. Simba Plugin will deserialize the data in data[3] and reconstruct/use it.
The 10 Simple steps can be seen in the following diagram (Please note that it's up to you what channel/index you want the data on:
Writing An Include. Functions, Pointers, Etc..:
Ahh so we finally got to the Simba/Pascal side of things. Don't worry, we aren't out of the C++ world yet as that's quite important.
Let's discuss Pointers first as that seems to be what most people have trouble on.
Pascal script doesn't allow Pointers. Thus we cannot use the data that SimbaPlugin gives us! OMG!! Well the solution is Pascal.. Not Pascal Script. Lape is a Pascal Interpreter and attempts to emulate Pascal itself!
Thus in Lape, we can use pointers.
Lets dive in shall we?
The first thing I'll teach about pointers is the difference between the three kinds. Yes Pascal has 4 kinds of pointers/reference operators whereas C++ has 3.
The first kind is the address of operator. In Pascal, there are two address operators. Well technically one but for our purposes I'd like you to know that you already know one of them.
It is the var operator. The var operator means pass by reference. This operator can be used to return multiple values from a function in pascal. In C++, this is almost equivalent to the & (address-of) operator. Thus when translating a function from C++ to Pascal, if you see int &X, you must replace that with var on the pascal side.
The second operator is the @ operator. Also referred to as the address-of operator. This allows a user to grab the address of a function/procedures. In C++, this is equivalent to the typedef stuff we did earlier. It essentially allows us to create function pointers and variable pointers! Also equivalent to &X when used on a variable in C++.
The third is the ^ operator. This operator can be used as a suffix or a prefix. This operator is equivalent to the pointer/de-reference * operator in C++.
When used as a prefix, it is said that the caller is a pointer to a type of callee. When used as a suffix, it is said to de-reference the data that the pointer points to.
I know that was quite confusing so I'll include some code:
Simba Code:
type pByte = ^Byte; //pByte is a pointer to a Byte.
type pInt = ^Integer; //pInteger is a pointer to an int.
type pBool = ^Boolean; //Same concept..
type pFloat = ^Single; //Same concept.. Note a Single is equivalent to a float in C++.
var
Foo: ^Integer; //Foo is a pointer to an integer.
Meh: pInt; //Exactly the same as above. Also a pointer to an integer.
Those are pointers to a type as you can see. How do we de-reference? Well we use the ^ operator as a suffix instead of a prefix!
Simba Code:
var
Foo: ^Integer;
Boo: Integer;
begin
Boo := 191;
Foo := @Boo; //Make Foo pointer to Boo.
WriteLn( Foo^ ); //Should print 191. Notice that ^ is after Foo.
end.
When used as a suffix, it essentially says: "Get the data pointed to". Foo points at some data. Foo^ is equivalent to the data that Foo points to. Thus Foo^ == 191 or Foo^ == Boo.
Finally the Fourth type of Pointer is actually "Pointer".
A "Pointer" in pascal can point to ANYTHING! It is equivalent to a void pointer in C++ (void* )..
A Pointer has no type in pascal. Thus the following can be used:
Simba Code:
var
Foo: Pointer;
Bleh: ^Integer;
Gah: Integer;
begin
Gah := 177;
Foo := @Gah; //Foo points to Gah.
Bleh := Foo; //Bleh Points to Foo/Gah.
Writeln( Bleh^ ); //Should print 177.
end.
Another example is as follows:
Simba Code:
type TModel = record
ID: Cardinal;
SX, SY: Integer;
Stride: Cardinal;
TriangleCount: Integer;
end;
var
Foo: Pointer;
Meh: ^TModel;
begin
Foo := GLH_GetModelByID(145252); //This function returns a "Pointer".
Meh := Foo;
writeln(Meh^); //Would print everything about the model.
//The above is the same as:
Meh := GLH_GetModelByID(145252);
writeln(Meh^);
end.
The difference in the two examples above is that Foo can be iterated whereas Meh cannot! Thus Foo is probably more useful. Example is that if the function returned a pointer to an array of models, Foo will have to be used!
Example:
Simba Code:
type TModel = record
ID: Cardinal;
SX, SY: Integer;
Stride: Cardinal;
TriangleCount: Integer;
end;
type TModelArray = array of TModel;
Function GLHook_GetAllModels: TModelArray;
var
Ptr: Pointer;
Size, I: Integer;
ModelPtr: PModel;
begin
Ptr := GLHGetModels(Size); //As you can see, grab a pointer to an array of models.
SetLength(Result, Size);
For I := 0 To Size - 1 Do
begin
ModelPtr := Ptr;
Result[I] := ModelPtr^; //Dereference it and add it to the array.
Ptr := Ptr + sizeof(TModel); //Move onto the next model in the pointed array.
end;
end;
As you can see above, Pointer was very useful as that was the only way we could move to the next model in the array pointed to.
I really hope you guys enjoyed my extremely LONG tutorial! Please leave some feedback. If you request any knowledge, please leave a comment. If I did not see your comment or you want to speak directly with me, please leave me a PM and I'll get back to you whenever I can.
For More Info on GLHook see the source: https://github.com/Brandon-T/GLHook
Also note that you should NOT have to write all 362 functions at all! I have a generator that does that in the same link. Just click on OGL-Editing to see how it's generated. The source is: Click Here To View The Link.
I'm sorry if my tutorial isn't very pretty. I didn't have much time. Also didn't have much space to put everything so if anything is hard to understand/read, just leave some feedback and I'll try to fix it up.