Skip to content

Commit 8eb76fd

Browse files
committed
Added Update Checker
1 parent dc273dd commit 8eb76fd

6 files changed

Lines changed: 198 additions & 1 deletion

File tree

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ createJavaBindings.dependsOn(clean)
7474

7575
tasks.register('generateBuildInfo', GenerateBuildInfoTask) {
7676
outputFile = new File(temporaryDir, "build_info.json")
77+
forkReleaseVersion = providers.gradleProperty("fork_release_version").orElse("")
7778
}
7879

7980
tasks.register("prodClient", ClientProductionRunTask) {

buildSrc/src/main/java/dev/xpple/seedmapper/buildscript/GenerateBuildInfoTask.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import com.google.gson.JsonObject;
55
import org.gradle.api.DefaultTask;
66
import org.gradle.api.file.RegularFileProperty;
7+
import org.gradle.api.provider.Property;
78
import org.gradle.api.tasks.OutputFile;
9+
import org.gradle.api.tasks.Input;
810
import org.gradle.api.tasks.TaskAction;
911
import org.gradle.process.ExecOperations;
1012

@@ -22,6 +24,9 @@ public abstract class GenerateBuildInfoTask extends DefaultTask {
2224
@OutputFile
2325
public abstract RegularFileProperty getOutputFile();
2426

27+
@Input
28+
public abstract Property<String> getForkReleaseVersion();
29+
2530
@Inject
2631
protected abstract ExecOperations getExecOperations();
2732

@@ -39,6 +44,7 @@ protected void run() {
3944

4045
JsonObject object = new JsonObject();
4146
object.addProperty("version", version);
47+
object.addProperty("forkReleaseVersion", this.getForkReleaseVersion().getOrElse(""));
4248
object.addProperty("branch", branch);
4349
object.addProperty("shortCommitHash", shortCommitHash);
4450
object.addProperty("commitHash", commitHash);

src/main/java/dev/xpple/seedmapper/config/Configs.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ private static Component getDatapackAutoloadComment() {
9797
return Component.literal("Automatically load the saved datapack for the current server.");
9898
}
9999

100+
@Config(comment = "getUpdateCheckerComment")
101+
public static boolean UpdateChecker = true;
102+
private static Component getUpdateCheckerComment() {
103+
return Component.literal("Check Modrinth for fork updates on server join and notify in chat.");
104+
}
105+
100106
@Config(comment = "getDatapackSavedUrlsComment")
101107
public static Map<String, String> DatapackSavedUrls = new HashMap<>();
102108
private static Component getDatapackSavedUrlsComment() {

src/main/java/dev/xpple/seedmapper/mixin/ClientPacketListenerMixin.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import dev.xpple.seedmapper.render.RenderManager;
99
import dev.xpple.seedmapper.seedmap.SeedMapMinimapManager;
1010
import dev.xpple.seedmapper.util.BaritoneIntegration;
11+
import dev.xpple.seedmapper.util.ModrinthUpdateChecker;
1112
import net.minecraft.client.multiplayer.ClientPacketListener;
1213
import net.minecraft.network.protocol.game.ClientboundLoginPacket;
1314
import net.minecraft.network.protocol.game.ClientboundRespawnPacket;
@@ -40,6 +41,9 @@ private void onHandleLogin(ClientboundLoginPacket packet, CallbackInfo ci) {
4041
DatapackImportCommand.importUrlForCurrentServer(url);
4142
}
4243
}
44+
if (Configs.UpdateChecker) {
45+
ModrinthUpdateChecker.checkAndNotify();
46+
}
4347
}
4448

4549
@Inject(method = "handleRespawn", at = @At("HEAD"))

src/main/java/dev/xpple/seedmapper/util/BuildInfo.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,32 @@ private BuildInfo() {
1818
private static final Logger LOGGER = LogUtils.getLogger();
1919

2020
public static final String VERSION;
21+
public static final String FORK_RELEASE_VERSION;
2122
public static final String BRANCH;
2223
public static final String SHORT_COMMIT_HASH;
2324
public static final String COMMIT_HASH;
2425

2526
static {
26-
String version, branch, shortCommitHash, commitHash;
27+
String version, forkReleaseVersion, branch, shortCommitHash, commitHash;
2728
version = branch = shortCommitHash = commitHash = "unknown";
29+
forkReleaseVersion = "";
2830
try (BufferedReader reader = Files.newBufferedReader(FabricLoader.getInstance()
2931
.getModContainer(SeedMapper.MOD_ID).orElseThrow()
3032
.findPath("build_info.json").orElseThrow())
3133
) {
3234
JsonObject object = JsonParser.parseReader(reader).getAsJsonObject();
3335
version = object.get("version").getAsString();
36+
if (object.has("forkReleaseVersion")) {
37+
forkReleaseVersion = object.get("forkReleaseVersion").getAsString();
38+
}
3439
branch = object.get("branch").getAsString();
3540
shortCommitHash = object.get("shortCommitHash").getAsString();
3641
commitHash = object.get("commitHash").getAsString();
3742
} catch (RuntimeException | IOException e) {
3843
LOGGER.error("Error while reading build_info.json", e);
3944
}
4045
VERSION = version;
46+
FORK_RELEASE_VERSION = forkReleaseVersion;
4147
BRANCH = branch;
4248
SHORT_COMMIT_HASH = shortCommitHash;
4349
COMMIT_HASH = commitHash;
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package dev.xpple.seedmapper.util;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonElement;
5+
import com.google.gson.JsonObject;
6+
import com.google.gson.JsonParser;
7+
import com.mojang.logging.LogUtils;
8+
import net.minecraft.ChatFormatting;
9+
import net.minecraft.client.Minecraft;
10+
import net.minecraft.network.chat.Component;
11+
import net.minecraft.network.chat.MutableComponent;
12+
import org.slf4j.Logger;
13+
14+
import java.io.IOException;
15+
import java.net.URI;
16+
import java.net.http.HttpClient;
17+
import java.net.http.HttpRequest;
18+
import java.net.http.HttpResponse;
19+
import java.nio.charset.StandardCharsets;
20+
import java.time.Duration;
21+
import java.util.Locale;
22+
import java.util.concurrent.CompletableFuture;
23+
24+
import static dev.xpple.seedmapper.util.ChatBuilder.accent;
25+
26+
public final class ModrinthUpdateChecker {
27+
private static final Logger LOGGER = LogUtils.getLogger();
28+
private static final String MODRINTH_PROJECT_ID = "2qXosh15";
29+
private static final HttpClient HTTP = HttpClient.newBuilder()
30+
.connectTimeout(Duration.ofSeconds(5))
31+
.build();
32+
33+
private static volatile boolean hasCheckedThisSession = false;
34+
35+
private ModrinthUpdateChecker() {
36+
}
37+
38+
public static void checkAndNotify() {
39+
if (hasCheckedThisSession) {
40+
return;
41+
}
42+
hasCheckedThisSession = true;
43+
44+
String localVersion = normalizeVersion(BuildInfo.FORK_RELEASE_VERSION);
45+
if (localVersion.isEmpty()) {
46+
LOGGER.warn("Skipping update check because fork release version is missing in build_info.json");
47+
return;
48+
}
49+
50+
CompletableFuture
51+
.supplyAsync(() -> fetchLatestForkVersion().orElse(null))
52+
.thenAccept(remoteVersion -> {
53+
Minecraft minecraft = Minecraft.getInstance();
54+
minecraft.execute(() -> {
55+
if (minecraft.player == null || remoteVersion == null) {
56+
return;
57+
}
58+
int cmp = compareVersions(localVersion, remoteVersion);
59+
if (cmp < 0) {
60+
minecraft.player.sendSystemMessage(Component.empty()
61+
.append(Component.literal("[SeedMapper] ").withStyle(ChatFormatting.GOLD))
62+
.append(Component.literal("Update available: ").withStyle(ChatFormatting.YELLOW))
63+
.append(Component.literal("you are on "))
64+
.append(accent(localVersion))
65+
.append(Component.literal(", latest is "))
66+
.append(accent(remoteVersion))
67+
.append(Component.literal(".")));
68+
}
69+
});
70+
})
71+
.exceptionally(ex -> {
72+
LOGGER.debug("Update check failed", ex);
73+
return null;
74+
});
75+
}
76+
77+
private static java.util.Optional<String> fetchLatestForkVersion() {
78+
try {
79+
URI uri = URI.create("https://api.modrinth.com/v2/project/" + MODRINTH_PROJECT_ID + "/version?featured=true&include_changelog=false");
80+
HttpRequest request = HttpRequest.newBuilder(uri)
81+
.GET()
82+
.timeout(Duration.ofSeconds(8))
83+
.header("User-Agent", "SeedMapper/" + BuildInfo.VERSION + " (update-check)")
84+
.build();
85+
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
86+
if (response.statusCode() != 200) {
87+
LOGGER.debug("Update check returned HTTP {}", response.statusCode());
88+
return java.util.Optional.empty();
89+
}
90+
return parseLatestVersion(response.body());
91+
} catch (IOException | InterruptedException e) {
92+
if (e instanceof InterruptedException) {
93+
Thread.currentThread().interrupt();
94+
}
95+
LOGGER.debug("Update check request failed", e);
96+
return java.util.Optional.empty();
97+
} catch (RuntimeException e) {
98+
LOGGER.debug("Update check parse failed", e);
99+
return java.util.Optional.empty();
100+
}
101+
}
102+
103+
private static java.util.Optional<String> parseLatestVersion(String body) {
104+
JsonElement parsed = JsonParser.parseString(body);
105+
if (!parsed.isJsonArray()) {
106+
return java.util.Optional.empty();
107+
}
108+
JsonArray versions = parsed.getAsJsonArray();
109+
String latestVersion = null;
110+
String latestDate = "";
111+
for (JsonElement element : versions) {
112+
if (!element.isJsonObject()) {
113+
continue;
114+
}
115+
JsonObject obj = element.getAsJsonObject();
116+
if (!obj.has("version_number") || !obj.has("date_published")) {
117+
continue;
118+
}
119+
String version = normalizeVersion(obj.get("version_number").getAsString());
120+
String date = obj.get("date_published").getAsString();
121+
if (version.isEmpty()) {
122+
continue;
123+
}
124+
if (latestVersion == null || date.compareTo(latestDate) > 0) {
125+
latestVersion = version;
126+
latestDate = date;
127+
}
128+
}
129+
return java.util.Optional.ofNullable(latestVersion);
130+
}
131+
132+
private static String normalizeVersion(String version) {
133+
if (version == null) {
134+
return "";
135+
}
136+
String normalized = version.trim();
137+
if (normalized.startsWith("v") || normalized.startsWith("V")) {
138+
normalized = normalized.substring(1);
139+
}
140+
return normalized;
141+
}
142+
143+
private static int compareVersions(String local, String remote) {
144+
String[] localParts = local.split("[^A-Za-z0-9]+");
145+
String[] remoteParts = remote.split("[^A-Za-z0-9]+");
146+
int len = Math.max(localParts.length, remoteParts.length);
147+
for (int i = 0; i < len; i++) {
148+
String a = i < localParts.length ? localParts[i] : "0";
149+
String b = i < remoteParts.length ? remoteParts[i] : "0";
150+
int cmp;
151+
if (isDigits(a) && isDigits(b)) {
152+
cmp = Integer.compare(Integer.parseInt(a), Integer.parseInt(b));
153+
} else {
154+
cmp = a.toLowerCase(Locale.ROOT).compareTo(b.toLowerCase(Locale.ROOT));
155+
}
156+
if (cmp != 0) {
157+
return cmp;
158+
}
159+
}
160+
return 0;
161+
}
162+
163+
private static boolean isDigits(String value) {
164+
if (value == null || value.isEmpty()) {
165+
return false;
166+
}
167+
for (int i = 0; i < value.length(); i++) {
168+
if (!Character.isDigit(value.charAt(i))) {
169+
return false;
170+
}
171+
}
172+
return true;
173+
}
174+
}

0 commit comments

Comments
 (0)