diff --git a/PackageScript b/PackageScript index 509759c98..cda3090e0 100644 --- a/PackageScript +++ b/PackageScript @@ -120,7 +120,7 @@ for task in MMSPlugin.binaries: builder.AddCopy(os.path.join('configs', 'zr', 'playerclass.jsonc.example'), zr_folder) builder.AddCopy(os.path.join('configs', 'zr', 'weapons.cfg.example'), zr_folder) builder.AddCopy(os.path.join('configs', 'zr', 'hitgroups.cfg.example'), zr_folder) - builder.AddCopy(os.path.join('configs', 'entwatch', 'maps', 'example_config.jsonc'), ew_maps_folder) + builder.AddCopy(os.path.join('configs', 'entwatch', 'maps', 'template.jsonc'), ew_maps_folder) builder.AddCopy(os.path.join('gamedata', 'cs2fixes.games.txt'), gamedata_folder) particles_cs2f_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'particles', MMSPlugin.metadata['name'])) diff --git a/configs/entwatch/maps/example_config.jsonc b/configs/entwatch/maps/example_config.jsonc deleted file mode 100644 index 92cb463a5..000000000 --- a/configs/entwatch/maps/example_config.jsonc +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "name": "Gravity Magic" - "shortname": "Gravity", - "hammerid": "12345", - "message": true, - "ui": true, - "transfer": true, - "color": "purple", - "templated": true, // only set false if the weapon is NOT in a template - "handlers": [ - { - "type": "button", - "hammerid": "buttonhammerid", - "event": "OnPressed", - "mode": 2, - "cooldown": 60, - "maxuses": 0, - "message": true, - "ui": true, - "templated": true // only set false if this entity is NOT in a template - } - ], - "triggers": ["3333", "22222"] - }, - { - "name": "Item 2", - // ... - } -] diff --git a/configs/entwatch/maps/template.jsonc b/configs/entwatch/maps/template.jsonc new file mode 100644 index 000000000..c1abd6767 --- /dev/null +++ b/configs/entwatch/maps/template.jsonc @@ -0,0 +1,52 @@ +[ + { + "name": "Item Name", // Name of item that appears in chat + "shortname": "Short Name", // Name of item that appears on the HUD + "hammerid": "", // Hammerid of the weapon entity + "message": true, // Whether to show pickup/drop messages in chat + "ui": true, // Whether to show this item on the HUD + "transfer": true, // Whether to allow this item to be transferred (this auto detects false for knife items) + "color": "", // Color of the item for chat messages (see list of colors) + "triggers": [""], // Array of hammerids of any triggers that this item is associated with + "templated": true, // Whether the entity of this handler is templated with the item weapon, (auto detected if not specified) + "handlers": [ + { + "name": "Handler", // extra name to show in chat when used e.g. XXX has used Item Name (Handler) + "type": "button", // "button", + // "counterdown" - counter stops OnHitMin + // "counterup" - counter stops OnHitMax + // (anything else is ignored) + "hammerid": "", // hammerid of the entity + "event": "OnPressed", // Name of the output, counterup/down types always force "OutValue" + "mode": 2, // Mode of the handler + // 0/1 = None + // 2 = Cooldown, 3 = MaxUses (cooldown between each) + // 4 = CooldownAfterUses, 5 = CounterValue + "offset": [5,-9], // ADDS the specified offset to counter values, + // First number is counter value, Second is counter max + "cooldown": 60, // Cooldown duration if mode = 2,3,4 + "maxuses": 0, // Maxuses if mode = 3,4 + "message": true, // Whether to show when this is used in chat + "ui": true, // Whether to track this handler on the HUD + "templated": true // Whether the entity of this handler is templated with the item weapon, + } // (this will attempt to auto detect if not specified) + ] + } +] + +// LIST OF COLOR NAMES (grouped if they are the same) +// white, default +// darkred +// team +// green +// lightgreen +// olive +// red +// gray, grey +// yellow +// silver +// blue +// darkblue +// purple, pink +// red2 +// orange, gold \ No newline at end of file diff --git a/sdk b/sdk index 20ae6c37f..3aa193690 160000 --- a/sdk +++ b/sdk @@ -1 +1 @@ -Subproject commit 20ae6c37f6c23551897821645a66cf8af547f6d7 +Subproject commit 3aa1936902b9730341f6f785999d51727cbcf394 diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp index f10483b56..4625da83c 100644 --- a/src/cs2fixes.cpp +++ b/src/cs2fixes.cpp @@ -757,69 +757,76 @@ void CS2Fixes::Hook_PostEvent(CSplitScreenSlot nSlot, bool bLocalOnly, int nClie if (g_cvarEnableNoShake.Get()) *(uint64*)clients &= ~g_playerManager->GetNoShakeMask(); } - else if (g_cvarEnableStopSound.Get() && info->m_MessageId == GE_SosStartSoundEvent) + else if (info->m_MessageId == GE_SosStartSoundEvent) { - static std::set soundEventHashes; auto msg = const_cast(pData)->ToPB(); - ExecuteOnce( - soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.HitWall")); - soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.Slash")); - soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.Hit")); - soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.Stab")); - soundEventHashes.insert(GetSoundEventHash("Weapon_sg556.ZoomIn")); - soundEventHashes.insert(GetSoundEventHash("Weapon_sg556.ZoomOut")); - soundEventHashes.insert(GetSoundEventHash("Weapon_AUG.ZoomIn")); - soundEventHashes.insert(GetSoundEventHash("Weapon_AUG.ZoomOut")); - soundEventHashes.insert(GetSoundEventHash("Weapon_SSG08.Zoom")); - soundEventHashes.insert(GetSoundEventHash("Weapon_SSG08.ZoomOut")); - soundEventHashes.insert(GetSoundEventHash("Weapon_SCAR20.Zoom")); - soundEventHashes.insert(GetSoundEventHash("Weapon_SCAR20.ZoomOut")); - soundEventHashes.insert(GetSoundEventHash("Weapon_G3SG1.Zoom")); - soundEventHashes.insert(GetSoundEventHash("Weapon_G3SG1.ZoomOut")); - soundEventHashes.insert(GetSoundEventHash("Weapon_AWP.Zoom")); - soundEventHashes.insert(GetSoundEventHash("Weapon_AWP.ZoomOut")); - soundEventHashes.insert(GetSoundEventHash("Weapon_Revolver.Prepare")); - soundEventHashes.insert(GetSoundEventHash("Weapon.AutoSemiAutoSwitch"));); - - if (!soundEventHashes.contains(msg->soundevent_hash())) - return; - - uint64 stopSoundMask = g_playerManager->GetStopSoundMask(); - uint64 silenceSoundMask = g_playerManager->GetSilenceSoundMask(); - - if (!msg->has_source_entity_index()) - return; - - CBaseEntity* pSourceEntity = (CBaseEntity*)g_pEntitySystem->GetEntityInstance(CEntityIndex(msg->source_entity_index())); - int playerSlot = -1; - - if (!pSourceEntity) - return; - - if (!V_strcasecmp(pSourceEntity->GetClassname(), "player")) - { - playerSlot = ((CCSPlayerPawn*)pSourceEntity)->GetController()->GetPlayerSlot(); - } - else if (!V_strncasecmp(pSourceEntity->GetClassname(), "weapon_", 7)) + if (g_cvarEnableZR.Get()) + ZR_PostEventAbstract_SosStartSoundEvent(clients, msg); + + if (g_cvarEnableStopSound.Get()) { - CCSPlayerPawn* pPawn = (CCSPlayerPawn*)pSourceEntity->m_hOwnerEntity().Get(); + static std::set soundEventHashes; + + ExecuteOnce( + soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.HitWall")); + soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.Slash")); + soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.Hit")); + soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.Stab")); + soundEventHashes.insert(GetSoundEventHash("Weapon_sg556.ZoomIn")); + soundEventHashes.insert(GetSoundEventHash("Weapon_sg556.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_AUG.ZoomIn")); + soundEventHashes.insert(GetSoundEventHash("Weapon_AUG.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_SSG08.Zoom")); + soundEventHashes.insert(GetSoundEventHash("Weapon_SSG08.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_SCAR20.Zoom")); + soundEventHashes.insert(GetSoundEventHash("Weapon_SCAR20.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_G3SG1.Zoom")); + soundEventHashes.insert(GetSoundEventHash("Weapon_G3SG1.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_AWP.Zoom")); + soundEventHashes.insert(GetSoundEventHash("Weapon_AWP.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_Revolver.Prepare")); + soundEventHashes.insert(GetSoundEventHash("Weapon.AutoSemiAutoSwitch"));); + + if (!soundEventHashes.contains(msg->soundevent_hash())) + return; + + uint64 stopSoundMask = g_playerManager->GetStopSoundMask(); + uint64 silenceSoundMask = g_playerManager->GetSilenceSoundMask(); + + if (!msg->has_source_entity_index()) + return; + + CBaseEntity* pSourceEntity = (CBaseEntity*)g_pEntitySystem->GetEntityInstance(CEntityIndex(msg->source_entity_index())); + int playerSlot = -1; + + if (!pSourceEntity) + return; + + if (!V_strcasecmp(pSourceEntity->GetClassname(), "player")) + { + playerSlot = ((CCSPlayerPawn*)pSourceEntity)->GetController()->GetPlayerSlot(); + } + else if (!V_strncasecmp(pSourceEntity->GetClassname(), "weapon_", 7)) + { + CCSPlayerPawn* pPawn = (CCSPlayerPawn*)pSourceEntity->m_hOwnerEntity().Get(); - if (pPawn && pPawn->IsPawn()) - playerSlot = pPawn->GetController()->GetPlayerSlot(); - } + if (pPawn && pPawn->IsPawn()) + playerSlot = pPawn->GetController()->GetPlayerSlot(); + } - // Remove player who triggered this sound from masks - // Because some of these sounds never get played locally (Zoom's, Knife Hit/Stab) - if (playerSlot != -1 && g_playerManager->IsPlayerUsingStopSound(playerSlot)) - stopSoundMask &= ~((uint64)1 << playerSlot); + // Remove player who triggered this sound from masks + // Because some of these sounds never get played locally (Zoom's, Knife Hit/Stab) + if (playerSlot != -1 && g_playerManager->IsPlayerUsingStopSound(playerSlot)) + stopSoundMask &= ~((uint64)1 << playerSlot); - if (playerSlot != -1 && g_playerManager->IsPlayerUsingSilenceSound(playerSlot)) - silenceSoundMask &= ~((uint64)1 << playerSlot); + if (playerSlot != -1 && g_playerManager->IsPlayerUsingSilenceSound(playerSlot)) + silenceSoundMask &= ~((uint64)1 << playerSlot); - // Filter out people using stop/silence sound from hearing this sound from other players - *(uint64*)clients &= ~stopSoundMask; - *(uint64*)clients &= ~silenceSoundMask; + // Filter out people using stop/silence sound from hearing this sound from other players + *(uint64*)clients &= ~stopSoundMask; + *(uint64*)clients &= ~silenceSoundMask; + } } } @@ -953,9 +960,8 @@ void CS2Fixes::Hook_ClientDisconnect(CPlayerSlot slot, ENetworkDisconnectionReas if (g_cvarEnableZR.Get()) { - if (player) - ZR_CheckTeamWinConditions(player->m_iTeamNum == CS_TEAM_T ? CS_TEAM_CT : CS_TEAM_T); - else if (!ZR_CheckTeamWinConditions(CS_TEAM_T)) // If we cant get team num, just check both + // Controller team num is not valid post-disconnect, so just check both teams + if (!ZR_CheckTeamWinConditions(CS_TEAM_T)) ZR_CheckTeamWinConditions(CS_TEAM_CT); } @@ -1113,7 +1119,11 @@ bool CS2Fixes::Hook_OnTakeDamage_Alive(CTakeDamageResult* pDamageResult) CCSPlayerPawn* pPawn = META_IFACEPTR(CCSPlayerPawn); if (g_cvarEnableZR.Get() && ZR_Hook_OnTakeDamage_Alive(pDamageResult->m_pOriginatingInfo, pPawn)) + { + pDamageResult->m_bWasDamageSuppressed = true; + pDamageResult->m_nDamageDealt = 0; RETURN_META_VALUE(MRES_SUPERCEDE, false); + } // This is a shit place to be doing this, but player_death event is too late and there is no pre-hook alternative // Check if this is going to kill the player diff --git a/src/cs2fixes.h b/src/cs2fixes.h index dd6732341..b9c17ff06 100644 --- a/src/cs2fixes.h +++ b/src/cs2fixes.h @@ -53,6 +53,7 @@ extern CCSGameRules* g_pGameRules; extern CSpawnGroupMgrGameSystem* g_pSpawnGroupMgr; extern double g_flUniversalTime; extern CGlobalVars* GetGlobals(); +extern uint32 GetSoundEventHash(const char* pszSoundEventName); extern CUtlVector* GetClientList(); extern CServerSideClient* GetClientBySlot(CPlayerSlot slot); extern void FullUpdateAllClients(); diff --git a/src/detours.cpp b/src/detours.cpp index 9d38e5639..ae928b26b 100644 --- a/src/detours.cpp +++ b/src/detours.cpp @@ -151,7 +151,7 @@ int64 FASTCALL Detour_CBaseEntity_TakeDamageOld(CBaseEntity* pThis, CTakeDamageI CBaseEntity_TakeDamageOld(pThis, pInfo, pResult); - if (pResult->m_nDamageDealt > 0 && g_cvarEnableZR.Get() && pThis->IsPawn()) + if (pResult->m_nDamageDealt > 0 && !pResult->m_bWasDamageSuppressed && g_cvarEnableZR.Get() && pThis->IsPawn()) ZR_OnPlayerTakeDamage(reinterpret_cast(pThis), pInfo, pResult->m_nDamageDealt); return 1; @@ -288,13 +288,21 @@ void SayChatMessageWithTimer(IRecipientFilter& filter, const char* pText, CCSPla uiNextWordLength = strlen(pNextWord); } - // Case: ... X sec(onds) ... or ... X min(utes) ... - if (pNextWord != NULL && uiNextWordLength > 2 && uiCurrentValue > 0) + // Case: ... X sec(onds) ... or ... X s ... or ... X min(utes) ... + if (pNextWord != NULL && uiCurrentValue > 0) { - if (pNextWord[0] == 's' && pNextWord[1] == 'e' && pNextWord[2] == 'c') - uiTriggerTimerLength = uiCurrentValue; - if (pNextWord[0] == 'm' && pNextWord[1] == 'i' && pNextWord[2] == 'n') - uiTriggerTimerLength = uiCurrentValue * 60; + if (uiNextWordLength == 1) + { + if (pNextWord[0] == 's') + uiTriggerTimerLength = uiCurrentValue; + } + else if (uiNextWordLength > 2) + { + if (pNextWord[0] == 's' && pNextWord[1] == 'e' && pNextWord[2] == 'c') + uiTriggerTimerLength = uiCurrentValue; + if (pNextWord[0] == 'm' && pNextWord[1] == 'i' && pNextWord[2] == 'n') + uiTriggerTimerLength = uiCurrentValue * 60; + } } // Case: ... Xs - only support up to 3 digit numbers (in seconds) for this timer parse method diff --git a/src/playermanager.cpp b/src/playermanager.cpp index 14d003965..7af260f8d 100644 --- a/src/playermanager.cpp +++ b/src/playermanager.cpp @@ -1772,6 +1772,22 @@ void CPlayerManager::SetPlayerSilenceSound(int slot, bool set) g_pUserPreferencesSystem->SetPreferenceInt(slot, SOUND_STATUS_PREF_KEY_NAME, iStopPreferenceStatus + iSilencePreferenceStatus); } +void CPlayerManager::SetPlayerZSounds(int slot, bool set) +{ + if (set) + m_nUsingZSounds |= ((uint64)1 << slot); + else + m_nUsingZSounds &= ~((uint64)1 << slot); + + // Set the user prefs if the player is ingame + ZEPlayer* pPlayer = m_vecPlayers[slot]; + if (!pPlayer) return; + + uint64 iSlotMask = (uint64)1 << slot; + int iZSoundsPreferenceStatus = (m_nUsingZSounds & iSlotMask) ? 1 : 0; + g_pUserPreferencesSystem->SetPreferenceInt(slot, ZSOUNDS_PREF_KEY_NAME, iZSoundsPreferenceStatus); +} + void CPlayerManager::SetPlayerStopDecals(int slot, bool set) { if (set) @@ -1808,6 +1824,7 @@ void CPlayerManager::ResetPlayerFlags(int slot) { SetPlayerStopSound(slot, true); SetPlayerSilenceSound(slot, false); + SetPlayerZSounds(slot, true); SetPlayerStopDecals(slot, true); SetPlayerNoShake(slot, false); } diff --git a/src/playermanager.h b/src/playermanager.h index c8e08933b..5cf1e3204 100644 --- a/src/playermanager.h +++ b/src/playermanager.h @@ -53,6 +53,7 @@ extern CConVar g_cvarFlashLightAttachment; #define SOUND_STATUS_PREF_KEY_NAME "sound_status" #define NO_SHAKE_PREF_KEY_NAME "no_shake" #define BUTTON_WATCH_PREF_KEY_NAME "button_watch" +#define ZSOUNDS_PREF_KEY_NAME "zsounds" #define INVALID_ZEPLAYERHANDLE_INDEX 0u static uint32 iZEPlayerHandleSerial = 0u; // this should actually be 3 bytes large, but no way enough players join in servers lifespan for this to be an issue @@ -398,6 +399,7 @@ class CPlayerManager V_memset(m_vecPlayers, 0, sizeof(m_vecPlayers)); m_nUsingStopSound = -1; // On by default m_nUsingSilenceSound = 0; + m_nUsingZSounds = -1; // On by default m_nUsingStopDecals = -1; // On by default m_nUsingNoShake = 0; } @@ -425,11 +427,13 @@ class CPlayerManager uint64 GetStopSoundMask() { return m_nUsingStopSound; } uint64 GetSilenceSoundMask() { return m_nUsingSilenceSound; } + uint64 GetZSoundsMask() { return m_nUsingZSounds; } uint64 GetStopDecalsMask() { return m_nUsingStopDecals; } uint64 GetNoShakeMask() { return m_nUsingNoShake; } void SetPlayerStopSound(int slot, bool set); void SetPlayerSilenceSound(int slot, bool set); + void SetPlayerZSounds(int slot, bool set); void SetPlayerStopDecals(int slot, bool set); void SetPlayerNoShake(int slot, bool set); @@ -437,6 +441,7 @@ class CPlayerManager bool IsPlayerUsingStopSound(int slot) { return m_nUsingStopSound & ((uint64)1 << slot); } bool IsPlayerUsingSilenceSound(int slot) { return m_nUsingSilenceSound & ((uint64)1 << slot); } + bool IsPlayerUsingZSounds(int slot) { return m_nUsingZSounds & ((uint64)1 << slot); } bool IsPlayerUsingStopDecals(int slot) { return m_nUsingStopDecals & ((uint64)1 << slot); } bool IsPlayerUsingNoShake(int slot) { return m_nUsingNoShake & ((uint64)1 << slot); } @@ -450,6 +455,7 @@ class CPlayerManager uint64 m_nUsingStopSound; uint64 m_nUsingSilenceSound; + uint64 m_nUsingZSounds; uint64 m_nUsingStopDecals; uint64 m_nUsingNoShake; }; diff --git a/src/user_preferences.cpp b/src/user_preferences.cpp index 7953fe9e9..1e32219cf 100644 --- a/src/user_preferences.cpp +++ b/src/user_preferences.cpp @@ -112,6 +112,7 @@ void CUserPreferencesSystem::OnPutPreferences(int iSlot) bool bHideDecals = (bool)GetPreferenceInt(iSlot, DECAL_PREF_KEY_NAME, 1); bool bNoShake = (bool)GetPreferenceInt(iSlot, NO_SHAKE_PREF_KEY_NAME, 0); int iButtonWatchMode = GetPreferenceInt(iSlot, BUTTON_WATCH_PREF_KEY_NAME, 0); + bool bZSounds = (bool)GetPreferenceInt(iSlot, ZSOUNDS_PREF_KEY_NAME, 1); // EntWatch int iEntwatchMode = GetPreferenceInt(iSlot, EW_PREF_HUD_MODE, 0); @@ -125,6 +126,7 @@ void CUserPreferencesSystem::OnPutPreferences(int iSlot) // Set the values that we just loaded --- the player is guaranteed available g_playerManager->SetPlayerStopSound(iSlot, bStopSound); g_playerManager->SetPlayerSilenceSound(iSlot, bSilenceSound); + g_playerManager->SetPlayerZSounds(iSlot, bZSounds); g_playerManager->SetPlayerStopDecals(iSlot, bHideDecals); g_playerManager->SetPlayerNoShake(iSlot, bNoShake); diff --git a/src/zombiereborn.cpp b/src/zombiereborn.cpp index d72a324f1..08c134ab2 100644 --- a/src/zombiereborn.cpp +++ b/src/zombiereborn.cpp @@ -1750,6 +1750,37 @@ void ZR_EndRoundAndAddTeamScore(int iTeamNum) } } +void ZR_PostEventAbstract_SosStartSoundEvent(const uint64* pClients, CNetMessagePB* pMsg) +{ + static std::set soundEventHashes; + + ExecuteOnce( + soundEventHashes.insert(GetSoundEventHash("zr.amb.scream")); + soundEventHashes.insert(GetSoundEventHash("zr.amb.zombie_die")); + soundEventHashes.insert(GetSoundEventHash("zr.amb.zombie_pain")); + soundEventHashes.insert(GetSoundEventHash("zr.amb.zombie_voice_idle"));); + + // Filter out people with zsounds disabled from hearing this sound + if (soundEventHashes.contains(pMsg->soundevent_hash())) + *(uint64*)pClients &= g_playerManager->GetZSoundsMask(); +} + +CON_COMMAND_CHAT(zsounds, "- Toggle zombie sounds") +{ + if (!player) + { + ClientPrint(player, HUD_PRINTCONSOLE, ZR_PREFIX "You cannot use this command from the server console."); + return; + } + + int iPlayer = player->GetPlayerSlot(); + bool bSet = !g_playerManager->IsPlayerUsingZSounds(iPlayer); + + g_playerManager->SetPlayerZSounds(iPlayer, bSet); + + ClientPrint(player, HUD_PRINTTALK, ZR_PREFIX "You have %s zombie sounds.", bSet ? "enabled" : "disabled"); +} + CON_COMMAND_CHAT(ztele, "- Teleport to spawn") { // Silently return so the command is completely hidden diff --git a/src/zombiereborn.h b/src/zombiereborn.h index 8cdc6f5a5..dd3729ecc 100644 --- a/src/zombiereborn.h +++ b/src/zombiereborn.h @@ -24,6 +24,7 @@ #include "entity/ccsplayerpawn.h" #include "eventlistener.h" #include "gamesystem.h" +#include "gameevents.pb.h" #include "vendor/nlohmann/json_fwd.hpp" using ordered_json = nlohmann::ordered_json; @@ -276,4 +277,5 @@ void ZR_Detour_CEntityIdentity_AcceptInput(CEntityIdentity* pThis, CUtlSymbolLar void ZR_Hook_ClientPutInServer(CPlayerSlot slot, char const* pszName, int type, uint64 xuid); void ZR_Hook_ClientCommand_JoinTeam(CPlayerSlot slot, const CCommand& args); void ZR_Precache(IEntityResourceManifest* pResourceManifest); -bool ZR_CheckTeamWinConditions(int iTeamNum); \ No newline at end of file +bool ZR_CheckTeamWinConditions(int iTeamNum); +void ZR_PostEventAbstract_SosStartSoundEvent(const uint64* pClients, CNetMessagePB* pMsg); \ No newline at end of file