Headers
Introduction
Headers allow us to define a few things. They allow us to use cleaner manually created types than those provided in il2cpp.h
and they allow us to reference methods in the main program.
il2cpp.h
The il2cpp.h
header includes all the types defined in the main program but they are not very pretty, they're all in the global namespace and have poor names. We do not include that file in the repo as we want to have clean types that are easier to deal with. When you want to use a specific type that is not available yet, you should create a new header in the src/mod/externals
directory.
Complete Example
See here an example header file for the PokemonParam Class. It includes function definitions, a super class, nested namespaces, the TypeInfo, etc.
#pragma once
#include "externals/il2cpp-api.h"
#include "externals/Pml/PokePara/CoreParam.h"
#include "externals/Pml/PokePara/InitialSpec.h"
#include "externals/System/Primitives.h"
namespace Pml::PokePara {
struct PokemonParam : ILClass<PokemonParam, 0x04c59c10> {
struct Fields : CoreParam::Fields {
// No new fields
};
struct StaticFields {
System::Byte_array* sParamSerializeBuffer;
};
inline void ctor(int32_t monsno, uint16_t level, uint64_t id) {
external<void>(0x02054fe0, this, monsno, level, id);
}
inline void ctor(Pml::PokePara::InitialSpec::Object* spec) {
external<void>(0x02055140, this, spec);
}
};
}
The offset given as a template argument to ILClass
points to the TypeInfo of the type you're defining. This is generally only needed if you need to access static fields or need to create a new instance.
If the type you're defining has a parent type (when the first field is named super
in il2cpp.h
), you should inherit its fields by making the inner Fields
struct that you're defining a derived struct of the Fields
struct of the parent type. If that was too confusing to follow, just remember that this is how you do it:
struct Fields : Parent::Fields {
// New fields
};
ILClass vs ILStruct
There are a few ways to determine if the header you're creating should be an ILClass
or an ILStruct
.
In il2cpp.h
You can check the Object
struct for the type you're creating in il2cpp.h
(The one that ends in _o
). If there are both a klass
and monitor
field, then an ILClass
should be used. If there is only the fields
field, then an ILStruct
is appropriate.
In dump.cs
The type you're looking for will be defined either as a class
or a struct
in this file. No need to think too much about it: class
corresponds to ILClass
and struct
corresponds to ILStruct
.
Referencing internal functions
Referencing internal functions is usually pretty simple.
Non-static function
Here is an example for a non-static function:
inline void ctor(Pml::PokePara::InitialSpec::Object* spec) {
external<void>(0x02055140, this, spec);
}
external<T>()
does most of the work for you. All you have to do is give it, in that order:
- The offset for the start of the function
this
, the current instance of the object- All the arguments to call the function with, in order
If there is a return type that is not void
, you can simply return the result of external<T>()
directly. Make sure you do not return if the return type is void
! This is undefined behavior and could cause crashes.
Static function
For a static function, the format is similar but changes slightly:
static inline bool IsGet(int32_t id) {
return external<bool>(0x01d603e0, id);
}
The two differences are the addition of the static
keyword at the front of the function, and not passing this
to external<T>()
. The rest is the same.
You can generally tell if a function is static or not by its arguments. If the first argument in ghidra is of the class's type and has the name __this
, then it's usually non-static.
MethodInfo
99% of the time, we can ignore the MethodInfo argument that is always the last argument of a function. It'll always be 0 or null.
In the rare chance that the MethodInfo of a function you wish to call is needed, its type will generally have the offset of the function appended to it (something like MethodInfo_1CFA100
).
You can declare the function similarly to this:
template <typename T>
inline T::Object* GetCurrentUIWindow(ILMethod<T>& method) {
return external<typename T::Object*>(0x01cfa100, this, *method);
}
In the ILClass
, you should define a StaticILMethod
to use when calling:
static inline StaticILMethod<0x04c90130, Dpr::UI::UIWindow> Method$$GetCurrentUIWindow_UIWindow_ {};
And when calling it, you can pass in this StaticILMethod
like so:
auto window = uiManager->GetCurrentUIWindow(Dpr::UI::UIManager::Method$$GetCurrentUIWindow_UIWindow_);
Usage
When using these headers you've created, there's a few things to keep in mind.
Ghidra type equivalence
Types that end in _o
refer to an Object. You'll want to declare your variables similarly to this:
Pml::PokePara::PokemonParam::Object* pokeParam = //...
Types that end in _c
refer to a Class. You can access it through the getClass()
static function, when the TypeInfo is defined.
Pml::PokePara::PokemonParam::Class* pokeParamClass = Pml::PokePara::PokemonParam::getClass();
Types that end in _Fields
refer to the fields. You can access them through the Object like this:
Pml::PokePara::PokemonParam::Fields pokeParamFields = pokeParam->fields;
Types that end in _StaticFields
refer to the static fields. You can access them through the Class like this:
Pml::PokePara::PokemonParam::StaticFields* pokeParamStaticFields = pokeParamClass->static_fields;
Types that end in _array
refer to an array of Objects. You can declare one like so:
Pml::PokePara::PokemonParam::Array* pokeParamArray = //...
New instance
You can create new Object instances with the newInstance()
function. It will call the defined ctor
function that matches the given arguments. For example, for a defined ctor
that looks like this:
inline void ctor(Pml::PokePara::InitialSpec::Object* spec) {
external<void>(0x02055140, this, spec);
}
You can create a new instance (in this case, it's a Pml::PokePara::PokemonParam::Object*
) like so:
Pml::PokePara::InitialSpec::Object* initialSpec = //...
Pml::PokePara::PokemonParam::Object* param = Pml::PokePara::PokemonParam::newInstance(initialSpec);
And this will automatically call the appropriate memory allocation functions and the ctor
function defined above.