Release LexianDEV v1.0.0
Build and publish release / Build and publish LexianDEV (push) Successful in 1m0s

This commit is contained in:
root
2026-08-04 19:54:23 +02:00
parent 3dc2915604
commit 7f869855c9
14 changed files with 58 additions and 54 deletions
@@ -0,0 +1,324 @@
package dev.lexian.lexiandev;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import org.geysermc.geyser.api.GeyserApi;
import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent;
import org.geysermc.geyser.api.pack.PackCodec;
import org.geysermc.geyser.api.pack.ResourcePack;
import org.geysermc.geyser.api.pack.option.ResourcePackOption;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/** Maintains a managed Bedrock pack and registers its current version for new Geyser sessions. */
public final class BedrockPackManager {
private static final int MAX_MANIFEST_BYTES = 1024 * 1024;
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
private static final Pattern HEADER_PROPERTY = Pattern.compile("\\\"header\\\"\\s*:\\s*\\{");
private static final Pattern UUID_PROPERTY = Pattern.compile("\\\"uuid\\\"\\s*:\\s*\\\"[^\\\"]*\\\"");
private final JavaPlugin plugin;
private final AtomicReference<Snapshot> currentPack = new AtomicReference<>();
private volatile BedrockPackSettings settings;
private BukkitTask refreshTask;
private GeyserEventRegistrar geyserEventRegistrar;
private boolean subscribed;
public BedrockPackManager(JavaPlugin plugin) {
this.plugin = plugin;
}
public void configure(BedrockPackSettings newSettings) {
stop();
currentPack.set(null);
settings = newSettings;
if (newSettings == null) {
plugin.getLogger().info("Bedrock resource-pack delivery is disabled: bedrockResourcePack is blank.");
return;
}
subscribeToGeyser();
if (newSettings.isRemote()) {
// Geyser's URL codec validates the remote download and obtains its metadata.
// Geyser itself downloads remote packs once during its lifecycle, so do not
// repeatedly recreate the URL codec on a timer.
refreshTask = Bukkit.getScheduler().runTaskAsynchronously(plugin, this::refreshPack);
plugin.getLogger().info("Bedrock remote resource-pack URL configured. Geyser will fetch it for new connections.");
} else {
refreshTask = Bukkit.getScheduler().runTaskTimerAsynchronously(
plugin, this::refreshPack, 0L, newSettings.refreshIntervalTicks());
plugin.getLogger().info("Bedrock local resource-pack refresh enabled every "
+ (newSettings.refreshIntervalTicks() / 20L) + " seconds.");
}
}
public void stop() {
if (refreshTask != null) {
refreshTask.cancel();
refreshTask = null;
}
if (geyserEventRegistrar != null) {
try {
GeyserApi.api().eventBus().unregisterAll(geyserEventRegistrar);
} catch (RuntimeException | LinkageError exception) {
plugin.getLogger().log(Level.FINE, "Could not unregister the Geyser Bedrock resource-pack handler.", exception);
} finally {
geyserEventRegistrar = null;
subscribed = false;
}
}
}
private void subscribeToGeyser() {
if (subscribed) {
return;
}
if (Bukkit.getPluginManager().getPlugin("Geyser-Spigot") == null) {
plugin.getLogger().warning("bedrockResourcePack is configured, but Geyser-Spigot is not installed; "
+ "the Bedrock pack will not be sent until Geyser is available and LexianDEV is reloaded.");
return;
}
try {
geyserEventRegistrar = new GeyserEventRegistrar();
GeyserApi.api().eventBus().subscribe(
geyserEventRegistrar, SessionLoadResourcePacksEvent.class, this::registerForSession);
subscribed = true;
plugin.getLogger().info("Registered Geyser Bedrock resource-pack session handler.");
} catch (RuntimeException | LinkageError exception) {
geyserEventRegistrar = null;
plugin.getLogger().log(Level.WARNING, "Could not initialize Geyser Bedrock resource-pack support: "
+ exception.getMessage(), exception);
}
}
private void registerForSession(SessionLoadResourcePacksEvent event) {
Snapshot snapshot = currentPack.get();
if (snapshot == null) {
return;
}
try {
event.register(snapshot.resourcePack(), new ResourcePackOption<?>[0]);
} catch (RuntimeException exception) {
plugin.getLogger().log(Level.WARNING, "Could not register the Bedrock resource pack for a connecting player: "
+ exception.getMessage(), exception);
}
}
private void refreshPack() {
BedrockPackSettings activeSettings = settings;
if (activeSettings == null) {
return;
}
try {
if (activeSettings.isRemote()) {
validateRemotePackUrl(activeSettings.remoteUrl());
ResourcePack resourcePack = ResourcePack.create(PackCodec.url(activeSettings.remoteUrl()));
if (activeSettings != settings) {
return;
}
currentPack.set(new Snapshot(null, resourcePack));
plugin.getLogger().info("Bedrock remote resource-pack URL validated and registered: "
+ activeSettings.remoteUrl());
return;
}
Path source = activeSettings.sourceFile();
if (!Files.isRegularFile(source)) {
throw new IOException("Configured Bedrock pack does not exist or is not a regular file: " + source);
}
byte[] sourceSha256 = sha256(source);
Snapshot previous = currentPack.get();
if (previous != null && Arrays.equals(previous.sourceSha256(), sourceSha256)) {
return;
}
Path managedPack = writeManagedPack(source, sourceSha256);
ResourcePack resourcePack = ResourcePack.create(PackCodec.path(managedPack));
if (activeSettings != settings) {
return;
}
currentPack.set(new Snapshot(sourceSha256, resourcePack));
plugin.getLogger().info("Bedrock resource pack refreshed (SHA-256: "
+ HexFormat.of().formatHex(resourcePack.codec().sha256()) + "). "
+ "New Geyser connections will receive it.");
} catch (Exception exception) {
plugin.getLogger().log(Level.WARNING, "Could not refresh the Bedrock resource pack: "
+ exception.getMessage(), exception);
}
}
/**
* Geyser gives URL packs to Bedrock clients as URLs. Unlike Geyser's server-side
* download, the Bedrock client requires a direct response with application/zip
* and a real Content-Length, so catch bad hosting before registering the pack.
*/
private static void validateRemotePackUrl(String url) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.header("Accept", "application/zip")
.build();
HttpResponse<Void> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.discarding());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IOException("Remote Bedrock pack returned HTTP " + response.statusCode() + ".");
}
validateRemotePackHeaders(
response.headers().firstValue("Content-Type").orElse(null),
response.headers().firstValue("Content-Length").orElse(null));
}
static void validateRemotePackHeaders(String contentType, String contentLength) throws IOException {
if (contentType == null || !contentType.toLowerCase(java.util.Locale.ROOT).startsWith("application/zip")) {
throw new IOException("Remote Bedrock pack must return Content-Type: application/zip, but returned "
+ (contentType == null ? "no Content-Type header" : contentType) + ".");
}
try {
if (contentLength == null || Long.parseLong(contentLength) <= 0) {
throw new NumberFormatException();
}
} catch (NumberFormatException exception) {
throw new IOException("Remote Bedrock pack must return a positive, exact Content-Length header.");
}
}
private Path writeManagedPack(Path source, byte[] sourceSha256) throws IOException {
String sourceName = source.getFileName().toString().toLowerCase(java.util.Locale.ROOT);
String extension = sourceName.endsWith(".mcpack") ? ".mcpack" : ".zip";
Path directory = plugin.getDataFolder().toPath().resolve("bedrock-managed");
Files.createDirectories(directory);
Path target = directory.resolve("LexianDEV-Bedrock" + extension);
Path temporary = Files.createTempFile(directory, "LexianDEV-Bedrock-", extension);
UUID cacheBustingUuid = UUID.nameUUIDFromBytes(sourceSha256);
boolean foundManifest = false;
try (ZipFile input = new ZipFile(source.toFile());
OutputStream fileOutput = Files.newOutputStream(temporary);
ZipOutputStream output = new ZipOutputStream(fileOutput)) {
var entries = input.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
ZipEntry copiedEntry = new ZipEntry(entry.getName());
copiedEntry.setTime(entry.getTime());
output.putNextEntry(copiedEntry);
if (!entry.isDirectory()) {
try (InputStream entryInput = input.getInputStream(entry)) {
if (entry.getName().equalsIgnoreCase("manifest.json")) {
byte[] manifest = entryInput.readNBytes(MAX_MANIFEST_BYTES + 1);
if (manifest.length > MAX_MANIFEST_BYTES) {
throw new IOException("manifest.json exceeds " + MAX_MANIFEST_BYTES + " bytes");
}
output.write(replaceHeaderUuid(new String(manifest, StandardCharsets.UTF_8), cacheBustingUuid)
.getBytes(StandardCharsets.UTF_8));
foundManifest = true;
} else {
entryInput.transferTo(output);
}
}
}
output.closeEntry();
}
} catch (Exception exception) {
Files.deleteIfExists(temporary);
throw exception;
}
if (!foundManifest) {
Files.deleteIfExists(temporary);
throw new IOException("The Bedrock pack must contain manifest.json at the archive root.");
}
try {
Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING);
}
return target;
}
static String replaceHeaderUuid(String manifest, UUID uuid) throws IOException {
Matcher headerMatcher = HEADER_PROPERTY.matcher(manifest);
if (!headerMatcher.find()) {
throw new IOException("manifest.json does not contain a header object.");
}
int headerStart = headerMatcher.end() - 1;
int headerEnd = matchingObjectEnd(manifest, headerStart);
String header = manifest.substring(headerStart, headerEnd + 1);
Matcher uuidMatcher = UUID_PROPERTY.matcher(header);
if (!uuidMatcher.find()) {
throw new IOException("manifest.json header does not contain a UUID.");
}
String updatedHeader = uuidMatcher.replaceFirst("\\\"uuid\\\": \\\"" + uuid + "\\\"");
return manifest.substring(0, headerStart) + updatedHeader + manifest.substring(headerEnd + 1);
}
private static int matchingObjectEnd(String json, int start) throws IOException {
int depth = 0;
boolean inString = false;
boolean escaped = false;
for (int index = start; index < json.length(); index++) {
char character = json.charAt(index);
if (inString) {
if (escaped) {
escaped = false;
} else if (character == '\\') {
escaped = true;
} else if (character == '"') {
inString = false;
}
continue;
}
if (character == '"') {
inString = true;
} else if (character == '{') {
depth++;
} else if (character == '}' && --depth == 0) {
return index;
}
}
throw new IOException("manifest.json header object is not closed.");
}
private static byte[] sha256(Path file) throws IOException {
try (InputStream input = Files.newInputStream(file)) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[8192];
for (int read; (read = input.read(buffer)) != -1;) {
digest.update(buffer, 0, read);
}
return digest.digest();
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable in this Java runtime.", exception);
}
}
private record Snapshot(byte[] sourceSha256, ResourcePack resourcePack) {
}
}
@@ -0,0 +1,51 @@
package dev.lexian.lexiandev;
import org.bukkit.configuration.file.FileConfiguration;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Optional;
/** Validated configuration for an optional local or remote Bedrock resource pack. */
public record BedrockPackSettings(Path sourceFile, String remoteUrl, long refreshIntervalTicks) {
private static final long MINIMUM_REFRESH_SECONDS = 30L;
public static Optional<BedrockPackSettings> from(FileConfiguration config, Path dataDirectory) {
String configuredUrl = config.getString("bedrockResourcePackUrl", "").trim();
String configuredPath = config.getString("bedrockResourcePack", "").trim();
if (!configuredUrl.isEmpty() && !configuredPath.isEmpty()) {
throw new IllegalArgumentException("Configure only one of bedrockResourcePackUrl or bedrockResourcePack.");
}
if (configuredUrl.isEmpty() && configuredPath.isEmpty()) {
return Optional.empty();
}
Path source = null;
if (!configuredPath.isEmpty()) {
source = Path.of(configuredPath);
if (!source.isAbsolute()) {
source = dataDirectory.resolve(source);
}
source = source.normalize();
String fileName = source.getFileName().toString().toLowerCase(Locale.ROOT);
if (!fileName.endsWith(".mcpack") && !fileName.endsWith(".zip")) {
throw new IllegalArgumentException("bedrockResourcePack must reference a .mcpack or .zip file.");
}
} else {
if (!configuredUrl.startsWith("https://") && !configuredUrl.startsWith("http://")) {
throw new IllegalArgumentException("bedrockResourcePackUrl must be an HTTP(S) direct download URL.");
}
}
long refreshSeconds = config.getLong("refreshInterval", 300L);
if (refreshSeconds < MINIMUM_REFRESH_SECONDS) {
throw new IllegalArgumentException("refreshInterval must be at least " + MINIMUM_REFRESH_SECONDS + " seconds.");
}
return Optional.of(new BedrockPackSettings(source, configuredUrl.isEmpty() ? null : configuredUrl,
Math.multiplyExact(refreshSeconds, 20L)));
}
public boolean isRemote() {
return remoteUrl != null;
}
}
@@ -0,0 +1,7 @@
package dev.lexian.lexiandev;
import org.geysermc.geyser.api.event.EventRegistrar;
/** Separate registrar keeps Geyser entirely optional for Java-only servers. */
public final class GeyserEventRegistrar implements EventRegistrar {
}
@@ -0,0 +1,122 @@
package dev.lexian.lexiandev;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/** Entry point for LexianDEV. Uses only stable Bukkit/Spigot APIs. */
public final class LexianDEVPlugin extends JavaPlugin implements Listener, CommandExecutor, TabCompleter {
private ResourcePackManager resourcePackManager;
private BedrockPackManager bedrockPackManager;
@Override
public void onEnable() {
saveDefaultConfig();
getConfig().addDefault("bedrockResourcePackUrl", "");
getConfig().addDefault("bedrockResourcePack", "");
getConfig().options().copyDefaults(true);
saveConfig();
resourcePackManager = new ResourcePackManager(this);
getServer().getPluginManager().registerEvents(this, this);
PluginCommand command = getCommand("lexiandev");
if (command != null) {
command.setExecutor(this);
command.setTabCompleter(this);
}
reloadPackConfiguration();
}
@Override
public void onDisable() {
if (resourcePackManager != null) {
resourcePackManager.stop();
}
if (bedrockPackManager != null) {
bedrockPackManager.stop();
}
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
// Give the initial asynchronous checksum refresh a chance to complete first.
getServer().getScheduler().runTaskLater(this, () -> resourcePackManager.sendTo(event.getPlayer()), 1L);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length != 1 || !args[0].equalsIgnoreCase("reload")) {
sender.sendMessage("§eUsage: /" + label + " reload");
return true;
}
if (!sender.hasPermission("lexiandev.reload")) {
sender.sendMessage("§cYou do not have permission to reload LexianDEV.");
return true;
}
reloadPackConfiguration();
sender.sendMessage("§aLexianDEV configuration reloaded.");
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1 && "reload".startsWith(args[0].toLowerCase())) {
return List.of("reload");
}
return Collections.emptyList();
}
private void reloadPackConfiguration() {
reloadConfig();
try {
Optional<PackSettings> settings = PackSettings.from(getConfig());
resourcePackManager.configure(settings);
} catch (IllegalArgumentException exception) {
resourcePackManager.configure(Optional.empty());
getLogger().severe("Invalid Java resource-pack config: " + exception.getMessage());
}
try {
Optional<BedrockPackSettings> bedrockSettings = BedrockPackSettings.from(
getConfig(), getDataFolder().toPath());
configureBedrockPack(bedrockSettings);
} catch (IllegalArgumentException exception) {
if (bedrockPackManager != null) {
bedrockPackManager.configure(null);
}
getLogger().severe("Invalid Bedrock resource-pack config: " + exception.getMessage());
}
}
private void configureBedrockPack(Optional<BedrockPackSettings> settings) {
if (settings.isEmpty()) {
if (bedrockPackManager != null) {
bedrockPackManager.configure(null);
}
return;
}
if (getServer().getPluginManager().getPlugin("Geyser-Spigot") == null) {
if (bedrockPackManager != null) {
bedrockPackManager.configure(null);
}
getLogger().warning("bedrockResourcePack is configured, but Geyser-Spigot is not installed; Bedrock delivery is disabled.");
return;
}
if (bedrockPackManager == null) {
bedrockPackManager = new BedrockPackManager(this);
}
bedrockPackManager.configure(settings.get());
}
}
@@ -0,0 +1,48 @@
package dev.lexian.lexiandev;
import org.bukkit.configuration.file.FileConfiguration;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
/** Immutable, validated configuration for resource-pack delivery. */
public record PackSettings(URI resourcePackUrl, URI sha1Url, long refreshIntervalTicks) {
private static final long MINIMUM_REFRESH_SECONDS = 30L;
public static Optional<PackSettings> from(FileConfiguration config) throws IllegalArgumentException {
String resourcePackUrl = config.getString("resourcePackUrl", "").trim();
String sha1Url = config.getString("sha1Url", "").trim();
if (resourcePackUrl.isEmpty() && sha1Url.isEmpty()) {
return Optional.empty();
}
if (resourcePackUrl.isEmpty() || sha1Url.isEmpty()) {
throw new IllegalArgumentException("resourcePackUrl and sha1Url must both be configured, or both left blank.");
}
long refreshSeconds = config.getLong("refreshInterval", 300L);
if (refreshSeconds < MINIMUM_REFRESH_SECONDS) {
throw new IllegalArgumentException("refreshInterval must be at least " + MINIMUM_REFRESH_SECONDS + " seconds.");
}
return Optional.of(new PackSettings(
parseHttpUrl("resourcePackUrl", resourcePackUrl),
parseHttpUrl("sha1Url", sha1Url),
Math.multiplyExact(refreshSeconds, 20L)
));
}
private static URI parseHttpUrl(String key, String value) {
try {
URI uri = new URI(value);
if (!uri.isAbsolute() || uri.getHost() == null
|| !("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme()))) {
throw new IllegalArgumentException(key + " must be an absolute HTTP or HTTPS URL.");
}
return uri;
} catch (URISyntaxException exception) {
throw new IllegalArgumentException(key + " is not a valid URL.", exception);
}
}
}
@@ -0,0 +1,47 @@
package dev.lexian.lexiandev;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HexFormat;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Downloads and validates a SHA-1 checksum without downloading the resource pack itself. */
public final class RemoteSha1Fetcher {
private static final Pattern SHA1_PATTERN = Pattern.compile("(?i)(?<![0-9a-f])[0-9a-f]{40}(?![0-9a-f])");
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(15);
private static final int MAX_RESPONSE_BYTES = 64 * 1024;
private final HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
public byte[] fetch(URI sha1Url) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(sha1Url)
.GET()
.timeout(REQUEST_TIMEOUT)
.header("Accept", "text/plain, text/*;q=0.9, */*;q=0.1")
.header("User-Agent", "LexianDEV/v1.0.0")
.build();
HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IOException("SHA-1 endpoint returned HTTP " + response.statusCode());
}
if (response.body().length > MAX_RESPONSE_BYTES) {
throw new IOException("SHA-1 response exceeds " + MAX_RESPONSE_BYTES + " bytes");
}
String responseText = new String(response.body(), java.nio.charset.StandardCharsets.UTF_8);
Matcher matcher = SHA1_PATTERN.matcher(responseText);
if (!matcher.find()) {
throw new IOException("SHA-1 response does not contain a 40-character hexadecimal SHA-1");
}
return HexFormat.of().parseHex(matcher.group());
}
}
@@ -0,0 +1,92 @@
package dev.lexian.lexiandev;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
/** Fetches the remote checksum on an async scheduler and delivers it through the Bukkit API. */
public final class ResourcePackManager {
private final JavaPlugin plugin;
private final RemoteSha1Fetcher fetcher;
private final AtomicReference<byte[]> currentSha1 = new AtomicReference<>();
private volatile PackSettings settings;
private BukkitTask refreshTask;
public ResourcePackManager(JavaPlugin plugin) {
this.plugin = plugin;
this.fetcher = new RemoteSha1Fetcher();
}
public void configure(Optional<PackSettings> newSettings) {
stop();
currentSha1.set(null);
settings = newSettings.orElse(null);
if (settings == null) {
plugin.getLogger().info("Resource-pack delivery is disabled: resourcePackUrl and sha1Url are blank.");
return;
}
refreshTask = Bukkit.getScheduler().runTaskTimerAsynchronously(
plugin, this::refreshChecksum, 0L, settings.refreshIntervalTicks());
plugin.getLogger().info("Remote SHA-1 refresh enabled every " + (settings.refreshIntervalTicks() / 20L) + " seconds.");
}
public void stop() {
if (refreshTask != null) {
refreshTask.cancel();
refreshTask = null;
}
}
public void sendTo(Player player) {
PackSettings activeSettings = settings;
byte[] activeSha1 = currentSha1.get();
if (activeSettings == null || activeSha1 == null) {
return;
}
player.setResourcePack(activeSettings.resourcePackUrl().toString(), Arrays.copyOf(activeSha1, activeSha1.length));
}
private void refreshChecksum() {
PackSettings activeSettings = settings;
if (activeSettings == null) {
return;
}
try {
byte[] downloadedSha1 = fetcher.fetch(activeSettings.sha1Url());
// A reload may have replaced the settings while this HTTP request was in flight.
if (activeSettings != settings) {
return;
}
byte[] previousSha1 = currentSha1.getAndSet(downloadedSha1);
if (Arrays.equals(previousSha1, downloadedSha1)) {
return;
}
Bukkit.getScheduler().runTask(plugin, () -> {
if (activeSettings != settings) {
return;
}
for (Player player : Bukkit.getOnlinePlayers()) {
sendTo(player);
}
plugin.getLogger().info("Resource-pack SHA-1 refreshed; sent the pack to "
+ Bukkit.getOnlinePlayers().size() + " online player(s).");
});
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
} catch (Exception exception) {
plugin.getLogger().log(Level.WARNING, "Could not refresh remote resource-pack SHA-1: "
+ exception.getMessage());
}
}
}