/* cl_spray.c - spray conversion for GoldSrc protocol Copyright (C) 2025 Xash3D FWGS contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ #include "common.h" #include "client.h" #include "imagelib.h" #define SPRAY_MAX_SURFACE 12228 #define SPRAY_PALETTE_SIZE 256 #define SPRAY_PALETTE_BYTES ( SPRAY_PALETTE_SIZE * 3 ) #define SPRAY_ALPHA_THRESHOLD ( SPRAY_PALETTE_SIZE / 2 ) #define SPRAY_FILENAME "tempdecal.wad" // adjusts spray dimensions static void CL_AdjustSprayDimensions( int *width, int *height ) { float aspect = (float)( *width ) / (float)( *height ); for( int h = (( *height ) / 16 ) * 16; h >= 16; h -= 16 ) { int w = ((int)( h * aspect ) / 16 ) * 16; if( w < 16 || w > *width ) continue; if( w * h < SPRAY_MAX_SURFACE ) { *width = w; *height = h; return; } } // fallback: minimal size *width = 16; *height = 16; } // loads and prepares the image static rgbdata_t *CL_LoadAndPrepareImage( const char *filename, int *width, int *height ) { rgbdata_t *image = NULL; Image_SetForceFlags( IL_KEEP_8BIT ); image = FS_LoadImage( filename, NULL, 0 ); if( !image ) return NULL; *width = image->width; *height = image->height; CL_AdjustSprayDimensions( width, height ); if( *width != image->width || *height != image->height ) { const int bpp = PFDesc[image->type].bpp; const int palette_size = 256 * ( image->type == PF_INDEXED_32 ? 4 : 3 ); rgbdata_t *scaled; qboolean resampled; // resample image to fit spray size constraints byte *resampled_buf = Image_ResampleInternal( image->buffer, image->width, image->height, *width, *height, image->type, &resampled ); if( !resampled_buf ) { FS_FreeImage( image ); return NULL; } scaled = Mem_Malloc( host.imagepool, sizeof( *scaled )); *scaled = *image; scaled->width = *width; scaled->height = *height; scaled->size = *width * *height * bpp; scaled->buffer = Mem_Malloc( host.imagepool, scaled->size ); memcpy( scaled->buffer, resampled_buf, scaled->size ); if( image->palette ) { // copy 8-bit palette for resampled bmp scaled->palette = Mem_Malloc( host.imagepool, palette_size ); memcpy( scaled->palette, image->palette, palette_size ); } FS_FreeImage( image ); image = scaled; } return image; } // converts an image to WAD3 spray or miptex format qboolean CL_ConvertImageToWAD3( const char *filename ) { qboolean is_indexed_img; int width = 0, height = 0; byte palette[SPRAY_PALETTE_BYTES]; byte *indexed = NULL; rgbdata_t *image = NULL; rgbdata_t *quant = NULL; rgbdata_t temp_image = {0}; image = CL_LoadAndPrepareImage( filename, &width, &height ); if( !image ) return false; is_indexed_img = image->palette != NULL; if( is_indexed_img ) { // copy bmp palette from rgba to rgb for( int i = 0; i < 256; ++i ) { palette[i * 3 + 0] = image->palette[i * 4 + 0]; // R palette[i * 3 + 1] = image->palette[i * 4 + 1]; // G palette[i * 3 + 2] = image->palette[i * 4 + 2]; // B } indexed = image->buffer; } else { quant = FS_CopyImage( image ); Image_Quantize( quant ); // it's so weird, it writes result to same structure as used for input data if( !quant || !quant->buffer || !quant->palette ) goto cleanup; // set index 255 for transparent pixels in rgba images if( image->type == PF_RGBA_32 ) { for( int i = 0; i < width * height; ++i ) { if( image->buffer[i * 4 + 3] <= SPRAY_ALPHA_THRESHOLD ) quant->buffer[i] = 255; } } quant->palette[255 * 3 + 0] = 0; quant->palette[255 * 3 + 1] = 0; quant->palette[255 * 3 + 2] = 255; memcpy( palette, quant->palette, SPRAY_PALETTE_BYTES ); indexed = quant->buffer; } temp_image.width = width; temp_image.height = height; temp_image.type = PF_INDEXED_32; temp_image.buffer = indexed; temp_image.size = width * height; temp_image.palette = palette; if( is_indexed_img ) temp_image.flags |= IMAGE_GRADIENT_DECAL; return FS_SaveImage( SPRAY_FILENAME, &temp_image ); cleanup: if( image ) FS_FreeImage( image ); if( quant ) FS_FreeImage( quant ); return false; }