diff --git a/CVRMelonAssistant/CVRMelonAssistant.csproj b/CVRMelonAssistant/CVRMelonAssistant.csproj
index 02ae264..434dc3f 100644
--- a/CVRMelonAssistant/CVRMelonAssistant.csproj
+++ b/CVRMelonAssistant/CVRMelonAssistant.csproj
@@ -308,12 +308,21 @@
-
-
- $(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework)
-
-
-
-
-
+
+ $(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CVRMelonAssistant/Classes/InstallHandlers.cs b/CVRMelonAssistant/Classes/InstallHandlers.cs
index 859a6f8..a0259f8 100644
--- a/CVRMelonAssistant/Classes/InstallHandlers.cs
+++ b/CVRMelonAssistant/Classes/InstallHandlers.cs
@@ -2,6 +2,8 @@
using System.IO;
using System.IO.Compression;
using System.Linq;
+using System.Net.Http;
+using System.Text.Json.Nodes;
using System.Threading.Tasks;
using System.Windows;
using CVRMelonAssistant.Pages;
@@ -41,6 +43,29 @@ public static bool RemoveMelonLoader()
return true;
}
+ private static async Task GetLatestWindowsMelonLoaderNightlyDownloadUrl()
+ {
+ const string apiUrl =
+ "https://api.github.com/repos/LavaGang/MelonLoader/actions/workflows/5411546/runs" +
+ "?branch=alpha-development&event=push&status=success&per_page=1&sort=created&direction=desc";
+
+ using var http = new HttpClient();
+
+ // GitHub requires a user-agent header
+ http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0");
+
+ var resp = await http.GetStringAsync(apiUrl);
+ var json = JsonNode.Parse(resp)!["workflow_runs"]!.AsArray();
+
+ if (json.Count == 0)
+ return null;
+
+ var run = json[0];
+ var runId = run!["id"]!.ToString();
+
+ return $"https://nightly.link/LavaGang/MelonLoader/actions/runs/{runId}/MelonLoader.Windows.x64.CI.Release.zip";
+ }
+
public static async Task InstallMelonLoader()
{
if (!RemoveMelonLoader()) return;
@@ -49,7 +74,8 @@ public static async Task InstallMelonLoader()
{
MainWindow.Instance.MainText = $"{(string) App.Current.FindResource("Mods:DownloadingMelonLoader")}...";
- using var installerZip = await DownloadFileToMemory("https://github.com/LavaGang/MelonLoader/releases/latest/download/MelonLoader.x64.zip");
+ string nightlyLink = await GetLatestWindowsMelonLoaderNightlyDownloadUrl();
+ using var installerZip = await DownloadFileToMemory(nightlyLink);
using var zipReader = new ZipArchive(installerZip, ZipArchiveMode.Read);
MainWindow.Instance.MainText = $"{(string) App.Current.FindResource("Mods:UnpackingMelonLoader")}...";
diff --git a/CVRMelonAssistant/Classes/Mod.cs b/CVRMelonAssistant/Classes/Mod.cs
index ed0ce2d..86d85f7 100644
--- a/CVRMelonAssistant/Classes/Mod.cs
+++ b/CVRMelonAssistant/Classes/Mod.cs
@@ -16,6 +16,7 @@ public class Mod
public string installedVersion;
public bool installedInBrokenDir;
public bool installedInRetiredDir;
+ public int flag;
public class ModVersion
{
@@ -38,4 +39,9 @@ public class ModVersion
public bool IsPlugin => modType.Equals("plugin", StringComparison.InvariantCultureIgnoreCase);
}
}
+ public class FlagEntry
+ {
+ public int _id;
+ public int flag;
+ }
}
diff --git a/CVRMelonAssistant/Classes/Updater.cs b/CVRMelonAssistant/Classes/Updater.cs
index ddeea9f..5bc3c3f 100644
--- a/CVRMelonAssistant/Classes/Updater.cs
+++ b/CVRMelonAssistant/Classes/Updater.cs
@@ -9,7 +9,7 @@ namespace CVRMelonAssistant
{
class Updater
{
- private static readonly string APILatestURL = "https://api.github.com/repos/knah/CVRMelonAssistant/releases/latest";
+ private static readonly string APILatestURL = "https://api.github.com/repos/Nirv-git/CVRMelonAssistant/releases/latest";
private static Update LatestUpdate;
private static Version CurrentVersion;
diff --git a/CVRMelonAssistant/Classes/Utils.cs b/CVRMelonAssistant/Classes/Utils.cs
index e4abaec..d765da7 100644
--- a/CVRMelonAssistant/Classes/Utils.cs
+++ b/CVRMelonAssistant/Classes/Utils.cs
@@ -26,6 +26,7 @@ public class Constants
{
public const string ChilloutVRAppId = "661130";
public const string CVRMGModsJson = "https://api.cvrmg.com/v1/mods";
+ public const string CVRMGModsFlagsJson = "https://gist.githubusercontent.com/Nirv-git/1963e20d855c401349820a93b4d2639b/raw/cvrModFlags.json";
public const string WeebCDNAPIURL = "https://pat.assistant.moe/api/v1.0/";
public const string MD5Spacer = " ";
public static readonly char[] IllegalCharacters = new char[]
diff --git a/CVRMelonAssistant/Localisation/en.xaml b/CVRMelonAssistant/Localisation/en.xaml
index 88436f1..4c150a5 100644
--- a/CVRMelonAssistant/Localisation/en.xaml
+++ b/CVRMelonAssistant/Localisation/en.xaml
@@ -38,7 +38,7 @@
ChilloutVR allows modding the game under certain conditions, specifically as long as mods are not used for evil.
- ChilloutVR Modding Assistant and mods provided here are not associated with or endorsed by Alpha Blend Interactive (the devloper).
+ ChilloutVR Modding Assistant and mods provided here are not associated with or endorsed by ChilloutVR.
Mods
@@ -113,6 +113,8 @@
Failed to extract {0}, trying again in {1} seconds. ({2}/{3})
Failed to extract {0} after max attempts ({1}), skipping. This mod might not work properly so proceed at your own risk
Search...
+ ★ - Recommend for most users ⓘ - Niche, make sure you understand what the mod does
+
About
@@ -199,5 +201,5 @@
No description available
Download link:
Source code:
- Internal ID: {0} {1}
+ Internal ID: {0} - {1}
diff --git a/CVRMelonAssistant/ModInfoWindow.xaml b/CVRMelonAssistant/ModInfoWindow.xaml
index 307bd32..3736d67 100644
--- a/CVRMelonAssistant/ModInfoWindow.xaml
+++ b/CVRMelonAssistant/ModInfoWindow.xaml
@@ -18,7 +18,7 @@
-
+
diff --git a/CVRMelonAssistant/ModInfoWindow.xaml.cs b/CVRMelonAssistant/ModInfoWindow.xaml.cs
index 7df44b1..47a1dfb 100644
--- a/CVRMelonAssistant/ModInfoWindow.xaml.cs
+++ b/CVRMelonAssistant/ModInfoWindow.xaml.cs
@@ -1,6 +1,9 @@
using System;
+using System.Text.RegularExpressions;
using System.Windows;
+using System.Windows.Controls;
using System.Windows.Documents;
+using static System.Windows.Forms.LinkLabel;
namespace CVRMelonAssistant
{
@@ -16,6 +19,10 @@ public void SetMod(Mod mod)
Title = string.Format((string) FindResource("ModInfoWindow:Title"), mod.versions[0].name);
ModDescription.Text = mod.versions[0].description ?? (string) FindResource("ModInfoWindow:NoDescription");
+
+ var desc = mod.versions[0].description ?? (string)FindResource("ModInfoWindow:NoDescription");
+ SetMarkdownText(ModDescription, desc);
+
ModName.Text = mod.versions[0].name;
ModAuthor.Text = string.Format((string) FindResource("ModInfoWindow:Author"), mod.versions[0].author ?? FindResource("ModInfoWindow:NoAuthor"));
ModVersion.Text = mod.versions[0].modVersion;
@@ -39,9 +46,96 @@ public void SetMod(Mod mod)
InternalIds.Text = string.Format((string) FindResource("ModInfoWindow:InternalIds"), mod._id, mod.versions[0]._version);
}
+ private static readonly Regex Linkish = new(
+ @"\[(?[^\]]+)\]\((?https?://[^\s)]+)\)|<(?https?://[^>\s]+)>",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ private static readonly Regex Emphasis = new(
+ @"(?\*\*\*.+?\*\*\*|___.+?___)|(?\*\*.+?\*\*|__.+?__)|(?\*(?!\s).+?\*|_(?!\s).+?_)",
+ RegexOptions.Compiled);
+
+ public void SetMarkdownText(TextBlock tb, string? text)
+ {
+ tb.Inlines.Clear();
+ if (string.IsNullOrEmpty(text)) return;
+
+ int last = 0;
+ foreach (Match m in Linkish.Matches(text))
+ {
+ if (m.Index > last)
+ AddFormattedText(tb.Inlines, text.Substring(last, m.Index - last));
+
+ if (m.Groups["url"].Success)
+ { // [text](url)
+ var url = m.Groups["url"].Value;
+ var linkText = m.Groups["text"].Value;
+ var link = CreateHyperlink(linkText, url);
+ tb.Inlines.Add(link);
+ }
+ else
+ { //
+ string url = m.Groups["auto"].Value;
+ tb.Inlines.Add(CreateHyperlink(url));
+ }
+ last = m.Index + m.Length;
+ }
+
+ if (last < text.Length)
+ AddFormattedText(tb.Inlines, text.Substring(last));
+ }
+
+ // Parse **bold**, *italics*, and ***bold+italics***
+ private static void AddFormattedText(InlineCollection inlines, string text)
+ {
+ int last = 0;
+ foreach (Match m in Emphasis.Matches(text))
+ {
+ if (m.Index > last)
+ inlines.Add(new Run(text.Substring(last, m.Index - last)));
+
+ Inline styled;
+ if (m.Groups["strongem"].Success)
+ {
+ string inner = StripOuter(m.Value, 3);
+ styled = new Run(inner) { FontWeight = FontWeights.Bold, FontStyle = FontStyles.Italic };
+ }
+ else if (m.Groups["strong"].Success)
+ {
+ string inner = StripOuter(m.Value, 2);
+ styled = new Run(inner) { FontWeight = FontWeights.Bold };
+ }
+ else // em
+ {
+ string inner = StripOuter(m.Value, 1);
+ styled = new Run(inner) { FontStyle = FontStyles.Italic };
+ }
+
+ inlines.Add(styled);
+ last = m.Index + m.Length;
+ }
+
+ if (last < text.Length)
+ inlines.Add(new Run(text.Substring(last)));
+ }
+
+ private static string StripOuter(string s, int n)
+ {
+ if (s.Length >= 2 * n) return s.Substring(n, s.Length - 2 * n);
+ return s;
+ }
+
private static Hyperlink CreateHyperlink(string uri)
{
- var link = new Hyperlink(new Run(uri)) {NavigateUri = new Uri(uri)};
+ var link = new Hyperlink { NavigateUri = new Uri(uri) };
+ link.Inlines.Add(new Run(uri));
+ link.RequestNavigate += HyperlinkExtensions.Hyperlink_RequestNavigate;
+ return link;
+ }
+
+ private static Hyperlink CreateHyperlink(string text, string uri)
+ {
+ var link = new Hyperlink { NavigateUri = new Uri(uri) };
+ link.Inlines.Add(new Run(text));
link.RequestNavigate += HyperlinkExtensions.Hyperlink_RequestNavigate;
return link;
}
diff --git a/CVRMelonAssistant/Pages/Mods.xaml b/CVRMelonAssistant/Pages/Mods.xaml
index ed3e2ae..d2890a7 100644
--- a/CVRMelonAssistant/Pages/Mods.xaml
+++ b/CVRMelonAssistant/Pages/Mods.xaml
@@ -28,34 +28,45 @@
+ Text="{DynamicResource Mods:SearchLabel}"
+ FontSize="20"/>
+ TextChanged="SearchBar_TextChanged"
+ FontSize="20"/>
+ MouseDoubleClick="ModsListView_OnMouseDoubleClick"
+ Margin="0,0,0,20"
+ ScrollViewer.CanContentScroll="True"
+ VirtualizingStackPanel.IsVirtualizing="True"
+ VirtualizingStackPanel.VirtualizationMode="Recycling"
+ VirtualizingPanel.IsVirtualizingWhenGrouping="True"
+ VirtualizingPanel.ScrollUnit="Pixel"
+ VirtualizingPanel.CacheLengthUnit="Item"
+ VirtualizingPanel.CacheLength="2">
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
@@ -159,6 +185,17 @@
+
+
+
diff --git a/CVRMelonAssistant/Pages/Mods.xaml.cs b/CVRMelonAssistant/Pages/Mods.xaml.cs
index 1003790..c0d7c40 100644
--- a/CVRMelonAssistant/Pages/Mods.xaml.cs
+++ b/CVRMelonAssistant/Pages/Mods.xaml.cs
@@ -21,6 +21,7 @@
using MessageBox = System.Windows.MessageBox;
using TextBox = System.Windows.Controls.TextBox;
+
namespace CVRMelonAssistant.Pages
{
///
@@ -137,6 +138,8 @@ public async Task CheckInstalledMods()
{
await GetAllMods();
+ await GetFlagMapping();
+
await Task.Run(() =>
{
CheckInstallDir("Plugins");
@@ -171,6 +174,31 @@ public async Task GetAllMods()
}
}
+ public async Task GetFlagMapping()
+ {
+ try
+ {
+ var resp = await HttpClient.GetAsync(Utils.Constants.CVRMGModsFlagsJson);
+ var body = await resp.Content.ReadAsStringAsync();
+
+ var entries = JsonSerializer.Deserialize(body);
+
+ if (entries == null || entries.Length == 0 || AllModsList == null) return;
+
+ var flagById = new Dictionary(entries.Length);
+ foreach (var e in entries)
+ flagById[e._id] = e.flag;
+
+ foreach (var mod in AllModsList)
+ mod.flag = flagById.TryGetValue(mod._id, out var f) ? f : 0;
+
+ }
+ catch (Exception e)
+ {
+ System.Windows.MessageBox.Show($"{FindResource("Mods:LoadFailed")} - Flags.\n\n" + e);
+ }
+ }
+
private void CheckInstallDir(string directory, bool isBrokenDir = false, bool isRetiredDir = false)
{
if (!Directory.Exists(Path.Combine(App.ChilloutInstallDirectory, directory)))
@@ -180,8 +208,8 @@ private void CheckInstallDir(string directory, bool isBrokenDir = false, bool is
foreach (string file in Directory.GetFileSystemEntries(Path.Combine(App.ChilloutInstallDirectory, directory), "*.dll", SearchOption.TopDirectoryOnly))
{
- if (!File.Exists(file) || Path.GetExtension(file) != ".dll") continue;
+ if (!File.Exists(file) || Path.GetExtension(file) != ".dll") continue;
var modInfo = ExtractModVersions(file);
if (modInfo.Item1 != null && modInfo.Item2 != null)
{
@@ -295,6 +323,7 @@ ModListItem.CategoryInfo GetCategory(Mod mod)
{
IsSelected = preSelected,
IsEnabled = true,
+ Flag = mod.flag,
ModName = latestVersion.name,
ModVersion = latestVersion.modVersion,
ModAuthor = HardcodedCategories.FixupAuthor(latestVersion.author),
@@ -373,6 +402,7 @@ public class ModListItem
public bool IsEnabled { get; set; }
public bool IsSelected { get; set; }
+ public int Flag { get; set; }
public Mod ModInfo { get; set; }
public CategoryInfo Category { get; set; }
@@ -399,6 +429,34 @@ public string InstalledVersion
}
}
+ public string GetFlagString
+ {
+ get
+ {
+ switch (Flag)
+ {
+ case 1: return "★";
+ case 2: return "♥";
+ case 3: return "ⓘ"; //!! ⓘ Ⓘ
+ default: return "";
+ }
+ }
+ }
+
+ public string GetFlagColor
+ {
+ get
+ {
+ switch(Flag)
+ {
+ case 1: return "#d6b600"; //FFD700
+ case 2: return "#98002e";
+ case 3: return "#e06c00"; //orange #e06c00 purpl #7000e0
+ default: return "";
+ }
+ }
+ }
+
public string GetVersionColor
{
get
@@ -519,7 +577,8 @@ private void SearchButton_Click(object sender, RoutedEventArgs e)
SearchBar.Focus();
Animate(SearchBar, 0, 20, new TimeSpan(0, 0, 0, 0, 300));
Animate(SearchText, 0, 20, new TimeSpan(0, 0, 0, 0, 300));
- ModsListView.Items.Filter = new Predicate