posted 01-09-18 11:45 AM EDT (US)   
Starting from Unhardcode Patch 1.5, we support loading plugins in the form of DLL, which modders can implement their own XS functions or cheat codes to the game.

Basic setup
1. Before writing the plug-in, clone into our Github repository, you will need the files from Documentation\Include.

2. Setup a C++ DLL project in your IDE, and include "UHCPlugin.h" and "syscalls.h" to your headers, while add "syscalls.cpp" to your compile sources.
(We recommend using Visual Studio as the IDE for better compatibility.)

3. Define UHCPluginMain() in the main source, which will be called by UHC to register syscalls and cheat codes.
(Note: Do not define UHCPluginMain() as the module entry point, if you initialized data that requires clean up, use DllMain() for this purpose.)


extern "C" __declspec(dllexport)
int UHCPluginMain(UHCPluginInfo *pluginInfo)
{
// Register your syscalls and cheat codes here...
return 0;
}


4. Compile the DLL and output it to the same directory of UHC.dll which is the root directory of your AoE3 installation. Also make sure you change the extension from ".dll" to ".upl".

Registering syscalls

Syscalls are AoE3 functions used in XS scripting and UI customization. They can be separated into 8 groups as shown in the code:


/**
* @brief XS function groups.
*/
enum UHCSyscallGroupName {
/**
* @brief Functions used in AI scripts.
*/
GroupAI,
/**
* @brief Functions used in homecity scripts.
*/
GroupHC,
/**
* @brief Functions used in AI scripts and triggers.
*/
GroupKB,
/**
* @brief Functions used in random map scripts.
*/
GroupRM,
/**
* @brief Functions used in triggers.
*/
GroupTR,
/**
* @brief Functions used in UI xmls and triggers.
*/
GroupUI,
/**
* @brief Functions used in UI xmls.
*/
GroupUI2,
/**
* @brief Functions used in all XS scripts and triggers.
*/
GroupXS
};

struct UHCPluginInfo {

/**
* @brief Adds a new cheat code to AoE3.
*
* @param stringCheat code with ASCII only and lower case characters.
*
* @param enableAlways set to true.
*
* @param fPtrCheat function pointer.
*
*/
void(*const RegisterCheat)(const char* string, bool enable, void(CHEATCALL * fPtr)(void*));

/**
* @brief Adds a XS function to the given function group.
*
* @param retTypeReturn type of the the function.
*
* @param nameThe function name in XS.
*
* @param fPtrThe function pointer.
*
* @param paramCountCount of function parameters.
*
* @param commentThe function comment.
*
* @return returns the reference of UHCSyscall.
*/
UHCSyscall&(*const RegisterSyscall)(unsigned int groupName, unsigned int retType, const char* name, const void* fPtr, unsigned int paramCount, const char* comment);

/**
* @brief Sets the XS function parameter.
*
* @param syscallThe reference returned by RegisterSyscall.
*
* @param paramIdIndex of the parameter in the function.
*
* @param typeParameter type.
*
* @param defaultValPointer to default value, must be accessible globally.
*/
void(*const SyscallSetParam)(UHCSyscall& syscall, unsigned int paramId, unsigned int type, const void* defaultVal);

bool(*const CheatAddResource)(void* playerData, int resourceID, float resourceAmount, bool unk);

void(*const CheatSpawnUnit)(void* playerData, const char* protoUnitName);
};


For example, you want to implement trigonometric functions in the AI or triggers to measure distances between units as AoE3 doesn't provide them by default:


UHCSyscall& sSin = pluginInfo->RegisterSyscall(GroupKB, SyscallFloat, "sin", sinf, 1, "sin");
pluginInfo->SyscallSetParam(sSin, 0, SyscallFloat, &g_ZeroFloat);


And well, it might be possible that a syscall originally in a different group will work under the group, ex. it was originally not possible to repair buildings in the AI, but by using this technique, it does work:


UHCSyscall& sAiRepairUnit = pluginInfo->RegisterSyscall(GroupAI, SyscallVoid, "aiRepairUnit", repairUnit, 1, "aiRepairUnit");
pluginInfo->SyscallSetParam(sAiRepairUnit, 0, SyscallInteger, &g_InvalidUnitID);


And the sample xs code:


rule repairTest
active
minInterval 30
{
int unitID = getUnit(cUnitTypeTownCenter, cMyID);
if (unitID >= 0)
{
if (kbUnitGetCurrentHitpoints(unitID) < 6400)
{
aiRepairUnit(unitID);
int i = 0;
for (i = 1; < cNumberPlayers)
{
aiChat(i, "Attempting to repair town center of player "+cMyID);
}
}
}
}


As of RM scripts, since it doesn't make use of any rules or event handlers, it is possible to pack the entire map creation inside a syscall and just simply call the function in main() of a RM script.
Mostly because you can take advantage of auto completion and syntax highlighting in modern C++ IDEs, and debugging in IDEs is simply more convenient than the built-in XS debugger.

Registering Cheats

Every cheat code is stored as an encrypted unicode string internally, when the game detects chat message matches the cheat code, the related cheat function is executed.

To add resource or spawn units at town center like most cheats, you can use the functions provided by pluginInfo. However, to create special cheat effects, like transforming, teleporting, trigger syscalls are good for these purposes.
Besides the fun part of cheat codes, they can also be useful to enable editor features like the AIDebug info menu. In the example, with the help of trSetPlayerActive(), we can see information provided by the debug menu in all AI players.


#define CHEAT_PLAYER(i) void CHEATCALL player ## i ## (void*) { trPlayerSetActive(i); }
#define REGISTER_CHEAT_PLAYER(i) pluginInfo->RegisterCheat(#i, true, player ## i)

CHEAT_PLAYER(1);
CHEAT_PLAYER(2);
CHEAT_PLAYER(3);
CHEAT_PLAYER(4);
CHEAT_PLAYER(5);
CHEAT_PLAYER(6);
CHEAT_PLAYER(7);
CHEAT_PLAYER(8);

void CHEATCALL AIDebugInfo(void*)
{
AIDebugInfoToggle();
}


And well, for other examples for syscalls and cheat codes, refer to the VS Project in Documentation\Examples\UHCPluginDemo.