Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Knossos.NET/Classes/KnUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1391,5 +1391,28 @@ public static void CreateDesktopShortcut(string shortcutName, string destFileFul
Log.Add(Log.LogSeverity.Error, "KnUtils.CreateDesktopShortcut()", ex);
}
}

/// <summary>
/// Determines if the current OS meets the requirements for .NET 10 / "latest" channel updates.
/// Windows 10+ or macOS 12.0+ qualify.
/// </summary>
public static bool IsModernOS()
{
if (IsWindows)
{
// Windows build 10240 = Windows 10 RTM
return Environment.OSVersion.Version.Major >= 10;
}

if (IsMacOS)
{
var ver = Environment.OSVersion.Version;
// macOS 12.0
return ver.Major >= 12;
}

// Linux and anything else: treat as modern (no restriction planned)
return true;
}
}
}
115 changes: 109 additions & 6 deletions Knossos.NET/Models/GitHubApi.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using System;
using Knossos.NET.Classes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading.Tasks;

Expand All @@ -9,28 +13,127 @@ namespace Knossos.NET.Models
public static class GitHubApi
{
/// <summary>
/// Get the last release on Knet github repo
/// URL of this repo is set on the Knossos class
/// Gets the latest applicable release from GitHub.
/// - Modern platforms (Win10+ / macOS 12.0+): uses /releases/latest (current behaviour).
/// - Legacy platforms: scans /releases, keeps only v1.3.x tags, returns the newest one.
/// </summary>
/// <returns>GitHubRelease or null if the api call failed</returns>
/// <returns>GitHubRelease or null if the API call failed.</returns>
public static async Task<GitHubRelease?> GetLastRelease()
{
if (KnUtils.IsModernOS())
{
return await GetLatestRelease();
}
else
{
return await GetLatestLegacyRelease();
}
}

/// <summary>
/// Calls /releases/latest — for modern platforms.
/// </summary>
private static async Task<GitHubRelease?> GetLatestRelease()
{
try
{
var client = KnUtils.GetHttpClient();
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("product", "1"));
using var response = await client.GetAsync(Knossos.GitHubUpdateRepoURL + "/releases/latest");
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<GitHubRelease>(json)!;
return JsonSerializer.Deserialize<GitHubRelease>(json);
}
catch (Exception ex)
{
Log.Add(Log.LogSeverity.Error, "GitHubApi.GetLastRelease()", ex);
Log.Add(Log.LogSeverity.Error, "GitHubApi.GetLatestRelease()", ex);
return null;
}
}

/// <summary>
/// Scans /releases pages looking for v1.3.x tags and returns the newest one.
/// Stops paginating early once tags leave the 1.3.x range (assumes releases
/// are returned newest-first by the API).
/// </summary>
private static async Task<GitHubRelease?> GetLatestLegacyRelease()
{
try
{
var client = KnUtils.GetHttpClient();
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("product", "1"));

var candidates = new List<GitHubRelease>();
int page = 1;
const int perPage = 30; // GitHub default; max is 100

while (true)
{
var url = $"{Knossos.GitHubUpdateRepoURL}/releases?per_page={perPage}&page={page}";
using var response = await client.GetAsync(url);
var json = await response.Content.ReadAsStringAsync();
var releases = JsonSerializer.Deserialize<List<GitHubRelease>>(json);

if (releases == null || releases.Count == 0)
break;

foreach (var release in releases)
{
if (IsLegacyTag(release.tag_name))
{
candidates.Add(release);
}
}

// If the oldest release on this page is already below 1.3.7, no need to go further
var lastTag = releases.Last().tag_name;
if (lastTag != null && IsBelowLegacyMinor(lastTag))
break;

if (releases.Count < perPage)
break; // last page

page++;
}

// Return the candidate with the highest semantic version
return candidates
.Where(r => r.tag_name != null)
.MaxBy(r => new SemanticVersion(NormalizeTag(r.tag_name!)));
}
catch (Exception ex)
{
Log.Add(Log.LogSeverity.Error, "GitHubApi.GetLatestLegacyRelease()", ex);
return null;
}
}

/// <summary>
/// Returns true if the tag is a v1.3.x release (e.g. "v1.3.7", "v1.3.10-rc1").
/// </summary>
private static bool IsLegacyTag(string? tag)
{
if (string.IsNullOrWhiteSpace(tag)) return false;
var version = new SemanticVersion(NormalizeTag(tag));
// Check major == 1 and minor == 3 by comparing against sentinels
return SemanticVersion.Compare(NormalizeTag(tag), "1.3.0") >= 0 &&
SemanticVersion.Compare(NormalizeTag(tag), "1.4.0") < 0;
}

/// <summary>
/// Returns true if the tag is strictly older than 1.3.7 — used as an early-exit
/// hint while paginating (GitHub returns releases newest-first).
/// </summary>
private static bool IsBelowLegacyMinor(string tag)
{
return SemanticVersion.Compare(NormalizeTag(tag), "1.3.7") < 0;
}

/// <summary>Strips the leading "v" that GitHub tags typically have.</summary>
private static string NormalizeTag(string tag) =>
tag.ToLower().Replace("v", "").Trim();
}


public class GitHubRelease
{
public string? url { get; set; }
Expand Down
12 changes: 11 additions & 1 deletion Knossos.NET/ViewModels/GlobalSettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ public partial class GlobalSettingsViewModel : ViewModelBase
private const long speed10MB = 170000000;

/* For display only */
[ObservableProperty]
[ObservableProperty]
internal string knossosUpdateChannelInfo = "";
[ObservableProperty]
internal bool isPortableMode = false;
[ObservableProperty]
internal bool flagDataLoaded = false;
Expand Down Expand Up @@ -552,6 +554,14 @@ public string GlobalCmd
public GlobalSettingsViewModel()
{
isPortableMode = Knossos.inPortableMode;
if (KnUtils.IsModernOS())
{
knossosUpdateChannelInfo = "(Main Update Channel)";
Copy link
Copy Markdown
Contributor

@JohnAFernandez JohnAFernandez Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my humble opinion, we should only need to display text if we are in the legacy update channel to provide a smoother experience.

And there should be a tooltip when it is displayed, whether we display it all times or not.

Something along the lines of

Main update channel:
"Your operating system is compatible with all versions of Knossos and will receive both features and bugfixes."

Legacy update Channel:
"Your version of <operating system> is not compatible with version 1.4 of Knossos.NET and above. To receive new features, you should upgrade to version <version>. You will still receive bugfix updates when they apply to version 1.3."

}
else
{
knossosUpdateChannelInfo = "(Legacy Update Channel)";
}
}

public void CheckDisplaySettingsWarning()
Expand Down
1 change: 1 addition & 0 deletions Knossos.NET/Views/GlobalSettingsView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<WrapPanel Margin="5,10,0,0">
<Label Margin="0,5,0,0" Width="205">Auto Update KnosssosNET</Label>
<ToggleSwitch IsVisible="{Binding CheckUpdates}" Margin="5,0,0,0" IsChecked="{Binding AutoUpdate}" ToolTip.Tip="The update will be automatically downloaded and installed. KnossosNET will be restarted without any user input."></ToggleSwitch>
<Label Margin="10,1,0,0" Content="{Binding KnossosUpdateChannelInfo}" VerticalAlignment="Center"/>
</WrapPanel>
<WrapPanel Margin="5,10,0,0">
<Label Margin="0,5,0,0" Width="205">Auto Update FSO Builds</Label>
Expand Down