Skip to content

Commit f7fc5cd

Browse files
Track'n'Truck DevsTrack'n'Truck Devs
authored andcommitted
Smart Update System: Multi-Language Changelogs, GitHub-Style Alerts, and Release Automation
Markdown Engine & UI Enhancements: - Implemented GitHub-style Alert blocks in MarkdownRenderer: Added support for [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION] with dynamic icons and framework-standard colors. - Upgraded WelcomeWindow: Added a dedicated "Update Experience" mode to display remote changelogs after a version bump. - Modernized MainWindow Update Popup: Migrated the update changelog view to the full Markdown engine, enabling rich text and alerts. - UI Component Fix: Updated UIElements::Button to correctly respect ImGui::BeginDisabled() state (visual dimming and text color). API & Integration: - Standardized Response Parsing: Aligned ApiService with the server-side JSON 'data' wrapper across all endpoints (Updates, Release Notes, Patrons). - Intelligent Language Fallback: Implemented client-side support for smart translation matching (User Language -> English -> Default). - Robust Version Parsing: Improved Version::FromString to handle various semantic version formats more reliably. Framework Core & Infrastructure: - CommunicationManager Refactoring: Implemented asynchronous release notes fetching logic and new signals (OnReleaseNotesReceived) for real-time UI updates. - Decoupled System Metadata: Finalized SystemUtils integration for centralized OS versioning, architecture, and high-precision locale detection. - Smart Post-Update Logic: UIManager now automatically detects an "Updated" state and triggers a background fetch for localized release announcements.
1 parent ed71c87 commit f7fc5cd

17 files changed

Lines changed: 502 additions & 175 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@
9696
✔ Markdown Rendering (MD4C):
9797
To provide rich text formatting, SPF uses a proprietary Markdown renderer built directly on the **md4c (Markdown for C) parser**. This custom engine ensures perfect layout flow across different styles, supporting GFM tables, custom inline colors, and integrated clipboard support for code blocks.
9898

99+
✔ Image Loading (stb_image):
100+
For efficient and memory-safe image decoding, SPF integrates the **stb_image** library. This allows plugins to load textures directly from common formats like PNG and JPG into GPU-ready buffers.
101+
99102

100103

