diff --git a/README.md b/README.md
index 7d0892a1b..d224265e1 100644
--- a/README.md
+++ b/README.md
@@ -70,8 +70,10 @@ This means that plugins that do binary code analysis (Orpheu for example) probab
| mp_timelimit | 0 | - | - | Period between map rotations.
`0` means no limit |
| mp_forcerespawn | 0 | 0 | - | Players will automatically respawn when killed.
`0` disabled
`>0.00001` time delay to respawn |
| mp_hostage_hurtable | 1 | 0 | 1 | The hostages can take damage.
`0` disabled
`1` from any team
`2` only from `CT`
`3` only from `T` |
+| mp_hostage_fear | 1 | 0 | 1 | The hostages with improved AI (`hostage_ai_enable 1`) get scared of gunfire, explosions and nearby deaths.
`0` disabled (hostages stay calm)
`1` enabled |
| mp_show_radioicon | 1 | 0 | 1 | Show radio icon.
`0` disabled
`1` enabled |
| mp_show_scenarioicon | 0 | 0 | 1 | Show scenario icon in HUD such as count of alive hostages or ticking bomb.
`0` disabled
`1` enabled |
+| mp_show_bomb_timer | 0 | 0 | 1 | Show the time until the planted bomb explodes on the HUD round timer.
`0` disabled
`1` enabled (when the bomb is planted, the round timer shows the C4 countdown) |
| mp_old_bomb_defused_sound | 1 | 0 | 1 | Play "Bomb has been defused" sound instead of "Counter-Terrorists win" when bomb was defused
`0` disabled
`1` enabled |
| showtriggers | 0 | 0 | 1 | Debug cvar shows triggers. |
| sv_alltalk | 0 | 0 | 5 | When players can hear each other ([further explanation](../../wiki/sv_alltalk)).
`0` dead don't hear alive
`1` no restrictions
`2` teammates hear each other
`3` Same as 2, but spectators hear everybody
`4` alive hear alive, dead hear dead and alive.
`5` alive hear alive teammates, dead hear dead and alive.
@@ -143,6 +145,7 @@ This means that plugins that do binary code analysis (Orpheu for example) probab
| mp_playerid_showhealth | 1 | 0 | 2 | Player ID display mode.
`0` don't show health
`1` show health for teammates only (default CS behaviour)
`2` show health for all players |
| mp_playerid_field | 3 | 0 | 3 | Player ID field display mode.
`0` don't show additional information
`1` show team name
`2` show health percentage
`3` show both team name and health percentage |
| mp_knockback | 170 | - | - | Knockback force applied to the victim when damaged by strong weapons (e.g. `AWP`, `AK47`).
Works only if not crouching, and not hit in the legs.
Set to `0` to disable. |
+| mp_knife_wall_sparks | 0 | 0 | 1 | Show a spark effect at the point of impact when a knife hits a wall.
`0` disabled
`1` enabled |
diff --git a/dist/game.cfg b/dist/game.cfg
index 4ca190530..fc59a237e 100644
--- a/dist/game.cfg
+++ b/dist/game.cfg
@@ -177,6 +177,14 @@ mp_forcerespawn "0"
// Default value: "1"
mp_hostage_hurtable "1"
+// The hostages with improved AI (hostage_ai_enable 1)
+// get scared of gunfire, explosions and nearby deaths.
+// 0 - disabled (hostages stay calm)
+// 1 - enabled (default behaviour)
+//
+// Default value: "1"
+mp_hostage_fear "1"
+
// Show radio icon.
// 0 - disabled
// 1 - enabled (default behavior)
@@ -191,6 +199,13 @@ mp_show_radioicon "1"
// Default value: "0"
mp_show_scenarioicon "0"
+// Show the time until the planted bomb explodes on the HUD round timer
+// 0 - disabled (default behavior)
+// 1 - enabled (when the bomb is planted, the round timer shows the C4 countdown)
+//
+// Default value: "0"
+mp_show_bomb_timer "0"
+
// Play "Bomb has been defused" sound instead of "Counter-Terrorists win" when bomb was defused
// 0 - disabled (default behavior)
// 1 - enabled
@@ -698,3 +713,10 @@ mp_playerid_field "3"
//
// Default: "170"
mp_knockback "170"
+
+// Show a spark effect at the point of impact when a knife hits a wall
+// 0 - disabled (default behavior)
+// 1 - enabled
+//
+// Default value: "0"
+mp_knife_wall_sparks "0"
diff --git a/regamedll/CMakeLists.txt b/regamedll/CMakeLists.txt
index 60199e526..b1166d023 100644
--- a/regamedll/CMakeLists.txt
+++ b/regamedll/CMakeLists.txt
@@ -348,6 +348,7 @@ endif()
set(UNITTESTS_SRCS
"unittests/animation_tests.cpp"
+ "unittests/shared_util_tests.cpp"
"unittests/struct_offsets_tests.cpp"
"unittests/TestRunner.cpp"
)
diff --git a/regamedll/dlls/client.cpp b/regamedll/dlls/client.cpp
index d15cffb93..daee11d12 100644
--- a/regamedll/dlls/client.cpp
+++ b/regamedll/dlls/client.cpp
@@ -1893,6 +1893,15 @@ BOOL EXT_FUNC __API_HOOK(HandleMenu_ChooseTeam)(CBasePlayer *pPlayer, int slot)
}
}
+#ifdef REGAMEDLL_FIXES
+ // Both teams are full and no bot could be vacated, deny;
+ // otherwise the player would get into the game while on UNASSIGNED team
+ if (team == UNASSIGNED)
+ {
+ return FALSE;
+ }
+#endif
+
break;
}
case MENU_SLOT_TEAM_SPECT:
diff --git a/regamedll/dlls/game.cpp b/regamedll/dlls/game.cpp
index 2c6d9fbff..01e07b92a 100644
--- a/regamedll/dlls/game.cpp
+++ b/regamedll/dlls/game.cpp
@@ -123,6 +123,7 @@ cvar_t round_restart_delay = { "mp_round_restart_delay", "5", FCVAR_SERVER, 0.
cvar_t showtriggers = { "showtriggers", "0", 0, 0.0f, nullptr }; // debug cvar shows triggers
// TODO: Maybe it's better to register in the engine?
cvar_t hostagehurtable = { "mp_hostage_hurtable", "1", FCVAR_SERVER, 1.0f, nullptr };
+cvar_t hostagefear = { "mp_hostage_fear", "1", FCVAR_SERVER, 1.0f, nullptr };
cvar_t roundover = { "mp_roundover", "0", FCVAR_SERVER, 0.0f, nullptr };
cvar_t forcerespawn = { "mp_forcerespawn", "0", FCVAR_SERVER, 0.0f, nullptr };
cvar_t show_radioicon = { "mp_show_radioicon", "1", 0, 1.0f, nullptr };
@@ -201,6 +202,9 @@ cvar_t playerid_field = { "mp_playerid_field", "3", 0, 3.0f, nullptr };
cvar_t knockback = { "mp_knockback", "170", 0, 170.0f, nullptr };
+cvar_t knife_wall_sparks = { "mp_knife_wall_sparks", "0", 0, 0.0f, nullptr };
+cvar_t show_bomb_timer = { "mp_show_bomb_timer", "0", 0, 0.0f, nullptr };
+
void GameDLL_Version_f()
{
if (Q_stricmp(CMD_ARGV(1), "version") != 0)
@@ -403,6 +407,7 @@ void EXT_FUNC GameDLLInit()
CVAR_REGISTER(&showtriggers);
CVAR_REGISTER(&hostagehurtable);
+ CVAR_REGISTER(&hostagefear);
CVAR_REGISTER(&roundover);
CVAR_REGISTER(&forcerespawn);
CVAR_REGISTER(&show_radioicon);
@@ -489,6 +494,9 @@ void EXT_FUNC GameDLLInit()
CVAR_REGISTER(&knockback);
+ CVAR_REGISTER(&knife_wall_sparks);
+ CVAR_REGISTER(&show_bomb_timer);
+
// print version
CONSOLE_ECHO("ReGameDLL version: " APP_VERSION "\n");
diff --git a/regamedll/dlls/game.h b/regamedll/dlls/game.h
index ecf1001c8..741e0a571 100644
--- a/regamedll/dlls/game.h
+++ b/regamedll/dlls/game.h
@@ -152,6 +152,7 @@ extern cvar_t round_restart_delay;
extern cvar_t showtriggers;
extern cvar_t hostagehurtable;
+extern cvar_t hostagefear;
extern cvar_t roundover;
extern cvar_t forcerespawn;
extern cvar_t show_radioicon;
@@ -217,6 +218,8 @@ extern cvar_t randomspawn;
extern cvar_t playerid_showhealth;
extern cvar_t playerid_field;
extern cvar_t knockback;
+extern cvar_t knife_wall_sparks;
+extern cvar_t show_bomb_timer;
#endif
diff --git a/regamedll/dlls/ggrenade.cpp b/regamedll/dlls/ggrenade.cpp
index 5d89900ff..c130729bc 100644
--- a/regamedll/dlls/ggrenade.cpp
+++ b/regamedll/dlls/ggrenade.cpp
@@ -119,6 +119,13 @@ void CGrenade::__API_HOOK(Explode2)(TraceResult *pTrace, int bitsDamageType)
m_bJustBlew = true;
CSGameRules()->CheckWinConditions();
+#ifdef REGAMEDLL_ADD
+ // restore the HUD round timer if the round did not end on the explosion
+ // (e.g. mp_round_infinite blocks the bomb round-end check)
+ if (show_bomb_timer.value != 0.0f)
+ SyncRoundTimerForAll();
+#endif
+
// Pull out of the wall a bit
if (pTrace->flFraction != 1.0f)
{
@@ -1155,6 +1162,12 @@ void CGrenade::__API_HOOK(DefuseBombEnd)(CBasePlayer *pPlayer, bool bDefused)
g_pGameRules->m_bBombDropped = FALSE;
m_pBombDefuser = nullptr;
m_bStartDefuse = false;
+
+#ifdef REGAMEDLL_ADD
+ // restore the HUD round timer
+ if (show_bomb_timer.value != 0.0f)
+ SyncRoundTimerForAll();
+#endif
}
else
{
diff --git a/regamedll/dlls/hostage/hostage.cpp b/regamedll/dlls/hostage/hostage.cpp
index c6f57091c..666b04078 100644
--- a/regamedll/dlls/hostage/hostage.cpp
+++ b/regamedll/dlls/hostage/hostage.cpp
@@ -1450,6 +1450,11 @@ void CHostageManager::ServerActivate()
#ifdef REGAMEDLL_ADD
if (!AreRunningCZero()) {
+ // Free stale navigation data of the previous map,
+ // CCSBotManager::LoadNavigationMap does this only when bots are allowed
+ if (!AreBotsAllowed())
+ DestroyNavigationMap();
+
LoadNavigationMap();
}
#endif
diff --git a/regamedll/dlls/hostage/hostage_improv.cpp b/regamedll/dlls/hostage/hostage_improv.cpp
index 6932d2185..c98a64f98 100644
--- a/regamedll/dlls/hostage/hostage_improv.cpp
+++ b/regamedll/dlls/hostage/hostage_improv.cpp
@@ -1560,6 +1560,12 @@ bool CHostageImprov::IsScared() const
void CHostageImprov::Frighten(ScareType scare)
{
+#ifdef REGAMEDLL_ADD
+ // keep hostages calm, they won't get scared of gunfire, explosions or nearby deaths
+ if (hostagefear.value <= 0.0f)
+ return;
+#endif
+
const float ignoreTime = 10.0f;
if (!IsScared())
diff --git a/regamedll/dlls/player.cpp b/regamedll/dlls/player.cpp
index 90572ada2..25b538ef2 100644
--- a/regamedll/dlls/player.cpp
+++ b/regamedll/dlls/player.cpp
@@ -3240,13 +3240,14 @@ void CBasePlayer::WaterMove()
if (pev->dmg > 5)
pev->dmg = 5;
- TakeDamage(VARS(eoNullEntity), VARS(eoNullEntity), pev->dmg, DMG_DROWN);
-
#ifdef REGAMEDLL_FIXES
- // NOTE: If we died after the damage of above and was followed respawn,
- // so we should get out of code.
- if (!(m_bitsDamageType & DMG_DROWN))
+ // Don't track drowning damage that wasn't actually applied (player is
+ // invulnerable/DAMAGE_NO, or died from it and possibly respawned) -
+ // otherwise it would be "given back" as free health when surfacing.
+ if (!TakeDamage(VARS(eoNullEntity), VARS(eoNullEntity), pev->dmg, DMG_DROWN))
return;
+#else
+ TakeDamage(VARS(eoNullEntity), VARS(eoNullEntity), pev->dmg, DMG_DROWN);
#endif
pev->pain_finished = gpGlobals->time + 1;
@@ -3603,15 +3604,60 @@ void CBasePlayer::ResetMenu()
MESSAGE_END();
}
+#ifdef REGAMEDLL_ADD
+// Find the planted C4 in the world, if any
+static CGrenade *FindPlantedBomb()
+{
+ CGrenade *pBomb = nullptr;
+ while ((pBomb = UTIL_FindEntityByClassname(pBomb, "grenade")))
+ {
+ // skip the ones marked for removal (just defused)
+ if (pBomb->m_bIsC4 && !(pBomb->pev->flags & FL_KILLME))
+ return pBomb;
+ }
+
+ return nullptr;
+}
+
+// Resync the HUD round timer for all players
+// (used when the timer switches between the round time and the C4 countdown)
+void SyncRoundTimerForAll()
+{
+ for (int i = 1; i <= gpGlobals->maxClients; i++)
+ {
+ CBasePlayer *pPlayer = UTIL_PlayerByIndex(i);
+ if (UTIL_IsValidPlayer(pPlayer))
+ pPlayer->SyncRoundTimer();
+ }
+}
+#endif
+
void CBasePlayer::SyncRoundTimer()
{
float tmRemaining = 0;
BOOL bFreezePeriod = g_pGameRules->IsFreezePeriod();
+#ifdef REGAMEDLL_ADD
+ bool bBombTimer = false;
+#endif
+
if (g_pGameRules->IsMultiplayer())
{
tmRemaining = CSGameRules()->GetRoundRemainingTimeReal();
+#ifdef REGAMEDLL_ADD
+ // show the time until the planted bomb explodes instead of the round timer
+ if (show_bomb_timer.value != 0.0f && !bFreezePeriod)
+ {
+ CGrenade *pBomb = FindPlantedBomb();
+ if (pBomb && pBomb->m_flC4Blow > gpGlobals->time)
+ {
+ tmRemaining = pBomb->m_flC4Blow - gpGlobals->time;
+ bBombTimer = true;
+ }
+ }
+#endif
+
#ifdef REGAMEDLL_FIXES
// hide timer HUD because it is useless.
if (tmRemaining <= 0.0f && CSGameRules()->m_iRoundTime <= 0) {
@@ -3631,6 +3677,15 @@ void CBasePlayer::SyncRoundTimer()
if (tmRemaining < 0)
tmRemaining = 0;
+#ifdef REGAMEDLL_ADD
+ // the client hides the HUD timer once the bomb is planted, re-show it
+ if (bBombTimer)
+ {
+ MESSAGE_BEGIN(MSG_ONE, gmsgShowTimer, nullptr, pev);
+ MESSAGE_END();
+ }
+#endif
+
MESSAGE_BEGIN(MSG_ONE, gmsgRoundTime, nullptr, pev);
WRITE_SHORT(int(tmRemaining));
MESSAGE_END();
@@ -9851,7 +9906,7 @@ void CBasePlayer::PrioritizeAutoBuyString(char (&autobuyString)[MAX_AUTOBUY_LENG
int i = 0;
// get the next token from the priority string.
- while (*priorityChar != '\0' && *priorityChar != ' ')
+ while (*priorityChar != '\0' && *priorityChar != ' ' && i < (int)sizeof(priorityToken) - 1)
{
priorityToken[i++] = *priorityChar;
priorityChar++;
@@ -9859,6 +9914,10 @@ void CBasePlayer::PrioritizeAutoBuyString(char (&autobuyString)[MAX_AUTOBUY_LENG
priorityToken[i] = '\0';
+ // skip the rest of an oversized token that did not fit
+ while (*priorityChar != '\0' && *priorityChar != ' ')
+ priorityChar++;
+
// skip spaces
while (*priorityChar == ' ')
priorityChar++;
@@ -10731,7 +10790,8 @@ bool EXT_FUNC CBasePlayer::__API_HOOK(GetIntoGame)()
void CBasePlayer::PlayerRespawnThink()
{
#ifdef REGAMEDLL_ADD
- if (GetObserverMode() != OBS_NONE && (m_iTeam == UNASSIGNED || m_iTeam == SPECTATOR))
+ // Players without a team can never respawn (e.g. stuck at team menu when both teams are full)
+ if (m_iTeam == UNASSIGNED || m_iTeam == SPECTATOR)
return;
// Player cannot respawn while in the Choose Appearance menu
diff --git a/regamedll/dlls/player.h b/regamedll/dlls/player.h
index 78098344f..3e916a44b 100644
--- a/regamedll/dlls/player.h
+++ b/regamedll/dlls/player.h
@@ -1060,3 +1060,7 @@ bool IsSecondaryWeaponClass(int classId);
bool IsSecondaryWeaponId(int id);
const char *GetWeaponAliasFromName(const char *weaponName);
bool CurrentWeaponSatisfies(CBasePlayerWeapon *pWeapon, int id, int classId);
+
+#ifdef REGAMEDLL_ADD
+void SyncRoundTimerForAll();
+#endif
diff --git a/regamedll/dlls/weapons.cpp b/regamedll/dlls/weapons.cpp
index b7e10162c..6663e2e16 100644
--- a/regamedll/dlls/weapons.cpp
+++ b/regamedll/dlls/weapons.cpp
@@ -626,7 +626,8 @@ void CBasePlayerItem::DefaultTouch(CBaseEntity *pOther)
&& m_iId != WEAPON_DEAGLE
&& m_iId != WEAPON_KNIFE
#else
- !IsSecondaryWeapon(m_iId)
+ m_iId != WEAPON_KNIFE
+ && !IsSecondaryWeapon(m_iId)
#endif
)
diff --git a/regamedll/dlls/wpn_shared/wpn_c4.cpp b/regamedll/dlls/wpn_shared/wpn_c4.cpp
index f5c52b01a..5b8cc93d0 100644
--- a/regamedll/dlls/wpn_shared/wpn_c4.cpp
+++ b/regamedll/dlls/wpn_shared/wpn_c4.cpp
@@ -197,6 +197,13 @@ void CC4::PrimaryAttack()
WRITE_BYTE(BOMB_FLAG_PLANTED);
MESSAGE_END();
+#ifdef REGAMEDLL_ADD
+ // show the C4 countdown on the HUD round timer;
+ // must be sent after gmsgBombDrop, the client hides the timer on it
+ if (show_bomb_timer.value != 0.0f)
+ SyncRoundTimerForAll();
+#endif
+
UTIL_ClientPrintAll(HUD_PRINTCENTER, "#Bomb_Planted");
if (TheBots)
{
diff --git a/regamedll/dlls/wpn_shared/wpn_famas.cpp b/regamedll/dlls/wpn_shared/wpn_famas.cpp
index 6364606f7..0c0355165 100644
--- a/regamedll/dlls/wpn_shared/wpn_famas.cpp
+++ b/regamedll/dlls/wpn_shared/wpn_famas.cpp
@@ -216,7 +216,7 @@ void CFamas::FamasFire(float flSpread, float flCycleTime, BOOL fUseAutoAim, BOOL
void CFamas::Reload()
{
-#ifndef REGAMEDLL_FXES
+#ifndef REGAMEDLL_FIXES
if (m_pPlayer->ammo_556nato <= 0)
return;
#endif
diff --git a/regamedll/dlls/wpn_shared/wpn_knife.cpp b/regamedll/dlls/wpn_shared/wpn_knife.cpp
index 43a2f28f2..45269d5e5 100644
--- a/regamedll/dlls/wpn_shared/wpn_knife.cpp
+++ b/regamedll/dlls/wpn_shared/wpn_knife.cpp
@@ -456,6 +456,12 @@ BOOL CKnife::Swing(BOOL fFirst)
{
// also play knife strike
EMIT_SOUND_DYN(m_pPlayer->edict(), CHAN_ITEM, "weapons/knife_hitwall1.wav", VOL_NORM, ATTN_NORM, 0, RANDOM_LONG(0, 3) + 98);
+
+#ifdef REGAMEDLL_ADD
+ // spark effect at the point of impact
+ if (knife_wall_sparks.value)
+ UTIL_Sparks(tr.vecEndPos);
+#endif
}
}
@@ -633,6 +639,12 @@ BOOL CKnife::Stab(BOOL fFirst)
{
// also play knife strike
EMIT_SOUND_DYN(m_pPlayer->edict(), CHAN_ITEM, "weapons/knife_hitwall1.wav", VOL_NORM, ATTN_NORM, 0, RANDOM_LONG(0, 3) + 98);
+
+#ifdef REGAMEDLL_ADD
+ // spark effect at the point of impact
+ if (knife_wall_sparks.value)
+ UTIL_Sparks(tr.vecEndPos);
+#endif
}
}
diff --git a/regamedll/game_shared/shared_util.cpp b/regamedll/game_shared/shared_util.cpp
index c7a2a19dc..380c4d29f 100644
--- a/regamedll/game_shared/shared_util.cpp
+++ b/regamedll/game_shared/shared_util.cpp
@@ -162,14 +162,17 @@ char *SharedParse(char *data)
return data;
}
- s_shared_token[len++] = c;
+ // prevent overflow of the fixed-size token buffer
+ if (len < (int)sizeof(s_shared_token) - 1)
+ s_shared_token[len++] = c;
}
}
// parse single characters
if (c == '{' || c == '}'|| c == ')'|| c == '(' || c == '\'' || c == ',')
{
- s_shared_token[len++] = c;
+ if (len < (int)sizeof(s_shared_token) - 1)
+ s_shared_token[len++] = c;
s_shared_token[len] = '\0';
return data + 1;
}
@@ -177,9 +180,9 @@ char *SharedParse(char *data)
// parse a regular word
do
{
- s_shared_token[len] = c;
+ if (len < (int)sizeof(s_shared_token) - 1)
+ s_shared_token[len++] = c;
data++;
- len++;
c = *data;
if (c == '{' || c == '}'|| c == ')'|| c == '(' || c == '\'' || c == ',')
diff --git a/regamedll/msvc/ReGameDLL.vcxproj b/regamedll/msvc/ReGameDLL.vcxproj
index 80e93c922..1aa8081b5 100644
--- a/regamedll/msvc/ReGameDLL.vcxproj
+++ b/regamedll/msvc/ReGameDLL.vcxproj
@@ -579,6 +579,12 @@
true
true
+
+ true
+ true
+ true
+ true
+
true
true
diff --git a/regamedll/unittests/shared_util_tests.cpp b/regamedll/unittests/shared_util_tests.cpp
new file mode 100644
index 000000000..a60a96f0b
--- /dev/null
+++ b/regamedll/unittests/shared_util_tests.cpp
@@ -0,0 +1,47 @@
+#include "precompiled.h"
+#include "cppunitlite/TestHarness.h"
+
+// Regression test for the SharedParse() global-buffer overflow.
+// s_shared_token is char[1500]; SharedParse() must never emit a token that
+// does not fit, regardless of the size of a single unquoted/quoted token in
+// the input. On the unpatched code the oversized-token cases below smash the
+// global buffer (crash / memory corruption) before the CHECK is even reached.
+const int SHARED_TOKEN_SIZE = 1500; // must match char s_shared_token[1500]
+
+TEST(RegularWordOverflow, SharedParse, 10)
+{
+ // One unquoted "word" of 5000 non-space characters (no quotes/specials).
+ static char input[5001];
+ Q_memset(input, 'A', sizeof(input) - 1);
+ input[sizeof(input) - 1] = '\0';
+
+ SharedParse(input);
+ CHECK("oversized word must be truncated", Q_strlen(SharedGetToken()) < SHARED_TOKEN_SIZE);
+}
+
+TEST(QuotedStringOverflow, SharedParse, 10)
+{
+ // One quoted string of 5000 characters between the quotes.
+ static char input[5003];
+ input[0] = '\"';
+ Q_memset(input + 1, 'B', 5000);
+ input[5001] = '\"';
+ input[5002] = '\0';
+
+ SharedParse(input);
+ CHECK("oversized quoted string must be truncated", Q_strlen(SharedGetToken()) < SHARED_TOKEN_SIZE);
+}
+
+TEST(NormalTokensStillWork, SharedParse, 10)
+{
+ // The fix must not change behavior for well-formed input.
+ char input[] = " primaryWeapon \"hello world\" , ";
+ char *p = SharedParse(input);
+ CHECK("plain word token", Q_strcmp(SharedGetToken(), "primaryWeapon") == 0);
+
+ p = SharedParse(p);
+ CHECK("quoted string token", Q_strcmp(SharedGetToken(), "hello world") == 0);
+
+ p = SharedParse(p);
+ CHECK("comma token", Q_strcmp(SharedGetToken(), ",") == 0);
+}