Cosmetics
LiquidBounce features a cosmetics system that allows users to display capes and other visual customizations. The system is designed to be efficient, reducing API stress while providing reliable cosmetic loading.
Overview
The cosmetics system consists of three main components:
CosmeticService Manages cosmetic carriers and fetching
CapeCosmeticsManager Handles cape texture loading and caching
ClientAccountManager Manages authenticated client account
Cosmetic Service
The CosmeticService implements a carrier-based system to reduce API load:
object CosmeticService : EventListener , ValueGroup ( "Cosmetics" ) {
// Collection of MD5-hashed UUIDs of players with cosmetics
internal var carriers = emptySet < String >()
// Cache of fetched cosmetics per UUID
internal val carriersCosmetics = hashMapOf < UUID , Set < Cosmetic >>()
}
How It Works
Carrier List
Maintains a set of MD5-hashed UUIDs of all players who have cosmetics
Lazy Loading
Only fetches cosmetics when a player with cosmetics is encountered
Caching
Caches fetched cosmetics to avoid repeated API calls
Periodic Refresh
Updates the carrier list every 60 seconds
This approach significantly reduces API stress compared to checking every player individually.
Cosmetic Categories
Cosmetics are categorized by type:
enum class CosmeticCategory {
CAPE,
// Future categories can be added here
}
Currently, only capes are supported, but the system is designed to be extensible.
Fetching Cosmetics
Fetch a cosmetic for a specific player:
CosmeticService. fetchCosmetic (uuid, category) { cosmetic ->
// Use the cosmetic
}
The fetch process (from CosmeticService.kt:99):
Check if Local Client
if (uuid == mc.user.profileId || uuid == mc.player?.uuid) {
// Use client account cosmetics
clientAccount.cosmetics?. let { cosmetics ->
done (cosmetics. find { it.category == category } ?: return )
}
}
Refresh Carrier List
refreshCarriers {
if (uuid. toMD5 () ! in carriers) {
return @refreshCarriers // Player has no cosmetics
}
}
Check Cache
carriersCosmetics[uuid]?. let { cosmetics ->
done (cosmetics. find { it.category == category } ?: return )
}
Fetch from API
val cosmetics = CosmeticApi. getCarrierCosmetics (uuid)
carriersCosmetics[uuid] = cosmetics
done (cosmetics. find { it.category == category } ?: return )
Carrier Refresh
The carrier list is refreshed periodically:
fun refreshCarriers (force: Boolean = false , done: () -> Unit) {
// Only refresh if 60 seconds elapsed or forced
if (lastUpdate. hasElapsed (REFRESH_DELAY) || force) {
task = withScope {
carriers = CosmeticApi. getCarriers ()
lastUpdate. reset ()
mc. execute (done)
}
} else {
done () // No refresh needed
}
}
Refresh operations use a task lock to prevent concurrent refreshes.
Cape System
The CapeCosmeticsManager handles cape-specific functionality:
Loading Player Capes
fun loadPlayerCape (player: GameProfile , callback: Consumer < Identifier >) {
CosmeticService. fetchCosmetic (player.id, CosmeticCategory.CAPE) { cosmetic ->
val name = getCapeName (cosmetic) ?: return @fetchCosmetic
// Check cache
cachedCapes[name]?. let { capeId ->
callback. accept (capeId)
return @fetchCosmetic
}
// Download cape texture
val texture = CapeApi. getCape (name)
val id = LiquidBounce. identifier ( "cape- $name " )
// Register and cache
texture. registerTexture (id)
cachedCapes[name] = id
callback. accept (id)
}
}
Cape Caching
Capes are cached in memory to avoid repeated downloads:
private val cachedCapes = hashMapOf < String , Identifier >()
The cache is automatically cleared on disconnect:
private val disconnectHandler = handler < DisconnectEvent > {
cachedCapes.values. forEach { mc.textureManager. release (it) }
cachedCapes. clear ()
}
Capes are cached by name, not UUID, allowing multiple players with the same cape to share textures.
Client Account Integration
For the authenticated client, cosmetics are managed through ClientAccountManager:
object ClientAccountManager : Config ( "account" ) {
var clientAccount by value (
"account" ,
ClientAccount.ENV_ACCOUNT ?: ClientAccount.EMPTY_ACCOUNT
)
}
The client account includes:
User information
Owned cosmetics
Session tokens
Updating Client Cosmetics
clientAccount. updateCosmetics ()
This fetches the latest cosmetics for the authenticated user.
Temporary Ownership Transfer
Cosmetics can be temporarily transferred to another UUID:
private suspend fun transferTemporaryOwnership (uuid: UUID ) {
clientAccount. transferTemporaryOwnership (uuid)
// Refresh carrier list after transfer
refreshCarriers ( true ) {
logger. info ( "Successfully loaded ${ carriers.size } cosmetics carriers." )
}
}
This is automatically triggered on session changes:
private val sessionHandler = suspendHandler < SessionEvent > {
val uuid = it.session.profileId
transferTemporaryOwnership (uuid)
}
Checking Cosmetic Availability
Check if a player has a specific cosmetic:
if (CosmeticService. hasCosmetic (uuid, CosmeticCategory.CAPE)) {
// Player has a cape
}
This performs a non-blocking check using cached data.
Player UUIDs are MD5-hashed in the carrier set to reduce memory usage and improve lookup performance. if (uuid. toMD5 () ! in carriers) {
return // No cosmetics
}
Empty sets are pre-allocated to prevent multiple concurrent requests for the same cosmetic. // Pre-allocate to prevent multiple requests
carriersCosmetics[uuid] = emptySet ()
Cosmetics are only fetched when needed, not preloaded for all players.
Caches are cleared on disconnect to prevent memory leaks.
Error Handling
All API calls use runCatching for robust error handling:
runCatching {
val cosmetics = CosmeticApi. getCarrierCosmetics (uuid)
carriersCosmetics[uuid] = cosmetics
}. onFailure {
logger. error ( "Failed to get cosmetics of carrier $uuid " , it)
}
Code References
CosmeticService.kt Core cosmetic service - line 51
features/cosmetic/CosmeticService.kt
CapeCosmeticsManager.kt Cape loading and caching - line 42
features/cosmetic/CapeCosmeticsManager.kt
ClientAccountManager.kt Client account management - line 25
features/cosmetic/ClientAccountManager.kt
Best Practices
Use Callbacks
Always use callbacks when fetching cosmetics as operations are asynchronous
Check Carriers First
The carrier set provides a fast pre-check before fetching cosmetics
Handle Missing Cosmetics
Not all players have cosmetics - handle null/empty results gracefully
Respect Cache
Don’t force refresh unless necessary - trust the 60-second refresh cycle