@@ -34,6 +34,11 @@ object CacheBinaryFormat {
3434
3535 private const val MAGIC = " ORCA"
3636 private const val SPRITE_SHA_LEN = 32
37+ private const val TRAILER_SPRITE_META = " SMET"
38+ private const val TRAILER_SPRITE_RASTER = " SMRP"
39+ private const val TRAILER_OPENRS2_ID = " OCID"
40+ private const val TRAILER_INTERFACE_MANIFEST = " IFMF"
41+ private const val TRAILER_CLIENT_SCRIPTS = " CSRB"
3742
3843 private val gson = Gson ()
3944
@@ -43,6 +48,19 @@ object CacheBinaryFormat {
4348 val sub : Map <Int , String > = emptyMap(),
4449 )
4550
51+ data class IndexedSpriteMeta (
52+ val offsetX : Int = 0 ,
53+ val offsetY : Int = 0 ,
54+ val width : Int = 0 ,
55+ val height : Int = 0 ,
56+ val averageColor : Int = -1 ,
57+ val subHeight : Int = 0 ,
58+ val subWidth : Int = 0 ,
59+ val alphaBase64 : String? = null ,
60+ val rasterBase64 : String = " " ,
61+ val palette : List <Int > = emptyList(),
62+ )
63+
4664 data class DecodedRev (
4765 val revision : Int ,
4866 val openRs2CacheId : Long? = null ,
@@ -51,9 +69,12 @@ object CacheBinaryFormat {
5169 val gameval : Map <String , Map <Int , GamevalExtra >> = emptyMap(),
5270 val sprites : Map <Int , ByteArray > = emptyMap(),
5371 val spriteSha256 : Map <Int , ByteArray > = emptyMap(),
72+ val spriteMetadata : Map <Int , List <IndexedSpriteMeta >> = emptyMap(),
5473 val mapObjects : Map <Int , List <LocationCustom >> = emptyMap(),
5574 val mapRegions : Map <Int , RegionData > = emptyMap(),
5675 val xteasByRegion : Map <Int , IntArray > = emptyMap(),
76+ val interfaceManifest : List <InterfaceManifestEntry > = emptyList(),
77+ val clientScripts : Map <Int , ByteArray > = emptyMap(),
5778 )
5879
5980 fun encode (
@@ -63,9 +84,12 @@ object CacheBinaryFormat {
6384 configs : Map <String , Map <Int , DefinitionSnapshot >>,
6485 gameval : Map <String , Map <Int , GamevalExtra >> = emptyMap(),
6586 sprites : Map <Int , ByteArray > = emptyMap(),
87+ spriteMetadata : Map <Int , List <IndexedSpriteMeta >> = emptyMap(),
6688 mapObjects : Map <Int , List <LocationCustom >> = emptyMap(),
6789 mapRegions : Map <Int , RegionData > = emptyMap(),
6890 xteasByRegion : Map <Int , IntArray > = emptyMap(),
91+ interfaceManifest : List <InterfaceManifestEntry > = emptyList(),
92+ clientScripts : Map <Int , ByteArray > = emptyMap(),
6993 ): ByteArray {
7094 val body = ByteArrayOutputStream ()
7195 val configTypes = ConfigDiffType .diffTypeNames
@@ -175,11 +199,74 @@ object CacheBinaryFormat {
175199 repeat(4 ) { writeInt32LE(body, norm[it]) }
176200 }
177201
178- // Optional source OpenRS2 cache id trailer for boot-time freshness checks.
202+ // Optional trailers (marker-prefixed for backward compatibility).
203+ body.write(TRAILER_SPRITE_META .toByteArray(Charsets .UTF_8 ))
204+ writeVarint(body, spriteMetadata.size)
205+ spriteMetadata.entries.sortedBy { it.key }.forEach { (id, metas) ->
206+ writeVarint(body, id)
207+ writeVarint(body, metas.size)
208+ metas.forEach { meta ->
209+ writeSVarint(body, meta.offsetX)
210+ writeSVarint(body, meta.offsetY)
211+ writeVarint(body, meta.width)
212+ writeVarint(body, meta.height)
213+ writeSVarint(body, meta.averageColor)
214+ writeVarint(body, meta.subHeight)
215+ writeVarint(body, meta.subWidth)
216+ val alpha = meta.alphaBase64
217+ if (alpha == null ) {
218+ body.write(0 )
219+ } else {
220+ body.write(1 )
221+ writeString(body, alpha)
222+ }
223+ }
224+ }
225+
226+ // SMRP: raster + palette data (only entries that have raster populated)
227+ val spriteMetaWithRaster = spriteMetadata.filterValues { metas -> metas.any { it.rasterBase64.isNotEmpty() } }
228+ if (spriteMetaWithRaster.isNotEmpty()) {
229+ body.write(TRAILER_SPRITE_RASTER .toByteArray(Charsets .UTF_8 ))
230+ writeVarint(body, spriteMetaWithRaster.size)
231+ spriteMetaWithRaster.entries.sortedBy { it.key }.forEach { (id, metas) ->
232+ writeVarint(body, id)
233+ writeVarint(body, metas.size)
234+ metas.forEach { meta ->
235+ writeString(body, meta.rasterBase64)
236+ writeVarint(body, meta.palette.size)
237+ meta.palette.forEach { writeInt32LE(body, it) }
238+ }
239+ }
240+ }
241+
179242 if (openRs2CacheId != null ) {
243+ body.write(TRAILER_OPENRS2_ID .toByteArray(Charsets .UTF_8 ))
180244 writeInt64LE(body, openRs2CacheId)
181245 }
182246
247+ body.write(TRAILER_INTERFACE_MANIFEST .toByteArray(Charsets .UTF_8 ))
248+ writeVarint(body, interfaceManifest.size)
249+ interfaceManifest.sortedBy { it.interfaceId }.forEach { entry ->
250+ writeVarint(body, entry.interfaceId)
251+ body.write(if (entry.gameval != null ) 1 else 0 )
252+ if (entry.gameval != null ) writeString(body, entry.gameval)
253+ body.write(
254+ when (entry.iflegacy) {
255+ true -> 1
256+ false -> 0
257+ null -> 2
258+ }
259+ )
260+ }
261+
262+ body.write(TRAILER_CLIENT_SCRIPTS .toByteArray(Charsets .UTF_8 ))
263+ writeVarint(body, clientScripts.size)
264+ clientScripts.entries.sortedBy { it.key }.forEach { (scriptId, raw) ->
265+ writeVarint(body, scriptId)
266+ writeVarint(body, raw.size)
267+ body.write(raw)
268+ }
269+
183270 val bodyBytes = body.toByteArray()
184271 val compressed = Zstd .compress(bodyBytes)
185272 val header = ByteBuffer .allocate(4 + 4 + 4 ).order(ByteOrder .LITTLE_ENDIAN )
@@ -309,7 +396,129 @@ object CacheBinaryFormat {
309396 xteasByRegion[sq] = IntArray (4 ) { readInt32LE(input) }
310397 }
311398
312- val openRs2CacheId = if (input.available() >= 8 ) readInt64LE(input) else null
399+ val spriteMetadata = HashMap <Int , List <IndexedSpriteMeta >>()
400+ val interfaceManifest = ArrayList <InterfaceManifestEntry >()
401+ val clientScripts = HashMap <Int , ByteArray >()
402+ var openRs2CacheId: Long? = null
403+ if (input.available() > 0 ) {
404+ val trailer = readBytes(input, input.available())
405+ val trailerInput = ByteArrayInputStream (trailer)
406+ var parsedWithMarkers = false
407+
408+ while (trailerInput.available() >= 4 ) {
409+ val marker = String (readBytes(trailerInput, 4 ), Charsets .UTF_8 )
410+ when (marker) {
411+ TRAILER_SPRITE_META -> {
412+ parsedWithMarkers = true
413+ val idCount = readVarint(trailerInput)
414+ repeat(idCount) {
415+ val id = readVarint(trailerInput)
416+ val count = readVarint(trailerInput)
417+ val metas = ArrayList <IndexedSpriteMeta >(count)
418+ repeat(count) {
419+ val offsetX = readSVarint(trailerInput)
420+ val offsetY = readSVarint(trailerInput)
421+ val width = readVarint(trailerInput)
422+ val height = readVarint(trailerInput)
423+ val averageColor = readSVarint(trailerInput)
424+ val subHeight = readVarint(trailerInput)
425+ val subWidth = readVarint(trailerInput)
426+ val hasAlpha = trailerInput.read() == 1
427+ val alphaBase64 = if (hasAlpha) readString(trailerInput) else null
428+ metas.add(
429+ IndexedSpriteMeta (
430+ offsetX = offsetX,
431+ offsetY = offsetY,
432+ width = width,
433+ height = height,
434+ averageColor = averageColor,
435+ subHeight = subHeight,
436+ subWidth = subWidth,
437+ alphaBase64 = alphaBase64,
438+ )
439+ )
440+ }
441+ spriteMetadata[id] = metas
442+ }
443+ }
444+ TRAILER_SPRITE_RASTER -> {
445+ parsedWithMarkers = true
446+ val idCount = readVarint(trailerInput)
447+ repeat(idCount) {
448+ val id = readVarint(trailerInput)
449+ val count = readVarint(trailerInput)
450+ val existingMetas = spriteMetadata[id]?.toMutableList()
451+ if (existingMetas == null ) {
452+ // No SMET entry — skip raster data
453+ repeat(count) {
454+ readString(trailerInput)
455+ val palSize = readVarint(trailerInput)
456+ repeat(palSize) { readInt32LE(trailerInput) }
457+ }
458+ return @repeat
459+ }
460+ val updated = ArrayList <IndexedSpriteMeta >(count)
461+ repeat(count) { idx ->
462+ val rasterBase64 = readString(trailerInput)
463+ val palSize = readVarint(trailerInput)
464+ val palette = List (palSize) { readInt32LE(trailerInput) }
465+ val base = existingMetas.getOrNull(idx) ? : IndexedSpriteMeta ()
466+ updated.add(base.copy(rasterBase64 = rasterBase64, palette = palette))
467+ }
468+ spriteMetadata[id] = updated
469+ }
470+ }
471+ TRAILER_OPENRS2_ID -> {
472+ parsedWithMarkers = true
473+ if (trailerInput.available() >= 8 ) {
474+ openRs2CacheId = readInt64LE(trailerInput)
475+ }
476+ }
477+ TRAILER_INTERFACE_MANIFEST -> {
478+ parsedWithMarkers = true
479+ val count = readVarint(trailerInput)
480+ repeat(count) {
481+ val interfaceId = readVarint(trailerInput)
482+ val hasGameval = trailerInput.read() == 1
483+ val gameval = if (hasGameval) readString(trailerInput) else null
484+ val legacyFlag = trailerInput.read()
485+ val iflegacy = when (legacyFlag) {
486+ 1 -> true
487+ 0 -> false
488+ else -> null
489+ }
490+ interfaceManifest.add(
491+ InterfaceManifestEntry (
492+ interfaceId = interfaceId,
493+ gameval = gameval,
494+ iflegacy = iflegacy,
495+ )
496+ )
497+ }
498+ }
499+ TRAILER_CLIENT_SCRIPTS -> {
500+ parsedWithMarkers = true
501+ val count = readVarint(trailerInput)
502+ repeat(count) {
503+ val scriptId = readVarint(trailerInput)
504+ val len = readVarint(trailerInput)
505+ clientScripts[scriptId] = readBytes(trailerInput, len)
506+ }
507+ }
508+ else -> {
509+ // Backward compatibility: old binaries had an optional raw 8-byte OpenRS2 id trailer.
510+ if (! parsedWithMarkers && trailer.size >= 8 ) {
511+ openRs2CacheId = readInt64LE(ByteArrayInputStream (trailer.copyOfRange(0 , 8 )))
512+ }
513+ break
514+ }
515+ }
516+ }
517+
518+ if (! parsedWithMarkers && openRs2CacheId == null && trailer.size >= 8 ) {
519+ openRs2CacheId = readInt64LE(ByteArrayInputStream (trailer.copyOfRange(0 , 8 )))
520+ }
521+ }
313522
314523 return DecodedRev (
315524 revision = revision,
@@ -319,9 +528,12 @@ object CacheBinaryFormat {
319528 gameval = gameval,
320529 sprites = sprites,
321530 spriteSha256 = spriteSha256,
531+ spriteMetadata = spriteMetadata,
322532 mapObjects = mapObjects,
323533 mapRegions = mapRegions,
324534 xteasByRegion = xteasByRegion,
535+ interfaceManifest = interfaceManifest,
536+ clientScripts = clientScripts,
325537 )
326538 }
327539
@@ -333,13 +545,29 @@ object CacheBinaryFormat {
333545 configs : Map <String , Map <Int , DefinitionSnapshot >>,
334546 gameval : Map <String , Map <Int , GamevalExtra >> = emptyMap(),
335547 sprites : Map <Int , ByteArray > = emptyMap(),
548+ spriteMetadata : Map <Int , List <IndexedSpriteMeta >> = emptyMap(),
336549 mapObjects : Map <Int , List <LocationCustom >> = emptyMap(),
337550 mapRegions : Map <Int , RegionData > = emptyMap(),
338551 xteasByRegion : Map <Int , IntArray > = emptyMap(),
552+ interfaceManifest : List <InterfaceManifestEntry > = emptyList(),
553+ clientScripts : Map <Int , ByteArray > = emptyMap(),
339554 ) {
340555 file.parentFile?.mkdirs()
341556 file.writeBytes(
342- encode(revision, openRs2CacheId, manifest, configs, gameval, sprites, mapObjects, mapRegions, xteasByRegion)
557+ encode(
558+ revision,
559+ openRs2CacheId,
560+ manifest,
561+ configs,
562+ gameval,
563+ sprites,
564+ spriteMetadata,
565+ mapObjects,
566+ mapRegions,
567+ xteasByRegion,
568+ interfaceManifest,
569+ clientScripts,
570+ )
343571 )
344572 }
345573
0 commit comments