parent
2430416915
commit
e5d2938845
10 changed files with 137 additions and 142 deletions
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2024 Minecon724
|
||||
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
|
||||
* in the project root for the full license text.
|
||||
*/
|
||||
|
||||
package eu.m724.tweaks.updater;
|
||||
|
||||
import eu.m724.tweaks.updater.cache.SpigotResource;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class PluginScanner {
|
||||
private final Plugin thisPlugin;
|
||||
|
||||
PluginScanner(Plugin thisPlugin) {
|
||||
this.thisPlugin = thisPlugin;
|
||||
}
|
||||
|
||||
public Set<SpigotResource> load() throws IOException {
|
||||
File installedPluginsYml = new File(thisPlugin.getDataFolder(), "installed_plugins.yml");
|
||||
|
||||
if (!installedPluginsYml.exists()) {
|
||||
thisPlugin.saveResource("installed_plugins.yml", false);
|
||||
}
|
||||
|
||||
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(installedPluginsYml);
|
||||
|
||||
|
||||
Plugin[] plugins = thisPlugin.getServer().getPluginManager().getPlugins();
|
||||
Set<SpigotResource> spigotResources = new HashSet<>();
|
||||
|
||||
for (Plugin plugin : plugins) {
|
||||
String pluginName = plugin.getName();
|
||||
|
||||
if (!configuration.isSet(pluginName)) {
|
||||
configuration.set(pluginName, -1);
|
||||
continue;
|
||||
}
|
||||
|
||||
int pluginId = configuration.getInt(pluginName);
|
||||
if (pluginId > 0) {
|
||||
spigotResources.add(
|
||||
new SpigotResource(plugin, pluginId, pluginName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
configuration.save(installedPluginsYml);
|
||||
|
||||
return spigotResources;
|
||||
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2024 Minecon724
|
||||
* Copyright (C) 2025 Minecon724
|
||||
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
|
||||
* in the project root for the full license text.
|
||||
*/
|
||||
|
@ -7,7 +7,8 @@
|
|||
package eu.m724.tweaks.updater;
|
||||
|
||||
import eu.m724.tweaks.Language;
|
||||
import eu.m724.tweaks.updater.cache.VersionedResource;
|
||||
import eu.m724.tweaks.updater.backend.UpdateChecker;
|
||||
import eu.m724.tweaks.updater.object.VersionedResource;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
|
@ -20,18 +21,25 @@ import org.bukkit.command.CommandSender;
|
|||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class UpdaterCommands implements CommandExecutor {
|
||||
private final UpdateChecker updateChecker;
|
||||
|
||||
public UpdaterCommands(UpdateChecker updateChecker) {
|
||||
this.updateChecker = updateChecker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if (UpdateChecker.lastChecked == null) {
|
||||
var lastChecked = updateChecker.getLastChecked();
|
||||
if (updateChecker.getLastChecked() == -1) {
|
||||
sender.sendMessage(Language.getString("updatesNotChecked"));
|
||||
return true;
|
||||
}
|
||||
|
||||
String lastChecked = UpdateChecker.lastChecked.format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||
int n = UpdateChecker.availableUpdates.size();
|
||||
int n = updateChecker.getAvailableUpdates().size();
|
||||
|
||||
if (n > 0) {
|
||||
sender.spigot().sendMessage(
|
||||
|
@ -39,13 +47,14 @@ public class UpdaterCommands implements CommandExecutor {
|
|||
);
|
||||
|
||||
int i = 0;
|
||||
for (VersionedResource v : UpdateChecker.availableUpdates) {
|
||||
for (VersionedResource v : updateChecker.getAvailableUpdates()) {
|
||||
sender.spigot().sendMessage(
|
||||
new ComponentBuilder(++i + ". ").color(ChatColor.GRAY).build(), resourceToBaseComponent(v)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
sender.spigot().sendMessage(Language.getComponent("updatesNoUpdates", ChatColor.GREEN, lastChecked));
|
||||
var lastCheckedFormat = DateTimeFormatter.ofPattern("HH:mm").format(Instant.ofEpochMilli(lastChecked));
|
||||
sender.spigot().sendMessage(Language.getComponent("updatesNoUpdates", ChatColor.GREEN, lastCheckedFormat));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -8,9 +8,13 @@ package eu.m724.tweaks.updater;
|
|||
|
||||
import eu.m724.tweaks.DebugLogger;
|
||||
import eu.m724.tweaks.TweaksModule;
|
||||
import eu.m724.tweaks.updater.cache.ResourceVersion;
|
||||
import eu.m724.tweaks.updater.cache.SpigotResource;
|
||||
import eu.m724.tweaks.updater.cache.VersionedResource;
|
||||
import eu.m724.tweaks.updater.backend.UpdateChecker;
|
||||
import eu.m724.tweaks.updater.backend.VersionCache;
|
||||
import eu.m724.tweaks.updater.object.ResourceVersion;
|
||||
import eu.m724.tweaks.updater.object.SpigotResource;
|
||||
import eu.m724.tweaks.updater.object.VersionedResource;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
@ -23,38 +27,72 @@ import java.util.stream.Collectors;
|
|||
public class UpdaterModule extends TweaksModule {
|
||||
@Override
|
||||
protected void onInit() {
|
||||
// scan installed plugins
|
||||
Set<SpigotResource> resources;
|
||||
try {
|
||||
resources = new PluginScanner(getPlugin()).load();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Loading plugins", e);
|
||||
}
|
||||
|
||||
var resources = loadInstalledPlugins();
|
||||
|
||||
// load installed versions from cache
|
||||
var cacheFile = new File(getPlugin().getDataFolder(), "storage/cache/updater");
|
||||
|
||||
Set<ResourceVersion> installedVersions;
|
||||
final var installedVersions = loadInstalledVersionCache(cacheFile);
|
||||
|
||||
var versionedResources = resources.stream()
|
||||
.map(res -> new VersionedResource(
|
||||
res, installedVersions.stream().filter(iv -> iv.resourceId() == res.resourceId()).findFirst().orElse(null), null
|
||||
))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
|
||||
var updateChecker = new UpdateChecker(getPlugin().getLogger(), cacheFile, versionedResources);
|
||||
updateChecker.runTaskTimerAsynchronously(getPlugin(), 600, 12 * 3600 * 20); // 12 hours
|
||||
|
||||
registerCommand("updates", new UpdaterCommands(updateChecker));
|
||||
}
|
||||
|
||||
private Set<ResourceVersion> loadInstalledVersionCache(File cacheFile) {
|
||||
try (FileInputStream inputStream = new FileInputStream(cacheFile)) {
|
||||
installedVersions = VersionCheckCache.loadAll(inputStream);
|
||||
} catch (FileNotFoundException e) {
|
||||
installedVersions = new HashSet<>();
|
||||
return VersionCache.loadAll(inputStream);
|
||||
} catch (FileNotFoundException ignored) {
|
||||
} catch (IOException e) {
|
||||
DebugLogger.warning("Error loading installed version cache, starting fresh. " + e.getMessage());
|
||||
installedVersions = new HashSet<>();
|
||||
}
|
||||
|
||||
final Set<ResourceVersion> ivf = installedVersions;
|
||||
Set<VersionedResource> versionedResources = resources.stream()
|
||||
.map(res -> new VersionedResource(
|
||||
res, ivf.stream().filter(iv -> iv.resourceId() == res.resourceId()).findFirst().orElse(null), null
|
||||
)).collect(Collectors.toSet());
|
||||
return new HashSet<>();
|
||||
}
|
||||
|
||||
private Set<SpigotResource> loadInstalledPlugins() {
|
||||
File installedPluginsYml = new File(getPlugin().getDataFolder(), "installed_plugins.yml");
|
||||
|
||||
new UpdateChecker(getPlugin().getLogger(), cacheFile, versionedResources)
|
||||
.runTaskTimerAsynchronously(getPlugin(), 600, 12 * 3600 * 20); // 12 hours
|
||||
if (!installedPluginsYml.exists()) {
|
||||
getPlugin().saveResource("installed_plugins.yml", false);
|
||||
}
|
||||
|
||||
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(installedPluginsYml);
|
||||
|
||||
Plugin[] plugins = getPlugin().getServer().getPluginManager().getPlugins();
|
||||
Set<SpigotResource> spigotResources = new HashSet<>();
|
||||
|
||||
for (Plugin plugin : plugins) {
|
||||
String pluginName = plugin.getName();
|
||||
|
||||
if (!configuration.isSet(pluginName)) {
|
||||
configuration.set(pluginName, -1);
|
||||
continue;
|
||||
}
|
||||
|
||||
int pluginId = configuration.getInt(pluginName);
|
||||
if (pluginId > 0) {
|
||||
spigotResources.add(
|
||||
new SpigotResource(plugin, pluginId, pluginName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
configuration.save(installedPluginsYml);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to update installed_plugins.yml", e);
|
||||
}
|
||||
|
||||
return spigotResources;
|
||||
|
||||
registerCommand("updates", new UpdaterCommands());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,16 @@
|
|||
* in the project root for the full license text.
|
||||
*/
|
||||
|
||||
package eu.m724.tweaks.updater;
|
||||
package eu.m724.tweaks.updater.backend;
|
||||
|
||||
import eu.m724.tweaks.DebugLogger;
|
||||
import eu.m724.tweaks.Language;
|
||||
import eu.m724.tweaks.updater.cache.VersionedResource;
|
||||
import eu.m724.tweaks.updater.object.VersionedResource;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
@ -28,10 +26,10 @@ public class UpdateChecker extends BukkitRunnable {
|
|||
private final File cacheFile;
|
||||
private final Set<VersionedResource> resources;
|
||||
|
||||
static final Set<VersionedResource> availableUpdates = new HashSet<>();
|
||||
static LocalTime lastChecked = null;
|
||||
private final Set<VersionedResource> availableUpdates = new HashSet<>();
|
||||
private long lastChecked = -1;
|
||||
|
||||
UpdateChecker(Logger logger, File cacheFile, Set<VersionedResource> resources) {
|
||||
public UpdateChecker(Logger logger, File cacheFile, Set<VersionedResource> resources) {
|
||||
this.logger = logger;
|
||||
this.cacheFile = cacheFile;
|
||||
this.resources = resources; // TODO make a copy?
|
||||
|
@ -39,7 +37,7 @@ public class UpdateChecker extends BukkitRunnable {
|
|||
|
||||
private void checkAll() {
|
||||
DebugLogger.fine("Checking for updates");
|
||||
lastChecked = LocalTime.now(ZoneOffset.UTC);
|
||||
lastChecked = System.currentTimeMillis();
|
||||
availableUpdates.clear();
|
||||
|
||||
for (VersionedResource versionedResource : Set.copyOf(resources)) {
|
||||
|
@ -47,19 +45,21 @@ public class UpdateChecker extends BukkitRunnable {
|
|||
int page = versionedResource.running() != null ? versionedResource.running().page() : 1;
|
||||
|
||||
try {
|
||||
VersionedResource newResource = new VersionFinder(versionedResource.resource(), page).join(); // this runs async so it's ok
|
||||
VersionedResource newResource = new VersionScanner(versionedResource.resource(), page).join(); // this runs async so it's ok
|
||||
if (!versionedResource.equals(newResource)) {
|
||||
resources.remove(versionedResource);
|
||||
|
||||
if (newResource.running() == null) {
|
||||
logger.warning("Unable to find installed version of %s".formatted(pluginName));
|
||||
DebugLogger.warning("Unable to find installed version of %s", pluginName);
|
||||
if (versionedResource.running() != null) {
|
||||
logger.warning("Did you downgrade %s? If so, clear cache".formatted(pluginName));
|
||||
DebugLogger.warning("Did you downgrade %s? If so, clear cache", pluginName);
|
||||
}
|
||||
} else {
|
||||
if (!newResource.running().equals(newResource.latest())) {
|
||||
availableUpdates.add(newResource);
|
||||
}
|
||||
}
|
||||
|
||||
resources.add(newResource);
|
||||
}
|
||||
} catch (CompletionException e) {
|
||||
|
@ -85,11 +85,19 @@ public class UpdateChecker extends BukkitRunnable {
|
|||
DebugLogger.finer("Done checking, now saving");
|
||||
cacheFile.getParentFile().mkdirs();
|
||||
try (FileOutputStream outputStream = new FileOutputStream(cacheFile)) {
|
||||
VersionCheckCache.writeAll(outputStream, resources.stream().map(VersionedResource::running).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
VersionCache.writeAll(outputStream, resources.stream().map(VersionedResource::running).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
alert();
|
||||
}
|
||||
|
||||
public long getLastChecked() {
|
||||
return lastChecked;
|
||||
}
|
||||
|
||||
public Set<VersionedResource> getAvailableUpdates() {
|
||||
return availableUpdates;
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
/*
|
||||
* Copyright (C) 2024 Minecon724
|
||||
* Copyright (C) 2025 Minecon724
|
||||
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
|
||||
* in the project root for the full license text.
|
||||
*/
|
||||
|
||||
package eu.m724.tweaks.updater;
|
||||
package eu.m724.tweaks.updater.backend;
|
||||
|
||||
import eu.m724.tweaks.updater.cache.ResourceVersion;
|
||||
import eu.m724.tweaks.updater.object.ResourceVersion;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
|
@ -15,7 +15,7 @@ import java.io.OutputStream;
|
|||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class VersionCheckCache {
|
||||
public class VersionCache {
|
||||
private static final byte FILE_VERSION = 1;
|
||||
|
||||
public static Set<ResourceVersion> loadAll(FileInputStream inputStream) throws IOException {
|
|
@ -1,19 +1,20 @@
|
|||
/*
|
||||
* Copyright (C) 2024 Minecon724
|
||||
* Copyright (C) 2025 Minecon724
|
||||
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
|
||||
* in the project root for the full license text.
|
||||
*/
|
||||
|
||||
package eu.m724.tweaks.updater;
|
||||
package eu.m724.tweaks.updater.backend;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import eu.m724.tweaks.updater.cache.SpigotResource;
|
||||
import eu.m724.tweaks.updater.cache.ResourceVersion;
|
||||
import eu.m724.tweaks.updater.cache.UpdateDescription;
|
||||
import eu.m724.tweaks.updater.cache.VersionedResource;
|
||||
import eu.m724.tweaks.DebugLogger;
|
||||
import eu.m724.tweaks.updater.object.SpigotResource;
|
||||
import eu.m724.tweaks.updater.object.ResourceVersion;
|
||||
import eu.m724.tweaks.updater.object.UpdateDescription;
|
||||
import eu.m724.tweaks.updater.object.VersionedResource;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
@ -26,23 +27,25 @@ import java.util.concurrent.Executors;
|
|||
|
||||
// TODO optimize
|
||||
|
||||
public class VersionFinder extends CompletableFuture<VersionedResource> {
|
||||
public class VersionScanner extends CompletableFuture<VersionedResource> {
|
||||
private final SpigotResource resource;
|
||||
private final int fromPage;
|
||||
|
||||
VersionFinder(SpigotResource resource, int fromPage) {
|
||||
VersionScanner(SpigotResource resource, int fromPage) {
|
||||
this.resource = resource;
|
||||
this.fromPage = fromPage;
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
VersionFinder(SpigotResource resource) {
|
||||
VersionScanner(SpigotResource resource) {
|
||||
this(resource, 1);
|
||||
}
|
||||
|
||||
private void start() {
|
||||
//System.out.printf("STarting for %d %s\n", resource.resourceId(), resource.plugin().getName());
|
||||
DebugLogger.finer("Scanning %s (#%d) from page %d", resource.name(), resource.resourceId(), fromPage);
|
||||
|
||||
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
|
@ -51,13 +54,14 @@ public class VersionFinder extends CompletableFuture<VersionedResource> {
|
|||
|
||||
int page;
|
||||
for (page = fromPage; page < 1000; page++) {
|
||||
//System.out.println("Page " + page);
|
||||
DebugLogger.finer("Scan %s now at page %d", resource.name(), fromPage);
|
||||
|
||||
String url = "https://api.spigotmc.org/simple/0.2/index.php?action=getResourceUpdates&page=%d&id=%d".formatted(page, resource.resourceId());
|
||||
|
||||
HttpRequest request;
|
||||
try {
|
||||
request = HttpRequest.newBuilder(new URI(url))
|
||||
.header("User-Agent", "twu/1")
|
||||
.header("User-Agent", "twu/1") // tweaks updater v1
|
||||
.build();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
@ -68,6 +72,7 @@ public class VersionFinder extends CompletableFuture<VersionedResource> {
|
|||
|
||||
String body = response.body();
|
||||
if (body.isBlank()) {
|
||||
DebugLogger.finer("Body is blank, stopping");
|
||||
page--;
|
||||
break;
|
||||
}
|
||||
|
@ -75,15 +80,7 @@ public class VersionFinder extends CompletableFuture<VersionedResource> {
|
|||
JsonArray jsonArray = JsonParser.parseString(body).getAsJsonArray();
|
||||
for (JsonElement ele : jsonArray) {
|
||||
JsonObject versionJson = ele.getAsJsonObject();
|
||||
if (isRunningVersion(versionJson)) {
|
||||
runningVersion = new ResourceVersion(
|
||||
resource.resourceId(),
|
||||
page,
|
||||
versionJson.get("id").getAsInt(),
|
||||
versionJson.get("resource_version").getAsString(),
|
||||
null // no need for changelog of running version
|
||||
);
|
||||
}
|
||||
|
||||
latestVersion = new ResourceVersion(
|
||||
resource.resourceId(),
|
||||
page,
|
||||
|
@ -94,14 +91,17 @@ public class VersionFinder extends CompletableFuture<VersionedResource> {
|
|||
versionJson.get("message").getAsString()
|
||||
)
|
||||
);
|
||||
//System.out.printf("%d %d %s\n", page, versionJson.get("id").getAsInt(), versionJson.get("resource_version").getAsString());
|
||||
|
||||
if (isRunningVersion(versionJson))
|
||||
runningVersion = latestVersion;
|
||||
|
||||
DebugLogger.finer("%s - %s #%d", resource.name(), latestVersion.updateId(), latestVersion.updateId());
|
||||
}
|
||||
|
||||
if (jsonArray.size() < 10) break;
|
||||
}
|
||||
|
||||
}
|
||||
//System.out.println("Done");
|
||||
|
||||
if (page > 999) {
|
||||
throw new Exception("Too many pages");
|
||||
|
@ -117,7 +117,8 @@ public class VersionFinder extends CompletableFuture<VersionedResource> {
|
|||
}
|
||||
|
||||
private boolean isRunningVersion(JsonObject versionJson) {
|
||||
// TODO
|
||||
return versionJson.get("resource_version").getAsString().equals(resource.plugin().getDescription().getVersion());
|
||||
// TODO make it work with more advanced strings
|
||||
return versionJson.get("resource_version").getAsString()
|
||||
.equals(resource.plugin().getDescription().getVersion());
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
* Copyright (C) 2024 Minecon724
|
||||
* Copyright (C) 2025 Minecon724
|
||||
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
|
||||
* in the project root for the full license text.
|
||||
*/
|
||||
|
||||
package eu.m724.tweaks.updater.cache;
|
||||
package eu.m724.tweaks.updater.object;
|
||||
|
||||
import java.util.Objects;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
* Copyright (C) 2024 Minecon724
|
||||
* Copyright (C) 2025 Minecon724
|
||||
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
|
||||
* in the project root for the full license text.
|
||||
*/
|
||||
|
||||
package eu.m724.tweaks.updater.cache;
|
||||
package eu.m724.tweaks.updater.object;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
|
@ -12,5 +12,4 @@ public record SpigotResource(
|
|||
Plugin plugin,
|
||||
int resourceId,
|
||||
String name
|
||||
) {
|
||||
}
|
||||
) { }
|
|
@ -1,13 +1,12 @@
|
|||
/*
|
||||
* Copyright (C) 2024 Minecon724
|
||||
* Copyright (C) 2025 Minecon724
|
||||
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
|
||||
* in the project root for the full license text.
|
||||
*/
|
||||
|
||||
package eu.m724.tweaks.updater.cache;
|
||||
package eu.m724.tweaks.updater.object;
|
||||
|
||||
public record UpdateDescription(
|
||||
String title,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
) { }
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
* Copyright (C) 2024 Minecon724
|
||||
* Copyright (C) 2025 Minecon724
|
||||
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
|
||||
* in the project root for the full license text.
|
||||
*/
|
||||
|
||||
package eu.m724.tweaks.updater.cache;
|
||||
package eu.m724.tweaks.updater.object;
|
||||
|
||||
public record VersionedResource(
|
||||
SpigotResource resource,
|
Loading…
Add table
Reference in a new issue