Skip to content

Commit a7ea159

Browse files
committed
Added Update Checker
1 parent dc273dd commit a7ea159

6 files changed

Lines changed: 204 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: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
return;
69+
}
70+
minecraft.player.sendSystemMessage(Component.empty()
71+
.append(Component.literal("[SeedMapper] ").withStyle(ChatFormatting.DARK_GREEN))
72+
.append(Component.literal("You are up to date ("))
73+
.append(accent(localVersion))
74+
.append(Component.literal(").")));
75+
});
76+
})
77+
.exceptionally(ex -> {
78+
LOGGER.debug("Update check failed", ex);
79+
return null;
80+
});
81+
}
82+
83+
private static java.util.Optional<String> fetchLatestForkVersion() {
84+
try {
85+
URI uri = URI.create("https://api.modrinth.com/v2/project/" + MODRINTH_PROJECT_ID + "/version?featured=true&include_changelog=false");
86+
HttpRequest request = HttpRequest.newBuilder(uri)
87+
.GET()
88+
.timeout(Duration.ofSeconds(8))
89+
.header("User-Agent", "SeedMapper/" + BuildInfo.VERSION + " (update-check)")
90+
.build();
91+
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
92+
if (response.statusCode() != 200) {
93+
LOGGER.debug("Update check returned HTTP {}", response.statusCode());
94+
return java.util.Optional.empty();
95+
}
96+
return parseLatestVersion(response.body());
97+
} catch (IOException | InterruptedException e) {
98+
if (e instanceof InterruptedException) {
99+
Thread.currentThread().interrupt();
100+
}
101+
LOGGER.debug("Update check request failed", e);
102+
return java.util.Optional.empty();
103+
} catch (RuntimeException e) {
104+
LOGGER.debug("Update check parse failed", e);
105+
return java.util.Optional.empty();
106+
}
107+
}
108+
109+
private static java.util.Optional<String> parseLatestVersion(String body) {
110+
JsonElement parsed = JsonParser.parseString(body);
111+
if (!parsed.isJsonArray()) {
112+
return java.util.Optional.empty();
113+
}
114+
JsonArray versions = parsed.getAsJsonArray();
115+
String latestVersion = null;
116+
String latestDate = "";
117+
for (JsonElement element : versions) {
118+
if (!element.isJsonObject()) {
119+
continue;
120+
}
121+
JsonObject obj = element.getAsJsonObject();
122+
if (!obj.has("version_number") || !obj.has("date_published")) {
123+
continue;
124+
}
125+
String version = normalizeVersion(obj.get("version_number").getAsString());
126+
String date = obj.get("date_published").getAsString();
127+
if (version.isEmpty()) {
128+
continue;
129+
}
130+
if (latestVersion == null || date.compareTo(latestDate) > 0) {
131+
latestVersion = version;
132+
latestDate = date;
133+
}
134+
}
135+
return java.util.Optional.ofNullable(latestVersion);
136+
}
137+
138+
private static String normalizeVersion(String version) {
139+
if (version == null) {
140+
return "";
141+
}
142+
String normalized = version.trim();
143+
if (normalized.startsWith("v") || normalized.startsWith("V")) {
144+
normalized = normalized.substring(1);
145+
}
146+
return normalized;
147+
}
148+
149+
private static int compareVersions(String local, String remote) {
150+
String[] localParts = local.split("[^A-Za-z0-9]+");
151+
String[] remoteParts = remote.split("[^A-Za-z0-9]+");
152+
int len = Math.max(localParts.length, remoteParts.length);
153+
for (int i = 0; i < len; i++) {
154+
String a = i < localParts.length ? localParts[i] : "0";
155+
String b = i < remoteParts.length ? remoteParts[i] : "0";
156+
int cmp;
157+
if (isDigits(a) && isDigits(b)) {
158+
cmp = Integer.compare(Integer.parseInt(a), Integer.parseInt(b));
159+
} else {
160+
cmp = a.toLowerCase(Locale.ROOT).compareTo(b.toLowerCase(Locale.ROOT));
161+
}
162+
if (cmp != 0) {
163+
return cmp;
164+
}
165+
}
166+
return 0;
167+
}
168+
169+
private static boolean isDigits(String value) {
170+
if (value == null || value.isEmpty()) {
171+
return false;
172+
}
173+
for (int i = 0; i < value.length(); i++) {
174+
if (!Character.isDigit(value.charAt(i))) {
175+
return false;
176+
}
177+
}
178+
return true;
179+
}
180+
}

0 commit comments

Comments
 (0)