Skip to content

Orpheu Module (v2.5.1)

The Orpheu module lets a scripter use functions that would be impossible to use without it (including functions from other MetaMod plugins). The module works on both Windows and Linux. Starting with version 2.1 (January 29, 2010), Orpheu also supports direct memory access…

Coding·9 min read·Dani MinchViews:26

The Orpheu module lets a scripter use functions that would be impossible to use without it (including functions from other MetaMod plugins). The module works on both Windows and Linux.
Starting with version 2.1 (January 29, 2010), Orpheu also supports direct memory access.

Here is how it works:
The module converts between C++ and Pawn types, and lets you call a function by specifying its address in memory and the arguments it uses. This method includes the general principles of working with C++ structures.

For example, suppose you want to use this function:
void PM_Move( struct playermove_s *ppmove, qboolean server)

It takes one argument of type "playermove_s" and another of type "qboolean". The function returns no value.

As long as the types the function needs are supported by the module, you can use it. If one of the types is not supported, you can hook it directly and still keep working with the function, for example hook it, since when hooking a function you don't need to care about all of its arguments.

There are several ways to find functions in libraries:

  • by searching by name: in libraries compiled for Linux, the symbolic names of functions are attached to the functions themselves. Unfortunately, this is not the case if the library is compiled for Windows, which makes this method useless unless you have the library's source and can compile it yourself so that it exports symbolic names for the functions. This article describes methods for using the functions of the MonsterMod module.
  • by finding a set of bytes in memory that belongs specifically to the given function. This method is called "signature scanning". In general, this method is described here, and in more detail here and here. In short, this method lets you reach any function in memory once you have found it. To really understand it, you need to learn a bit more about this method, which you can do through the links above. (Translator's note: these articles will soon be translated on our site; once we do, we will replace the links)
  • by obtaining addresses while the plugin is running. If you can find the address of a function programmatically, you can use this option. This is possible because the module uses the engine's core functions. These functions are provided in AMXModX even without this module, but with it you can intercept these addresses in most cases (for example, when the functions are called by other MetaMod plugins).

Added in 2.1

  • by looking up function offsets in the library. This method is used for quick testing and should not be relied on, since the offsets found may turn out to be wrong when the library is updated or changed.
  • by finding the function pointer in a class's virtual function table. This method lets you use many functions by finding just one pointer. The method was taken from Hamsandwich and reworked for use in Orpheu. It makes it possible to hook a virtual function of an entity and of an object, for example CGameRules. Compared to the Hamsandwich version, the following was added: the set of hard-coded functions is no longer limited, and you can also hook some mod-dependent functions that could not be hooked before.

To give the module information about a function, you need to create a file formatted according to the JSON standard and place it in the "configs/orpheu/functions" folder.

For the function:
CMBaseMonster* spawn_monster(int monster_type, Vector& origin, float angle, int respawn_index)

The file contents will be as follows:

C++
{
    "name" : "spawn_monster",
    "library" : "monstermod",
    "info" : "Spawns a monstermod monster in the world",
    "arguments" :
    [
        {
            "type" : "int",
            "info" : "The type of the monster"
        },
        {
            "type" : "Vector &",
            "info" : "The position where the monster will be placed at"
        },
        {
            "type" : "float",
            "info" : "Angle of the monster"
        },
        {
            "type" : "int",
            "info" : "Respawn index. Related to the order of a monster respawning if failed to spawn at the first try"
        }
    ],
    "return" :
    {
        "type" : "CMBaseMonster *",
        "info" : "The monster created"
    },
    "identifiers":
    [
        {
            "os" : "windows",
            "value" : "?spawn_monster@@YAPAVCMBaseMonster@@HAAVVector@@MH@Z"
        },
        {
            "os" : "linux",
            "value" : "_Z13spawn_monsteriR6Vectorfi"
        }
    ]
}
  • the "info" fields are optional;
  • the "name" field must match the file name;
  • the library name is "mod" for mods like cstrike, and “engine” for the engine DLL. For MetaMod libraries you need to create a file like the one you can find in the "configs\orpheu\libraries" directory, which contains a libraryname/libraryCvar pair, so that the module is recognized as one of the Cvar modules.
  • “identidiers” is a list of "os"/"value" groups for identifying the function. If the library is “mod”, you need to add an extra “mod” field. Although the meaning of this field is purely formal, the field itself is required. It should look like this: "mod" : "cstrike"

In this case, name lookup is used to reference the function. When using signatures, the “value” field will be a sequence of bytes, "*" or "?", for example:
"value" : [0x1,"*","?"]

"*" must be used when the value of those bytes does not matter.

That is, the value
"value" : [0x1,"*"]
is equivalent to
[0x1,0x0] , [0x1,0x1] , ... [0x1,0xFF].

"?" must be used when neither the value of those bytes nor even their existence matters.
That is,
"value" : [0x1,"?"]
assumes the values
[0x1] , [0x1,0x0] , [0x1,0x1] , ... [0x1,0xFF].

JSON is a widely used file type. To make sure a file is formatted correctly, you can use a validator. In this project I modified the library a little so that JSON could read bytes. This is done to validate files that do not contain signatures.

Functions that belong to a class deserve separate mention, for example:
void CMController :: HandleAnimEvent( MonsterEvent_t *pEvent )

In this case you need to create a folder named "CMController", and inside it a file "HandleAnimEvent". This is mandatory and is needed for clearer organization. You also need to add a “class” field to the file. The resulting file will look like this:

C++
{
    "name" : "HandleAnimEvent",
    "class" : "CMController",
    "library" : "monstermod",
    "arguments" :
    [
        {
            "type" : "MonsterEvent_t *"
        }
    ],
    "identifiers":
    [
        {
            "os" : "windows",
            "value" : "?HandleAnimEvent@CMController@@UAEXPAUMonsterEvent_t@@@Z"
        },
        {
            "os" : "linux",
            "value" : "_ZN12CMController15HandleAnimEventEP14MonsterEvent_t"
        }
    ]
}

Another way of using functions involves obtaining the address programmatically. Take a look at http://metamod.org/sdk/dox/eiface_8h-source.html - line 100. It shows the "enginefuncs_s" structure. This structure contains the addresses of engine functions. The module is able to operate on it. Say we want to hook a function located in the structure:
void (*pfnServerPrint)( const char *szMsg );

You need to create this file:

C++
{
    "name" : "ServerPrint",
    "library" : "engine",
    "arguments" :
    [
        {
            "type" : "char *"
        }
    ]
}

Identifiers are not needed, since the address will be found programmatically at startup. In the plugin you will have:
native OrpheuFunction:OrpheuCreateFunction(address,libFunctionName[],classname[]="")

Then you can hook or call it. When hooking the function, you can intercept the console message, the same one that appears when using the server command "metamod".
This is one variant (hand-coded) of the method that uses a function whose address is in the plugin. Another variant is to use OrpheuGetDLLFunction to retrieve functions from the DLL_FUNCTIONS structure. http://metamod.org/sdk/dox/eiface_8h-source.html - line 384. For ordinary cases use the native:
native OrpheuFunction:OrpheuCreateFunction(address,libFunctionName[],classname[]="")

The module lets you work with structures in the usual way. Say we need to hook the PM_Move function, which can be found in the DLL_FUNCTIONS structure. The file:

C++
{
    "name" : "PM_Move",
    "library" : "mod",
    "arguments" :
    [
        {
            "type" : "playermove_s *"
        },
        {
            "type" : "qboolean"
        }
    ]
}

The hook:

Pawn
public plugin_init() 
{ 
    new OrpheuFunction:PM_Move = OrpheuGetDLLFunction("pfnPM_Move","PM_Move") 
    OrpheuRegisterHook(PM_Move,"OnPM_Move") 
} 

public OnPM_Move(ppmove,server) 
{ 
     
}

The function's first argument is a structure. You can see it here. http://metamod.org/sdk/dox/eiface_8h-source.html - line 92. Next we deal with the structure data:

C++
public OnPM_Move(ppmove,server) 
{ 
    new id = OrpheuGetParamStructMember(1,"player_index") + 1 
    new Float:friction = OrpheuGetParamStructMember(1,"friction") 
     
    new sztexturename[20] 
    OrpheuGetParamStructMember(1,"sztexturename",sztexturename,charsmax(sztexturename)) 
     
    server_print("ID %d friction %f texturename %s",id,friction,sztexturename) 
}

For more information on working with structures, see the links. The same structures contain addresses of other functions. Below is an example of how to hook one of them:

C++
{
    "name" : "PM_PlaySound",
    "library" : "engine",
    "arguments" :
    [
        {
            "type" : "int"
        },
        {
            "type" : "char *"
        },
        {
            "type" : "float"
        },
        {
            "type" : "float"
        },
        {
            "type" : "int"
        },
        {
            "type" : "int"
        }
    ]
}
Pawn
new OrpheuHook:PM_PlaySoundHook 

public plugin_init() 
{ 
    new OrpheuFunction:PM_Move = OrpheuGetDLLFunction("pfnPM_Move","PM_Move") 
    OrpheuRegisterHook(PM_Move,"OnPM_Move") 
    OrpheuRegisterHook(PM_Move,"OnPM_MovePost",OrpheuHookPost) 
} 

public OnPM_Move(ppmove,server) 
{ 
    // Retrieves the address of the function to hook 
    new PM_PlaySoundAddress = OrpheuGetParamStructMember(1,"PM_PlaySound") 
     
    // Creates the function in the module  
    new OrpheuFunction:PM_PlaySound = OrpheuCreateFunction(PM_PlaySoundAddress,"PM_PlaySound") 
     
    // Hooks it 
    PM_PlaySoundHook = OrpheuRegisterHook(PM_PlaySound,"OnPM_PlaySoundHook") 
} 

public OnPM_PlaySoundHook(channel,sample[],Float:volume,Float:attenuation,fFlags,pitch) 
{ 
    server_print("Sample %s",sample) 
} 

public OnPM_MovePost(ppmove,server) 
{ 
    OrpheuUnregisterHook(PM_PlaySoundHook) 
}

List of supported data types:

C++
    "bool"
    "byte"
    "long"
    "CBaseEntity *"
    "char *"
    "edict_s *"
    "float"
    "Vector *"
    "CMBaseMonster *"
    "char"
    "short"
    "entvars_s *"

List of supported structures:

C++
    "movevars_s *"
    "usercmd_s *"
    "MonsterEvent_t *"
    "DLL_FUNCTIONS *"
    "playermove_s *"
    "enginefuncs_t *"
    "TraceResult *"
    "physent_s *"
    "pmplane_s *"
    "pmtrace_s *"
    "weapon_data_s *"
    "AmmoInfo *"
    "ItemInfo *"
    "Task_t *"
    "Schedule_t *"

I will add some practical examples later. If the module does not start on your Linux machine, you should install libstdc++.
The following material applies to Orpheu 2.1 and above.
Version 2.1 added support for virtual functions (based on Hamsandwich) and memory search/patching (based on Mem Hack).

Virtual functions:
Virtual functions are functions that provide shared resources to several different classes (like Spawn) and, accordingly, implement that provision differently. The way the functions are compiled lets you locate them in memory by giving them simple numeric offsets. References to the functions are stored in a table so that every object of the class can interact with them.
This is the table for the Counter Strike mod, recovered from the native Linux file of the CHalfLifeMultiplay class for CGameRules. (Each line contains the symbolic name of the function it represents):

C++
Think__18CHalfLifeMultiplay 
IsAllowedToSpawn__18CHalfLifeMultiplayP11CBaseEntity  
FAllowFlashlight__18CHalfLifeMultiplay 
FShouldSwitchWeapon__18CHalfLifeMultiplayP11CBasePlayerP15CBasePlayerItem 
GetNextBestWeapon__18CHalfLifeMultiplayP11CBasePlayerP15CBasePlayerItem 
IsMultiplayer__18CHalfLifeMultiplay 
IsDeathmatch__18CHalfLifeMultiplay 
IsTeamplay__10CGameRules 
IsCoOp__18CHalfLifeMultiplay 
GetGameDescription__10CGameRules 
ClientConnected__18CHalfLifeMultiplayP7edict_sPCcT2Pc 
InitHUD__18CHalfLifeMultiplayP11CBasePlayer 
ClientDisconnected__18CHalfLifeMultiplayP7edict_s 
UpdateGameMode__18CHalfLifeMultiplayP11CBasePlayer 
FlPlayerFallDamage__18CHalfLifeMultiplayP11CBasePlayer 
FPlayerCanTakeDamage__18CHalfLifeMultiplayP11CBasePlayerP11CBaseEntity 
ShouldAutoAim__10CGameRulesP11CBasePlayerP7edict_s 
PlayerSpawn__18CHalfLifeMultiplayP11CBasePlayer 
PlayerThink__18CHalfLifeMultiplayP11CBasePlayer 
FPlayerCanRespawn__18CHalfLifeMultiplayP11CBasePlayer 
FlPlayerSpawnTime__18CHalfLifeMultiplayP11CBasePlayer 
GetPlayerSpawnSpot__18CHalfLifeMultiplayP11CBasePlayer 
AllowAutoTargetCrosshair__18CHalfLifeMultiplay 
ClientCommand_DeadOrAlive__18CHalfLifeMultiplayP11CBasePlayerPCc 
ClientCommand__18CHalfLifeMultiplayP11CBasePlayerPCc 
ClientUserInfoChanged__18CHalfLifeMultiplayP11CBasePlayerPc 
IPointsForKill__18CHalfLifeMultiplayP11CBasePlayerT1 
PlayerKilled__18CHalfLifeMultiplayP11CBasePlayerP9entvars_sT2 
DeathNotice__18CHalfLifeMultiplayP11CBasePlayerP9entvars_sT2 
CanHavePlayerItem__18CHalfLifeMultiplayP11CBasePlayerP15CBasePlayerItem 
PlayerGotWeapon__18CHalfLifeMultiplayP11CBasePlayerP15CBasePlayerItem 
WeaponShouldRespawn__18CHalfLifeMultiplayP15CBasePlayerItem 
FlWeaponRespawnTime__18CHalfLifeMultiplayP15CBasePlayerItem 
FlWeaponTryRespawn__18CHalfLifeMultiplayP15CBasePlayerItem 
VecWeaponRespawnSpot__18CHalfLifeMultiplayP15CBasePlayerItem 
CanHaveItem__18CHalfLifeMultiplayP11CBasePlayerP5CItem 
PlayerGotItem__18CHalfLifeMultiplayP11CBasePlayerP5CItem 
ItemShouldRespawn__18CHalfLifeMultiplayP5CItem 
FlItemRespawnTime__18CHalfLifeMultiplayP5CItem 
VecItemRespawnSpot__18CHalfLifeMultiplayP5CItem 
CanHaveAmmo__10CGameRulesP11CBasePlayerPCci 
PlayerGotAmmo__18CHalfLifeMultiplayP11CBasePlayerPci 
AmmoShouldRespawn__18CHalfLifeMultiplayP15CBasePlayerAmmo 
FlAmmoRespawnTime__18CHalfLifeMultiplayP15CBasePlayerAmmo 
VecAmmoRespawnSpot__18CHalfLifeMultiplayP15CBasePlayerAmmo 
FlHealthChargerRechargeTime__18CHalfLifeMultiplay 
FlHEVChargerRechargeTime__18CHalfLifeMultiplay 
DeadPlayerWeapons__18CHalfLifeMultiplayP11CBasePlayer 
DeadPlayerAmmo__18CHalfLifeMultiplayP11CBasePlayer 
GetTeamID__18CHalfLifeMultiplayP11CBaseEntity 
PlayerRelationship__18CHalfLifeMultiplayP11CBasePlayerP11CBaseEntity 
GetTeamIndex__10CGameRulesPCc 
GetIndexedTeamName__10CGameRulesi 
IsValidTeam__10CGameRulesPCc 
ChangePlayerTeam__10CGameRulesP11CBasePlayerPCcii 
SetDefaultPlayerTeam__10CGameRulesP11CBasePlayer 
PlayTextureSounds__18CHalfLifeMultiplay 
FAllowMonsters__18CHalfLifeMultiplay 
EndMultiplayerGame__18CHalfLifeMultiplay 
IsFreezePeriod__10CGameRules 
ServerDeactivate__18CHalfLifeMultiplay 
CheckMapConditions__18CHalfLifeMultiplay 
CleanUpMap__18CHalfLifeMultiplay 
RestartRound__18CHalfLifeMultiplay 
CheckWinConditions__18CHalfLifeMultiplay 
RemoveGuns__18CHalfLifeMultiplay 
GiveC4__18CHalfLifeMultiplay 
ChangeLevel__18CHalfLifeMultiplay 
GoToIntermission__18CHalfLifeMultiplay

To use one of them you need to create a file that should look like this:

C++
{
    "name" : "GetNextBestWeapon",
    "class" : "CGameRules",
    "library" : "mod",
    "arguments" : 
    [
        {
            "type" : "CBasePlayer *"
        },
        {
            "type" : "CBasePlayerItem *"
        }
    ],
    "return" :
    {
        "type" : "bool"
    },
    "indexes" : 
    [
        {
            "os" : "windows",
            "mod" : "cstrike",
            "value" : 5
        },
        {
            "os" : "linux",
            "mod" : "cstrike",
            "value" : 6
        }
    ]
}

(On Linux the virtual table includes an extra function at the beginning, so the offsets must be increased by one)

This function description must be placed in the “VirtualFunctions” directory, in the “CGameRules” folder, in the file “GetNextBestWeapon”.

Now you can use these functions in a plugin with several commands that look like this:

C++
OrpheuGetFunctionFromClass(entityClassName[],libFunctionName[],libClassName[]) 
OrpheuGetFunctionFromEntity(id,libFunctionName[],libClassName[]) 
OrpheuGetFunctionFromObject(object,libFunctionName[],libClassName[]) 
OrpheuGetFunctionFromMonster(id,libFunctionName[],libClassName[])

The first two work exactly the same as in Hamsandwich:

  • The first one calls a function based on the entity's class.
  • The second one calls a function based on the entity itself.

To use the CHalfLifeMultiplay functions described above, we need the native OrpheuGetFunctionFromObject. This function calls a function based on the object whose class you need, so we need an extra step to get the object. Here is one way to solve this particular case:

C++
{ 
    "name" : "InstallGameRules", 
    "library" : "mod", 
    "return" :  
    { 
        "type" : "CHalfLifeMultiplay *" 
    }, 
    "identifiers": 
    [ 
        { 
            "os" : "windows", 
            "mod" : "cstrike", 
            "value" : [0x68,0x8c,0xea,"*",0xa,0xff,0x15,0xdc,0x23,"*",0xa,0x83,0xc4,0x4,0xff,0x15,0xe0,0x23,"*",0xa,0xa1,0xb8,0x25] 
        }, 
        { 
            "os" : "linux", 
            "mod" : "cstrike", 
            "value" : "InstallGameRules__Fv" 
        } 
    ] 
}
Pawn
public plugin_precache() 
{     
    new OrpheuFunction:InstallGameRules = OrpheuGetFunction("InstallGameRules") 
    OrpheuRegisterHook(InstallGameRules,"OnInstallGameRules",OrpheuHookPost) 
} 

public OnInstallGameRules() 
{ 
    new obj = OrpheuGetReturn()     
    new OrpheuFunction:GetNextBestWeapon = OrpheuGetFunctionFromObject(obj,"GetNextBestWeapon","CGameRules") 
    OrpheuRegisterHook(GetNextBestWeapon,"OnGetNextBestWeapon") 
} 

public OnGetNextBestWeapon(obj,id,weaponID) 
{ 
    //     
}

Memory management
The module can now search for and set a value directly at any position in memory where the library is stored. For example, this lets you change a weapon's price or replace strings to change the text that appears in the console. This can be done by using offsets to refer to the memory you want to change, knowing their location or signatures. As with functions, you need to create a file. This file contains one or more descriptions of the memory cell you need to access, or of its location.
Example of changing the price of the AWP:

C++
[ 
    { 
        "name" : "awpCost", 
        "library" : "mod", 
        "type" : "long", 
        "memoryType" : "data", 
        "identifiers" :  
        [ 
            { 
                "os" : "windows", 
                "mod" : "cstrike", 
                "value" : [0x8E,0x12,0x00,0x00,0x7D,0x00,0x00,0x00,0x0A,0x00,0x00,0x00,0x0A,0x00,0x00,0x00,0x1E,0x00,0x00,0x00,0x07] 
            } 
        ] 
    } 
]

OrpheuMemorySet("awpCost",1,1000)

This is only an example for "windows" and "cstrike", but the module itself can be used on both Windows and Linux, and with any mod. Read the links below for more information.

Note: if you need to replace strings placed directly in memory (which is most of them), you should use the "string" type instead of "char *".

Links.
http://jsoncpp.sourceforge.net/
http://www.boost.org/
http://forums.alliedmods.net/showpost.php?p=994447&postcount=2

Translation: Dani Minch
PS: the article was translated over about 3-4 hours straight and then barely edited, so there may be glitches) if so, let me know in the comments

Related articles:
Orpheu: Finding functions in libraries
Function signatures.