diff --git a/game/neo/resource/NeoModEvents.res b/game/neo/resource/NeoModEvents.res index 2bec1b4a93..2f62787edd 100644 --- a/game/neo/resource/NeoModEvents.res +++ b/game/neo/resource/NeoModEvents.res @@ -114,4 +114,24 @@ "area" "long" "blocked" "bool" } + + "comp_match_start" + { + } + + "comp_match_end" + { + } + + "match_start" + { + } + + "comp_round_start" + { + } + + "lobby_all_players_ready" + { + } } diff --git a/src/game/client/CMakeLists.txt b/src/game/client/CMakeLists.txt index da90412331..906eae2eb4 100644 --- a/src/game/client/CMakeLists.txt +++ b/src/game/client/CMakeLists.txt @@ -1570,6 +1570,7 @@ target_sources_grouped( neo/ui/neo_loading.h neo/ui/neo_ui.cpp neo/ui/neo_ui.h + neo/ui/neo_ui_shared.h neo/ui/neo_theme.cpp neo/ui/neo_theme.h neo/ui/neo_utils.cpp diff --git a/src/game/client/neo/ui/neo_root.cpp b/src/game/client/neo/ui/neo_root.cpp index de9f6034ee..7e14517570 100644 --- a/src/game/client/neo/ui/neo_root.cpp +++ b/src/game/client/neo/ui/neo_root.cpp @@ -21,6 +21,8 @@ #include "tier1/interface.h" #include #include "ui/neo_loading.h" +#include "ui/neo_ui.h" +#include "ui/neo_ui_shared.h" #include "ui/neo_utils.h" #include "neo_gamerules.h" #include "neo_misc.h" @@ -77,6 +79,19 @@ enum ENeoPopup ConCommand neo_toggleconsole("neo_toggleconsole", NeoToggleconsole, "toggle the console", FCVAR_DONTRECORD); +ConVar neo_flash_taskbar("neo_flash_taskbar", "0", FCVAR_ARCHIVE, + "Flash inactive game window in the operating system taskbar. " + "0: Never" + " 1: When comp match starts" + " 2: When comp round starts" + " 3: When any match starts" + " 4: When any round starts", + true, 0, true, NeoUI::ENeoFlashTaskbarOption::MaxValue); + +ConVar neo_flash_taskbar_no_spec("neo_flash_taskbar_no_spec", "1", FCVAR_ARCHIVE, + "Whether to only apply neo_flash_taskbar when you are in a player (not spectator) team.", + true, false, true, true); + struct YMD { YMD(const struct tm tm) @@ -388,8 +403,14 @@ CNeoRoot::CNeoRoot(VPANEL parent) SetMouseInputEnabled(true); UpdateControls(); ivgui()->AddTickSignal(GetVPanel(), 200); - ListenForGameEvent("server_spawn"); + ListenForGameEvent("comp_match_start"); + ListenForGameEvent("comp_round_start"); ListenForGameEvent("game_newmap"); + ListenForGameEvent("game_start"); + ListenForGameEvent("lobby_all_players_ready"); + ListenForGameEvent("match_start"); + ListenForGameEvent("round_start"); + ListenForGameEvent("server_spawn"); vgui::IScheme *pScheme = vgui::scheme()->GetIScheme(neoscheme); ApplySchemeSettings(pScheme); @@ -562,6 +583,44 @@ void CNeoRoot::FireGameEvent(IGameEvent *event) { g_pVGuiLocalize->ConvertANSIToUnicode(event->GetString("mapname"), m_wszMap, sizeof(m_wszMap)); } + else if (!neo_flash_taskbar_no_spec.GetBool() || + C_NEO_Player::GetLocalPlayer()->GetObserverMode() == OBS_MODE_NONE) + { + Assert(engine); + if (neo_flash_taskbar.GetBool())// && !engine->IsActiveApp()) + { + CUtlVector targetEvents(0, 2); + switch (neo_flash_taskbar.GetInt()) + { + case NeoUI::ENeoFlashTaskbarOption::AnyMatchStart: + targetEvents.AddToTail("match_start"); + break; + case NeoUI::ENeoFlashTaskbarOption::AnyRoundStart: + targetEvents.AddToTail("round_start"); + break; + case NeoUI::ENeoFlashTaskbarOption::CompMatchStart: + targetEvents.AddToTail("lobby_all_players_ready"); + targetEvents.AddToTail("comp_match_start"); + break; + case NeoUI::ENeoFlashTaskbarOption::CompRoundStart: + targetEvents.AddToTail("comp_round_start"); + break; + default: + Assert(false); + return; + } + + for (const auto& targetEvent: targetEvents) + { + if (FStrEq(targetEvent, type)) + { + DevMsg("Flash window for event: %s\n", type); + engine->FlashWindow(); + break; + } + } + } + } } void CNeoRoot::OnRelayedKeyCodeTyped(vgui::KeyCode code) diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index 44a6eb96e4..d4e697a689 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -116,6 +116,14 @@ static const wchar_t* AUTOMATIC_LEAN_LABELS[] = { L"Always", }; +static const wchar_t* FLASH_TASKBAR_LABELS[NeoUI::ENeoFlashTaskbarOption::EnumCount] = { + L"Never", + L"When competitive match starts", + L"When competitive round starts", + L"When any match starts", + L"When any round starts", +}; + static inline CUtlVector g_vecConVarRefPtrs; ConVarRefEx::ConVarRefEx(const char *pName, const bool bExcludeGlobalPtrs) @@ -410,6 +418,8 @@ void NeoSettingsRestore(NeoSettings *ns, const NeoSettings::Keys::Flags flagsKey pGeneral->bAutoDetectOBS = cvr->cl_neo_streamermode_autodetect_obs.GetBool(); pGeneral->bTachiFullAutoPreferred = cvr->cl_neo_tachi_prefer_auto.GetBool(); pGeneral->bTakingDamageSounds = cvr->cl_neo_taking_damage_sounds.GetBool(); + pGeneral->iFlashTaskbarOption = cvr->neo_flash_taskbar.GetInt(); + pGeneral->bDontFlashTaskbarIfObserver = cvr->neo_flash_taskbar_no_spec.GetBool(); pGeneral->iBackground = clamp(cvr->sv_unlockedchapters.GetInt(), 0, ns->iCBListSize - 1); NeoSettingsBackgroundWrite(ns); NeoUI::ResetTextures(); @@ -741,6 +751,8 @@ void NeoSettingsSave(const NeoSettings *ns) cvr->cl_neo_tachi_prefer_auto.SetValue(pGeneral->bTachiFullAutoPreferred); cvr->sv_unlockedchapters.SetValue(pGeneral->iBackground); cvr->cl_neo_taking_damage_sounds.SetValue(pGeneral->bTakingDamageSounds); + cvr->neo_flash_taskbar.SetValue(pGeneral->iFlashTaskbarOption); + cvr->neo_flash_taskbar_no_spec.SetValue(pGeneral->bDontFlashTaskbarIfObserver); NeoSettingsBackgroundWrite(ns); } { @@ -1038,6 +1050,11 @@ void NeoSettings_General(NeoSettings *ns) NeoUI::EndOverrideFgColor(); } + NeoUI::RingBox(L"Flash inactive game window in OS taskbar", FLASH_TASKBAR_LABELS, ARRAYSIZE(FLASH_TASKBAR_LABELS), &pGeneral->iFlashTaskbarOption); + // Hide this option if it's irrelevant for the user, to make the UI less cluttered. + if (pGeneral->iFlashTaskbarOption != NeoUI::ENeoFlashTaskbarOption::Never) + NeoUI::RingBoxBool(L"Don't flash the taskbar if spectating", &pGeneral->bDontFlashTaskbarIfObserver); + NeoUI::Pad(); NeoUI::Pad(); NeoUI::RingBoxBool(L"Streamer mode", &pGeneral->bStreamerMode); diff --git a/src/game/client/neo/ui/neo_root_settings.h b/src/game/client/neo/ui/neo_root_settings.h index ad2f62e717..3745463b0f 100644 --- a/src/game/client/neo/ui/neo_root_settings.h +++ b/src/game/client/neo/ui/neo_root_settings.h @@ -6,6 +6,8 @@ #include "neo_crosshair.h" #include "neo_hud_friendly_marker.h" +#include "neo_ui_shared.h" + // NEO TODO (nullsystem): Implement our own file IO dialog #include "vgui_controls/FileOpenDialog.h" @@ -73,6 +75,8 @@ struct NeoSettings bool bTachiFullAutoPreferred; int iBackground; bool bTakingDamageSounds; + std::underlying_type_t iFlashTaskbarOption; + bool bDontFlashTaskbarIfObserver; }; struct Keys @@ -268,6 +272,8 @@ struct NeoSettings CONVARREF_DEF(cl_neo_hud_context_hint_enabled); CONVARREF_DEF(cl_neo_equip_utility_priority); CONVARREF_DEF(cl_neo_taking_damage_sounds); + CONVARREF_DEF(neo_flash_taskbar); + CONVARREF_DEF(neo_flash_taskbar_no_spec); // Multiplayer CONVARREF_DEF(cl_spraydisable); diff --git a/src/game/client/neo/ui/neo_ui_shared.h b/src/game/client/neo/ui/neo_ui_shared.h new file mode 100644 index 0000000000..a9bcddc1fa --- /dev/null +++ b/src/game/client/neo/ui/neo_ui_shared.h @@ -0,0 +1,21 @@ +#pragma once +// Stuff shared by NeoUI, without necessarily wanting to bring in a whole other header. + +namespace NeoUI { + +// Whether to flash the app window in OS task bar to get user's attention for whatever reason +enum ENeoFlashTaskbarOption : int +{ + Never = 0, + CompMatchStart, + CompRoundStart, + AnyMatchStart, + AnyRoundStart, + + // Any new options must go ABOVE this line. + // Please don't reorder existing choices for config compatibility. + EnumCount, + MaxValue = EnumCount +}; + +} // namespace NeoUI diff --git a/src/game/server/gameinterface.cpp b/src/game/server/gameinterface.cpp index 97f6094c79..91eba01736 100644 --- a/src/game/server/gameinterface.cpp +++ b/src/game/server/gameinterface.cpp @@ -610,7 +610,11 @@ bool CServerGameDLL::DLLInit( CreateInterfaceFn appSystemFactory, return false; if ( (filesystem = (IFileSystem *)fileSystemFactory(FILESYSTEM_INTERFACE_VERSION,NULL)) == NULL ) return false; +#if defined(NEO) && defined(DBGFLAG_ASSERT) + if ( (gameeventmanager = new IDebugGameEventManager{ (IGameEventManager2 *)appSystemFactory(INTERFACEVERSION_GAMEEVENTSMANAGER2,NULL) }) == NULL ) +#else if ( (gameeventmanager = (IGameEventManager2 *)appSystemFactory(INTERFACEVERSION_GAMEEVENTSMANAGER2,NULL)) == NULL ) +#endif return false; if ( (datacache = (IDataCache*)appSystemFactory(DATACACHE_INTERFACE_VERSION, NULL )) == NULL ) return false; diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 3668288ce5..e7e271a874 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -2587,6 +2587,10 @@ void CNEORules::StartNextRound() m_flNeoRoundStartTime = gpGlobals->curtime; m_flNeoNextRoundStartTime = gpGlobals->curtime + sv_neo_readyup_countdown.GetFloat(); UTIL_CenterPrintAll("- ALL PLAYERS READY: MATCH STARTING SOON... -\n"); + + auto* event = gameeventmanager->CreateEvent("lobby_all_players_ready"); + if (event)gameeventmanager->FireEvent(event); + return; } else @@ -2750,16 +2754,25 @@ void CNEORules::StartNextRound() SetGameRelatedVars(); + gameeventmanager->CreateEvent("noexist"); + IGameEvent *event = gameeventmanager->CreateEvent("round_start"); + Assert(event); if (event) { event->SetInt("fraglimit", 0); event->SetInt("priority", 6); // HLTV event priority, not transmitted - event->SetString("objective", "DEATHMATCH"); - gameeventmanager->FireEvent(event); } + + if (m_iRoundNumber == 1) + { + event = gameeventmanager->CreateEvent("match_start"); + Assert(event); + if (event) gameeventmanager->FireEvent(event); + } + FireLegacyEvent_NeoRoundStart(); DevMsg("New round start here!\n"); @@ -3215,6 +3228,7 @@ void CNEORules::RestartGame() SetGameRelatedVars(); IGameEvent * event = gameeventmanager->CreateEvent("round_start"); + Assert(event); if (event) { event->SetInt("fraglimit", 0); @@ -3224,6 +3238,20 @@ void CNEORules::RestartGame() gameeventmanager->FireEvent(event); } + + event = gameeventmanager->CreateEvent("match_start"); + Assert(event); + if (event) + gameeventmanager->FireEvent(event); + + if (GetActiveGameConfig() && sv_neo_comp.GetBool()) + { + event = gameeventmanager->CreateEvent("comp_match_start"); + Assert(event); + if (event) + gameeventmanager->FireEvent(event); + } + FireLegacyEvent_NeoRoundStart(); } #endif @@ -3499,13 +3527,27 @@ void CNEORules::SetWinningTeam(int team, int iWinReason, bool bForceMapReset, bo return; } - if (auto pEntGameCfg = GetActiveGameConfig()) + auto* pEntGameCfg = GetActiveGameConfig(); + if (pEntGameCfg) { pEntGameCfg->m_OnRoundEnd.Set(team, nullptr, pEntGameCfg); } if (bForceMapReset) { + auto* event = gameeventmanager->CreateEvent("match_end"); + Assert(event); + if (event) + gameeventmanager->FireEvent(event); + + if (pEntGameCfg && sv_neo_comp.GetBool()) + { + event = gameeventmanager->CreateEvent("comp_match_start"); + Assert(event); + if (event) + gameeventmanager->FireEvent(event); + } + RestartGame(); } else diff --git a/src/public/igameevents.h b/src/public/igameevents.h index 43ce3417d4..9e124c2511 100644 --- a/src/public/igameevents.h +++ b/src/public/igameevents.h @@ -164,6 +164,117 @@ abstract_class IGameEventManager2 : public IBaseInterface virtual IGameEvent *UnserializeEvent( bf_read *buf ) = 0; // create new KeyValues, must be deleted }; +#if defined(NEO) && defined(DBGFLAG_ASSERT) +// Debug shim for IGameEventManager2 with extra asserts +class IDebugGameEventManager : public IGameEventManager2 +{ + IGameEventManager2* m_impl; + +public: + IDebugGameEventManager(IGameEventManager2* impl) + : m_impl(impl) + { + Assert(impl); + } + + virtual ~IDebugGameEventManager() + { + delete m_impl; + } + + // load game event descriptions from a file eg "resource\gameevents.res" + virtual int LoadEventsFromFile(const char* filename) + { + return m_impl->LoadEventsFromFile(filename); + } + + // removes all and anything + virtual void Reset() + { + return m_impl->Reset(); + } + + // adds a listener for a particular event + virtual bool AddListener(IGameEventListener2* listener, const char* name, bool bServerSide) + { + return m_impl->AddListener(listener, name, bServerSide); + } + + // returns true if this listener is listens to given event + virtual bool FindListener(IGameEventListener2* listener, const char* name) + { + auto ret = m_impl->FindListener(listener, name); + Assert(ret); + return ret; + } + + // removes a listener + virtual void RemoveListener(IGameEventListener2* listener) + { + return m_impl->RemoveListener(listener); + } + + // create an event by name, but doesn't fire it. returns NULL is event is not + // known or no listener is registered for it. bForce forces the creation even if no listener is active + virtual IGameEvent* CreateEvent(const char* name, bool bForce = false) + { + auto ret = m_impl->CreateEvent(name, bForce); + constexpr const char* modEventsFile = "resource/NeoModEvents.res"; + if (bForce) + { + AssertMsg2(ret, "Event \"%s\" unknown or lacks listener; check %s", name, modEventsFile); + } + else + { + AssertMsg2(ret, "Event \"%s\" unknown; check %s", name, modEventsFile); + } + return ret; + } + + // fires a server event created earlier, if bDontBroadcast is set, event is not send to clients + virtual bool FireEvent(IGameEvent* event, bool bDontBroadcast = false) + { + auto ret = m_impl->FireEvent(event, bDontBroadcast); + Assert(ret); + return ret; + } + + // fires an event for the local client only, should be used only by client code + virtual bool FireEventClientSide(IGameEvent* event) + { + auto ret = m_impl->FireEventClientSide(event); + Assert(ret); + return ret; + } + + // create a new copy of this event, must be free later + virtual IGameEvent* DuplicateEvent(IGameEvent* event) + { + return m_impl->DuplicateEvent(event); + } + + // if an event was created but not fired for some reason, it has to bee freed, same UnserializeEvent + virtual void FreeEvent(IGameEvent* event) + { + return m_impl->FreeEvent(event); + } + + // write/read event to/from bitbuffer + virtual bool SerializeEvent(IGameEvent* event, bf_write* buf) + { + auto res = m_impl->SerializeEvent(event, buf); + Assert(res); + return res; + } + + // create new KeyValues, must be deleted + virtual IGameEvent *UnserializeEvent( bf_read *buf ) + { + return m_impl->UnserializeEvent(buf); + } +}; +#endif + // the old game event manager interface, don't use it. Rest is legacy support: abstract_class IGameEventListener