diff options
Diffstat (limited to 'src/main')
6 files changed, 225 insertions, 23 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index 0bd3c8e3..2987f493 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -2,6 +2,8 @@ package de.hysky.skyblocker; import com.google.gson.Gson; import com.google.gson.GsonBuilder; + +import de.hysky.skyblocker.config.ImageRepoLoader; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.skyblock.*; @@ -94,6 +96,7 @@ public class SkyblockerMod implements ClientModInitializer { SkyblockerConfigManager.init(); Tips.init(); NEURepoManager.init(); + ImageRepoLoader.init(); ItemRepository.init(); PlayerHeadHashCache.init(); HotbarSlotLock.init(); diff --git a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java index 8b0f27a7..781f7f15 100644 --- a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java +++ b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java @@ -1,16 +1,26 @@ package de.hysky.skyblocker.config; import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.OptionDescription; import dev.isxander.yacl3.api.controller.*; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.FileUtils; +import java.nio.file.Path; import java.util.function.Function; public class ConfigUtils { public static final ValueFormatter<Formatting> FORMATTING_FORMATTER = formatting -> Text.literal(StringUtils.capitalize(formatting.getName().replaceAll("_", " "))); public static final ValueFormatter<Float> FLOAT_TWO_FORMATTER = value -> Text.literal(String.format("%,.2f", value).replaceAll("[\u00a0\u202F]", " ")); + private static final Path IMAGE_DIRECTORY = ImageRepoLoader.REPO_DIRECTORY.resolve("Skyblocker-Assets-images"); public static BooleanControllerBuilder createBooleanController(Option<Boolean> opt) { return BooleanControllerBuilder.create(opt).yesNoFormatter().coloured(true); @@ -34,4 +44,15 @@ public class ConfigUtils { public static <E extends Enum<E>> Function<Option<E>, ControllerBuilder<E>> getEnumDropdownControllerFactory(ValueFormatter<E> formatter) { return opt -> EnumDropdownControllerBuilder.create(opt).formatValue(formatter); } + + /** + * Creates an {@link OptionDescription} with an image and text. + */ + @SafeVarargs + public static OptionDescription withImage(Path imagePath, @Nullable Text... texts) { + return OptionDescription.createBuilder() + .text(ArrayUtils.isNotEmpty(texts) ? texts : new Text[] {}) + .image(IMAGE_DIRECTORY.resolve(imagePath), new Identifier(SkyblockerMod.NAMESPACE, "config_image_" + FileUtils.normalizePath(imagePath))) + .build(); + } } diff --git a/src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java b/src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java new file mode 100644 index 00000000..0591cd96 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java @@ -0,0 +1,144 @@ +package de.hysky.skyblocker.config; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.CompletableFuture; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.slf4j.Logger; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.FileUtils; +import de.hysky.skyblocker.utils.Http; + +public class ImageRepoLoader { + private static final Logger LOGGER = LogUtils.getLogger(); + static final Path REPO_DIRECTORY = SkyblockerMod.CONFIG_DIR.resolve("image-repo"); + private static final String BRANCH_INFO = "https://api.github.com/repos/SkyblockerMod/Skyblocker-Assets/branches/images"; + private static final String REPO_DOWNLOAD = "https://github.com/SkyblockerMod/Skyblocker-Assets/archive/refs/heads/images.zip"; + private static final String PLACEHOLDER_HASH = "None!"; + + public static void init() { + update(0); + } + + /** + * Attempts to update/load the image repository, if any errors are encountered it will try 3 times. + */ + private static void update(int retries) { + CompletableFuture.runAsync(() -> { + if (retries < 3) { + try { + long start = System.currentTimeMillis(); + //Retrieve the saved commit hash + String savedCommitHash = checkSavedCommitData(); + + //Fetch the latest commit data + JsonObject response = JsonParser.parseString(Http.sendGetRequest(BRANCH_INFO)).getAsJsonObject(); + String latestCommitHash = response.getAsJsonObject("commit").get("sha").getAsString(); + + //Download the repository if there was a new commit + if (!savedCommitHash.equals(latestCommitHash)) { + InputStream in = Http.downloadContent(REPO_DOWNLOAD); + + //Delete all directories to clear potentially now unused/old files + //TODO change this to only delete periodically? + deleteDirectories(); + + try (ZipInputStream zis = new ZipInputStream(in)) { + ZipEntry entry; + + while ((entry = zis.getNextEntry()) != null) { + Path outputFile = REPO_DIRECTORY.resolve(entry.getName()); + + if (entry.isDirectory()) { + Files.createDirectories(outputFile); + } else { + Files.createDirectories(outputFile.getParent()); + Files.copy(zis, outputFile, StandardCopyOption.REPLACE_EXISTING); + } + } + } + + writeCommitData(latestCommitHash); + + long end = System.currentTimeMillis(); + LOGGER.info("[Skyblocker] Successfully updated the Image Respository in {} ms! {} → {}", end - start, savedCommitHash, latestCommitHash); + } else { + LOGGER.info("[Skyblocker] The Image Respository is up to date!"); + } + } catch (Exception e) { + LOGGER.error("[Skyblocker] Error while downloading image repo on attempt {}!", retries, e); + update(retries + 1); + } + } + }); + } + + /** + * @return The stored hash or the {@link #PLACEHOLDER_HASH}. + */ + private static String checkSavedCommitData() throws IOException { + Path file = REPO_DIRECTORY.resolve("image_repo.json"); + + if (Files.exists(file)) { + try (BufferedReader reader = Files.newBufferedReader(file)) { + CommitData commitData = CommitData.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).result().orElseThrow(); + + return commitData.commit(); + } + } + + return PLACEHOLDER_HASH; + } + + /** + * Writes the {@code newHash} into a file to be used to check for repo updates. + * + * @implNote Checking whether the directory exists or not isn't needed as this is called after all files are written successfully. + */ + private static void writeCommitData(String newHash) throws IOException { + Path file = REPO_DIRECTORY.resolve("image_repo.json"); + CommitData commitData = new CommitData(newHash, System.currentTimeMillis()); + + try (BufferedWriter writer = Files.newBufferedWriter(file)) { + SkyblockerMod.GSON.toJson(CommitData.CODEC.encodeStart(JsonOps.INSTANCE, commitData).result().orElseThrow(), writer); + } + } + + /** + * Deletes all directories (not files) inside of the {@link #REPO_DIRECTORY} + * @throws IOException + */ + private static void deleteDirectories() throws IOException { + Files.list(REPO_DIRECTORY) + .filter(Files::isDirectory) + .forEach(dir -> { + try { + FileUtils.recursiveDelete(dir); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Encountered an exception while deleting a directory! Path: {}", dir.toAbsolutePath(), e); + } + }); + } + + record CommitData(String commit, long lastUpdated) { + static final Codec<CommitData> CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.fieldOf("commit").forGetter(CommitData::commit), + Codec.LONG.fieldOf("lastUpdated").forGetter(CommitData::lastUpdated)) + .apply(instance, CommitData::new)); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/FileUtils.java b/src/main/java/de/hysky/skyblocker/utils/FileUtils.java new file mode 100644 index 00000000..22611441 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/FileUtils.java @@ -0,0 +1,36 @@ +package de.hysky.skyblocker.utils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +public class FileUtils { + private static final Logger LOGGER = LogUtils.getLogger(); + + public static void recursiveDelete(Path dir) throws IOException { + if (Files.isDirectory(dir) && !Files.isSymbolicLink(dir)) { + Files.list(dir).forEach(child -> { + try { + recursiveDelete(child); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Encountered an exception while deleting a file! Path: {}", child.toAbsolutePath(), e); + } + }); + } + + Files.delete(dir); + } + + /** + * Replaces any characters that do not match the regex: [^a-z0-9_.-] + * + * @implNote Designed to convert a file path to an {@link net.minecraft.util.Identifier} + */ + public static String normalizePath(Path path) { + return path.toString().toLowerCase().replaceAll("[^a-z0-9_.-]", ""); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java index 58deced2..871eac78 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Http.java +++ b/src/main/java/de/hysky/skyblocker/utils/Http.java @@ -33,10 +33,6 @@ public class Http { .followRedirects(Redirect.NORMAL) .build(); - public static String sendGetRequest(String url) throws IOException, InterruptedException { - return sendCacheableGetRequest(url).content(); - } - private static ApiResponse sendCacheableGetRequest(String url) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder() .GET() @@ -55,6 +51,26 @@ public class Http { return new ApiResponse(body, response.statusCode(), getCacheStatuses(headers), getAge(headers)); } + + public static InputStream downloadContent(String url) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("User-Agent", USER_AGENT) + .version(Version.HTTP_2) + .uri(URI.create(url)) + .build(); + + HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream()); + InputStream decodedInputStream = getDecodedInputStream(response); + + return decodedInputStream; + } + + public static String sendGetRequest(String url) throws IOException, InterruptedException { + return sendCacheableGetRequest(url).content(); + } public static HttpHeaders sendHeadRequest(String url) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder() diff --git a/src/main/java/de/hysky/skyblocker/utils/NEURepoManager.java b/src/main/java/de/hysky/skyblocker/utils/NEURepoManager.java index 870e94da..c779d666 100644 --- a/src/main/java/de/hysky/skyblocker/utils/NEURepoManager.java +++ b/src/main/java/de/hysky/skyblocker/utils/NEURepoManager.java @@ -13,13 +13,10 @@ import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.stream.Stream; /** * Initializes the NEU repo, which contains item metadata and fairy souls location data. Clones the repo if it does not exist and checks for updates. Use {@link #runAsyncAfterLoad(Runnable)} to run code after the repo is initialized. @@ -76,7 +73,7 @@ public class NEURepoManager { CompletableFuture.runAsync(() -> { try { ItemRepository.setFilesImported(false); - recursiveDelete(NEURepoManager.LOCAL_REPO_DIR); + FileUtils.recursiveDelete(NEURepoManager.LOCAL_REPO_DIR); } catch (Exception ex) { if (MinecraftClient.getInstance().player != null) MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.updaterepository.failed")), false); @@ -86,21 +83,6 @@ public class NEURepoManager { }); } - @SuppressWarnings("ResultOfMethodCallIgnored") - private static void recursiveDelete(Path dir) throws IOException { - if (Files.isDirectory(dir) && !Files.isSymbolicLink(dir)) { - Files.list(dir).forEach(child -> { - try { - recursiveDelete(child); - } catch (Exception e) { - LOGGER.error("[Skyblocker] Encountered an exception while deleting a file! Path: {}", child.toAbsolutePath(), e); - } - }); - } - - Files.delete(dir); - } - /** * Runs the given runnable after the NEU repo is initialized. * @param runnable the runnable to run |