diff --git a/.gitignore b/.gitignore index 3976c4c30..c5f21c37a 100644 --- a/.gitignore +++ b/.gitignore @@ -206,3 +206,6 @@ mp/game/neo/cfg/sourcemod # Auto-generated by CMake mp/game/neo/resource/GameMenu.res mp/src/game/shared/neo/neo_version_info.cpp + +# News cache +mp/game/neo/news.txt diff --git a/mp/src/CMakeLists.txt b/mp/src/CMakeLists.txt index bd8a9bc82..555006f69 100644 --- a/mp/src/CMakeLists.txt +++ b/mp/src/CMakeLists.txt @@ -338,6 +338,8 @@ if(OS_LINUX) add_compile_definitions( LINUX _LINUX + _FILE_OFFSET_BITS=64 + _TIME_BITS=64 ) endif() diff --git a/mp/src/game/client/neo/ui/neo_root.cpp b/mp/src/game/client/neo/ui/neo_root.cpp index 97b80b725..7966b011e 100644 --- a/mp/src/game/client/neo/ui/neo_root.cpp +++ b/mp/src/game/client/neo/ui/neo_root.cpp @@ -17,6 +17,8 @@ #include #include #include +#include "tier1/interface.h" +#include #include #include @@ -50,8 +52,33 @@ int g_iRowsInScreen; int g_iAvatar = 64; int g_iRootSubPanelWide = 600; constexpr wchar_t WSZ_GAME_TITLE[] = L"neatbkyoc ue"; +#define SZ_WEBSITE "https://neotokyorebuild.github.io" ConCommand neo_toggleconsole("neo_toggleconsole", NeoToggleconsole); + +struct YMD +{ + YMD(const struct tm tm) + : year(tm.tm_year + 1900) + , month(tm.tm_mon + 1) + , day(tm.tm_mday) + { + } + + bool operator==(const YMD &other) const + { + return year == other.year && month == other.month && day == other.day; + } + bool operator!=(const YMD &other) const + { + return !(*this == other); + } + + int year; + int month; + int day; +}; + } void OverrideGameUI() @@ -178,6 +205,40 @@ CNeoRoot::CNeoRoot(VPANEL parent) m_serverBrowser[i].m_pSortCtx = &m_sortCtx; } + // NEO TODO (nullsystem): What will happen in 2038? 64-bit Source 1 SDK when? Source 2 SDK when? + // We can use GCC 64-bit compiled time_t or Win32 API direct to side-step IFileSystem "long" 32-bit limitation for now. + // If _FILE_OFFSET_BITS=64 and _TIME_BITS=64 is set on Linux, time_t will be 64-bit even on 32-bit executable + // + // If news.txt doesn't exists, it'll just give 1970-01-01 which will always be different to ymdNow anyway + const long lFileTime = filesystem->GetFileTime("news.txt"); + const time_t ttFileTime = lFileTime; + struct tm tmFileStruct; + const struct tm tmFile = *(Plat_localtime(&ttFileTime, &tmFileStruct)); + const YMD ymdFile{tmFile}; + + struct tm tmNowStruct; + const time_t tNow = time(nullptr); + const struct tm tmNow = *(Plat_localtime(&tNow, &tmNowStruct)); + const YMD ymdNow{tmNow}; + + // Read the cached file regardless of needing update or not + if (filesystem->FileExists("news.txt")) + { + CUtlBuffer buf(0, 0, CUtlBuffer::TEXT_BUFFER | CUtlBuffer::READ_ONLY); + if (filesystem->ReadFile("news.txt", nullptr, buf)) + { + ReadNewsFile(buf); + } + } + if (ymdFile != ymdNow) + { + ISteamHTTP *http = steamapicontext->SteamHTTP(); + HTTPRequestHandle httpReqHdl = http->CreateHTTPRequest(k_EHTTPMethodGET, SZ_WEBSITE "/news.txt"); + SteamAPICall_t httpReqCallback; + http->SendHTTPRequest(httpReqHdl, &httpReqCallback); + m_ccallbackHttp.Set(httpReqCallback, this, &CNeoRoot::HTTPCallbackRequest); + } + SetKeyBoardInputEnabled(true); SetMouseInputEnabled(true); UpdateControls(); @@ -219,6 +280,7 @@ void CNeoRoot::UpdateControls() g_uiCtx.iActiveSection = -1; V_memset(g_uiCtx.iYOffset, 0, sizeof(g_uiCtx.iYOffset)); m_ns.bBack = false; + m_bShowBrowserLabel = false; RequestFocus(); m_panelCaptureInput->RequestFocus(); InvalidateLayout(); @@ -460,7 +522,7 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) const int iRightXPos = iBtnPlaceXMid + (m_iBtnWide / 2) + g_uiCtx.iMarginX; int iRightSideYStart = yTopPos; - if (param.eMode == NeoUI::MODE_PAINT) + // Draw top steam section portion { // Draw title m_iBtnWide = m_iTitleWidth + (2 * g_uiCtx.iMarginX); @@ -572,14 +634,26 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) NeoUI::Label(L"News"); NeoUI::SwapFont(NeoUI::FONT_NTSMALL); - // Write some headlines - static constexpr const wchar_t *WSZ_NEWS_HEADLINES[] = { - L"2024-08-03: NT;RE v7.1 Released", - }; - for (const wchar_t *wszHeadline : WSZ_NEWS_HEADLINES) + g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_LEFT; + NeoUI::SwapColorNormal(COLOR_TRANSPARENT); + for (int i = 0; i < m_iNewsSize; ++i) { - NeoUI::Label(wszHeadline); + if (NeoUI::Button(m_news[i].wszTitle).bPressed) + { + NeoUI::OpenURL(SZ_WEBSITE, m_news[i].szSitePath); + m_bShowBrowserLabel = true; + } } + + if (m_bShowBrowserLabel) + { + surface()->DrawSetTextColor(Color(178, 178, 178, 178)); + NeoUI::Label(L"Link opened in your web browser"); + surface()->DrawSetTextColor(COLOR_NEOPANELTEXTNORMAL); + } + + g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_CENTER; + NeoUI::SwapColorNormal(COLOR_NEOPANELACCENTBG); } } NeoUI::EndSection(); @@ -1276,6 +1350,75 @@ void CNeoRoot::MainLoopPopup(const MainLoopParam param) NeoUI::EndContext(); } +void CNeoRoot::HTTPCallbackRequest(HTTPRequestCompleted_t *request, bool bIOFailure) +{ + ISteamHTTP *http = steamapicontext->SteamHTTP(); + if (request->m_bRequestSuccessful && !bIOFailure) + { + uint32 unBodySize = 0; + http->GetHTTPResponseBodySize(request->m_hRequest, &unBodySize); + + if (unBodySize > 0) + { + uint8 *pData = new uint8[unBodySize + 1]; + http->GetHTTPResponseBodyData(request->m_hRequest, pData, unBodySize); + + CUtlBuffer buf(0, 0, CUtlBuffer::TEXT_BUFFER); + buf.CopyBuffer(pData, unBodySize); + filesystem->WriteFile("news.txt", nullptr, buf); + ReadNewsFile(buf); + + delete[] pData; + } + } + http->ReleaseHTTPRequest(request->m_hRequest); +} + +void CNeoRoot::ReadNewsFile(CUtlBuffer &buf) +{ + buf.SeekGet(CUtlBuffer::SEEK_HEAD, 0); + m_iNewsSize = 0; + while (buf.IsValid() && m_iNewsSize < MAX_NEWS) + { + // TSV row: Path\tDate\tTitle + char szLine[512] = {}; + buf.GetLine(szLine, ARRAYSIZE(szLine) - 1); + char *pszDate = strchr(szLine, '\t'); + if (!pszDate) + { + continue; + } + + *pszDate = '\0'; + ++pszDate; + if (!*pszDate) + { + continue; + } + + char *pszTitle = strchr(pszDate, '\t'); + if (!pszTitle) + { + continue; + } + + *pszTitle = '\0'; + ++pszTitle; + if (!*pszTitle) + { + continue; + } + + V_strcpy_safe(m_news[m_iNewsSize].szSitePath, szLine); + wchar_t wszDate[12]; + wchar_t wszTitle[235]; + g_pVGuiLocalize->ConvertANSIToUnicode(pszDate, wszDate, sizeof(wszDate)); + g_pVGuiLocalize->ConvertANSIToUnicode(pszTitle, wszTitle, sizeof(wszTitle)); + V_swprintf_safe(m_news[m_iNewsSize].wszTitle, L"%ls - %ls", wszDate, wszTitle); + ++m_iNewsSize; + } +} + // NEO NOTE (nullsystem): NeoRootCaptureESC is so that ESC keybinds can be recognized by non-root states, but root // state still want to have ESC handled by the game as IsVisible/HasFocus isn't reliable indicator to depend on. // This goes along with NeoToggleconsole which if the toggleconsole is activated on non-root state that can end up diff --git a/mp/src/game/client/neo/ui/neo_root.h b/mp/src/game/client/neo/ui/neo_root.h index 79b1f0bf4..b495a7fc4 100644 --- a/mp/src/game/client/neo/ui/neo_root.h +++ b/mp/src/game/client/neo/ui/neo_root.h @@ -2,6 +2,8 @@ #include #include "GameUI/IGameUI.h" +#include +#include #include "neo_ui.h" #include "neo_root_serverbrowser.h" @@ -167,6 +169,21 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener wchar_t m_wszMap[128]; wchar_t m_wszServerPassword[128] = {}; + + CCallResult m_ccallbackHttp; + void HTTPCallbackRequest(HTTPRequestCompleted_t *request, bool bIOFailure); + + // Display maximum of 5 items on home screen + struct NewsEntry + { + char szSitePath[64]; + wchar_t wszTitle[256]; + }; + static constexpr int MAX_NEWS = 5; + NewsEntry m_news[MAX_NEWS] = {}; + int m_iNewsSize = 0; + void ReadNewsFile(CUtlBuffer &buf); + bool m_bShowBrowserLabel = false; }; extern CNeoRoot *g_pNeoRoot; diff --git a/mp/src/game/client/neo/ui/neo_ui.cpp b/mp/src/game/client/neo/ui/neo_ui.cpp index 7869c38fb..22d9739e3 100644 --- a/mp/src/game/client/neo/ui/neo_ui.cpp +++ b/mp/src/game/client/neo/ui/neo_ui.cpp @@ -65,6 +65,12 @@ void SwapFont(const EFont eFont) surface()->DrawSetTextFont(g_pCtx->fonts[g_pCtx->eFont].hdl); } +void SwapColorNormal(const Color &color) +{ + g_pCtx->normalBgColor = color; + surface()->DrawSetColor(g_pCtx->normalBgColor); +} + void BeginContext(NeoUI::Context *ctx, const NeoUI::Mode eMode, const wchar_t *wszTitle, const char *pSzCtxName) { g_pCtx = ctx; @@ -80,6 +86,7 @@ void BeginContext(NeoUI::Context *ctx, const NeoUI::Mode eMode, const wchar_t *w g_pCtx->eLabelTextStyle = TEXTSTYLE_LEFT; g_pCtx->bTextEditIsPassword = false; g_pCtx->selectBgColor = COLOR_NEOPANELSELECTBG; + g_pCtx->normalBgColor = COLOR_NEOPANELACCENTBG; // Different pointer, change context if (g_pCtx->pSzCurCtxName != pSzCtxName) { @@ -242,7 +249,7 @@ void BeginSection(const bool bDefaultFocus) g_pCtx->dPanel.x + g_pCtx->dPanel.wide, g_pCtx->dPanel.y + g_pCtx->dPanel.tall); - surface()->DrawSetColor(COLOR_NEOPANELACCENTBG); + surface()->DrawSetColor(g_pCtx->normalBgColor); surface()->DrawSetTextColor(COLOR_NEOPANELTEXTNORMAL); break; case MODE_KEYPRESSED: @@ -373,7 +380,7 @@ static void InternalUpdatePartitionState(const GetMouseinFocusedRet wdgState) ++g_pCtx->iWidget; if (wdgState.bActive || wdgState.bHot) { - surface()->DrawSetColor(COLOR_NEOPANELACCENTBG); + surface()->DrawSetColor(g_pCtx->normalBgColor); surface()->DrawSetTextColor(COLOR_NEOPANELTEXTNORMAL); } } @@ -589,7 +596,7 @@ void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex) { // NEO NOTE (nullsystem): On the final tab, just expand it to the end width as iTabWide isn't always going // to give a properly aligned width - surface()->DrawSetColor(bHoverTab ? COLOR_NEOPANELSELECTBG : COLOR_NEOPANELACCENTBG); + surface()->DrawSetColor(bHoverTab ? COLOR_NEOPANELSELECTBG : g_pCtx->normalBgColor); GCtxDrawFilledRectXtoX(iXPosTab, (i == (iLabelsSize - 1)) ? (g_pCtx->dPanel.wide) : (iXPosTab + iTabWide)); } const wchar_t *wszText = wszLabelsList[i]; @@ -976,4 +983,25 @@ bool Bind(const ButtonCode_t eCode) return g_pCtx->eMode == MODE_KEYPRESSED && g_pCtx->eCode == eCode; } +void OpenURL(const char *szBaseUrl, const char *szPath) +{ + Assert(szPath && szPath[0] == '/'); + if (!szPath || szPath[0] != '/') + { + // Must start with / otherwise don't open URL + return; + } + + static constexpr char CMD[] = +#ifdef _WIN32 + "start" +#else + "xdg-open" +#endif + ; + char syscmd[512] = {}; + V_sprintf_safe(syscmd, "%s %s%s", CMD, szBaseUrl, szPath); + system(syscmd); +} + } // namespace NeoUI diff --git a/mp/src/game/client/neo/ui/neo_ui.h b/mp/src/game/client/neo/ui/neo_ui.h index 017d882db..42b3fad3a 100644 --- a/mp/src/game/client/neo/ui/neo_ui.h +++ b/mp/src/game/client/neo/ui/neo_ui.h @@ -118,6 +118,7 @@ struct Context wchar_t unichar; Color bgColor; Color selectBgColor; + Color normalBgColor; // Mouse handling int iMouseAbsX; @@ -192,7 +193,9 @@ void BeginSection(const bool bDefaultFocus = false); void EndSection(); void BeginHorizontal(const int iHorizontalWidth); void EndHorizontal(); + void SwapFont(const EFont eFont); +void SwapColorNormal(const Color &color); struct RetButton { @@ -216,4 +219,5 @@ void SliderInt(const wchar_t *wszLeftLabel, int *iValue, const int iMin, const i const wchar_t *wszSpecialText = nullptr); void TextEdit(const wchar_t *wszLeftLabel, wchar_t *wszText, const int iMaxSize); bool Bind(const ButtonCode_t eCode); +void OpenURL(const char *szBaseUrl, const char *szPath); }