this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add N64 image converter and implement BackgroundAsset building

+483
+12
src/main/java/assets/ui/BackgroundAsset.kt
··· 2 2 3 3 import assets.Asset 4 4 import assets.AssetsDir 5 + import project.build.BuildCtx 6 + import util.N64ImageFormat 7 + import util.convertN64Image 5 8 import java.awt.Image 6 9 import java.awt.image.BufferedImage 7 10 import java.io.IOException 8 11 import java.nio.file.Path 9 12 import javax.imageio.ImageIO 13 + import kotlin.io.path.writeBytes 10 14 11 15 class BackgroundAsset(assetsDir: AssetsDir, relativePath: Path) : Asset(assetsDir, relativePath) { 12 16 @JvmField ··· 21 25 } 22 26 23 27 override fun loadThumbnail(): Image? = bimg 28 + 29 + override fun getArtifacts(ctx: BuildCtx): List<Path> = listOf(ctx.artifact(this)) 30 + 31 + override suspend fun build(ctx: BuildCtx) { 32 + val output = ctx.artifact(this) 33 + val result = util.convertN64Image(util.N64ImageFormat.BG, getFile(), flipY = true) 34 + output.writeBytes(result.bytes) 35 + } 24 36 }
+471
src/main/java/util/N64ImageConverter.kt
··· 1 + package util 2 + 3 + import ar.com.hjg.pngj.ImageInfo 4 + import ar.com.hjg.pngj.ImageLineHelper 5 + import ar.com.hjg.pngj.ImageLineInt 6 + import ar.com.hjg.pngj.PngReader 7 + import java.io.File 8 + import java.nio.ByteBuffer 9 + import java.nio.ByteOrder 10 + import kotlin.math.floor 11 + import kotlin.math.round 12 + 13 + /** 14 + * Converts PNG images to N64 texture formats. 15 + * Ported from papermario/tools/build/img/build.py 16 + */ 17 + 18 + enum class N64ImageFormat { 19 + RGBA32, RGBA16, CI8, CI4, PALETTE, IA4, IA8, IA16, I4, I8, PARTY, BG 20 + } 21 + 22 + data class N64ImageConversionResult( 23 + val bytes: ByteArray, 24 + val width: Int, 25 + val height: Int 26 + ) 27 + 28 + fun convertN64Image( 29 + format: N64ImageFormat, 30 + infile: File, 31 + flipY: Boolean = false 32 + ): N64ImageConversionResult { 33 + val reader = PngReader(infile) 34 + val imgInfo = reader.imgInfo 35 + val width = imgInfo.cols 36 + val height = imgInfo.rows 37 + 38 + val context = ConversionContext(infile, reader, imgInfo, flipY) 39 + 40 + val outBytes = when (format) { 41 + N64ImageFormat.RGBA32 -> context.convertRgba32() 42 + N64ImageFormat.RGBA16 -> context.convertRgba16() 43 + N64ImageFormat.CI8 -> context.convertCi8() 44 + N64ImageFormat.CI4 -> context.convertCi4() 45 + N64ImageFormat.PALETTE -> context.convertPalette() 46 + N64ImageFormat.IA4 -> context.convertIa4() 47 + N64ImageFormat.IA8 -> context.convertIa8() 48 + N64ImageFormat.IA16 -> context.convertIa16() 49 + N64ImageFormat.I4 -> context.convertI4() 50 + N64ImageFormat.I8 -> context.convertI8() 51 + N64ImageFormat.PARTY -> context.convertParty() 52 + N64ImageFormat.BG -> context.convertBg() 53 + } 54 + 55 + reader.end() 56 + return N64ImageConversionResult(outBytes, width, height) 57 + } 58 + 59 + private class ConversionContext( 60 + val infile: File, 61 + val reader: PngReader, 62 + val imgInfo: ImageInfo, 63 + val flipY: Boolean 64 + ) { 65 + private var warned = false 66 + 67 + fun warn(msg: String) { 68 + if (!warned) { 69 + warned = true 70 + Logger.logWarning("${infile.name}: $msg") 71 + } 72 + } 73 + 74 + fun packColor(r: Int, g: Int, b: Int, a: Int): Int { 75 + val r5 = r shr 3 76 + val g5 = g shr 3 77 + val b5 = b shr 3 78 + val a1 = a shr 7 79 + return (r5 shl 11) or (g5 shl 6) or (b5 shl 1) or a1 80 + } 81 + 82 + fun rgbToIntensity(r: Int, g: Int, b: Int): Int { 83 + return round(r * 0.2126 + g * 0.7152 + b * 0.0722).toInt() 84 + } 85 + 86 + fun convertRgba32(): ByteArray { 87 + val buffer = ByteBuffer.allocate(imgInfo.cols * imgInfo.rows * 4) 88 + val rows = readAllRows() 89 + 90 + for (row in if (flipY) rows.reversed() else rows) { 91 + buffer.put(row) 92 + } 93 + 94 + return buffer.array() 95 + } 96 + 97 + fun convertRgba16(): ByteArray { 98 + val buffer = ByteBuffer.allocate(imgInfo.cols * imgInfo.rows * 2) 99 + buffer.order(ByteOrder.BIG_ENDIAN) 100 + val rows = readAllRows() 101 + 102 + for (row in if (flipY) rows.reversed() else rows) { 103 + for (i in 0 until row.size step 4) { 104 + val r = row[i].toInt() and 0xFF 105 + val g = row[i + 1].toInt() and 0xFF 106 + val b = row[i + 2].toInt() and 0xFF 107 + val a = row[i + 3].toInt() and 0xFF 108 + 109 + if (a !in listOf(0, 0xFF)) { 110 + warn("alpha mask mode but translucent pixels used") 111 + } 112 + 113 + val color = packColor(r, g, b, a) 114 + buffer.putShort(color.toShort()) 115 + } 116 + } 117 + 118 + return buffer.array() 119 + } 120 + 121 + fun convertCi8(): ByteArray { 122 + require(imgInfo.indexed) { "ci8 mode requires indexed PNG" } 123 + val buffer = ByteBuffer.allocate(imgInfo.cols * imgInfo.rows) 124 + val rows = readAllRowsIndexed() 125 + 126 + for (row in if (flipY) rows.reversed() else rows) { 127 + buffer.put(row) 128 + } 129 + 130 + return buffer.array() 131 + } 132 + 133 + fun convertCi4(): ByteArray { 134 + require(imgInfo.indexed) { "ci4 mode requires indexed PNG" } 135 + val buffer = ByteBuffer.allocate(imgInfo.cols * imgInfo.rows / 2) 136 + val rows = readAllRowsIndexed() 137 + 138 + for (row in if (flipY) rows.reversed() else rows) { 139 + for (i in 0 until row.size step 2) { 140 + val a = row[i].toInt() and 0xFF 141 + val b = row[i + 1].toInt() and 0xFF 142 + val byte = ((a shl 4) or b) and 0xFF 143 + buffer.put(byte.toByte()) 144 + } 145 + } 146 + 147 + return buffer.array() 148 + } 149 + 150 + fun convertPalette(): ByteArray { 151 + require(imgInfo.indexed) { "palette mode requires indexed PNG" } 152 + val palette = reader.metadata.plte 153 + val trans = reader.metadata.trns 154 + 155 + val buffer = ByteBuffer.allocate(palette.nentries * 2) 156 + buffer.order(ByteOrder.BIG_ENDIAN) 157 + 158 + for (i in 0 until palette.nentries) { 159 + val entry = palette.getEntry(i) 160 + val r = (entry shr 16) and 0xFF 161 + val g = (entry shr 8) and 0xFF 162 + val b = entry and 0xFF 163 + val a = if (trans != null && i < trans.palletteAlpha.size) trans.palletteAlpha[i] else 255 164 + 165 + if (a !in listOf(0, 255)) { 166 + warn("alpha mask mode but translucent pixels used") 167 + } 168 + 169 + val color = packColor(r, g, b, a) 170 + buffer.putShort(color.toShort()) 171 + } 172 + 173 + return buffer.array() 174 + } 175 + 176 + fun convertIa4(): ByteArray { 177 + val buffer = ByteBuffer.allocate(imgInfo.cols * imgInfo.rows / 2) 178 + val rows = readAllRows() 179 + 180 + for (row in if (flipY) rows.reversed() else rows) { 181 + var i = 0 182 + while (i < row.size) { 183 + val r1 = row[i].toInt() and 0xFF 184 + val g1 = row[i + 1].toInt() and 0xFF 185 + val b1 = row[i + 2].toInt() and 0xFF 186 + val a1 = row[i + 3].toInt() and 0xFF 187 + 188 + val r2 = row[i + 4].toInt() and 0xFF 189 + val g2 = row[i + 5].toInt() and 0xFF 190 + val b2 = row[i + 6].toInt() and 0xFF 191 + val a2 = row[i + 7].toInt() and 0xFF 192 + 193 + val i1 = rgbToIntensity(r1, g1, b1) shr 5 194 + val i2 = rgbToIntensity(r2, g2, b2) shr 5 195 + 196 + if (a1 !in listOf(0, 0xFF) || a2 !in listOf(0, 0xFF)) { 197 + warn("alpha mask mode but translucent pixels used") 198 + } 199 + if (r1 != g1 || g1 != b1) warn("grayscale mode but image is not") 200 + if (r2 != g2 || g2 != b2) warn("grayscale mode but image is not") 201 + 202 + val a1bit = if (a1 > 128) 1 else 0 203 + val a2bit = if (a2 > 128) 1 else 0 204 + 205 + val h = (i1 shl 1) or a1bit 206 + val l = (i2 shl 1) or a2bit 207 + val byte = ((h shl 4) or l) and 0xFF 208 + buffer.put(byte.toByte()) 209 + 210 + i += 8 211 + } 212 + } 213 + 214 + return buffer.array() 215 + } 216 + 217 + fun convertIa8(): ByteArray { 218 + val buffer = ByteBuffer.allocate(imgInfo.cols * imgInfo.rows) 219 + val rows = readAllRows() 220 + 221 + for (row in if (flipY) rows.reversed() else rows) { 222 + for (i in 0 until row.size step 4) { 223 + val r = row[i].toInt() and 0xFF 224 + val g = row[i + 1].toInt() and 0xFF 225 + val b = row[i + 2].toInt() and 0xFF 226 + val a = row[i + 3].toInt() and 0xFF 227 + 228 + val intensity = floor(15.0 * (rgbToIntensity(r, g, b) / 255.0)).toInt() 229 + val alpha = floor(15.0 * (a / 255.0)).toInt() 230 + 231 + if (r != g || g != b) warn("grayscale mode but image is not") 232 + 233 + val byte = ((intensity shl 4) or alpha) and 0xFF 234 + buffer.put(byte.toByte()) 235 + } 236 + } 237 + 238 + return buffer.array() 239 + } 240 + 241 + fun convertIa16(): ByteArray { 242 + val buffer = ByteBuffer.allocate(imgInfo.cols * imgInfo.rows * 2) 243 + val rows = readAllRows() 244 + 245 + for (row in if (flipY) rows.reversed() else rows) { 246 + for (i in 0 until row.size step 4) { 247 + val r = row[i].toInt() and 0xFF 248 + val g = row[i + 1].toInt() and 0xFF 249 + val b = row[i + 2].toInt() and 0xFF 250 + val a = row[i + 3].toInt() and 0xFF 251 + 252 + if (r != g || g != b) warn("grayscale mode but image is not") 253 + 254 + buffer.put(rgbToIntensity(r, g, b).toByte()) 255 + buffer.put(a.toByte()) 256 + } 257 + } 258 + 259 + return buffer.array() 260 + } 261 + 262 + fun convertI4(): ByteArray { 263 + val buffer = ByteBuffer.allocate(imgInfo.cols * imgInfo.rows / 2) 264 + val rows = readAllRows() 265 + 266 + for (row in if (flipY) rows.reversed() else rows) { 267 + var i = 0 268 + while (i < row.size) { 269 + val r1 = row[i].toInt() and 0xFF 270 + val g1 = row[i + 1].toInt() and 0xFF 271 + val b1 = row[i + 2].toInt() and 0xFF 272 + val a1 = row[i + 3].toInt() and 0xFF 273 + 274 + val r2 = row[i + 4].toInt() and 0xFF 275 + val g2 = row[i + 5].toInt() and 0xFF 276 + val b2 = row[i + 6].toInt() and 0xFF 277 + val a2 = row[i + 7].toInt() and 0xFF 278 + 279 + if (a1 != 0xFF || a2 != 0xFF) warn("discarding alpha channel") 280 + if (r1 != g1 || g1 != b1) warn("grayscale mode but image is not") 281 + if (r2 != g2 || g2 != b2) warn("grayscale mode but image is not") 282 + 283 + val i1 = floor(15.0 * (rgbToIntensity(r1, g1, b1) / 255.0)).toInt() 284 + val i2 = floor(15.0 * (rgbToIntensity(r2, g2, b2) / 255.0)).toInt() 285 + 286 + val byte = ((i1 shl 4) or i2) and 0xFF 287 + buffer.put(byte.toByte()) 288 + 289 + i += 8 290 + } 291 + } 292 + 293 + return buffer.array() 294 + } 295 + 296 + fun convertI8(): ByteArray { 297 + val buffer = ByteBuffer.allocate(imgInfo.cols * imgInfo.rows) 298 + val rows = readAllRows() 299 + 300 + for (row in if (flipY) rows.reversed() else rows) { 301 + for (i in 0 until row.size step 4) { 302 + val r = row[i].toInt() and 0xFF 303 + val g = row[i + 1].toInt() and 0xFF 304 + val b = row[i + 2].toInt() and 0xFF 305 + val a = row[i + 3].toInt() and 0xFF 306 + 307 + if (a != 0xFF) warn("discarding alpha channel") 308 + if (r != g || g != b) warn("grayscale mode but image is not") 309 + 310 + buffer.put(rgbToIntensity(r, g, b).toByte()) 311 + } 312 + } 313 + 314 + return buffer.array() 315 + } 316 + 317 + fun convertParty(): ByteArray { 318 + require(imgInfo.indexed) { "party mode requires indexed PNG" } 319 + val palette = reader.metadata.plte 320 + val trans = reader.metadata.trns 321 + 322 + val paletteSize = palette.nentries * 2 323 + val imageSize = imgInfo.cols * imgInfo.rows 324 + val buffer = ByteBuffer.allocate(paletteSize + imageSize + 10) 325 + buffer.order(ByteOrder.BIG_ENDIAN) 326 + 327 + // Write palette 328 + for (i in 0 until palette.nentries) { 329 + val entry = palette.getEntry(i) 330 + val r = (entry shr 16) and 0xFF 331 + val g = (entry shr 8) and 0xFF 332 + val b = entry and 0xFF 333 + val a = if (trans != null && i < trans.palletteAlpha.size) trans.palletteAlpha[i] else 255 334 + 335 + if (a !in listOf(0, 255)) { 336 + warn("alpha mask mode but translucent pixels used") 337 + } 338 + 339 + val color = packColor(r, g, b, a) 340 + buffer.putShort(color.toShort()) 341 + } 342 + 343 + // Write ci8 data 344 + val rows = readAllRowsIndexed() 345 + for (row in if (flipY) rows.reversed() else rows) { 346 + buffer.put(row) 347 + } 348 + 349 + // Write padding 350 + buffer.put(ByteArray(10)) 351 + 352 + return buffer.array() 353 + } 354 + 355 + fun convertBg(): ByteArray { 356 + require(imgInfo.indexed) { "bg mode requires indexed PNG" } 357 + 358 + // Read main palette 359 + data class PaletteData( 360 + val plte: ar.com.hjg.pngj.chunks.PngChunkPLTE, 361 + val trns: ar.com.hjg.pngj.chunks.PngChunkTRNS? 362 + ) 363 + 364 + val palettes = mutableListOf<PaletteData>() 365 + val mainPalette = reader.metadata.plte 366 + val mainTrans = reader.metadata.trns 367 + palettes.add(PaletteData(mainPalette, mainTrans)) 368 + 369 + // Read variant palettes (e.g., "background.1.png", "background.2.png") 370 + val baseName = infile.nameWithoutExtension 371 + val parentDir = infile.parentFile 372 + var variantIndex = 1 373 + while (true) { 374 + val variantFile = File(parentDir, "$baseName.$variantIndex.png") 375 + if (!variantFile.exists()) break 376 + 377 + val variantReader = PngReader(variantFile) 378 + val variantPalette = variantReader.metadata.plte 379 + val variantTrans = variantReader.metadata.trns 380 + palettes.add(PaletteData(variantPalette, variantTrans)) 381 + variantReader.end() 382 + variantIndex++ 383 + } 384 + 385 + val baseAddr = 0x80200000 // gBackgroundImage 386 + val headersLen = 0x10 * palettes.size 387 + val palettesLen = 0x200 * palettes.size 388 + val imageSize = imgInfo.cols * imgInfo.rows 389 + 390 + val buffer = ByteBuffer.allocate(headersLen + palettesLen + imageSize) 391 + buffer.order(ByteOrder.BIG_ENDIAN) 392 + 393 + // Write headers (struct BackgroundHeader) 394 + for (i in palettes.indices) { 395 + buffer.putInt((baseAddr + palettesLen + headersLen).toInt()) // raster offset 396 + buffer.putInt((baseAddr + headersLen + 0x200 * i).toInt()) // palette offset 397 + buffer.putShort(12) // startX 398 + buffer.putShort(20) // startY 399 + buffer.putShort(imgInfo.cols.toShort()) // width 400 + buffer.putShort(imgInfo.rows.toShort()) // height 401 + } 402 + 403 + // Write palettes 404 + for (paletteData in palettes) { 405 + val palette = paletteData.plte 406 + val trans = paletteData.trns 407 + 408 + for (i in 0 until 256) { 409 + if (i < palette.nentries) { 410 + val entry = palette.getEntry(i) 411 + val r = (entry shr 16) and 0xFF 412 + val g = (entry shr 8) and 0xFF 413 + val b = entry and 0xFF 414 + val a = if (trans != null && i < trans.palletteAlpha.size) trans.palletteAlpha[i] else 255 415 + 416 + if (a !in listOf(0, 255)) { 417 + warn("alpha mask mode but translucent pixels used") 418 + } 419 + 420 + val color = packColor(r, g, b, a) 421 + buffer.putShort(color.toShort()) 422 + } else { 423 + buffer.putShort(0) 424 + } 425 + } 426 + } 427 + 428 + // Write ci8 data 429 + val rows = readAllRowsIndexed() 430 + for (row in if (flipY) rows.reversed() else rows) { 431 + buffer.put(row) 432 + } 433 + 434 + return buffer.array() 435 + } 436 + 437 + private fun readAllRows(): List<ByteArray> { 438 + val rows = mutableListOf<ByteArray>() 439 + for (row in 0 until imgInfo.rows) { 440 + val line = reader.readRow() as ImageLineInt 441 + val rgba = ByteArray(imgInfo.cols * 4) 442 + val scanline = line.scanline 443 + 444 + for (col in 0 until imgInfo.cols) { 445 + val idx = col * 4 446 + val srcIdx = col * imgInfo.channels 447 + 448 + rgba[idx] = scanline[srcIdx].toByte() // R 449 + rgba[idx + 1] = scanline[srcIdx + 1].toByte() // G 450 + rgba[idx + 2] = scanline[srcIdx + 2].toByte() // B 451 + rgba[idx + 3] = if (imgInfo.alpha) scanline[srcIdx + 3].toByte() else 0xFF.toByte() // A 452 + } 453 + rows.add(rgba) 454 + } 455 + return rows 456 + } 457 + 458 + private fun readAllRowsIndexed(): List<ByteArray> { 459 + val rows = mutableListOf<ByteArray>() 460 + for (row in 0 until imgInfo.rows) { 461 + val line = reader.readRow() as ImageLineInt 462 + val indices = ByteArray(imgInfo.cols) 463 + val scanline = line.scanline 464 + for (i in 0 until imgInfo.cols) { 465 + indices[i] = scanline[i].toByte() 466 + } 467 + rows.add(indices) 468 + } 469 + return rows 470 + } 471 + }