Skip to main content
The cosmetic system allows LiquidBounce users to display custom capes and other cosmetic items that are fetched from the official API.

Overview

The CosmeticService (features/cosmetic/CosmeticService.kt:51) manages cosmetic loading, caching, and rendering with a focus on performance and reduced API stress.

How It Works

Carrier-Based System

Rather than checking every player, the system uses a carrier-based approach:
1

Refresh Carriers

Downloads a list of MD5-hashed UUIDs of players who have cosmetics (CosmeticService.kt:73)
2

Check Membership

Only requests cosmetics for players in the carrier list
3

Cache Results

Stores fetched cosmetics to avoid repeated API calls
This approach significantly reduces API load while ensuring cosmetics appear when needed.

Cosmetic Categories

The system supports multiple cosmetic types through CosmeticCategory:
  • Capes
  • Other cosmetic items (extensible)

Fetching Cosmetics

For Any Player

CosmeticService.fetchCosmetic(uuid, category) { cosmetic ->
    // Use the cosmetic
}
The service automatically:
  1. Checks if the UUID is in the carrier list
  2. Returns cached cosmetics if available
  3. Fetches from API if needed
  4. Calls the callback when ready

For Current User

The current user’s cosmetics are handled specially (CosmeticService.kt:103-120):
if ((uuid == mc.user.profileId || uuid == mc.player?.uuid) && 
    clientAccount != ClientAccount.EMPTY_ACCOUNT) {
    // Use ClientAccountManager for current user
}
This uses the client account’s cosmetic data directly, supporting temporary ownership transfers.

Carrier Management

Refresh Timing

Carriers are refreshed every 60 seconds:
private const val REFRESH_DELAY = 60000L

Manual Refresh

CosmeticService.refreshCarriers(force = true) {
    // Carriers updated
}
The force parameter bypasses the delay check (CosmeticService.kt:73-97).

Caching System

Carrier Cache

MD5-hashed UUIDs of cosmetic carriers:
internal var carriers = emptySet<String>()

Cosmetic Cache

Fetched cosmetics mapped by player UUID:
internal val carriersCosmetics = hashMapOf<UUID, Set<Cosmetic>>()

Pre-allocation

To prevent duplicate API requests, the system pre-allocates empty sets:
// Pre-allocate a set to prevent multiple requests
carriersCosmetics[uuid] = emptySet()
This ensures concurrent requests don’t trigger multiple API calls (CosmeticService.kt:135).

Temporary Ownership

Transferring Cosmetics

Users can temporarily transfer cosmetic ownership to their current session:
private suspend fun transferTemporaryOwnership(uuid: UUID)
This is automatically triggered on session changes (CosmeticService.kt:192-202):
private val sessionHandler = suspendHandler<SessionEvent> {
    val session = it.session
    
    // Check if the account is valid
    if (session.accessToken.length < 2) {
        return@suspendHandler
    }
    val uuid = session.profileId
    
    transferTemporaryOwnership(uuid)
}

Benefits

  • Allows testing cosmetics on alt accounts
  • Maintains cosmetic display when switching accounts
  • Automatically refreshes carrier list after transfer

Client Account Integration

The ClientAccountManager handles the authenticated user’s cosmetics:
val clientAccount = ClientAccountManager.clientAccount

if (clientAccount != ClientAccount.EMPTY_ACCOUNT) {
    clientAccount.cosmetics?.let { cosmetics ->
        // Use cosmetics
    }
}
Client accounts can update their cosmetics:
clientAccount.updateCosmetics()

Checking Cosmetic Availability

if (CosmeticService.hasCosmetic(uuid, category)) {
    // Player has this cosmetic type
}
This checks both the cache and initiates a fetch if needed (CosmeticService.kt:169).

Event Handling

Session Events

Automatically handles cosmetic transfers when logging into accounts.

Disconnect Events

Clears the cosmetic cache on disconnect to free memory:
private val disconnectHandler = handler<DisconnectEvent> {
    carriersCosmetics.clear()
}

Performance Optimization

Reduced API Stress

The carrier-based approach means:
  • Only players with cosmetics trigger API calls
  • Carrier list is shared across all cosmetic types
  • Updates happen at most once per minute

Asynchronous Loading

All API calls use coroutines:
withScope {
    runCatching {
        val cosmetics = CosmeticApi.getCarrierCosmetics(uuid)
        // Process cosmetics
    }.onFailure {
        logger.error("Failed to get cosmetics", it)
    }
}

Smart Caching

  • Carrier list cached for 60 seconds
  • Individual cosmetics cached until disconnect
  • Current user cosmetics use client account (no extra API calls)

Error Handling

The service gracefully handles failures:
.onFailure {
    logger.error("Failed to refresh cape carriers due to error.", it)
}
Failed requests don’t crash the system or prevent other cosmetics from loading.

Usage Example

// Fetch a player's cape
CosmeticService.fetchCosmetic(playerUuid, CosmeticCategory.CAPE) { cape ->
    // Render the cape
    renderCape(cape)
}

// Check if player has any cosmetics
if (CosmeticService.hasCosmetic(playerUuid, CosmeticCategory.CAPE)) {
    // Show cape indicator
}

// Force refresh carriers
CosmeticService.refreshCarriers(force = true) {
    logger.info("Loaded ${CosmeticService.carriers.size} cosmetic carriers")
}

API Integration

The service uses the official LiquidBounce API:
  • CosmeticApi.getCarriers() - Get list of cosmetic carriers
  • CosmeticApi.getCarrierCosmetics(uuid) - Get cosmetics for a specific player
  • clientAccount.updateCosmetics() - Update current user’s cosmetics
  • clientAccount.transferTemporaryOwnership(uuid) - Transfer cosmetics to session

Build docs developers (and LLMs) love