3#include "imgui/imgui.h"
4#include "threadpp/future.hpp"
16#include <hpp/string_view.hpp>
17#include <hpp/optional.hpp>
20#include <ser20/external/simdjson/simdjson.h>
32fs::path github_rate_limit_cfg = fs::persistent_path() /
"unravel" /
"github_rate_limit.cfg";
35constexpr auto github_check_interval = std::chrono::hours(6);
41auto read_last_check_time() -> std::int64_t
44 if(!fs::exists(github_rate_limit_cfg, ec) || ec)
48 std::ifstream file(github_rate_limit_cfg);
53 std::int64_t timestamp = 0;
65void write_last_check_time()
68 auto parent = github_rate_limit_cfg.parent_path();
69 if(!fs::exists(parent, ec))
71 fs::create_directories(parent, ec);
76 std::ofstream file(path, std::ios::trunc);
79 auto now = std::chrono::system_clock::now();
80 auto epoch_seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
81 file << epoch_seconds;
90auto should_check_github() ->
bool
92 auto last_check = read_last_check_time();
97 auto now = std::chrono::system_clock::now();
98 auto now_seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
99 auto elapsed = std::chrono::seconds(now_seconds - last_check);
100 return elapsed >= github_check_interval;
152static auto parse_engine_version(hpp::string_view ver) -> std::optional<EngineVersion>
160static auto compare_versions(
const EngineVersion&
a,
const EngineVersion&
b) ->
int
168static auto extract_version_from_asset_name(hpp::string_view asset_name) -> std::optional<EngineVersion>
171 constexpr hpp::string_view prefix =
"UnravelEngine-";
172 if(asset_name.size() < prefix.size() || asset_name.substr(0, prefix.size()) != prefix)
178 hpp::string_view version_part = asset_name.substr(prefix.size());
182 constexpr const char* platform_prefixes[] = {
183 "-Windows-",
"-Linux-",
"-macOS-",
"-Darwin-",
"-Android-"
186 size_t version_end = version_part.size();
187 for(
const char*
platform : platform_prefixes)
189 size_t pos = version_part.find(
platform);
190 if(pos != hpp::string_view::npos)
193 hpp::string_view candidate = version_part.substr(0, pos);
194 auto parsed = parse_engine_version(candidate);
204 if(version_end == version_part.size())
207 for(
size_t i = 1;
i < version_part.size(); ++
i)
209 if(version_part[i - 1] ==
'-' && version_part[i] >=
'A' && version_part[i] <=
'Z')
213 hpp::string_view candidate = version_part.substr(0, i - 1);
214 auto parsed = parse_engine_version(candidate);
225 hpp::string_view version_str = version_part.substr(0, version_end);
226 auto result = parse_engine_version(version_str);
229 if(!result && version_end == version_part.size())
239static auto is_newer_version_available(hpp::string_view current_version_str, hpp::string_view latest_version_str) -> hpp::optional<bool>
241 auto cur = parse_engine_version(current_version_str);
242 auto lat = parse_engine_version(latest_version_str);
247 return compare_versions(*cur, *lat) < 0;
252struct GitHubClientConfig
268 std::string
host =
"api.github.com";
275static auto parse_release_json(
const simdjson::dom::element& r) -> GitHubRelease
282 std::string_view tag_sv;
283 if(
tag_name.get(tag_sv) == simdjson::SUCCESS)
284 rel.tag_name = std::string(tag_sv);
287 auto name =
r[
"name"];
290 std::string_view name_sv;
291 if(
name.get(name_sv) == simdjson::SUCCESS)
292 rel.name = std::string(name_sv);
299 if(
draft.get(draft_val) == simdjson::SUCCESS)
300 rel.draft = draft_val;
307 if(
prerelease.get(prerelease_val) == simdjson::SUCCESS)
308 rel.prerelease = prerelease_val;
314 std::string_view published_sv;
316 rel.published_at = std::string(published_sv);
320 if(!
assets.error() &&
assets.type() == simdjson::dom::element_type::ARRAY)
326 auto asset_name =
a[
"name"];
327 if(!asset_name.error())
329 std::string_view name_sv;
330 if(asset_name.get(name_sv) == simdjson::SUCCESS)
331 asset.name = std::string(name_sv);
334 auto browser_url =
a[
"browser_download_url"];
335 if(!browser_url.error())
337 std::string_view url_sv;
338 if(browser_url.get(url_sv) == simdjson::SUCCESS)
339 asset.browser_download_url = std::string(url_sv);
342 auto size =
a[
"size"];
346 if(
size.get(size_val) == simdjson::SUCCESS)
347 asset.size = size_val;
354 rel.assets.push_back(std::move(asset));
371static auto fetch_github_releases(
const GitHubClientConfig& cfg) -> FetchResult
457static auto get_best_version_from_release(
const GitHubRelease& release) -> std::optional<EngineVersion>
460 auto tag_ver = parse_engine_version(release.tag_name);
463 std::optional<EngineVersion> asset_ver;
464 for(
const auto& asset : release.assets)
466 auto extracted = extract_version_from_asset_name(asset.name);
470 if(!asset_ver || compare_versions(*extracted, *asset_ver) > 0)
472 asset_ver = extracted;
478 if(tag_ver && asset_ver)
480 return (compare_versions(*tag_ver, *asset_ver) > 0) ? tag_ver : asset_ver;
492static auto pick_latest_by_version(
const std::vector<GitHubRelease>&
releases) -> std::optional<GitHubRelease>
494 std::optional<GitHubRelease> best;
495 std::optional<EngineVersion> best_ver;
499 auto release_ver = get_best_version_from_release(r);
506 if(!best || compare_versions(*release_ver, *best_ver) > 0)
509 best_ver = release_ver;
519static auto check_for_update(
const std::vector<GitHubRelease>&
releases,
520 hpp::string_view current_version) -> std::optional<GitHubRelease>
522 auto cur = parse_engine_version(current_version);
528 auto latest = pick_latest_by_version(
releases);
533 auto latest_ver = get_best_version_from_release(*latest);
537 if(compare_versions(*cur, *latest_ver) < 0)
546static void print_releases_and_assets(
const std::vector<GitHubRelease>&
releases)
550 std::string release_info =
"Release: " +
r.tag_name;
552 release_info +=
" (" +
r.name +
")";
553 if(!
r.published_at.empty())
554 release_info +=
" published: " +
r.published_at;
556 release_info +=
" [prerelease]";
558 release_info +=
" [draft]";
561 for(
const auto&
a :
r.assets)
563 APPLOG_TRACE(
" - {} ({:.2f} MB)",
a.name,
a.size / (1024.0 * 1024.0));
570auto check_for_update(
bool force_check =
false) ->
bool
572 if(!should_check_github() && !force_check)
574 APPLOG_TRACE(
"Skipping GitHub update check (last check was less than 6 hours ago).");
580 write_last_check_time();
583 GitHubClientConfig cfg;
584 cfg.owner =
"unravel-dev";
585 cfg.repo =
"UnravelEngine";
587 cfg.include_drafts =
false;
588 cfg.include_prereleases =
false;
594 auto fetch_result = fetch_github_releases(cfg);
595 if(fetch_result.has_error())
597 APPLOG_ERROR(
"Error fetching releases: {}", fetch_result.error_message);
603 print_releases_and_assets(fetch_result.releases);
606 if(
auto update = check_for_update(fetch_result.releases, current_version))
612 for(
const auto&
a :
update->assets)
620 APPLOG_TRACE(
"No update available. Current: {}", current_version);
632 return should_check_github();
634 .then(tpp::this_thread::get_id(),
637 auto has_update = result.get();
648 write_last_check_time();
652 [](
const ImGuiToast& toast,
float opacity,
const ImVec4& text_color)
654 ImGui::TextColored(text_color,
"New update available!");
656 if(ImGui::Button(
"Install Now"))
658 ImGui::OpenInShell(
"https://github.com/unravel-dev/UnravelEngine/releases");
677 return check_for_update(
true);
679 .then(tpp::this_thread::get_id(),
682 auto has_update = result.get();
686 toast.set_title(
"New version available.");
693 write_last_check_time();
697 [](
const ImGuiToast& toast,
float opacity,
const ImVec4& text_color)
699 ImGui::Text(
"Download from ");
701 ImGui::TextLinkOpenURL(
"Releases.",
"https://github.com/unravel-dev/UnravelEngine/releases");
708 toast.set_title(
"There are no updates available.");
709 toast.set_content(
"You are using the latest version.");
NOTIFY_INLINE auto set_show_dismiss_button(const bool &show) -> void
NOTIFY_INLINE auto set_on_dismiss(const std::function< void()> &callback) -> void
NOTIFY_INLINE auto set_horizontal_pos(const ImGuiToastHorizontalPos &pos) -> void
NOTIFY_INLINE auto set_draw_callback(const ImGuiToastDrawCallback &callback) -> void
@ ImGuiToastHorizontalPos_Left
#define APPLOG_ERROR(...)
#define APPLOG_TRACE(...)
NOTIFY_INLINE void PushNotification(const ImGuiToast &toast)
Insert a new toast in the list.
void update(dynamic_index_buffer_handle _handle, uint32_t _startIndex, const memory_view *_mem)
void atomic_write_file(const fs::path &dst, const std::function< void(const fs::path &)> &callback, fs::error_code &ec) noexcept
auto parse(std::string_view text) -> std::optional< engine_version >
Parses a version string. Returns nullopt on malformed input.
auto get_full() -> std::string
auto compare(const engine_version &a, const engine_version &b) -> int
void check_for_update_async()
auto init(rtti::context &ctx) -> bool
auto deinit(rtti::context &ctx) -> bool
std::string error_message
std::vector< GitHubAsset > assets
std::vector< GitHubRelease > releases
std::string browser_download_url