Skip to content

Commit 09b3adf

Browse files
committed
Add more mirror options for people that cannot access maddie480.ovh
1 parent 7967398 commit 09b3adf

File tree

14 files changed

+340
-37
lines changed

14 files changed

+340
-37
lines changed

changelog.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ and only contains the latest changes.
33
Its purpose is to be shown in Olympus when updating.
44

55
#changelog#
6-
Prevent "Disable All" from disabling favorited mods
6+
Added more mirror options for people that cannot access maddie480.ovh

sharp/CmdGetLoennLatestVersion.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
using System.Net.Http;
55

66
namespace Olympus {
7-
public class CmdGetLoennLatestVersion : Cmd<Tuple<string, string>> {
7+
public class CmdGetLoennLatestVersion : Cmd<bool, Tuple<string, string>> {
88

99
public override bool Taskable => true;
1010

11-
public override Tuple<string, string> Run() {
11+
public override Tuple<string, string> Run(bool apiMirror) {
1212
try {
1313
using (HttpClient client = new HttpClientWithCompressionSupport()) {
14-
string json = client.GetStringAsync("https://maddie480.ovh/celeste/loenn-versions").Result;
14+
string json = client.GetStringAsync(
15+
apiMirror ? "https://everestapi.github.io/updatermirror/loenn_versions.json" : "https://maddie480.ovh/celeste/loenn-versions"
16+
).Result;
1517
JObject latestVersion = (JObject) JToken.Parse(json);
1618
return new Tuple<string, string>((string) latestVersion["tag_name"], GetDownloadLink((JArray) latestVersion["assets"]));
1719
}

sharp/CmdGetModIdToNameMap.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
using System.Text;
77

88
namespace Olympus {
9-
public class CmdGetModIdToNameMap : Cmd<string, bool> {
9+
public class CmdGetModIdToNameMap : Cmd<string, bool, bool> {
1010
public override bool Taskable => true;
1111

1212
private static string cacheLocation;
13+
private static bool apiMirror;
1314

14-
public override bool Run(string cacheLocation) {
15+
public override bool Run(string cacheLocation, bool apiMirror) {
1516
CmdGetModIdToNameMap.cacheLocation = cacheLocation;
17+
CmdGetModIdToNameMap.apiMirror = apiMirror;
1618
Console.Error.WriteLine($"[CmdGetIdToNameMap] Cache location set to: {cacheLocation}");
1719
GetModIDsToNamesMap(ignoreCache: true);
1820
return true;
@@ -34,10 +36,12 @@ internal static Dictionary<string, string> GetModIDsToNamesMap(bool ignoreCache
3436
if (map.Count > 0) return map;
3537
}
3638

37-
Console.Error.WriteLine($"[CmdGetIdToNameMap] Loading mod IDs from maddie480.ovh");
39+
Console.Error.WriteLine($"[CmdGetIdToNameMap] Loading mod IDs from the Internet");
3840
map = tryRun(() => {
3941
using (HttpClient wc = new HttpClientWithCompressionSupport())
40-
using (Stream inputStream = wc.GetAsync("https://maddie480.ovh/celeste/mod_ids_to_names.json").Result.Content.ReadAsStream()) {
42+
using (Stream inputStream = wc.GetAsync(
43+
apiMirror ? "https://everestapi.github.io/updatermirror/mod_ids_to_names.json" : "https://maddie480.ovh/celeste/mod_ids_to_names.json"
44+
).Result.Content.ReadAsStream()) {
4145
return getModIDsToNamesMap(inputStream);
4246
}
4347
});

sharp/Cmds.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public static void Init() {
3030
new CmdCrash(),
3131
new CmdDummyTask(),
3232
new CmdEcho(),
33+
new CmdEmulatedModList(),
34+
new CmdEmulatedModSearch(),
3335
new CmdFree(),
3436
new CmdGetEnv(),
3537
new CmdGetLoennLatestVersion(),

sharp/GameBananaAPIEmulator.cs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
using Newtonsoft.Json;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Globalization;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Net.Http;
8+
using System.Text;
9+
using System.Text.RegularExpressions;
10+
11+
namespace Olympus {
12+
// https://github.com/maddie480/RandomStuffWebsite/blob/main/src/main/java/ovh/maddie480/randomstuff/frontend/CelesteModSearchService.java
13+
// but it's entirely client-side because some poor souls can't access maddie480.ovh (yay!)
14+
15+
class GameBananaAPIEmulator {
16+
private static List<Dictionary<string, object>> everything;
17+
public static List<Dictionary<string, object>> Get() {
18+
if (everything == null) {
19+
using (HttpClient client = new HttpClientWithCompressionSupport())
20+
using (Stream inputStream = client.GetAsync("https://everestapi.github.io/updatermirror/mod_search_database.yaml").Result.Content.ReadAsStream())
21+
using (TextReader reader = new StreamReader(inputStream)) {
22+
everything = YamlHelper.Deserializer.Deserialize<List<Dictionary<string, object>>>(reader);
23+
}
24+
}
25+
26+
return everything;
27+
}
28+
}
29+
30+
public class CmdEmulatedModList : Cmd<string, int, string, int?, int?, string> {
31+
public override string Run(string sort, int page, string type, int? category, int? subcategory) {
32+
// is there a type and/or a category filter?
33+
List<Predicate<Dictionary<string, object>>> typeFilters = new List<Predicate<Dictionary<string, object>>>();
34+
if (type != null) {
35+
typeFilters.Add(info => type.Equals(info["GameBananaType"]));
36+
}
37+
if (category != null) {
38+
typeFilters.Add(info => category == int.Parse((string) info["CategoryId"]));
39+
}
40+
if (subcategory != null) {
41+
typeFilters.Add(info => info.ContainsKey("SubcategoryId") && subcategory == int.Parse((string) info["SubcategoryId"]));
42+
}
43+
// typeFilter is a && of all typeFilters
44+
Predicate<Dictionary<string, object>> typeFilter = info => typeFilters.All(filter => filter(info));
45+
46+
// determine the field on which we want to sort. Sort by descending id to tell equal values apart.
47+
IComparer<Dictionary<string, object>> sortComparator;
48+
switch (sort) {
49+
case "views":
50+
sortComparator = new Sorter("Views");
51+
break;
52+
case "likes":
53+
sortComparator = new Sorter("Likes");
54+
break;
55+
case "downloads":
56+
sortComparator = new Sorter("Downloads");
57+
break;
58+
default:
59+
sortComparator = new Sorter("CreatedDate");
60+
break;
61+
}
62+
63+
// then sort on it.
64+
List<Dictionary<string, object>> response = GameBananaAPIEmulator.Get()
65+
.Where(d => typeFilter.Invoke(d))
66+
.OrderBy(d => d, sortComparator)
67+
.Skip((page - 1) * 20)
68+
.Take(20)
69+
.ToList();
70+
71+
return JsonConvert.SerializeObject(response);
72+
}
73+
74+
private class Sorter(string field) : IComparer<Dictionary<string, object>> {
75+
public int Compare(Dictionary<string, object> x, Dictionary<string, object> y) {
76+
int diff = int.Parse((string) y[field]) - int.Parse((string) x[field]);
77+
if (diff != 0) return diff;
78+
return int.Parse((string) x["GameBananaId"]) - int.Parse((string) y["GameBananaId"]);
79+
}
80+
}
81+
}
82+
83+
public partial class CmdEmulatedModSearch : Cmd<string, string> {
84+
public override string Run(string query) {
85+
string[] tokenizedRequest = tokenize(query);
86+
87+
List<Dictionary<string, object>> response = GameBananaAPIEmulator.Get()
88+
.Select(m => new Tuple<Dictionary<string, object>, double>(m, scoreMod(tokenizedRequest, tokenize((string) m["Name"]))))
89+
.Where(m => m.Item2 > 0.2 * tokenizedRequest.Length)
90+
.OrderBy(m => m, new Sorter())
91+
.Select(m => m.Item1)
92+
.Take(20)
93+
.ToList();
94+
95+
return JsonConvert.SerializeObject(response);
96+
}
97+
98+
private class Sorter : IComparer<Tuple<Dictionary<string, object>, double>> {
99+
public int Compare(Tuple<Dictionary<string, object>, double> x, Tuple<Dictionary<string, object>, double> y) {
100+
double diff = y.Item2 - x.Item2;
101+
if (diff != 0) return Math.Sign(diff);
102+
return int.Parse((string) y.Item1["Downloads"]) - int.Parse((string) x.Item1["Downloads"]);
103+
}
104+
}
105+
106+
private static double scoreMod(string[] query, string[] modName) {
107+
double score = 0;
108+
109+
foreach (string tokenSearch in query) {
110+
if (tokenSearch.EndsWith('*')) {
111+
// "starts with" search: add 1 if there's a word starting with the prefix
112+
string tokenSearchStart = tokenSearch.Substring(0, tokenSearch.Length - 1);
113+
foreach (string tokenModName in modName) {
114+
if (tokenModName.StartsWith(tokenSearchStart)) {
115+
score++;
116+
break;
117+
}
118+
}
119+
} else {
120+
// "equals" search: take the score of the word that is closest to the token
121+
double tokenScore = 0;
122+
foreach (string tokenModName in modName) {
123+
tokenScore = Math.Max(tokenScore, Math.Pow(0.5, levenshteinDistance(tokenSearch, tokenModName)));
124+
}
125+
score += tokenScore;
126+
}
127+
}
128+
129+
return score;
130+
}
131+
132+
private static string[] tokenize(string s) {
133+
s = removeDiacritics(s.ToLowerInvariant()) // "Pokémon" => "pokemon"
134+
.Replace("'", ""); // "Maddie's Helping Hand" => "maddies helping hand"
135+
s = notDigitOrLetter().Replace(s, " "); // "The D-Sides Pack" => "the d sides pack"
136+
while (s.Contains(" ")) s = s.Replace(" ", " ");
137+
return s.Split(" ");
138+
}
139+
140+
// Source - https://stackoverflow.com/a/249126
141+
// Posted by Blair Conrad, modified by community. See post 'Timeline' for change history
142+
// Retrieved 2025-11-09, License - CC BY-SA 4.0
143+
private static string removeDiacritics(string text) {
144+
var normalizedString = text.Normalize(NormalizationForm.FormD);
145+
var stringBuilder = new StringBuilder(capacity: normalizedString.Length);
146+
147+
for (int i = 0; i < normalizedString.Length; i++) {
148+
char c = normalizedString[i];
149+
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
150+
if (unicodeCategory != UnicodeCategory.NonSpacingMark) {
151+
stringBuilder.Append(c);
152+
}
153+
}
154+
155+
return stringBuilder
156+
.ToString()
157+
.Normalize(NormalizationForm.FormC);
158+
}
159+
160+
// Source - https://www.dotnetperls.com/levenshtein
161+
private static int levenshteinDistance(string s, string t) {
162+
int n = s.Length;
163+
int m = t.Length;
164+
int[,] d = new int[n + 1, m + 1];
165+
166+
// Verify arguments.
167+
if (n == 0) {
168+
return m;
169+
}
170+
if (m == 0) {
171+
return n;
172+
}
173+
174+
// Initialize arrays.
175+
for (int i = 0; i <= n; d[i, 0] = i++) {
176+
}
177+
for (int j = 0; j <= m; d[0, j] = j++) {
178+
}
179+
180+
// Begin looping.
181+
for (int i = 1; i <= n; i++) {
182+
for (int j = 1; j <= m; j++) {
183+
// Compute cost.
184+
int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
185+
d[i, j] = Math.Min(
186+
Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
187+
d[i - 1, j - 1] + cost);
188+
}
189+
}
190+
// Return cost.
191+
return d[n, m];
192+
}
193+
194+
[GeneratedRegex("[^a-z0-9* ]")]
195+
private static partial Regex notDigitOrLetter();
196+
}
197+
}

src/config.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ function config.load()
134134

135135
default(data, "updateModsOnStartup", "none")
136136
default(data, "useOpenGL", "disabled")
137+
137138
default(data, "mirrorPreferences", "gb,jade,otobot,wegfan")
139+
default(data, "apiMirror", false)
140+
default(data, "imageMirror", "jade") -- jade, otobot or none
138141

139142
default(data, "closeAfterOneClickInstall", "disabled")
140143

src/data/themes/light.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
"normalBorder": [0.08, 0.08, 0.08, 0.6, 1],
4343
"normalFG": [0, 0, 0, 0.8, 0]
4444
},
45+
"greentext": {
46+
"color": [0.1, 0.5, 0.1, 1]
47+
},
4548
"group": {
4649
"bg": [],
4750
"border": [],

src/elements.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ uie.add("modNameLabelColors", {
4242
}
4343
})
4444

45+
uie.add("greentext", {
46+
style = { color = { 0.5, 0.8, 0.5, 1 } }
47+
})
48+
4549
-- A simple clickable icon that can be toggled on and off. Child classes should override getColor()
4650
-- The fading mechanic is the same as in uie.button
4751
uie.add("clickableIcon", {

src/main.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ function love.load(args)
204204
else
205205
print("Olympus version was transmitted to sharp: " .. sharp.setOlympusVersion(utils.trim(utils.load("version.txt") or "ERROR")):result())
206206
threader.routine(function()
207-
sharp.getModIdToNameMap(fs.joinpath(fs.getStorageDir(), "cached-mod-ids-to-names.json"))
207+
sharp.getModIdToNameMap(fs.joinpath(fs.getStorageDir(), "cached-mod-ids-to-names.json"), config.apiMirror)
208208
end)
209209
end
210210

src/scenes/everest.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,12 @@ function scene.load()
427427
}):with(uiu.bottombound):with(uiu.rightbound):as("loadingVersions")
428428
)
429429

430-
local buildsTask = threader.wrap("utils").downloadJSON("https://maddie480.ovh/celeste/everest-versions")
430+
local buildsTask = threader.wrap("utils").downloadJSON(
431+
config.apiMirror
432+
and "https://everestapi.github.io/updatermirror/everest_versions.json"
433+
or "https://maddie480.ovh/celeste/everest-versions"
434+
)
435+
431436
local list = root:findChild("versions")
432437

433438
local manualItem = uie.listItem("Select .zip from disk", "manual"):with(uiu.fillWidth)

0 commit comments

Comments
 (0)