1+ import { createHash } from 'node:crypto' ;
2+
13import pLimit from 'p-limit' ;
24
35import * as hytaleApi from './api' ;
46import { loadHytaleAssets } from './assets' ;
5- import { getRequiredAssetPaths , resolveSkin } from './cosmetic-registry' ;
7+ import {
8+ type SkinDefinition ,
9+ getCosmeticJson ,
10+ getCosmeticJsonBytes ,
11+ getRequiredAssetPaths ,
12+ resolveSkin ,
13+ } from './cosmetic-registry' ;
614import { render_hytale_3d , render_text_avatar } from '../../../../pkg/mcavatar' ;
715import { EMPTY } from '../../data' ;
816import { IdentityKind , RequestedKind } from '../../request' ;
@@ -14,14 +22,16 @@ import {
1422 uuidVersion ,
1523} from '../../util/uuid' ;
1624
17- import type { HytaleProfile } from './api' ;
25+ import type { HytaleProfile , HytaleSkin } from './api' ;
1826import type { CraftheadRequest } from '../../request' ;
1927import type { CacheComputeResult } from '../../util/cache-helper' ;
2028
2129const CACHE_TTL_MS = 24 * 60 * 60 * 1000 ; // 24 hours in milliseconds
2230
23- function getRenderCacheKey ( request : CraftheadRequest ) : string {
24- return `renders/${ request . requested } /${ request . armored } /${ request . model || 'regular' } /${ request . identity . toLowerCase ( ) } /${ request . size } ` ;
31+ function getRenderCacheKey ( skin : HytaleSkin ) : string {
32+ // Hash the skin to generate a unique key per unique skin combo (so we can share renders with multiple users)
33+ const hash = createHash ( 'sha256' ) . update ( JSON . stringify ( skin ) ) . digest ( 'hex' ) ;
34+ return `renders/${ hash } ` ;
2535}
2636
2737async function getCachedRender ( cacheKey : string , env : Cloudflare . Env ) : Promise < Response | null > {
@@ -160,67 +170,54 @@ function generateAndReturnTextAvatar(username: string, request: CraftheadRequest
160170 * Uses R2 caching for 24 hours to reduce computational cost.
161171 */
162172export async function renderAvatar ( incomingRequest : Request , request : CraftheadRequest , env : Cloudflare . Env , ctx : ExecutionContext ) : Promise < Response > {
163- const cacheKey = getRenderCacheKey ( request ) ;
164-
165- const cachedRender = await getCachedRender ( cacheKey , env ) ;
166- if ( cachedRender ) {
167- return cachedRender ;
168- }
169-
170173 const { profile } = await normalizeRequest ( incomingRequest , request ) ;
171174 const username = profile ?. name ?? request . identity ;
172175 if ( ! profile ?. skin ) {
173176 // TODO: Replace with a deterministic skin generator
174177 return generateAndReturnTextAvatar ( username , request ) ;
175178 }
179+ const cacheKey = getRenderCacheKey ( profile . skin ) ;
180+
181+ const cachedRender = await getCachedRender ( cacheKey , env ) ;
182+ if ( cachedRender ) {
183+ return cachedRender ;
184+ }
176185
177186 try {
178187 // Load bundled Hytale assets (base model and animation)
179- const assets = await loadHytaleAssets ( ) ;
188+ const skinDefinitionJson = getCosmeticJson < SkinDefinition [ ] > ( 'Cosmetics/CharacterCreator/BodyCharacteristics.json' ) ;
189+ // Look for their skin type
190+ const skinType = profile . skin . bodyCharacteristic . split ( '.' ) [ 0 ] ?? 'Default' ;
191+ const skinPath = skinDefinitionJson . find ( skin => skin . Id === skinType ) ?. GreyscaleTexture ?? 'Common/Characters/Player_Textures/Player_Greyscale.png' ;
192+ const assets = await loadHytaleAssets ( skinPath , ctx ) ;
180193
181194 const resolvedSkin = await resolveSkin ( profile . skin ) ;
182195 const assetPaths : string [ ] = [ ] ;
183196 const assetBytes : Uint8Array [ ] = [ ] ;
184197
185198 const requiredAssets = getRequiredAssetPaths ( resolvedSkin ) ;
186199
187- const assetSet = new Set < string > ( [
200+ const skinSpecificAssetSet = new Set < string > ( [
188201 ...requiredAssets . models ,
189202 ...requiredAssets . textures ,
190203 ...requiredAssets . gradients ,
191- 'Cosmetics/CharacterCreator/HaircutFallbacks.json' ,
192- 'Cosmetics/CharacterCreator/Faces.json' ,
193- 'Cosmetics/CharacterCreator/Eyes.json' ,
194- 'Cosmetics/CharacterCreator/Eyebrows.json' ,
195- 'Cosmetics/CharacterCreator/Mouths.json' ,
196- 'Cosmetics/CharacterCreator/Ears.json' ,
197- 'Cosmetics/CharacterCreator/Haircuts.json' ,
198- 'Cosmetics/CharacterCreator/FacialHair.json' ,
199- 'Cosmetics/CharacterCreator/Underwear.json' ,
200- 'Cosmetics/CharacterCreator/FaceAccessory.json' ,
201- 'Cosmetics/CharacterCreator/Capes.json' ,
202- 'Cosmetics/CharacterCreator/EarAccessory.json' ,
203- 'Cosmetics/CharacterCreator/Gloves.json' ,
204- 'Cosmetics/CharacterCreator/HeadAccessory.json' ,
205- 'Cosmetics/CharacterCreator/GradientSets.json' ,
206- 'Cosmetics/CharacterCreator/Overpants.json' ,
207- 'Cosmetics/CharacterCreator/Overtops.json' ,
208- 'Cosmetics/CharacterCreator/Pants.json' ,
209- 'Cosmetics/CharacterCreator/Shoes.json' ,
210- 'Cosmetics/CharacterCreator/SkinFeatures.json' ,
211- 'Cosmetics/CharacterCreator/Undertops.json' ,
212204 ] ) ;
213205
206+ for ( const { path, bytes } of getCosmeticJsonBytes ( ) ) {
207+ assetPaths . push ( `assets/Common/${ path } ` ) ;
208+ assetBytes . push ( bytes ) ;
209+ }
210+
214211 const limit = pLimit ( 5 ) ;
215- const assetPromises = [ ...assetSet ] . map ( async ( assetPath ) => {
216- const data = await limit ( ( ) => readAssetFile ( assetPath , env ) ) ;
212+ const skinAssetPromises = [ ...skinSpecificAssetSet ] . map ( async ( assetPath ) => {
213+ const data = await limit ( ( ) => readAssetFile ( assetPath , env , ctx ) ) ;
217214 const providerPath = assetPath . startsWith ( 'Common/' )
218215 ? `assets/${ assetPath } `
219216 : `assets/Common/${ assetPath } ` ;
220217 return { providerPath, bytes : new Uint8Array ( data ) } ;
221218 } ) ;
222219
223- const results = await Promise . all ( assetPromises ) ;
220+ const results = await Promise . all ( skinAssetPromises ) ;
224221 for ( const result of results ) {
225222 assetPaths . push ( result . providerPath ) ;
226223 assetBytes . push ( result . bytes ) ;
0 commit comments