101104
<h2 align="center">❤️ Support the Project</h2>
@@ -374,6 +377,7 @@ This project would not be possible without the incredible work of the open-sourc
374377
* **[nlohmann/json](https://github.com/nlohmann/json)**: For easy and powerful JSON manipulation.
375378
* **[cpr (C++ Requests)](https://github.com/libcpr/cpr)**: For handling all external web requests with a clean, modern interface.
376379
* **[md4c](https://github.com/mity/md4c)**: For providing fast and lightweight Markdown rendering within the UI.
380+
* **[stb_image](https://github.com/nothings/stb)**: For reliable and easy image uploads.
377381
* **[zlib](https://github.com/madler/zlib)**: For data compression, used as a dependency by other core components.
378382
* **[SCS SDK](https://modding.scssoft.com/wiki/Documentation/Engine/SDK/Telemetry)**: For providing the official telemetry interface that makes this all possible.
379383

include/SPF/Modules/CommunicationManager.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,21 @@ namespace Modules {
6565
*/
6666
void RequestPatronsFetch(bool forceRefresh = false);
6767

68+
/**
69+
* @brief Requests release notes for the current framework version.
70+
*/
71+
void RequestReleaseNotesFetch();
72+
6873
// --- Data Submission (SET) ---
6974
/**
7075
* @brief Queues analytics session data for submission.
7176
*/
7277
void RequestTrackUsage();
7378

79+
// --- Signals ---
80+
Utils::Signal<void(const System::UpdateInfo&)> OnUpdateInfoReceived;
81+
Utils::Signal<void(const System::ChangelogData&)> OnReleaseNotesReceived;
82+
7483
private:
7584
// --- Event Handlers ---
7685
void OnRequestTrackUsage(const Events::System::OnRequestTrackUsage& e);
@@ -101,11 +110,13 @@ namespace Modules {
101110
// --- Cache States ---
102111
ResourceState<System::UpdateInfo> m_updateState;
103112
ResourceState<std::vector<System::Patron>> m_patronsState;
113+
ResourceState<System::ChangelogData> m_releaseNotesState;
104114
std::mutex m_stateMutex;
105115

106116
// --- Futures for async processing ---
107117
std::optional<std::future<System::ApiResult<System::UpdateInfo>>> m_updateFuture;
108118
std::optional<std::future<System::ApiResult<std::vector<System::Patron>>>> m_patronsFuture;
119+
std::optional<std::future<System::ApiResult<System::ChangelogData>>> m_releaseNotesFuture;
109120
std::optional<std::future<void>> m_trackUsageFuture;
110121

111122
// --- Sinks ---

include/SPF/System/ApiService.hpp

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,29 @@ namespace System {
5656
uint32_t count;
5757
};
5858

59+
/**
60+
* @brief Holds markdown changelog data for a specific version.
61+
*/
62+
struct ChangelogData {
63+
std::string title;
64+
std::string markdown;
65+
};
66+
5967
/**
6068
* @brief Holds information about the latest framework update.
6169
*/
6270
struct UpdateInfo {
6371
bool updateAvailable = false;
64-
std::string status; // e.g., "update_available", "up_to_date", "switch_to_release"
65-
std::string severity; // e.g., "major", "minor", "patch"
66-
Version latestVersion;
67-
std::string formattedLatestVersion;
72+
struct {
73+
Version ver;
74+
std::string full;
75+
} latestVersion;
6876
std::string downloadUrl;
69-
std::string changelog;
77+
struct {
78+
std::string archive;
79+
std::string binary;
80+
} md5;
81+
ChangelogData content;
7082
};
7183

7284
/**
@@ -91,9 +103,12 @@ namespace System {
91103
ApiService();
92104
~ApiService() = default;
93105

94-
// Asynchronously fetches the latest update information.
95-
std::future<ApiResult<UpdateInfo>> FetchUpdateInfoAsync(const std::string& baseUrl, int major, int minor, int patch, const std::string& channel);
106+
// Asynchronously fetches the latest update information using the new get_framework_update.php.
107+
std::future<ApiResult<UpdateInfo>> FetchUpdateInfoAsync(const std::string& baseUrl, int major, int minor, int patch, const std::string& channel, const std::string& lang);
96108

109+
// Asynchronously fetches localized release notes for a specific version using get_release_notes.php.
110+
std::future<ApiResult<ChangelogData>> FetchReleaseNotesAsync(const std::string& baseUrl, int major, int minor, int patch, const std::string& lang);
111+
97112
// Asynchronously fetches the list of patrons.
98113
std::future<ApiResult<std::vector<Patron>>> FetchPatronsAsync(const std::string& baseUrl);
99114

include/SPF/UI/MainWindow.hpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ class MainWindow : public BaseWindow {
9696
// State for Update Check
9797
bool m_updateCheckInitiated = false;
9898
Modules::CommunicationManager::UpdateStatus m_currentUpdateStatus = Modules::CommunicationManager::UpdateStatus::Unknown;
99-
std::string m_updateApiStatus; // To store the status string from the API, e.g., "switch_to_release"
10099
std::optional<System::UpdateInfo> m_lastUpdateInfo;
101100
std::optional<std::string> m_lastUpdateError;
102101
bool m_isUpdatePopupOpen = false;

include/SPF/UI/MarkdownRenderer.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ class MarkdownRenderer {
2626

2727
private:
2828
// --- Rendering Context ---
29+
enum class AlertType {
30+
None,
31+
Note, // [!NOTE] -> Blue
32+
Tip, // [!TIP] -> Green
33+
Important, // [!IMPORTANT] -> Purple
34+
Warning, // [!WARNING] -> Orange/Gold
35+
Caution // [!CAUTION] -> Red
36+
};
37+
2938
struct Style {
3039
bool isBold = false;
3140
bool isItalic = false;
@@ -34,6 +43,7 @@ class MarkdownRenderer {
3443
bool isCode = false;
3544
bool isLink = false;
3645
bool isBlockQuote = false;
46+
AlertType alertType = AlertType::None;
3747
unsigned int hLevel = 0; // 0 = no heading, 1-6 = H1-H6
3848
ImVec4 customColor = {0,0,0,0}; // (0,0,0,0) means no custom color
3949
};
@@ -71,6 +81,8 @@ class MarkdownRenderer {
7181
float m_quoteStartY = 0.0f;
7282
bool m_isAtStartOfLine = true;
7383
bool m_isInsideCodeBlock = false; // Flag to distinguish block vs inline code
84+
bool m_isWaitingForAlertMarker = false; // Flag to detect [!NOTE] etc.
85+
bool m_skipNextNewline = false;
7486
};
7587

7688
} // namespace UI

include/SPF/UI/UIManager.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class InputManager;
3030
}
3131
namespace Modules {
3232
class PluginManager;
33+
class CommunicationManager;
3334
}
3435
namespace Config {
3536
struct IConfigService;
@@ -58,7 +59,7 @@ class UIManager : public Config::IConfigurable {
5859

5960
// Initialize method to pass dependencies, replacing constructor parameters
6061
void Init(Events::EventManager& eventManager, Input::InputManager& inputManager, Config::IConfigService& configService, Modules::KeyBindsManager& keyBindsManager,
61-
Modules::PluginManager& pluginManager, Logging::LoggerFactory& loggerFactory, Modules::ITelemetryService& telemetryService);
62+
Modules::PluginManager& pluginManager, Modules::CommunicationManager& communicationManager, Logging::LoggerFactory& loggerFactory, Modules::ITelemetryService& telemetryService);
6263

6364
// Creates and registers all framework-defined UI windows.
6465
// This method centralizes UI window creation within UIManager.
@@ -125,6 +126,7 @@ class UIManager : public Config::IConfigurable {
125126
private:
126127
void OnPluginLoaded(const Events::OnPluginDidLoad& e);
127128
void OnPluginUnloaded(const Events::OnPluginWillBeUnloaded& e);
129+
void OnReleaseNotesReceived(const System::ChangelogData& data);
128130

129131
void InitializeImGui();
130132
void ShutdownImGui();
@@ -149,6 +151,7 @@ class UIManager : public Config::IConfigurable {
149151
Config::IConfigService* m_configService = nullptr;
150152
Modules::KeyBindsManager* m_keyBindsManager = nullptr;
151153
Modules::PluginManager* m_pluginManager = nullptr;
154+
Modules::CommunicationManager* m_communicationManager = nullptr;
152155
Logging::LoggerFactory* m_loggerFactory = nullptr;
153156
Modules::ITelemetryService* m_telemetryService = nullptr;
154157
Rendering::Renderer* m_renderer = nullptr;
@@ -165,6 +168,7 @@ class UIManager : public Config::IConfigurable {
165168

166169
std::unique_ptr<Utils::Sink<void(const Events::OnPluginDidLoad&)>> m_onPluginDidLoadSink;
167170
std::unique_ptr<Utils::Sink<void(const Events::OnPluginWillBeUnloaded&)>> m_onPluginWillBeUnloadedSink;
171+
std::unique_ptr<Utils::Sink<void(const System::ChangelogData&)>> m_onReleaseNotesReceivedSink;
168172
};
169173
} // namespace UI
170174

include/SPF/UI/WelcomeWindow.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ SPF_NS_BEGIN
1010

1111
namespace UI {
1212

13+
enum class WelcomeMode {
14+
FirstInstall,
15+
FrameworkUpdate
16+
};
17+
1318
class WelcomeWindow : public IWindow {
1419
public:
1520
WelcomeWindow(const std::string& componentName, const std::string& windowId);
@@ -31,6 +36,8 @@ class WelcomeWindow : public IWindow {
3136

3237
// Helpers
3338
void SetVisibility(bool visible) { m_isVisible = visible; }
39+
void SetMode(WelcomeMode mode) { m_mode = mode; }
40+
void SetUpdateContent(const std::string& title, const std::string& changelogMarkdown);
3441

3542
private:
3643
void InitializeResources();
@@ -44,6 +51,10 @@ class WelcomeWindow : public IWindow {
4451
bool m_isFocused = false;
4552
bool m_wasVisibleLastFrame = false;
4653

54+
WelcomeMode m_mode = WelcomeMode::FirstInstall;
55+
std::string m_updateTitle;
56+
std::string m_updateChangelog;
57+
4758
std::unique_ptr<Rendering::ITexture> m_logoTexture;
4859
MarkdownRenderer m_markdownRenderer;
4960
bool m_resourcesInitialized = false;

plugins/ExamplePlugin/ExamplePlugin.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1590,7 +1590,20 @@ void RenderStylingTab(SPF_UI_API* ui, void* user_data) {
15901590
"And `inline code` is still here.\n\n"
15911591
"| Header 1 | Header 2 |\n"
15921592
"|----------|----------|\n"
1593-
"| Cell 1 | Cell 2 |\n";
1593+
"| Cell 1 | Cell 2 |\n\n"
1594+
"--- \n"
1595+
"### 4. GitHub-style Alerts\n"
1596+
"> [!NOTE]\n"
1597+
"> This is a blue information block.\n"
1598+
"> This is a blue information block.\n\n"
1599+
"> [!TIP]\n"
1600+
"> This is a green tip block.\n\n"
1601+
"> [!IMPORTANT]\n"
1602+
"> This is a purple important block.\n\n"
1603+
"> [!WARNING]\n"
1604+
"> This is a gold warning block.\n\n"
1605+
"> [!CAUTION]\n"
1606+
"> This is a red caution block.\n";
15941607

15951608
// The base style for markdown can have padding, but the renderer will handle fonts/colors.
15961609
ui->UI_Style_SetPadding(markdown_base_style, 10.0f, 5.0f);

src/Core/Core.cpp

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,14 @@ void Core::InitFeatureHooks() {
371371
}
372372

373373
const auto& frameworkSettings = frameworkSettingsNode->at("framework");
374+
const bool isNewInstall = (System::EnvironmentManager::GetInstance().GetFrameworkInfo().installStatus == System::InstallationStatus::NewInstall);
375+
374376
if (!frameworkSettings.contains("hook_states")) {
375-
m_logger->Warn("'hook_states' not found in framework settings. Hooks will use default values.");
377+
if (isNewInstall) {
378+
m_logger->Debug("'hook_states' not found in framework settings (expected for new install). Hooks will use default values.");
379+
} else {
380+
m_logger->Warn("'hook_states' not found in framework settings. Hooks will use default values.");
381+
}
376382
return;
377383
}
378384

@@ -386,9 +392,14 @@ void Core::InitFeatureHooks() {
386392
hookManager.ReconcileHookState(hook, isEnabled); // Use ReconcileHookState
387393
m_logger->Info("Applied config for hook: {}. Enabled: {}.", hookName, isEnabled);
388394
} else {
389-
// If no config entry, use the hook's default enabled state (which is false for GameLogHook)
395+
// If no config entry, use the hook's default enabled state
390396
hookManager.ReconcileHookState(hook, hook->IsEnabled());
391-
m_logger->Warn("No config entry found for hook: {}. Using default state (Enabled: {}).", hookName, hook->IsEnabled());
397+
398+
if (isNewInstall) {
399+
m_logger->Debug("No config entry found for hook: {}. Using default state (Enabled: {}).", hookName, hook->IsEnabled());
400+
} else {
401+
m_logger->Warn("No config entry found for hook: {}. Using default state (Enabled: {}).", hookName, hook->IsEnabled());
402+
}
392403
}
393404
}
394405
}
@@ -438,7 +449,7 @@ void Core::InitManagersAndPlugins() {
438449
m_keyBindsManager = std::make_unique<KeyBindsManager>(*m_inputManager, *m_eventManager);
439450
m_configurableServices.push_back(m_keyBindsManager.get());
440451
// Initialize the UIManager singleton
441-
UIManager::GetInstance().Init(*m_eventManager, *m_inputManager, *m_configService, *m_keyBindsManager, PluginManager::GetInstance(), LoggerFactory::GetInstance(), *m_telemetryService);
452+
UIManager::GetInstance().Init(*m_eventManager, *m_inputManager, *m_configService, *m_keyBindsManager, PluginManager::GetInstance(), *m_communicationManager, LoggerFactory::GetInstance(), *m_telemetryService);
442453
m_configurableServices.push_back(&UIManager::GetInstance()); // Add the singleton to configurable services
443454
// Initialize the CommunicationManager
444455
reports.push_back(m_communicationManager->Initialize());

0 commit comments

Comments
 (0)