From 37621b9113a2c93dd396e6f290f8ee2197dee70f Mon Sep 17 00:00:00 2001 From: Kevin Masterson Date: Tue, 30 Dec 2025 02:09:15 +0200 Subject: [PATCH 1/4] begin adding Q2R and QUAKE2 support --- .github/build/linux/package.sh | 2 +- .github/build/windows/debug.bat | 2 +- .github/build/windows/package.bat | 2 +- .github/build/windows/release.bat | 2 +- Makefile | 6 ++--- games.lst | 2 ++ include/game.h | 12 ++++++--- msvc/rocketmod_qmm.sln | 24 +++++++++++++++++ msvc/rocketmod_qmm.vcxproj | 43 +++++++++++++++++++++++++++++++ src/main.cpp | 30 ++++++++++++++++----- 10 files changed, 109 insertions(+), 16 deletions(-) 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..34ae8e9 100644 --- a/include/game.h +++ b/include/game.h @@ -13,10 +13,16 @@ 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 #endif // __ROCKETMOD_QMM_GAME_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..54882fa 100644 --- a/msvc/rocketmod_qmm.vcxproj +++ b/msvc/rocketmod_qmm.vcxproj @@ -5,18 +5,42 @@ 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 +93,18 @@ $(ProjectName)_Q3A + + $(ProjectName)_QUAKE2 + + + $(ProjectName)_x86_64_Q2R + $(ProjectName)_x86_64_Q3A + + $(ProjectName)_x86_64_QUAKE2 + Level3 @@ -111,11 +144,21 @@ false + + + %(PreprocessorDefinitions);GAME_Q2R + + %(PreprocessorDefinitions);GAME_Q3A + + + %(PreprocessorDefinitions);GAME_QUAKE2 + + diff --git a/src/main.cpp b/src/main.cpp index f258f6a..afca771 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -52,21 +52,36 @@ C_DLLEXPORT void QMM_Query(plugininfo_t** pinfo) { QMM_GIVE_PINFO(); } + +struct engine_mod { + const char* engine; + const char* mod; +} allowed_games[] = { + { "Q3A", "baseq3" }, + { "Q2R", "baseq2" }, + { "QUAKE2", "baseq2" }, +}; 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; + // check game engine and mod + bool load = false; + const char* engine = QMM_GETGAMEENGINE(PLID); + const char* mod = QMM_GETSTRCVAR(PLID, "fs_game"); + for (int i = 0; i < sizeof(allowed_games) / sizeof(allowed_games[0]); i++) { + if (!strcmp(engine, allowed_games[i].engine) && !strcmp(engine, allowed_games[i].engine)) { + load = true; + break; + } } - - return 1; + return load; } + 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) { @@ -87,6 +102,7 @@ C_DLLEXPORT intptr_t QMM_vmMain(intptr_t cmd, intptr_t* args) { QMM_RET_IGNORED(1); } + C_DLLEXPORT intptr_t QMM_syscall(intptr_t cmd, intptr_t* args) { switch (cmd) { // store client data @@ -134,10 +150,12 @@ C_DLLEXPORT intptr_t QMM_syscall(intptr_t cmd, intptr_t* args) { QMM_RET_IGNORED(1); } + C_DLLEXPORT intptr_t QMM_vmMain_Post(intptr_t cmd, intptr_t* args) { QMM_RET_IGNORED(1); } + C_DLLEXPORT intptr_t QMM_syscall_Post(intptr_t cmd, intptr_t* args) { static bool is_classname = false; From 244ef3bb284ee8249b101bde869a0cc5254aba3b Mon Sep 17 00:00:00 2001 From: Kevin Masterson Date: Tue, 30 Dec 2025 23:18:46 +0200 Subject: [PATCH 2/4] split out per-game logic into 3 separate files, one for each engine. added utils. finished QUAKE2 version: found places to hook messages to handle respawns and set weapons/ammo --- include/game.h | 5 + include/main.h | 26 ++++ include/util.h | 27 ++++ msvc/rocketmod_qmm.vcxproj | 6 + msvc/rocketmod_qmm.vcxproj.filters | 18 +++ src/game_q2r.cpp | 41 +++++++ src/game_q3a.cpp | 105 ++++++++++++++++ src/game_quake2.cpp | 191 +++++++++++++++++++++++++++++ src/main.cpp | 123 +++---------------- src/util.cpp | 89 ++++++++++++++ 10 files changed, 524 insertions(+), 107 deletions(-) create mode 100644 include/main.h create mode 100644 include/util.h create mode 100644 src/game_q2r.cpp create mode 100644 src/game_q3a.cpp create mode 100644 src/game_quake2.cpp create mode 100644 src/util.cpp diff --git a/include/game.h b/include/game.h index 34ae8e9..a330b8f 100644 --- a/include/game.h +++ b/include/game.h @@ -25,4 +25,9 @@ Created By: #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.vcxproj b/msvc/rocketmod_qmm.vcxproj index 54882fa..f80f338 100644 --- a/msvc/rocketmod_qmm.vcxproj +++ b/msvc/rocketmod_qmm.vcxproj @@ -44,10 +44,16 @@ + + + + + + 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..cfb411a --- /dev/null +++ b/src/game_q2r.cpp @@ -0,0 +1,41 @@ +/* +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 "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) { + 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) { + 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..f1dcf6f --- /dev/null +++ b/src/game_q3a.cpp @@ -0,0 +1,105 @@ +/* +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 "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] = 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..1a1bcae --- /dev/null +++ b/src/game_quake2.cpp @@ -0,0 +1,191 @@ +/* +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); + + QMM_WRITEQMMLOG(PLID, QMM_VARARGS(PLID, "%d tokens loaded from engine\n", tokenlist.size()), QMMLOG_DEBUG); + + // 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; + } + } + + QMM_WRITEQMMLOG(PLID, QMM_VARARGS(PLID, "%d weapons set to rocket launcher\n", weapons), QMMLOG_DEBUG); + QMM_WRITEQMMLOG(PLID, QMM_VARARGS(PLID, "%d ammo set to rockets\n", ammo), QMMLOG_DEBUG); + + 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) { + 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 = 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 afca771..9a750a3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,45 +36,23 @@ 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(); } -struct engine_mod { - const char* engine; - const char* mod; -} allowed_games[] = { - { "Q3A", "baseq3" }, - { "Q2R", "baseq2" }, - { "QUAKE2", "baseq2" }, -}; 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 mod - bool load = false; - const char* engine = QMM_GETGAMEENGINE(PLID); - const char* mod = QMM_GETSTRCVAR(PLID, "fs_game"); - for (int i = 0; i < sizeof(allowed_games) / sizeof(allowed_games[0]); i++) { - if (!strcmp(engine, allowed_games[i].engine) && !strcmp(engine, allowed_games[i].engine)) { - load = true; - break; - } - } - return load; + return 1; } @@ -82,7 +60,6 @@ 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 @@ -94,99 +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); + 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(); +} From bdb3ea21b5024113abcb21eecc8762ed1ffc17c3 Mon Sep 17 00:00:00 2001 From: Kevin Masterson Date: Tue, 30 Dec 2025 23:32:38 +0200 Subject: [PATCH 3/4] fix for q3a missing include --- src/game_q3a.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/game_q3a.cpp b/src/game_q3a.cpp index f1dcf6f..9bda62a 100644 --- a/src/game_q3a.cpp +++ b/src/game_q3a.cpp @@ -16,6 +16,7 @@ Created By: #include "version.h" #include "game.h" +#include "util.h" #include "main.h" From 3b91e4e0ba24ce110e8b3e87b68bd3ae7dae10a7 Mon Sep 17 00:00:00 2001 From: Kevin Masterson Date: Wed, 31 Dec 2025 02:06:21 +0200 Subject: [PATCH 4/4] completed q2r. removed some debug messages from quake2 and added a comment to G_LINKENTITY section. added cast for a cvar intptr_t->int in q3a code. --- src/game_q2r.cpp | 170 ++++++++++++++++++++++++++++++++++++++++++++ src/game_q3a.cpp | 2 +- src/game_quake2.cpp | 10 ++- 3 files changed, 175 insertions(+), 7 deletions(-) diff --git a/src/game_q2r.cpp b/src/game_q2r.cpp index cfb411a..260e684 100644 --- a/src/game_q2r.cpp +++ b/src/game_q2r.cpp @@ -16,20 +16,190 @@ Created By: #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); } diff --git a/src/game_q3a.cpp b/src/game_q3a.cpp index 9bda62a..ff3f140 100644 --- a/src/game_q3a.cpp +++ b/src/game_q3a.cpp @@ -44,7 +44,7 @@ intptr_t GAME_syscall(intptr_t cmd, intptr_t* args) { 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"); + 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; diff --git a/src/game_quake2.cpp b/src/game_quake2.cpp index 1a1bcae..88c70c4 100644 --- a/src/game_quake2.cpp +++ b/src/game_quake2.cpp @@ -56,8 +56,6 @@ intptr_t GAME_vmMain(intptr_t cmd, intptr_t* args) { std::vector tokenlist = tokenlist_from_entstring(entstring); - QMM_WRITEQMMLOG(PLID, QMM_VARARGS(PLID, "%d tokens loaded from engine\n", tokenlist.size()), QMMLOG_DEBUG); - // the next token is the entity's classname bool is_classname = false; int weapons = 0; @@ -85,9 +83,6 @@ intptr_t GAME_vmMain(intptr_t cmd, intptr_t* args) { } } - QMM_WRITEQMMLOG(PLID, QMM_VARARGS(PLID, "%d weapons set to rocket launcher\n", weapons), QMMLOG_DEBUG); - QMM_WRITEQMMLOG(PLID, QMM_VARARGS(PLID, "%d ammo set to rockets\n", ammo), QMMLOG_DEBUG); - entstring = entstring_from_tokenlist(tokenlist); args[1] = (intptr_t)entstring; } @@ -107,6 +102,9 @@ intptr_t GAME_vmMain_Post(intptr_t cmd, intptr_t* args) { 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; @@ -177,7 +175,7 @@ intptr_t GAME_syscall_Post(intptr_t cmd, intptr_t* args) { // set weapon to rocket launcher gitem_t pers->weapon = item_rocketlauncher; // set ammo to cvar (cap at max) - int start_ammo = QMM_GETINTCVAR(PLID, "rocketmod_ammo"); + 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;