### Added
- **Myopia skin LOD** for CatSkinC-managed remote skins. When enabled, distant
players use lazily cached, pixel-perfect half, quarter, or eighth-resolution
copies while GUI, inventory, and tab-list skins retain full resolution.
- A Myopia settings screen with an enabled toggle, a 4–128 block base-distance
control, Normal and Panicked modes, and links to Minecraft's keybinding menu.
- Default keybindings for increasing and decreasing the Myopia range with the
Up and Down arrow keys. All settings are persisted in `catskinc.json`.
- **WebSocket client** using native `java.net.http.WebSocket` (Java 11+) for high-performance real-time skin sync.
- **Automatic protocol negotiation** — client sends `x-catskinc-protocol: 3` header on WebSocket handshake.
- **Resilient connection management**:
- Exponential backoff reconnection (1.5s → 60s max).
- Shared circuit breaker across REST, SSE, and WebSocket (unified failure tracking).
- Automatic fallback to SSE when WebSocket fails or protocol is unsupported.
- **Session-token authentication** on WebSocket with auto-subscription to local player UUID.
- **Protocol version verification** in welcome message — closes connection on mismatch.
- **In-game toast notifications** for WebSocket-originated events (skin updates, server messages).
### Changed
- **Join flow** now attempts WebSocket first, falls back to SSE on failure.
- **Shared circuit breaker state** between `ServerApiClient` and `WebSocketClient` — REST/SSE/WS failures all contribute to the same breaker.
- **Heartbeat handling** — application-level JSON ping/pong (30s) replaces binary WebSocket ping frames for consistency.
- **Managed-skin texture cleanup** now also releases all associated Myopia GPU
textures and resets per-player LOD state.
### Fixed
- **Broadcast framing** — WebSocket clients now receive raw JSON (SSE `data: ` prefix stripped server-side).
- **Handshake protocol header** — added `X-CatSkinC-Protocol` to WebSocket upgrade request.
- **Auth race** — session token acquired synchronously before WebSocket connection attempt.
- **Protocol defense** — client verifies `protocol_version` in welcome message, closes on mismatch.
- **Fallback trigger** — SSE fallback now activates on abnormal post-connect disconnections, not just initial connect failure.
- **Toast handler** — implemented proper `Toasts.info()` with translatable titles.
- **Select-Fetch race eliminated**: `selectSkin()` now chains `thenRun(refresh)` so the POST completes before the GET, eliminating the 4-8s skin delay after upload.
- **Pending selections cache**: `PENDING_SELECTIONS` returns the uploaded skin URL immediately after select, avoiding old-skin flashing.
- **LAST_CHECK on success only**: failed fetches no longer reset the 5s poll timer, preventing a 15s blackout window.
- **Fast-retry after null skin**: retries fetch in 2s when the server has no cached skin yet, reducing initial sync from 15s to 2s.
- **Poll interval reduced**: 15s → 5s for faster periodic sync.
- **SSE clear-skin handling**: clears `BASE_CACHE`/`TALKING_CACHE` on null URL, so players properly revert to the default skin.
- **SSE reconnect delay**: added 1.5s–60s exponential backoff on stream close (was instant reconnect), preventing server flooding.
- **Circuit breaker in SSE**: SSE checks `circuitOpenUntilMs` before connecting, eliminating useless retries during outage.
- **AtomicInteger**: `consecutiveFailures` switched to `AtomicInteger` for correct failure counting.
- **Always destroy old textures**: removed identity check gate on texture release to prevent GPU memory leaks.
- **Fixed HTTP 401 on skin downloads**: Added `signDownloadUrl()` method that appends `?exp=&sig=` HMAC-signed query parameters to download URLs using the client's `requestSigningKey`. This matches the server's `verify_download_signature()` check when `ENFORCE_SIGNED_DOWNLOADS=true`.
- **Replaced per-player TextureManager registrations with a single shared dynamic texture**: CatSkinC no longer registers per-player textures (`catskinc:remote/<uuid>`) with the vanilla `TextureManager`. Instead, downloaded `NativeImage` objects are cached in `SKIN_IMAGES` and pixel-copied into one shared `catskinc:dynamic/active` texture on each render frame. This prevents F3+S debug dumps from saving every player's skin to disk.
- **Broadcast framing** — WebSocket clients now receive raw JSON (SSE `data: ` prefix stripped server-side).
- **Handshake protocol header** — added `X-CatSkinC-Protocol` to WebSocket upgrade request.
- **Auth race** — session token acquired synchronously before WebSocket connection attempt.
- **Protocol defense** — client verifies `protocol_version` in welcome message, closes on mismatch.
- **Fallback trigger** — SSE fallback now activates on abnormal post-connect disconnections, not just initial connect failure.
- **Toast handler** — implemented proper `Toasts.info()` with translatable titles.
- **Select-Fetch race eliminated**: `selectSkin()` now chains `thenRun(refresh)` so the POST completes before the GET, eliminating the 4-8s skin delay after upload.
- **Pending selections cache**: `PENDING_SELECTIONS` returns the uploaded skin URL immediately after select, avoiding old-skin flashing.
- **LAST_CHECK on success only**: failed fetches no longer reset the 5s poll timer, preventing a 15s blackout window.
- **Fast-retry after null skin**: retries fetch in 2s when the server has no cached skin yet, reducing initial sync from 15s to 2s.
- **Poll interval reduced**: 15s → 5s for faster periodic sync.
- **SSE clear-skin handling**: clears `BASE_CACHE`/`TALKING_CACHE` on null URL, so players properly revert to the default skin.
- **SSE reconnect delay**: added 1.5s–60s exponential backoff on stream close (was instant reconnect), preventing server flooding.
- **Circuit breaker in SSE**: SSE checks `circuitOpenUntilMs` before connecting, eliminating useless retries during outage.
- **AtomicInteger**: `consecutiveFailures` switched to `AtomicInteger` for correct failure counting.
- **Always destroy old textures**: removed identity check gate on texture release to prevent GPU memory leaks.
- **Fixed HTTP 401 on skin downloads**: Added `signDownloadUrl()` method that appends `?exp=&sig=` HMAC-signed query parameters to download URLs using the client's `requestSigningKey`. This matches the server's `verify_download_signature()` check when `ENFORCE_SIGNED_DOWNLOADS=true`.
- **Replaced per-player TextureManager registrations with a single shared dynamic texture**: CatSkinC no longer registers per-player textures (`catskinc:remote/<uuid>`) with the vanilla `TextureManager`. Instead, downloaded `NativeImage` objects are cached in `SKIN_IMAGES` and pixel-copied into one shared `catskinc:dynamic/active` texture on each render frame. This prevents F3+S debug dumps from saving every player's skin to disk.
- **Security**: **Request signing** (HMAC-SHA256) applied to WebSocket upgrade request.
- **Hardened transitive dependencies against Dependabot advisories.** Added `resolutionStrategy { force ... }` constraints in `build.gradle` to pin patched versions of vulnerable transitive libraries on both the project and buildscript classpaths:
- Netty → `4.1.136.Final` (fixes GHSA-558v-64gr-wgg4 Bzip2Decoder infinite loop and related netty advisories).
- Log4j → `2.25.4` (fixes Log4j TLS hostname verification, XML escaping, and related advisories).
- Guava → `32.0.0-jre` (fixes temp-dir and information-disclosure advisories).
- Commons IO → `2.16.1`, Commons Compress → `1.26.0`, Commons Lang3 → `3.18.0`.
- Buildscript classpath: Commons Text → `1.12.0`, Commons Beanutils → `1.11.0`, Plexus Utils → `3.6.1`.
- These libraries are Minecraft-provided runtime/build-tooling dependencies and are **not bundled** into the shipped mod JAR, so the end-user artifact is unaffected; the constraints keep the dev runtime and CI dependency graph clean.
Optional dependencies
Plasmo Voice
Any compatible version
Simple Voice Chat
Any compatible version
Developer information
Projects on Modrinth are automatically available through a Maven repository for use with JVM build tools such as Gradle.
maven { name = "Modrinth"; url = "https://api.modrinth.com/maven" }