aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAaron <51387595+AzureAaron@users.noreply.github.com>2024-08-18 01:05:49 -0400
committerGitHub <noreply@github.com>2024-08-18 01:05:49 -0400
commit996ff078f92fe4c825eb53aab3ddd12601a685fa (patch)
tree2af3d332e5aa72e930f4e759b473ba83549c8712 /src
parent9fc533c7ee5b9d6693b351e15dba3b8df62233b8 (diff)
parent3270ccf82048f53869426982c6fe8dab76807ef3 (diff)
downloadSkyblocker-996ff078f92fe4c825eb53aab3ddd12601a685fa.tar.gz
Skyblocker-996ff078f92fe4c825eb53aab3ddd12601a685fa.tar.bz2
Skyblocker-996ff078f92fe4c825eb53aab3ddd12601a685fa.zip
Merge pull request #877 from AzureAaron/update-notification
Update Notifications
Diffstat (limited to 'src')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/UpdateNotifications.java215
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java16
-rw-r--r--src/main/resources/assets/skyblocker/lang/en_us.json11
-rw-r--r--src/test/java/de/hysky/skyblocker/UpdateNotificationsTest.java67
5 files changed, 310 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index 2cfd61a7..47ad0f41 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -116,6 +116,7 @@ public class SkyblockerMod implements ClientModInitializer {
SkyblockerScreen.initClass();
ProfileViewerScreen.initClass();
Tips.init();
+ UpdateNotifications.init();
NEURepoManager.init();
//ImageRepoLoader.init();
ItemRepository.init();
diff --git a/src/main/java/de/hysky/skyblocker/UpdateNotifications.java b/src/main/java/de/hysky/skyblocker/UpdateNotifications.java
new file mode 100644
index 00000000..2034de2f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/UpdateNotifications.java
@@ -0,0 +1,215 @@
+package de.hysky.skyblocker;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+import org.jetbrains.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+
+import com.google.gson.JsonParser;
+import com.mojang.logging.LogUtils;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.DataResult;
+import com.mojang.serialization.JsonOps;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Http;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.fabricmc.loader.api.SemanticVersion;
+import net.fabricmc.loader.api.Version;
+import net.fabricmc.loader.api.VersionParsingException;
+import net.minecraft.SharedConstants;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.toast.SystemToast;
+import net.minecraft.text.ClickEvent;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.StringIdentifiable;
+
+public class UpdateNotifications {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final String BASE_URL = "https://api.modrinth.com/v2/project/y6DuFGwJ/version?loaders=[%22fabric%22]&game_versions=";
+ private static final Version MOD_VERSION = SkyblockerMod.SKYBLOCKER_MOD.getMetadata().getVersion();
+ private static final String MC_VERSION = SharedConstants.getGameVersion().getId();
+ private static final Path CONFIG_PATH = SkyblockerMod.CONFIG_DIR.resolve("update_notifications.json");
+ @VisibleForTesting
+ protected static final Comparator<Version> COMPARATOR = Version::compareTo;
+ @VisibleForTesting
+ protected static final Codec<SemanticVersion> SEM_VER_CODEC = Codec.STRING.comapFlatMap(UpdateNotifications::parseVersion, SemanticVersion::toString);
+ private static final SystemToast.Type TOAST_TYPE = new SystemToast.Type(10000L);
+
+ public static Config config = Config.DEFAULT;
+ private static boolean sentUpdateNotification;
+
+ static void init() {
+ ClientLifecycleEvents.CLIENT_STARTED.register(client -> loadConfig());
+ ClientLifecycleEvents.CLIENT_STOPPING.register(client -> saveConfig());
+ SkyblockEvents.JOIN.register(() -> {
+ if (config.enabled() && !sentUpdateNotification) checkForNewVersion();
+ });
+ }
+
+ private static void loadConfig() {
+ CompletableFuture.supplyAsync(() -> {
+ try (BufferedReader reader = Files.newBufferedReader(CONFIG_PATH)) {
+ return Config.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow();
+ } catch (NoSuchFileException ignored) {
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Update Notifications] Failed to load config!", e);
+ }
+
+ return Config.DEFAULT;
+ }).thenAccept(loadedConfig -> config = loadedConfig);
+ }
+
+ private static void saveConfig() {
+ try (BufferedWriter writer = Files.newBufferedWriter(CONFIG_PATH)) {
+ SkyblockerMod.GSON.toJson(Config.CODEC.encodeStart(JsonOps.INSTANCE, config).getOrThrow(), writer);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Update Notifications] Failed to save config :(", e);
+ }
+ }
+
+ private static void checkForNewVersion() {
+ CompletableFuture.runAsync(() -> {
+ try {
+ SemanticVersion version = (SemanticVersion) MOD_VERSION; //Would only fail because someone changed it themselves
+ String response = Http.sendGetRequest(BASE_URL + "[%22" + MC_VERSION + "%22]");
+ List<MrVersion> mrVersions = MrVersion.LIST_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(response)).getOrThrow();
+
+ //Set it to true now so that we don't keep re-checking if the data should be discarded
+ sentUpdateNotification = true;
+
+ Optional<MrVersion> newestVersion = mrVersions.stream()
+ .filter(ver -> Arrays.stream(config.includedChannels()).anyMatch(channel -> channel == ver.channel()))
+ .filter(mrv -> COMPARATOR.compare(mrv.version(), version) > 0)
+ .max(Comparator.comparing(MrVersion::version, COMPARATOR));
+
+ if (newestVersion.isPresent() && CLIENT.player != null && !shouldDiscard(version, newestVersion.get().version())) {
+ MrVersion newVersion = newestVersion.get();
+ String downloadLink = "https://modrinth.com/mod/skyblocker-liap/version/" + newVersion.id();
+ Text versionText = Text.literal(newVersion.name()).styled(style -> style
+ .withFormatting(Formatting.GRAY)
+ .withUnderline(true)
+ .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, downloadLink)));
+
+ CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.updateNotifications.newUpdateMessage", versionText)));
+ SystemToast.add(CLIENT.getToastManager(), TOAST_TYPE, Text.translatable("skyblocker.updateNotifications.newUpdateToast.title"), Text.stringifiedTranslatable("skyblocker.updateNotifications.newUpdateToast.description", newVersion.version()));
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Update Notifications] Failed to determine if an update is available or not!", e);
+ }
+ });
+ }
+
+ private static DataResult<SemanticVersion> parseVersion(String version) {
+ String formattedVersion = switch (version) {
+ case String s when s.charAt(0) == 'v' -> s.substring(1);
+
+ default -> version;
+ };
+
+ try {
+ return DataResult.success(SemanticVersion.parse(formattedVersion));
+ } catch (VersionParsingException e) {
+ return DataResult.error(() -> "Failed to parse semantic version from string: " + formattedVersion);
+ }
+ }
+
+ private static boolean isUnofficialAlphaOrBeta(SemanticVersion version) {
+ return switch (version.getPrereleaseKey().orElse("")) {
+ case String s when s.startsWith("alpha") -> s.substring(5).charAt(0) == '-';
+ case String s when s.startsWith("beta") -> s.substring(4).charAt(0) == '-';
+
+ default -> false;
+ };
+ }
+
+ /**
+ * Since our "unofficial" betas and alphas (from actions) take after the latest release number we want to discard them from the checker
+ * if the current version is "unofficial" and the major, minor, and patch versions match.
+ */
+ @VisibleForTesting
+ protected static boolean shouldDiscard(SemanticVersion currentVersion, SemanticVersion latestVersion) {
+ if (isUnofficialAlphaOrBeta(currentVersion)) {
+ //We will expect all 3 components to be present
+
+ int currentMajor = currentVersion.getVersionComponent(0);
+ int currentMinor = currentVersion.getVersionComponent(1);
+ int currentPatch = currentVersion.getVersionComponent(2);
+
+ int latestMajor = latestVersion.getVersionComponent(0);
+ int latestMinor = latestVersion.getVersionComponent(1);
+ int latestPatch = latestVersion.getVersionComponent(2);
+
+ return currentMajor == latestMajor && currentMinor == latestMinor && currentPatch == latestPatch;
+ }
+
+ return false;
+ }
+
+ public record Config(boolean enabled, Channel channel) {
+ public static final Config DEFAULT = new Config(true, Channel.RELEASE);
+ private static final Codec<Config> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.BOOL.fieldOf("enabled").forGetter(Config::enabled),
+ Channel.CODEC.fieldOf("channel").forGetter(Config::channel))
+ .apply(instance, Config::new));
+
+ private Channel[] includedChannels() {
+ return switch (this.channel) {
+ case BETA -> new Channel[] { Channel.RELEASE, Channel.BETA };
+ case ALPHA -> Channel.values();
+
+ default -> new Channel[] { this.channel };
+ };
+ }
+
+ public Config withEnabled(boolean newEnabled) {
+ return new Config(newEnabled, this.channel);
+ }
+
+ public Config withChannel(Channel newChannel) {
+ return new Config(this.enabled, newChannel);
+ }
+ }
+
+ private record MrVersion(String id, String name, SemanticVersion version, Channel channel) {
+ private static final Codec<MrVersion> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.STRING.fieldOf("id").forGetter(MrVersion::id),
+ Codec.STRING.fieldOf("name").forGetter(MrVersion::name),
+ SEM_VER_CODEC.fieldOf("version_number").forGetter(MrVersion::version),
+ Channel.CODEC.fieldOf("version_type").forGetter(MrVersion::channel))
+ .apply(instance, MrVersion::new));
+ private static final Codec<List<MrVersion>> LIST_CODEC = CODEC.listOf();
+ }
+
+ public enum Channel implements StringIdentifiable {
+ RELEASE,
+ BETA,
+ ALPHA;
+
+ private static final Codec<Channel> CODEC = StringIdentifiable.createBasicCodec(Channel::values);
+
+ @Override
+ public String toString() {
+ return I18n.translate("skyblocker.config.general.updateChannel.channel." + name());
+ }
+
+ @Override
+ public String asString() {
+ return name().toLowerCase();
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
index ff089432..12a669de 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
@@ -1,6 +1,7 @@
package de.hysky.skyblocker.config.categories;
import de.hysky.skyblocker.SkyblockerScreen;
+import de.hysky.skyblocker.UpdateNotifications;
import de.hysky.skyblocker.config.ConfigUtils;
import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.configs.GeneralConfig;
@@ -35,6 +36,21 @@ public class GeneralCategory {
.controller(ConfigUtils::createBooleanController)
.build())
.option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.general.updateNotifications"))
+ .binding(UpdateNotifications.Config.DEFAULT.enabled(),
+ () -> UpdateNotifications.config.enabled(),
+ newValue -> UpdateNotifications.config = UpdateNotifications.config.withEnabled(newValue))
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<UpdateNotifications.Channel>createBuilder()
+ .name(Text.translatable("skyblocker.config.general.updateChannel"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.general.updateChannel.@Tooltip")))
+ .binding(UpdateNotifications.Config.DEFAULT.channel(),
+ () -> UpdateNotifications.config.channel(),
+ newValue -> UpdateNotifications.config = UpdateNotifications.config.withChannel(newValue))
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
.name(Text.translatable("skyblocker.config.general.acceptReparty"))
.binding(defaults.general.acceptReparty,
() -> config.general.acceptReparty,
diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json
index 158b3cc9..cbea310d 100644
--- a/src/main/resources/assets/skyblocker/lang/en_us.json
+++ b/src/main/resources/assets/skyblocker/lang/en_us.json
@@ -320,6 +320,13 @@
"skyblocker.config.general.specialEffects.rareDyeDropEffects": "Rare Dye Drop Effects",
"skyblocker.config.general.specialEffects.rareDyeDropEffects.@Tooltip": "Adds a special visual effect triggered upon dropping a rare dye!",
+ "skyblocker.config.general.updateChannel": "Update Channel",
+ "skyblocker.config.general.updateChannel.@Tooltip": "Choose between receiving notifications for releases, for both releases and betas, or for releases; betas; and alphas.",
+ "skyblocker.config.general.updateChannel.channel.ALPHA": "Alpha",
+ "skyblocker.config.general.updateChannel.channel.BETA": "Beta",
+ "skyblocker.config.general.updateChannel.channel.RELEASE": "Release",
+ "skyblocker.config.general.updateNotifications": "Update Notifications",
+
"skyblocker.config.general.wikiLookup": "Wiki Lookup",
"skyblocker.config.general.wikiLookup.enableWikiLookup": "Enable Wiki Lookup",
"skyblocker.config.general.wikiLookup.enableWikiLookup.@Tooltip": "Opens the wiki page of the hovered item with the F4 key.",
@@ -785,6 +792,10 @@
"skyblocker.config.uiAndVisuals.waypoints.waypointType.@Tooltip": "Waypoint: Displays a highlight and a beacon beam.\n\nOutlined Waypoint: Displays both a waypoint and an outline.\n\nHighlight: Only displays a highlight.\n\nOutlined Highlight: Displays both a highlight and an outline.\n\nOutline: Only displays an outline.",
"skyblocker.config.uiAndVisuals.waypoints.waypointType.generalNote": "\n\n\nThis option does not apply to all waypoints. Some waypoints such as secret waypoints have their own waypoint type option.",
+ "skyblocker.updateNotifications.newUpdateMessage": "There's a new Skyblocker update available! %s",
+ "skyblocker.updateNotifications.newUpdateToast.title": "Skyblocker Update Available!",
+ "skyblocker.updateNotifications.newUpdateToast.description": "Download version %s!",
+
"skyblocker.utils.locationUpdateError": "Failed to update your location! Some features of the mod may not work properly :(",
"skyblocker.reparty.notInPartyOrNotLeader": "You must be in a party and be the leader of it to reparty!",
diff --git a/src/test/java/de/hysky/skyblocker/UpdateNotificationsTest.java b/src/test/java/de/hysky/skyblocker/UpdateNotificationsTest.java
new file mode 100644
index 00000000..751ab570
--- /dev/null
+++ b/src/test/java/de/hysky/skyblocker/UpdateNotificationsTest.java
@@ -0,0 +1,67 @@
+package de.hysky.skyblocker;
+
+import java.util.Comparator;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.JavaOps;
+
+import net.fabricmc.loader.api.SemanticVersion;
+import net.fabricmc.loader.api.Version;
+import net.minecraft.Bootstrap;
+import net.minecraft.SharedConstants;
+
+public class UpdateNotificationsTest {
+ private final Comparator<Version> COMPARATOR = UpdateNotifications.COMPARATOR;
+ private final Codec<SemanticVersion> SEM_VER_CODEC = UpdateNotifications.SEM_VER_CODEC;
+ private final SemanticVersion LATEST_VERSION = SEM_VER_CODEC.parse(JavaOps.INSTANCE, "1.22.0+1.21").getOrThrow();
+
+ @BeforeAll
+ public static void setupEnvironment() {
+ SharedConstants.createGameVersion();
+ Bootstrap.initialize();
+ }
+
+ @Test
+ void testLatestAgainstRegular() {
+ SemanticVersion regular = SEM_VER_CODEC.parse(JavaOps.INSTANCE, "1.21.1+1.21").getOrThrow();
+
+ //Requires that the latest be newer than this normal release version
+ Assertions.assertTrue(COMPARATOR.compare(LATEST_VERSION, regular) > 0);
+ }
+
+ @Test
+ void testLatestAgainstBeta() {
+ SemanticVersion beta = SEM_VER_CODEC.parse(JavaOps.INSTANCE, "1.22.0-beta.1+1.21").getOrThrow();
+
+ //Requires that the latest be newer than the beta
+ Assertions.assertTrue(COMPARATOR.compare(LATEST_VERSION, beta) > 0);
+ }
+
+ @Test
+ void testLatestAgainstAlpha() {
+ SemanticVersion alpha = SEM_VER_CODEC.parse(JavaOps.INSTANCE, "1.22.0-alpha.1+1.21").getOrThrow();
+
+ //Requires that the latest be newer than the alpha
+ Assertions.assertTrue(COMPARATOR.compare(LATEST_VERSION, alpha) > 0);
+ }
+
+ @Test
+ void testLatestAgainstOldAlpha() {
+ SemanticVersion oldAlpha = SEM_VER_CODEC.parse(JavaOps.INSTANCE, "1.21.1-alpha-pr-888-afc81df+1.21").getOrThrow();
+
+ //Requires the alpha is older than the latest
+ Assertions.assertEquals(COMPARATOR.compare(oldAlpha, LATEST_VERSION), -1);
+ }
+
+ @Test
+ void testThatTheCurrentAlphaAgainstLatestShouldBeDiscarded() {
+ SemanticVersion currentAlpha = SEM_VER_CODEC.parse(JavaOps.INSTANCE, "1.22.0-alpha-pr-908-fe7d89a+1.21").getOrThrow();
+
+ //Requires that the current alpha be discarded against the latest version
+ Assertions.assertTrue(UpdateNotifications.shouldDiscard(currentAlpha, LATEST_VERSION));
+ }
+}