A serverside user interface library for Minecraft: Java Edition
—
Resource pack management system for sending, tracking, and handling resource pack requests with callback support. Adventure provides comprehensive resource pack management for custom content delivery.
Interface for creating and managing resource pack requests sent to audiences.
/**
* Request to send resource pack to audience
*/
interface ResourcePackRequest extends Examinable {
/**
* Gets the resource pack information
* @return the resource pack info
*/
ResourcePackInfo packs();
/**
* Checks if this request replaces existing packs
* @return true if should replace existing packs
*/
boolean replace();
/**
* Gets the optional prompt message
* @return the prompt or null
*/
@Nullable Component prompt();
/**
* Gets the callback for pack events
* @return the callback or null
*/
@Nullable ResourcePackCallback callback();
/**
* Creates a resource pack request
* @param pack the resource pack info
* @return new request
*/
static ResourcePackRequest resourcePackRequest(ResourcePackInfo pack);
/**
* Creates a builder for resource pack requests
* @return new builder
*/
static Builder resourcePackRequest();
interface Builder extends AbstractBuilder<ResourcePackRequest> {
Builder packs(ResourcePackInfo packs);
Builder replace(boolean replace);
Builder prompt(@Nullable ComponentLike prompt);
Builder callback(@Nullable ResourcePackCallback callback);
}
}Information about a resource pack including ID, URL, and hash for verification.
/**
* Information about a resource pack
*/
interface ResourcePackInfo extends Examinable {
/**
* Gets the unique resource pack ID
* @return the pack ID
*/
String id();
/**
* Gets the download URL
* @return the pack URL
*/
String uri();
/**
* Gets the SHA-1 hash for verification
* @return the hash string
*/
String hash();
/**
* Creates resource pack info
* @param id the pack ID
* @param uri the download URL
* @param hash the SHA-1 hash
* @return new resource pack info
*/
static ResourcePackInfo resourcePackInfo(String id, String uri, String hash);
/**
* Creates a builder
* @return new builder
*/
static Builder builder();
interface Builder extends AbstractBuilder<ResourcePackInfo> {
Builder id(String id);
Builder uri(String uri);
Builder hash(String hash);
}
}Enumeration of possible resource pack status values reported by clients.
/**
* Status of resource pack download/application
*/
enum ResourcePackStatus {
/**
* Resource pack loaded successfully
*/
SUCCESSFULLY_LOADED("successfully_loaded"),
/**
* Player declined the resource pack
*/
DECLINED("declined"),
/**
* Failed to download the resource pack
*/
FAILED_DOWNLOAD("failed_download"),
/**
* Player accepted the resource pack prompt
*/
ACCEPTED("accepted"),
/**
* Resource pack downloaded successfully
*/
DOWNLOADED("downloaded"),
/**
* Invalid URL provided
*/
INVALID_URL("invalid_url"),
/**
* Failed to reload resource pack
*/
FAILED_TO_RELOAD("failed_to_reload"),
/**
* Resource pack was discarded
*/
DISCARDED("discarded");
}Callback interface for handling resource pack events and status updates.
/**
* Callback interface for resource pack events
*/
interface ResourcePackCallback {
/**
* Called when a resource pack event occurs
* @param id the resource pack ID
* @param status the pack status
* @param audience the audience that triggered the event
*/
void packEventReceived(String id, ResourcePackStatus status, Audience audience);
/**
* Creates a simple callback
* @param callback the callback function
* @return new resource pack callback
*/
static ResourcePackCallback callback(ResourcePackEventConsumer callback);
/**
* Functional interface for resource pack events
*/
@FunctionalInterface
interface ResourcePackEventConsumer {
void accept(String id, ResourcePackStatus status, Audience audience);
}
}Usage Examples:
import net.kyori.adventure.resource.*;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
// Basic resource pack request
ResourcePackInfo pack = ResourcePackInfo.resourcePackInfo(
"my-pack-v1.0",
"https://example.com/packs/mypack.zip",
"abc123def456..." // SHA-1 hash
);
ResourcePackRequest request = ResourcePackRequest.resourcePackRequest(pack);
audience.sendResourcePacks(request);
// Resource pack with prompt and callback
ResourcePackRequest customRequest = ResourcePackRequest.resourcePackRequest()
.packs(pack)
.replace(true) // Replace existing packs
.prompt(Component.text("Download our custom textures?", NamedTextColor.YELLOW))
.callback((id, status, audience) -> {
switch (status) {
case SUCCESSFULLY_LOADED:
audience.sendMessage(Component.text("Resource pack loaded!", NamedTextColor.GREEN));
break;
case DECLINED:
audience.sendMessage(Component.text("Resource pack declined", NamedTextColor.RED));
break;
case FAILED_DOWNLOAD:
audience.sendMessage(Component.text("Failed to download resource pack", NamedTextColor.RED));
break;
}
})
.build();
audience.sendResourcePacks(customRequest);
// Remove resource packs
audience.removeResourcePacks(UUID.fromString("pack-uuid"));
audience.clearResourcePacks(); // Remove all packspublic class ResourcePackManager {
private final Map<String, ResourcePackInfo> packs = new HashMap<>();
public void registerPack(String name, String version, String url, String hash) {
String packId = name + "-" + version;
ResourcePackInfo pack = ResourcePackInfo.resourcePackInfo(packId, url, hash);
packs.put(name, pack);
}
public void sendLatestPack(Audience audience, String packName) {
ResourcePackInfo pack = packs.get(packName);
if (pack != null) {
ResourcePackRequest request = ResourcePackRequest.resourcePackRequest()
.packs(pack)
.replace(true)
.prompt(Component.text("Install " + packName + "?"))
.callback(createCallback(packName))
.build();
audience.sendResourcePacks(request);
}
}
private ResourcePackCallback createCallback(String packName) {
return (id, status, audience) -> {
logPackEvent(packName, id, status, audience);
handlePackStatus(packName, status, audience);
};
}
private void handlePackStatus(String packName, ResourcePackStatus status, Audience audience) {
switch (status) {
case SUCCESSFULLY_LOADED:
onPackLoaded(audience, packName);
break;
case DECLINED:
onPackDeclined(audience, packName);
break;
case FAILED_DOWNLOAD:
case INVALID_URL:
onPackFailed(audience, packName);
break;
}
}
}public class ConditionalPacks {
public void sendPackBasedOnVersion(Audience audience, String clientVersion) {
ResourcePackInfo pack;
if (isVersion(clientVersion, "1.20")) {
pack = getPackForVersion("1.20");
} else if (isVersion(clientVersion, "1.19")) {
pack = getPackForVersion("1.19");
} else {
pack = getLegacyPack();
}
if (pack != null) {
sendPackWithVersionInfo(audience, pack, clientVersion);
}
}
public void sendOptionalPacks(Audience audience, Set<String> playerPreferences) {
List<ResourcePackInfo> packsToSend = new ArrayList<>();
if (playerPreferences.contains("hd-textures")) {
packsToSend.add(getHDTexturePack());
}
if (playerPreferences.contains("custom-sounds")) {
packsToSend.add(getCustomSoundPack());
}
if (playerPreferences.contains("ui-improvements")) {
packsToSend.add(getUIImprovementPack());
}
// Send packs sequentially or as a bundle
sendPackBundle(audience, packsToSend);
}
}public class PackStatusTracker {
private final Map<String, Map<String, ResourcePackStatus>> playerPackStatus = new HashMap<>();
private final Set<ResourcePackStatusListener> listeners = new HashSet<>();
public ResourcePackCallback createTrackingCallback(String playerId) {
return (id, status, audience) -> {
updatePlayerPackStatus(playerId, id, status);
notifyListeners(playerId, id, status);
// Auto-retry on failure
if (status == ResourcePackStatus.FAILED_DOWNLOAD) {
scheduleRetry(audience, id);
}
};
}
private void updatePlayerPackStatus(String playerId, String packId, ResourcePackStatus status) {
playerPackStatus.computeIfAbsent(playerId, k -> new HashMap<>())
.put(packId, status);
}
public boolean hasPlayerLoadedPack(String playerId, String packId) {
return playerPackStatus.getOrDefault(playerId, Collections.emptyMap())
.get(packId) == ResourcePackStatus.SUCCESSFULLY_LOADED;
}
public Set<String> getLoadedPacks(String playerId) {
return playerPackStatus.getOrDefault(playerId, Collections.emptyMap())
.entrySet().stream()
.filter(entry -> entry.getValue() == ResourcePackStatus.SUCCESSFULLY_LOADED)
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
private void scheduleRetry(Audience audience, String packId) {
// Implementation-specific retry logic
scheduleTask(() -> {
ResourcePackInfo pack = getPackById(packId);
if (pack != null) {
ResourcePackRequest retry = ResourcePackRequest.resourcePackRequest()
.packs(pack)
.prompt(Component.text("Retrying resource pack download..."))
.callback(createTrackingCallback(getPlayerId(audience)))
.build();
audience.sendResourcePacks(retry);
}
}, 5000L); // Retry after 5 seconds
}
}public class PackValidator {
private static final int MAX_PACK_SIZE = 50 * 1024 * 1024; // 50MB
private static final Set<String> ALLOWED_DOMAINS = Set.of(
"example.com",
"cdn.example.com",
"packs.myserver.net"
);
public boolean validatePackInfo(ResourcePackInfo pack) {
// Validate URL
if (!isValidUrl(pack.uri())) {
return false;
}
// Validate hash format
if (!isValidSHA1Hash(pack.hash())) {
return false;
}
// Check domain whitelist
if (!isAllowedDomain(pack.uri())) {
return false;
}
return true;
}
private boolean isValidUrl(String url) {
try {
URL parsedUrl = new URL(url);
return "https".equals(parsedUrl.getProtocol()) ||
"http".equals(parsedUrl.getProtocol());
} catch (MalformedURLException e) {
return false;
}
}
private boolean isValidSHA1Hash(String hash) {
return hash != null &&
hash.length() == 40 &&
hash.matches("[a-fA-F0-9]+");
}
private boolean isAllowedDomain(String url) {
try {
String host = new URL(url).getHost();
return ALLOWED_DOMAINS.contains(host);
} catch (MalformedURLException e) {
return false;
}
}
public ResourcePackRequest createSecureRequest(ResourcePackInfo pack) {
if (!validatePackInfo(pack)) {
throw new IllegalArgumentException("Invalid resource pack info");
}
return ResourcePackRequest.resourcePackRequest()
.packs(pack)
.callback(createSecurityCallback())
.build();
}
private ResourcePackCallback createSecurityCallback() {
return (id, status, audience) -> {
if (status == ResourcePackStatus.INVALID_URL) {
logger.warn("Invalid URL detected for pack: {}", id);
} else if (status == ResourcePackStatus.FAILED_DOWNLOAD) {
logger.warn("Download failed for pack: {}", id);
}
};
}
}Install with Tessl CLI
npx tessl i tessl/maven-net-kyori--adventure-api