diff --git a/.github/build/linux/package.sh b/.github/build/linux/package.sh index 832ad04..448dfd2 100755 --- a/.github/build/linux/package.sh +++ b/.github/build/linux/package.sh @@ -5,7 +5,7 @@ rm -f * cp ../README.md ./ cp ../LICENSE ./ -for f in Q3A; do +for f in Q3A QUAKE2; do cp ../bin/release-$f/x86/rocketmod_qmm_$f.so ./ cp ../bin/release-$f/x86_64/rocketmod_qmm_x86_64_$f.so ./ done diff --git a/.github/build/windows/debug.bat b/.github/build/windows/debug.bat index 56c8eb3..cadd3c6 100644 --- a/.github/build/windows/debug.bat +++ b/.github/build/windows/debug.bat @@ -1,4 +1,4 @@ -for %%x in (Q3A) do ( +for %%x in (Q2R Q3A QUAKE2) do ( msbuild .\msvc\rocketmod_qmm.vcxproj /p:Configuration=Debug-%%x /p:Platform=x86 msbuild .\msvc\rocketmod_qmm.vcxproj /p:Configuration=Debug-%%x /p:Platform=x64 ) diff --git a/.github/build/windows/package.bat b/.github/build/windows/package.bat index 7e7f20c..3de062d 100644 --- a/.github/build/windows/package.bat +++ b/.github/build/windows/package.bat @@ -4,7 +4,7 @@ del /q * rem copy ..\README.md .\ rem copy ..\LICENSE .\ -for %%x in (Q3A) do ( +for %%x in (Q2R Q3A QUAKE2) do ( copy ..\bin\Release-%%x\x86\rocketmod_qmm_%%x.dll .\ copy ..\bin\Release-%%x\x64\rocketmod_qmm_x86_64_%%x.dll .\ ) diff --git a/.github/build/windows/release.bat b/.github/build/windows/release.bat index 4f2a6b2..3aeefca 100644 --- a/.github/build/windows/release.bat +++ b/.github/build/windows/release.bat @@ -1,4 +1,4 @@ -for %%x in (Q3A) do ( +for %%x in (Q2R Q3A QUAKE2) do ( msbuild .\msvc\rocketmod_qmm.vcxproj /p:Configuration=Release-%%x /p:Platform=x86 msbuild .\msvc\rocketmod_qmm.vcxproj /p:Configuration=Release-%%x /p:Platform=x64 ) diff --git a/Makefile b/Makefile index e491dc6..df4fc4a 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ -# RocketMod - Rocket Launchers-Only Plugin +# STUB_QMM - Example QMM Plugin # Copyright 2004-2025 -# https://github.com/thecybermind/rocketmod_qmm/ +# https://github.com/thecybermind/stub_qmm/ # 3-clause BSD license: https://opensource.org/license/bsd-3-clause # Created By: Kevin Masterson < k.m.masterson@gmail.com > BIN_32 := rocketmod_qmm BIN_64 := rocketmod_qmm_x86_64 -GAMES := Q3A +GAMES := Q3A QUAKE2 CC := g++ diff --git a/games.lst b/games.lst index 26d686c..bffa5ad 100644 --- a/games.lst +++ b/games.lst @@ -1 +1,3 @@ +Q2R Q3A +QUAKE2 diff --git a/include/game.h b/include/game.h index d897ab9..a330b8f 100644 --- a/include/game.h +++ b/include/game.h @@ -13,10 +13,21 @@ Created By: #define __ROCKETMOD_QMM_GAME_H__ #if defined(GAME_Q3A) -#include -#include + #include + #include +#elif defined(GAME_QUAKE2) + #include + #include +#elif defined(GAME_Q2R) + #include + #include #else -#error Only supported in Quake 3! + #error Only supported in Quake 2 and Quake 3! #endif +intptr_t GAME_vmMain(intptr_t cmd, intptr_t* args); +intptr_t GAME_syscall(intptr_t cmd, intptr_t* args); +intptr_t GAME_vmMain_Post(intptr_t cmd, intptr_t* args); +intptr_t GAME_syscall_Post(intptr_t cmd, intptr_t* args); + #endif // __ROCKETMOD_QMM_GAME_H__ diff --git a/include/main.h b/include/main.h new file mode 100644 index 0000000..edbc92e --- /dev/null +++ b/include/main.h @@ -0,0 +1,26 @@ +/* +RocketMod - Rocket Launchers-Only Plugin +Copyright 2004-2024 +https://github.com/thecybermind/rocketmod_qmm/ +3-clause BSD license: https://opensource.org/license/bsd-3-clause + +Created By: + Kevin Masterson < k.m.masterson@gmail.com > + +*/ + +#ifndef __ROCKETMOD_QMM_MAIN_H__ +#define __ROCKETMOD_QMM_MAIN_H__ + +#include + +#include "version.h" +#include "game.h" + +extern gentity_t* g_gents; +extern intptr_t g_numgents; +extern intptr_t g_gentsize; +extern gclient_t* g_clients; +extern intptr_t g_clientsize; + +#endif // __ROCKETMOD_QMM_MAIN_H__ diff --git a/include/util.h b/include/util.h new file mode 100644 index 0000000..03421aa --- /dev/null +++ b/include/util.h @@ -0,0 +1,27 @@ +/* +RocketMod - Rocket Launchers-Only Plugin +Copyright 2004-2024 +https://github.com/thecybermind/rocketmod_qmm/ +3-clause BSD license: https://opensource.org/license/bsd-3-clause + +Created By: + Kevin Masterson < k.m.masterson@gmail.com > + +*/ + +#ifndef __ROCKETMOD_QMM_UTIL_H__ +#define __ROCKETMOD_QMM_UTIL_H__ + +#include +#include + +// "safe" strncpy that always null-terminates +char* strncpyz(char* dest, const char* src, size_t count); + +// break an entstring into a list of string tokens +std::vector tokenlist_from_entstring(const char* entstring); + +// generate an entstring from a list of string tokens +const char* entstring_from_tokenlist(std::vector tokenlist); + +#endif // __ROCKETMOD_QMM_UTIL_H__ diff --git a/msvc/rocketmod_qmm.sln b/msvc/rocketmod_qmm.sln index 9985067..92e70b0 100644 --- a/msvc/rocketmod_qmm.sln +++ b/msvc/rocketmod_qmm.sln @@ -7,20 +7,44 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rocketmod_qmm", "rocketmod_ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug-Q2R|x86 = Debug-Q2R|x86 + Debug-Q2R|x64 = Debug-Q2R|x64 Debug-Q3A|x86 = Debug-Q3A|x86 Debug-Q3A|x64 = Debug-Q3A|x64 + Debug-QUAKE2|x86 = Debug-QUAKE2|x86 + Debug-QUAKE2|x64 = Debug-QUAKE2|x64 + Release-Q2R|x86 = Release-Q2R|x86 + Release-Q2R|x64 = Release-Q2R|x64 Release-Q3A|x86 = Release-Q3A|x86 Release-Q3A|x64 = Release-Q3A|x64 + Release-QUAKE2|x86 = Release-QUAKE2|x86 + Release-QUAKE2|x64 = Release-QUAKE2|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-Q2R|x86.ActiveCfg = Debug-Q2R|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-Q2R|x86.Build.0 = Debug-Q2R|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-Q2R|x64.ActiveCfg = Debug-Q2R|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-Q2R|x64.Build.0 = Debug-Q2R|x64 {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-Q3A|x86.ActiveCfg = Debug-Q3A|Win32 {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-Q3A|x86.Build.0 = Debug-Q3A|Win32 {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-Q3A|x64.ActiveCfg = Debug-Q3A|x64 {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-Q3A|x64.Build.0 = Debug-Q3A|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-QUAKE2|x86.ActiveCfg = Debug-QUAKE2|Win32 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-QUAKE2|x86.Build.0 = Debug-QUAKE2|Win32 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-QUAKE2|x64.ActiveCfg = Debug-QUAKE2|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Debug-QUAKE2|x64.Build.0 = Debug-QUAKE2|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-Q2R|x86.ActiveCfg = Release-Q2R|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-Q2R|x86.Build.0 = Release-Q2R|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-Q2R|x64.ActiveCfg = Release-Q2R|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-Q2R|x64.Build.0 = Release-Q2R|x64 {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-Q3A|x86.ActiveCfg = Release-Q3A|Win32 {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-Q3A|x86.Build.0 = Release-Q3A|Win32 {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-Q3A|x64.ActiveCfg = Release-Q3A|x64 {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-Q3A|x64.Build.0 = Release-Q3A|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-QUAKE2|x86.ActiveCfg = Release-QUAKE2|Win32 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-QUAKE2|x86.Build.0 = Release-QUAKE2|Win32 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-QUAKE2|x64.ActiveCfg = Release-QUAKE2|x64 + {1D5BE629-82D1-4511-8559-5116BFE4338C}.Release-QUAKE2|x64.Build.0 = Release-QUAKE2|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/msvc/rocketmod_qmm.vcxproj b/msvc/rocketmod_qmm.vcxproj index 0cd76f7..f80f338 100644 --- a/msvc/rocketmod_qmm.vcxproj +++ b/msvc/rocketmod_qmm.vcxproj @@ -5,25 +5,55 @@ Debug-Q3A Win32 + + Debug-QUAKE2 + Win32 + + + Debug-Q2R + x64 + Debug-Q3A x64 + + Debug-QUAKE2 + x64 + Release-Q3A Win32 + + Release-QUAKE2 + Win32 + + + Release-Q2R + x64 + Release-Q3A x64 + + Release-QUAKE2 + x64 + + + + + + + @@ -69,9 +99,18 @@ $(ProjectName)_Q3A + + $(ProjectName)_QUAKE2 + + + $(ProjectName)_x86_64_Q2R + $(ProjectName)_x86_64_Q3A + + $(ProjectName)_x86_64_QUAKE2 + Level3 @@ -111,11 +150,21 @@ false + + + %(PreprocessorDefinitions);GAME_Q2R + + %(PreprocessorDefinitions);GAME_Q3A + + + %(PreprocessorDefinitions);GAME_QUAKE2 + + diff --git a/msvc/rocketmod_qmm.vcxproj.filters b/msvc/rocketmod_qmm.vcxproj.filters index f5e7152..a700227 100644 --- a/msvc/rocketmod_qmm.vcxproj.filters +++ b/msvc/rocketmod_qmm.vcxproj.filters @@ -21,11 +21,29 @@ Header Files + + Header Files + + + Header Files + Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + diff --git a/src/game_q2r.cpp b/src/game_q2r.cpp new file mode 100644 index 0000000..260e684 --- /dev/null +++ b/src/game_q2r.cpp @@ -0,0 +1,211 @@ +/* +RocketMod - Rocket Launchers-Only Plugin +Copyright 2004-2024 +https://github.com/thecybermind/rocketmod_qmm/ +3-clause BSD license: https://opensource.org/license/bsd-3-clause + +Created By: + Kevin Masterson < k.m.masterson@gmail.com > + +*/ + +#if defined(GAME_Q2R) + +#include + +#include "version.h" +#include "game.h" + +#include "util.h" +#include "main.h" + +// pointer to start of item list +static gitem_t* s_itemlist = nullptr; + +// ignore G_INFO_VALUEFORKEY if inside GAME_CLIENT_USERINFO_CHANGED +static bool s_in_client_userinfo_changed = false; + + +// find entity by userinfo pointer +gentity_t* FindEntityByUserinfo(const char* userinfo) { + int i = 0; + gentity_t* ent = nullptr; + while (i < g_numgents) { + ent = ENT_FROM_NUM(i); + i++; + + if (!ent->client || !ent->inuse) + continue; + // gi.Info_ValueForKey(ent->client->pers.userinfo, "fov", val, sizeof(val)); + if (ent->client->pers.userinfo == userinfo) + return ent; + } + + return nullptr; +} + + +// find item in s_itemlist by classname +gitem_t* FindItemByClassname(const char* classname) { + if (!s_itemlist) + return nullptr; + + // skip the first blank entry + gitem_t* item = s_itemlist + 1; + while (item->classname) { + if (!strcmp(item->classname, classname)) + return item; + item++; + } + + return nullptr; +} + + +intptr_t GAME_vmMain(intptr_t cmd, intptr_t* args) { + // this is called to give the mod level-placed entity info at the start of the map + // unfortunately, stripper may modify weapons if it gets loaded AFTER rocketmod. no real way to fix this + // also, don't do this if rocketmod_enabled is 0 + if (cmd == GAME_SPAWN_ENTITIES && QMM_GETINTCVAR(PLID, "rocketmod_enabled")) { + // change spawn objects: + // weapon_* -> weapon_rocketlauncher + // ammo_* -> ammo_rockets + + // if entstring is null or empty, cancel + const char* entstring = (const char*)(args[1]); + if (!entstring || !*entstring) + QMM_RET_IGNORED(0); + + std::vector tokenlist = tokenlist_from_entstring(entstring); + + // the next token is the entity's classname + bool is_classname = false; + int weapons = 0; + int ammo = 0; + + for (auto& token : tokenlist) { + // if this is the value string for a "classname" key, check it + if (is_classname) { + is_classname = false; + + // if its a weapon entity, make it a rocket launcher + if (token.substr(0, 7) == "weapon_") { + token = "weapon_rocketlauncher"; + weapons++; + } + // if its an ammo entity, make it a rocket ammo pack + else if (token.substr(0, 5) == "ammo_") { + token = "ammo_rockets"; + ammo++; + } + } + // if this token is "classname", then the next token is the actual class name + else if (token == "classname") { + is_classname = true; + } + } + + entstring = entstring_from_tokenlist(tokenlist); + args[1] = (intptr_t)entstring; + } + + // we use G_INFO_VALUEFORKEY with a key of "fov" to determine if a user has respawned. but that is also checked + // inside ClientUserinfoChanged. so we set a flag to ignore it in GAME_CLIENT_USERINFO_CHANGED and clear in _Post + if (cmd == GAME_CLIENT_USERINFO_CHANGED) { + s_in_client_userinfo_changed = true; + } + + QMM_RET_IGNORED(0); +} + + +intptr_t GAME_syscall(intptr_t cmd, intptr_t* args) { + // this is called with "fov" as key in PutClientInServer after initialization and before ChangeWeapon. + // this is also called in ClientUserinfoChanged but we ignore that one + if (cmd == G_INFO_VALUEFORKEY && !s_in_client_userinfo_changed && QMM_GETINTCVAR(PLID, "rocketmod_enabled")) { + // pointer to rocket launcher item + static gitem_t* item_rocketlauncher = nullptr; + + // gi.Info_ValueForKey(ent->client->pers.userinfo, "fov", val, sizeof(val)); + const char* userinfo = (const char*)args[0]; + const char* key = (const char*)args[1]; + + // skip if key is not "fov" + if (strcmp(key, "fov") != 0) + QMM_RET_IGNORED(0); + + // we need to check each entity to see which has this userinfo string + gentity_t* ent = FindEntityByUserinfo(userinfo); + + // make sure ent is not null and it is in use + if (!ent || !ent->inuse || !ent->client) + QMM_RET_IGNORED(0); + + // make sure ent is a player + if (strcmp(ent->classname, "player") != 0) + QMM_RET_IGNORED(0); + + // most everything after this references the client persistant data, so get a shortcut + client_persistant_t* pers = &ent->client->pers; + + // make sure client is not a spectator + if (pers->spectator) + QMM_RET_IGNORED(0); + + // find the rocketlauncher item based on first spawned player's blaster: + // the item list starts and ends with empty gitem_t objects, so grab a valid pointer to a blaster, + // and scan back to the beginning null entry. then use FindItemByClassname to find items in the list + if (!item_rocketlauncher) { + gitem_t* item_weapon = pers->weapon; + if (item_weapon) { + gitem_t* item = item_weapon; + // go back to the beginning of the item list + while (item->classname) + item--; + // save for lookups + s_itemlist = item; + // look up item + item_rocketlauncher = FindItemByClassname("weapon_rocketlauncher"); + } + } + + // still couldn't find rocketlauncher item, cancel + if (!item_rocketlauncher) { + QMM_RET_IGNORED(0); + } + + // remove everything from inventory + pers->inventory.fill(0); + // set inventory count for rocket launcher to 1 + pers->inventory[IT_WEAPON_RLAUNCHER] = 1; + // set weapon to rocket launcher gitem_t + pers->weapon = item_rocketlauncher; + // set ammo to cvar (cap at max) + int start_ammo = (int)QMM_GETINTCVAR(PLID, "rocketmod_ammo"); + if (start_ammo > pers->max_ammo[AMMO_ROCKETS]) + start_ammo = pers->max_ammo[AMMO_ROCKETS]; + pers->inventory[IT_AMMO_ROCKETS] = start_ammo; + // give chainfist if gauntlet cvar is enabled + if (QMM_GETINTCVAR(PLID, "rocketmod_gauntlet")) + pers->inventory[IT_WEAPON_CHAINFIST] = 1; + } + QMM_RET_IGNORED(0); +} + + +intptr_t GAME_vmMain_Post(intptr_t cmd, intptr_t* args) { + // we use G_INFO_VALUEFORKEY with a key of "fov" to determine if a user has respawned. but that is also checked + // inside ClientUserinfoChanged. so we set a flag to ignore it in GAME_CLIENT_USERINFO_CHANGED and clear in _Post + if (cmd == GAME_CLIENT_USERINFO_CHANGED) { + s_in_client_userinfo_changed = false; + } + + QMM_RET_IGNORED(0); +} + + +intptr_t GAME_syscall_Post(intptr_t cmd, intptr_t* args) { + QMM_RET_IGNORED(0); +} + +#endif // GAME_Q2R diff --git a/src/game_q3a.cpp b/src/game_q3a.cpp new file mode 100644 index 0000000..ff3f140 --- /dev/null +++ b/src/game_q3a.cpp @@ -0,0 +1,106 @@ +/* +RocketMod - Rocket Launchers-Only Plugin +Copyright 2004-2024 +https://github.com/thecybermind/rocketmod_qmm/ +3-clause BSD license: https://opensource.org/license/bsd-3-clause + +Created By: + Kevin Masterson < k.m.masterson@gmail.com > + +*/ + +#if defined(GAME_Q3A) + +#include + +#include "version.h" +#include "game.h" + +#include "util.h" +#include "main.h" + + +intptr_t GAME_vmMain(intptr_t cmd, intptr_t* args) { + QMM_RET_IGNORED(0); +} + + +intptr_t GAME_syscall(intptr_t cmd, intptr_t* args) { + if (cmd == G_GET_USERCMD && QMM_GETINTCVAR(PLID, "rocketmod_enabled")) { + // this is a good place to hook when a player respawns. + // weird, i know, but if you look through ClientSpawn you'll + // see this is called after the starting machine gun is set + + gclient_t* client = CLIENT_FROM_NUM(args[0]); + + // if the user just respawned, and he has a machine gun, we need to + // remove it and give him a rocket launcher + if (((client->ps.pm_flags & PMF_RESPAWNED) == PMF_RESPAWNED) && ((client->ps.stats[STAT_WEAPONS] & (1 << WP_MACHINEGUN)) == (1 << WP_MACHINEGUN))) { + + // give user rocket launcher and gauntlet only + client->ps.stats[STAT_WEAPONS] = 1 << WP_ROCKET_LAUNCHER; + + if (QMM_GETINTCVAR(PLID, "rocketmod_gauntlet")) + client->ps.stats[STAT_WEAPONS] |= 1 << WP_GAUNTLET; + + // give rockets and clear machinegun ammo + client->ps.ammo[WP_ROCKET_LAUNCHER] = (int)QMM_GETINTCVAR(PLID, "rocketmod_ammo"); + if (client->ps.ammo[WP_ROCKET_LAUNCHER] <= 0) + client->ps.ammo[WP_ROCKET_LAUNCHER] = 10; + client->ps.ammo[WP_MACHINEGUN] = 0; + client->ps.ammo[WP_GAUNTLET] = -1; + client->ps.ammo[WP_GRAPPLING_HOOK] = -1; + + // set rocket launcher as the default weapon and set to ready + client->ps.weapon = WP_ROCKET_LAUNCHER; + client->ps.weaponstate = WEAPON_READY; + } + } + + QMM_RET_IGNORED(0); +} + + +intptr_t GAME_vmMain_Post(intptr_t cmd, intptr_t* args) { + QMM_RET_IGNORED(0); +} + + +intptr_t GAME_syscall_Post(intptr_t cmd, intptr_t* args) { + // the next token is the entity's classname + static bool is_classname = false; + + // this is called to get level-placed entity info at the start of the map + // moved to syscall_Post so that QMM or the engine has already written the entity token into buf + // also, don't do this if rocketmod_enabled is 0 + if (cmd == G_GET_ENTITY_TOKEN && QMM_GETINTCVAR(PLID, "rocketmod_enabled")) { + // change spawn objects: + // weapon_* -> weapon_rocketlauncher + // ammo_* -> ammo_rockets + + char* buf = (char*)args[0]; + intptr_t buflen = args[1]; + + // if this is the value string for a "classname" key, check it + if (is_classname) { + is_classname = false; + + // if its a weapon entity, make it a rocket launcher + if (!strncmp(buf, "weapon_", 7)) { + strncpyz(buf, "weapon_rocketlauncher", buflen); + } + // if its an ammo entity, make it a rocket ammo pack + else if (!strncmp(buf, "ammo_", 5)) { + strncpyz(buf, "ammo_rockets", buflen); + } + } + // if this token is "classname", then the next token is the actual class name + else if (!strcmp(buf, "classname")) { + is_classname = true; + } + } + + QMM_RET_IGNORED(0); +} + +#endif // GAME_Q3A diff --git a/src/game_quake2.cpp b/src/game_quake2.cpp new file mode 100644 index 0000000..88c70c4 --- /dev/null +++ b/src/game_quake2.cpp @@ -0,0 +1,189 @@ +/* +RocketMod - Rocket Launchers-Only Plugin +Copyright 2004-2024 +https://github.com/thecybermind/rocketmod_qmm/ +3-clause BSD license: https://opensource.org/license/bsd-3-clause + +Created By: + Kevin Masterson < k.m.masterson@gmail.com > + +*/ + +#if defined(GAME_QUAKE2) + +#include + +#include "version.h" +#include "game.h" + +#include "util.h" +#include "main.h" + +// pointer to start of item list +static gitem_t* s_itemlist = nullptr; + + +// find item in s_itemlist by classname +gitem_t* FindItemByClassname(const char* classname) { + if (!s_itemlist) + return nullptr; + + // skip the first blank entry + gitem_t* item = s_itemlist + 1; + while (item->classname) { + if (!strcmp(item->classname, classname)) + return item; + item++; + } + + return nullptr; +} + + +intptr_t GAME_vmMain(intptr_t cmd, intptr_t* args) { + // this is called to give the mod level-placed entity info at the start of the map + // unfortunately, stripper may modify weapons if it gets loaded AFTER rocketmod. no real way to fix this + // also, don't do this if rocketmod_enabled is 0 + if (cmd == GAME_SPAWN_ENTITIES && QMM_GETINTCVAR(PLID, "rocketmod_enabled")) { + // change spawn objects: + // weapon_* -> weapon_rocketlauncher + // ammo_* -> ammo_rockets + + // if entstring is null or empty, cancel + const char* entstring = (const char*)(args[1]); + if (!entstring || !*entstring) + QMM_RET_IGNORED(0); + + std::vector tokenlist = tokenlist_from_entstring(entstring); + + // the next token is the entity's classname + bool is_classname = false; + int weapons = 0; + int ammo = 0; + + for (auto& token : tokenlist) { + // if this is the value string for a "classname" key, check it + if (is_classname) { + is_classname = false; + + // if its a weapon entity, make it a rocket launcher + if (token.substr(0, 7) == "weapon_") { + token = "weapon_rocketlauncher"; + weapons++; + } + // if its an ammo entity, make it a rocket ammo pack + else if (token.substr(0, 5) == "ammo_") { + token = "ammo_rockets"; + ammo++; + } + } + // if this token is "classname", then the next token is the actual class name + else if (token == "classname") { + is_classname = true; + } + } + + entstring = entstring_from_tokenlist(tokenlist); + args[1] = (intptr_t)entstring; + } + + QMM_RET_IGNORED(0); +} + + +intptr_t GAME_syscall(intptr_t cmd, intptr_t* args) { + QMM_RET_IGNORED(0); +} + + +intptr_t GAME_vmMain_Post(intptr_t cmd, intptr_t* args) { + QMM_RET_IGNORED(0); +} + + +intptr_t GAME_syscall_Post(intptr_t cmd, intptr_t* args) { + // this is called near the end of PutClientInServer (respawn) and before ChangeWeapon(). + // this is also called in ClientThink regularly, so we check a few things to verify that this is a + // respawn event and not during a regular Think + if (cmd == G_LINKENTITY && QMM_GETINTCVAR(PLID, "rocketmod_enabled")) { + // pointer to rocket launcher item + static gitem_t* item_rocketlauncher = nullptr; + // default values based on baseq2 source, but we will find later with FindItemByClassname + static int item_index_rockets = 21; + static int item_index_rocketlauncher = 14; + + gentity_t* ent = (gentity_t*)args[0]; + // make sure ent is not null and it is in use + if (!ent || !ent->inuse) + QMM_RET_IGNORED(0); + + // make sure ent is a player + if (strcmp(ent->classname, "player") != 0) + QMM_RET_IGNORED(0); + + // make sure ent has a valid client pointer + gclient_t* client = ent->client; + if (!client) + QMM_RET_IGNORED(0); + + // most everything after this references the client persistant data, so get a shortcut + client_persistant_t* pers = &client->pers; + + // make sure client is not a spectator + if (pers->spectator) + QMM_RET_IGNORED(0); + + // find the rocketlauncher item based on first spawned player's blaster: + // the item list starts and ends with empty gitem_t objects, so grab a valid pointer to a blaster, + // and scan back to the beginning null entry. then use FindItemByClassname to find items in the list + if (!item_rocketlauncher) { + gitem_t* item_weapon = pers->weapon; + if (item_weapon) { + gitem_t* item = item_weapon; + // go back to the beginning of the item list + while (item->classname) + item--; + // save for lookups + s_itemlist = item; + // look up items and indexes + item_rocketlauncher = FindItemByClassname("weapon_rocketlauncher"); + item_index_rocketlauncher = item_rocketlauncher - s_itemlist; + item_index_rockets = FindItemByClassname("ammo_rockets") - s_itemlist; + } + } + + // still couldn't find rocketlauncher item, cancel + if (!item_rocketlauncher) { + QMM_RET_IGNORED(0); + } + + // check for respawn + // at respawn, ent->s.skinnum is set to a 0-based client index: + // ent->s.skinnum = ent - g_edicts - 1; + // AFTER LinkEntity is called, ent->s.skinnum is changed in ChangeWeapon(): + // ent->s.skinnum |= ((ent->client->pers.weapon->weapmodel & 0xff) << 8); + // so if ent->s.skinnum is > 0xff, it's not a respawn event + if (ent->s.skinnum > 0xff) + QMM_RET_IGNORED(0); + + // remove current weapon from inventory + pers->inventory[pers->selected_item] = 0; + // set current item to hold to rocket launcher + pers->selected_item = item_index_rocketlauncher; + // set inventory count for rocket launcher to 1 + pers->inventory[pers->selected_item] = 1; + // set weapon to rocket launcher gitem_t + pers->weapon = item_rocketlauncher; + // set ammo to cvar (cap at max) + int start_ammo = (int)QMM_GETINTCVAR(PLID, "rocketmod_ammo"); + if (start_ammo > pers->max_rockets) + start_ammo = pers->max_rockets; + pers->inventory[item_index_rockets] = start_ammo; + // set gun model + client->ps.gunindex = g_syscall(G_MODELINDEX, pers->weapon->view_model); + } + + QMM_RET_IGNORED(0); +} + +#endif // GAME_QUAKE2 diff --git a/src/main.cpp b/src/main.cpp index f258f6a..9a750a3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,38 +36,30 @@ pluginfuncs_t* g_pluginfuncs = nullptr; intptr_t g_vmbase = 0; pluginvars_t* g_pluginvars = nullptr; +gentity_t* g_gents = nullptr; +intptr_t g_numgents = 0; +intptr_t g_gentsize = 0; gclient_t* g_clients = nullptr; intptr_t g_clientsize = sizeof(gclient_t); -// "safe" strncpy that always null-terminates -char* strncpyz(char* dest, const char* src, size_t count) { - char* ret = strncpy(dest, src, count); - dest[count - 1] = '\0'; - return ret; -} - C_DLLEXPORT void QMM_Query(plugininfo_t** pinfo) { QMM_GIVE_PINFO(); } + C_DLLEXPORT int QMM_Attach(eng_syscall_t engfunc, mod_vmMain_t modfunc, pluginres_t* presult, pluginfuncs_t* pluginfuncs, pluginvars_t* pluginvars) { QMM_SAVE_VARS(); - // check game engine and cancel load - if (strcmp(QMM_GETGAMEENGINE(PLID), "Q3A") != 0) { - QMM_WRITEQMMLOG(PLID, "RocketMod is only designed to be run in Quake 3!\n", QMMLOG_INFO); - return 0; - } - return 1; } + C_DLLEXPORT void QMM_Detach() { } -bool s_enabled = false; + C_DLLEXPORT intptr_t QMM_vmMain(intptr_t cmd, intptr_t* args) { if (cmd == GAME_INIT) { // init msg @@ -79,96 +71,31 @@ C_DLLEXPORT intptr_t QMM_vmMain(intptr_t cmd, intptr_t* args) { g_syscall(G_CVAR_REGISTER, nullptr, "rocketmod_enabled", "1", CVAR_SERVERINFO | CVAR_ARCHIVE); g_syscall(G_CVAR_REGISTER, nullptr, "rocketmod_gauntlet", "1", CVAR_ARCHIVE); g_syscall(G_CVAR_REGISTER, nullptr, "rocketmod_ammo", "10", CVAR_ARCHIVE); - - // cache this in an int so we don't have to check it every time - // G_GET_ENTITY_TOKEN comes around. player spawning still checks the cvar - s_enabled = (bool)QMM_GETINTCVAR(PLID, "rocketmod_enabled"); } - QMM_RET_IGNORED(1); + + return GAME_vmMain(cmd, args); } + C_DLLEXPORT intptr_t QMM_syscall(intptr_t cmd, intptr_t* args) { - switch (cmd) { - // store client data - case G_LOCATE_GAME_DATA: - g_clients = (gclient_t*)args[3]; - g_clientsize = args[4]; - break; - - // this is a good place to hook when a player respawns. - // weird, i know, but if you look through ClientSpawn you'll - // see this is called after the starting machine gun is set - // also, don't do this if rocketmod_enabled is 0 - case G_GET_USERCMD: - if (!QMM_GETINTCVAR(PLID, "rocketmod_enabled")) - break; - - gclient_t* client = CLIENT_FROM_NUM(args[0]); - - // if the user just respawned, and he has a machine gun, we need to - // remove it and give him a rocket launcher - if (((client->ps.pm_flags & PMF_RESPAWNED) == PMF_RESPAWNED) && ((client->ps.stats[STAT_WEAPONS] & (1 << WP_MACHINEGUN)) == (1 << WP_MACHINEGUN))) { - - // give user rocket launcher and gauntlet only - client->ps.stats[STAT_WEAPONS] = 1 << WP_ROCKET_LAUNCHER; - - if (QMM_GETINTCVAR(PLID, "rocketmod_gauntlet")) - client->ps.stats[STAT_WEAPONS] |= 1 << WP_GAUNTLET; - - // give rockets and clear machinegun ammo - client->ps.ammo[WP_ROCKET_LAUNCHER] = QMM_GETINTCVAR(PLID, "rocketmod_ammo"); - if (client->ps.ammo[WP_ROCKET_LAUNCHER] <= 0) - client->ps.ammo[WP_ROCKET_LAUNCHER] = 10; - client->ps.ammo[WP_MACHINEGUN] = 0; - client->ps.ammo[WP_GAUNTLET] = -1; - client->ps.ammo[WP_GRAPPLING_HOOK] = -1; - - // set rocket launcher as the default weapon and set to ready - client->ps.weapon = WP_ROCKET_LAUNCHER; - client->ps.weaponstate = WEAPON_READY; - } - - break; + // store client data + if (cmd == G_LOCATE_GAME_DATA) { + g_gents = (gentity_t*)args[0]; + g_numgents = args[1]; + g_gentsize = args[2]; + g_clients = (gclient_t*)args[3]; + g_clientsize = args[4]; } - QMM_RET_IGNORED(1); + return GAME_syscall(cmd, args); } + C_DLLEXPORT intptr_t QMM_vmMain_Post(intptr_t cmd, intptr_t* args) { - QMM_RET_IGNORED(1); + return GAME_vmMain_Post(cmd, args); } -C_DLLEXPORT intptr_t QMM_syscall_Post(intptr_t cmd, intptr_t* args) { - static bool is_classname = false; - - // this is called to get level-placed entity info at the start of the map - // moved to syscall_Post to not interfere with stripper_qmm - // also, don't do this if rocketmod_enabled is 0 - if (cmd == G_GET_ENTITY_TOKEN && s_enabled) { - // change spawn objects: - // weapon_* -> weapon_rocketlauncher - // ammo_* -> ammo_rockets - - char* buf = (char*)args[0]; - intptr_t buflen = args[1]; - - //if this is the value string for a "classname" key, check it - if (is_classname) { - is_classname = false; - - //if its a weapon entity, make it a rocket launcher - if (!strncmp(buf, "weapon_", 7)) { - strncpyz(buf, "weapon_rocketlauncher", buflen); - } - //if its an ammo entity, make it a rocket ammo pack - else if (!strncmp(buf, "ammo_", 5)) { - strncpyz(buf, "ammo_rockets", buflen); - } - // if this token is "classname", then the next token is the actual class name - } else if (!strcmp(buf, "classname")) { - is_classname = true; - } - } - QMM_RET_IGNORED(1); +C_DLLEXPORT intptr_t QMM_syscall_Post(intptr_t cmd, intptr_t* args) { + return GAME_syscall_Post(cmd, args); } diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 0000000..118a646 --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,89 @@ +/* +RocketMod - Rocket Launchers-Only Plugin +Copyright 2004-2024 +https://github.com/thecybermind/rocketmod_qmm/ +3-clause BSD license: https://opensource.org/license/bsd-3-clause + +Created By: + Kevin Masterson < k.m.masterson@gmail.com > + +*/ + +#define _CRT_SECURE_NO_WARNINGS 1 + +#include +#include +#include +#include + + +// "safe" strncpy that always null-terminates +char* strncpyz(char* dest, const char* src, size_t count) { + char* ret = strncpy(dest, src, count); + dest[count - 1] = '\0'; + return ret; +} + + +// break an entstring into a list of string tokens +std::vector tokenlist_from_entstring(const char* entstring) { + std::vector tokenlist; + std::string build; + bool buildstr = false; + + for (const char* c = entstring; c && *c; c++) { + // end if null (shouldn't happen) + if (!*c) + break; + // skip whitespace outside strings + else if (std::isspace(*c) && !buildstr) + continue; + // handle opening braces + else if (*c == '{') + tokenlist.push_back("{"); + // handle closing braces + else if (*c == '}') + tokenlist.push_back("}"); + // handle quote, start of a key or value + else if (*c == '"' && !buildstr) { + build.clear(); + buildstr = true; + } + // handle quote, end of a key or value + else if (*c == '"' && buildstr) { + tokenlist.push_back(build); + build.clear(); + buildstr = false; + } + // all other chars, add to build string + else + build.push_back(*c); + } + + return tokenlist; +} + + +// generate an entstring from a list of string tokens +const char* entstring_from_tokenlist(std::vector tokenlist) { + static std::string entstring; + entstring = ""; + + auto iter = tokenlist.begin(); + if (*iter != "{") + return nullptr; + + while (iter != tokenlist.end()) { + if (*iter == "{" || *iter == "}") { + entstring += (*iter + "\n"); + } + else { + entstring += ("\"" + *iter + "\" "); + iter++; + entstring += ("\"" + *iter + "\"\n"); + } + iter++; + } + + return entstring.c_str(); +}