/*** * * Copyright (c) 1996-2002, Valve LLC. All rights reserved. * * This product contains software technology licensed from Id * Software, Inc. ("Id Technology"). Id Technology (c) 1996 Id Software, Inc. * All Rights Reserved. * ****/ // // spritegen.c: generates a .spr file from a series of .lbm frame files. // Result is stored in /raid/quake/id1/sprites/.spr. // #include #include "cmdlib.h" #include "mathlib.h" #include "stringlib.h" #include "scriplib.h" #include "imagelib.h" #include "sprite.h" #include "filesystem.h" #define MIN_INTERVAL 0.001f #define MAX_INTERVAL 64.0f #define MAX_BUFFER_SIZE (10 * 1024 * 1024) // 10 Mb for now #define MAX_FRAMES 512 dsprite_t sprite; rgbdata_t *frame = NULL; byte *lumpbuffer = NULL, *plump; static byte basepalette[256*3]; char spritedir[1024]; char spriteoutname[1024]; int framesmaxs[2]; int framecount; float g_gamma = 1.8f; float frameinterval; int origin_x; int origin_y; bool need_resample; bool ignore_resample; int resample_w; int resample_h; typedef struct { frametype_t type; // single frame or group of frames void *pdata; // either a dspriteframe_t or group info float interval; // only used for frames in groups int numgroupframes; // only used by group headers } spritepackage_t; spritepackage_t frames[MAX_FRAMES]; int verify_atoi( const char *token ) { if( token[0] != '-' && ( token[0] < '0' || token[0] > '9' )) { TokenError( "expecting number, got \"%s\"\n", token ); } return atoi( token ); } float verify_atof( const char *token ) { if( token[0] != '-' && token[0] != '.' && ( token[0] < '0' || token[0] > '9' )) { TokenError( "expecting number, got \"%s\"\n", token ); } return atof( token ); } /* ============ WriteFrame ============ */ void WriteFrame( vfile_t *file, int framenum ) { dspriteframe_t *pframe; dspriteframe_t frametemp; pframe = (dspriteframe_t *)frames[framenum].pdata; frametemp.origin[0] = pframe->origin[0]; frametemp.origin[1] = pframe->origin[1]; frametemp.width = pframe->width; frametemp.height = pframe->height; VFS_Write( file, &frametemp, sizeof( frametemp )); VFS_Write( file, (byte *)(pframe + 1), pframe->height * pframe->width ); } /* ============ WriteSprite ============ */ void WriteSprite( vfile_t *file ) { int i, groupframe, curframe; dsprite_t spritetemp; short cnt = 256; sprite.boundingradius = sqrt((( framesmaxs[0] >> 1 ) * ( framesmaxs[0] >> 1 )) + (( framesmaxs[1] >> 1 ) * ( framesmaxs[1] >> 1 ))); // write out the sprite header spritetemp.type = sprite.type; spritetemp.texFormat = sprite.texFormat; spritetemp.boundingradius = sprite.boundingradius; spritetemp.bounds[0] = framesmaxs[0]; spritetemp.bounds[1] = framesmaxs[1]; spritetemp.numframes = sprite.numframes; spritetemp.beamlength = sprite.beamlength; spritetemp.synctype = sprite.synctype; spritetemp.version = SPRITE_VERSION; spritetemp.ident = IDSPRITEHEADER; VFS_Write( file, &spritetemp, sizeof( spritetemp )); // Write out palette in 16bit mode VFS_Write( file, (void *)&cnt, sizeof( cnt )); VFS_Write( file, basepalette, sizeof( basepalette )); // write out the frames curframe = 0; for( i = 0; i < sprite.numframes; i++ ) { VFS_Write( file, &frames[curframe].type, sizeof( frames[curframe].type )); if( frames[curframe].type == FRAME_SINGLE ) { // single (non-grouped) frame WriteFrame( file, curframe ); curframe++; } else { int j, numframes; float totinterval; dspritegroup_t dsgroup; groupframe = curframe; curframe++; numframes = frames[groupframe].numgroupframes; // set and write the group header dsgroup.numframes = numframes; VFS_Write( file, &dsgroup, sizeof( dsgroup )); // write the interval array totinterval = 0.0f; for( j = 0; j < numframes; j++ ) { dspriteinterval_t temp; totinterval += frames[groupframe+1+j].interval; temp.interval = totinterval; VFS_Write( file, &temp, sizeof( temp )); } for( j = 0; j < numframes; j++ ) { WriteFrame( file, curframe ); curframe++; } } } } /* ============== ResetSpriteInfo ============== */ void ResetSpriteInfo( void ) { // set default sprite parms spriteoutname[0] = 0; memset( &sprite, 0, sizeof( sprite )); memset( frames, 0, sizeof( frames )); memset( basepalette, 0, sizeof( basepalette )); framecount = origin_x = origin_y = 0; frameinterval = 0.0f; g_gamma = 1.8f; framesmaxs[0] = -9999; framesmaxs[1] = -9999; sprite.type = SPR_FWD_PARALLEL; sprite.synctype = ST_RAND; // default if( frame ) Mem_Free( frame ); frame = NULL; } /* ============== WriteSPRFile ============== */ void WriteSPRFile( void ) { int i, groups = 0, grpframes = 0; int sngframes = framecount; if( sprite.numframes == 0 ) COM_FatalError( "WriteSPRFile: no frames\n" ); if( !Q_strlen( spriteoutname )) COM_FatalError( "WriteSPRFile: didn't name sprite file\n" ); vfile_t *f = VFS_Create(); WriteSprite( f ); Msg( "writing: %s\n", spriteoutname ); COM_SaveFile( spriteoutname, VFS_GetBuffer( f ), VFS_Tell( f ), true ); VFS_Close( f ); // count frames for( i = 0; i < framecount; i++ ) { if( frames[i].numgroupframes ) { sngframes -= frames[i].numgroupframes; grpframes += frames[i].numgroupframes; groups++; } } // display info about current sprite if( groups ) { Msg( "%d group%s,", groups, groups > 1 ? "s":"" ); Msg( " contain %d frame%s\n", grpframes, grpframes > 1 ? "s":"" ); } if( sngframes - groups ) Msg( "%d ungrouped frame%s\n", sngframes - groups, (sngframes - groups) > 1 ? "s" : "" ); ResetSpriteInfo(); if(( plump - lumpbuffer ) > MAX_BUFFER_SIZE ) COM_FatalError( "WriteSPRFile: memory buffer is corrupted. Check the sprite!\n" ); } /* ============== Cmd_Spritename ============== */ void Cmd_Spritename( void ) { if( sprite.numframes ) WriteSPRFile(); GetToken( false ); Q_snprintf( spriteoutname, sizeof( spriteoutname ), "%s/%s", spritedir, token ); COM_DefaultExtension( spriteoutname, ".spr" ); if( !lumpbuffer ) lumpbuffer = (byte *)Mem_Alloc( MAX_BUFFER_SIZE ); else memset( lumpbuffer, 0, MAX_BUFFER_SIZE ); plump = lumpbuffer; } /* ============== LoadScreen ============== */ void LoadScreen( const char *filename, const char *displayname ) { if( frame != NULL ) Mem_Free( frame ); // release previous frame if( !COM_FileExists( filename )) COM_FatalError( "%s doesn't exist\n", filename ); MsgDev( D_INFO, "grabbing: %s\t\t%s\n", filename, displayname ); frame = COM_LoadImage( filename ); if( !frame ) COM_FatalError( "%s couldn't load", filename ); // ??? // get support for doom-angled sprites while( TryToken( )) { if( !Q_stricmp( token, "flip_diagonal" )) SetBits( frame->flags, IMAGE_ROT_90 ); else if( !Q_stricmp( token, "flip_y" )) SetBits( frame->flags, IMAGE_FLIP_Y ); else if( !Q_stricmp( token, "flip_x" )) SetBits( frame->flags, IMAGE_FLIP_X ); } int new_width = Q_min( frame->width, MIP_MAXWIDTH ); int new_height = Q_min( frame->height, MIP_MAXHEIGHT ); // TODO: merge all frames into single image, quantize it and then deconstruct back to single frames // TODO: only for truecolor images... frame = Image_Resample( frame, new_width, new_height ); frame = Image_Quantize( frame ); // quantize if needs frame = Image_Flip( frame ); // rotate image if desired if( sprite.texFormat == SPR_ALPHTEST ) Image_MakeOneBitAlpha( frame ); // check alpha if( sprite.numframes == 0 ) memcpy( basepalette, frame->palette, sizeof( basepalette )); else if( memcmp( basepalette, frame->palette, sizeof( basepalette ))) MsgDev( D_WARN, "\"%s\" doesn't share a pallette with the previous bitmap\n", filename ); } /* =============== Cmd_Type syntax: "$type preset" =============== */ void Cmd_Type( void ) { GetToken( false ); if( !Q_stricmp( token, "vp_parallel_upright" )) sprite.type = SPR_FWD_PARALLEL_UPRIGHT; else if( !Q_stricmp( token, "facing_upright" )) sprite.type = SPR_FACING_UPRIGHT; else if( !Q_stricmp( token, "vp_parallel" )) sprite.type = SPR_FWD_PARALLEL; else if( !Q_stricmp( token, "oriented" )) sprite.type = SPR_ORIENTED; else if( !Q_stricmp( token, "vp_parallel_oriented" )) sprite.type = SPR_FWD_PARALLEL_ORIENTED; else COM_FatalError( "bad sprite type\n" ); } /* =============== Cmd_Texture syntax: "$texture preset" syntax: "$rendermode preset" =============== */ void Cmd_Texture( void ) { GetToken( false ); if( !Q_stricmp( token, "additive" )) sprite.texFormat = SPR_ADDITIVE; else if( !Q_stricmp( token, "normal" )) sprite.texFormat = SPR_NORMAL; else if( !Q_stricmp( token, "indexalpha" )) sprite.texFormat = SPR_INDEXALPHA; else if( !Q_stricmp( token, "alphatest" )) sprite.texFormat = SPR_ALPHTEST; else COM_FatalError( "bad sprite texture type\n" ); } /* =============== Cmd_Beamlength =============== */ void Cmd_Beamlength( void ) { GetToken( false ); sprite.beamlength = verify_atof( token ); } /* =============== Cmd_Framerate syntax: "$framerate value" =============== */ void Cmd_Framerate( void ) { GetToken( false ); float framerate = verify_atof( token ); if( framerate <= 0.0f ) COM_FatalError( "bad framerate %g\n", framerate ); frameinterval = bound( MIN_INTERVAL, (1.0f / framerate), MAX_INTERVAL ); } /* =============== Cmd_Resample syntax: "$resample " =============== */ void Cmd_Resample( void ) { GetToken( false ); resample_w = verify_atoi( token ); GetToken( false ); resample_h = verify_atoi( token ); if( !ignore_resample ) need_resample = true; } /* =============== Cmd_NoResample syntax: "$noresample" =============== */ void Cmd_NoResample( void ) { ignore_resample = true; } /* =============== Cmd_Load syntax "$load fire01.bmp" =============== */ void Cmd_Load( const char *displayname ) { char path[1024]; GetToken( false ); Q_snprintf( path, sizeof( path ), "%s/%s", spritedir, token ); LoadScreen( path, displayname );//COM_ExpandArg( token )); } /* =============== Cmd_Origin syntax: $origin "x_pos y_pos" =============== */ static void Cmd_Origin( void ) { GetToken( false ); origin_x = verify_atoi( token ); GetToken( false ); origin_y = verify_atoi( token ); } /* =============== Cmd_Frame syntax "$frame xoffset yoffset width height " =============== */ void Cmd_Frame( void ) { int x, y, xl, yl, xh, yh, w, h; byte *screen_p, *source; bool resampled = false; int org_x, org_y; int linedelta; dspriteframe_t *pframe; int pix; if( !frame || !frame->buffer ) COM_FatalError( "frame not loaded\n" ); if( framecount >= MAX_FRAMES ) COM_FatalError( "too many frames in package\n" ); GetToken( false ); xl = verify_atoi( token ); GetToken( false ); yl = verify_atoi( token ); GetToken( false ); w = verify_atoi( token ); GetToken( false ); h = verify_atoi( token ); // merge bounds if( xl <= 0 || xl > frame->width ) xl = 0; if( yl <= 0 || yl > frame->width ) yl = 0; if( w <= 0 || w > frame->width ) w = frame->width; if( h <= 0 || h > frame->height ) h = frame->height; if(( xl & 0x07 ) || ( yl & 0x07 ) || ( w & 0x07 ) || ( h & 0x07 )) { if( need_resample ) { rgbdata_t *dst = Image_Resample( frame, resample_w, resample_h ); if( frame != dst ) resampled = true; frame = dst; } if( !resampled ) MsgDev( D_WARN, "frame dimensions not multiples of 8\n" ); } if(( w > MIP_MAXWIDTH ) || ( h > MIP_MAXHEIGHT )) COM_FatalError( "sprite has a dimension longer than %dx%d\n", MIP_MAXWIDTH, MIP_MAXHEIGHT ); // get interval if( TokenAvailable( )) { GetToken( false ); frames[framecount].interval = verify_atof( token ); if( frames[framecount].interval <= 0.0f ) MsgDev( D_WARN, "non-positive interval\n" ); frames[framecount].interval = bound( MIN_INTERVAL, frames[framecount].interval, MAX_INTERVAL ); } else if( frameinterval != 0.0f ) { frames[framecount].interval = frameinterval; } else { // use default interval frames[framecount].interval = (float)0.1f; } if( TokenAvailable( )) { GetToken (false); org_x = -verify_atoi( token ); GetToken( false ); org_y = verify_atoi( token ); } else if(( origin_x != 0 ) && ( origin_y != 0 )) { // write shared origin org_x = -origin_x; org_y = origin_y; } else { // use center of rectangle org_x = -(w >> 1); org_y = h >> 1; } // merge all sprite info if( need_resample && resampled ) { // check for org[n] == size[n] and org[n] == size[n]/2 // another cases will be not changed if( org_x == -w ) org_x = -frame->width; else if( org_x == -( w >> 1 )) org_x = -frame->width >> 1; if( org_y == h ) org_y = frame->height; else if( org_y == ( h >> 1 )) org_y = frame->height >> 1; w = frame->width; h = frame->height; } xh = xl + w; yh = yl + h; pframe = (dspriteframe_t *)plump; frames[framecount].pdata = pframe; frames[framecount].type = FRAME_SINGLE; pframe->origin[0] = org_x; pframe->origin[1] = org_y; pframe->width = w; pframe->height = h; if( w > framesmaxs[0] ) framesmaxs[0] = w; if( h > framesmaxs[1] ) framesmaxs[1] = h; plump = (byte *)(pframe + 1); screen_p = frame->buffer + yl * frame->width + xl; linedelta = frame->width - w; source = plump; for( y = yl; y < yh; y++ ) { for( x = xl; x < xh; x++ ) { pix = *screen_p; *screen_p++ = 0; // DEBUG // if( pix == 255 ) // pix = 0; *plump++ = pix; } screen_p += linedelta; } framecount++; } /* =============== Cmd_Group syntax: $group or $angled { $load fire01.bmp $frame xoffset yoffset width height $load fire02.bmp $frame xoffset yoffset width height " $load fire03.bmp $frame xoffset yoffset width height } =============== */ void Cmd_Group( bool angled ) { int groupframe; int depth = 0; groupframe = framecount++; frames[groupframe].type = angled ? FRAME_ANGLED : FRAME_GROUP; resample_w = resample_h = 0; // invalidate resample for group frames[groupframe].numgroupframes = 0; need_resample = false; while( 1 ) { GetToken( true ); if( endofscript ) { if( depth != 0 ) COM_FatalError( "missing }\n" ); break; } if( !Q_stricmp( token, "{" )) { depth++; } else if( !Q_stricmp( token, "}" )) { depth--; break; // end of group } else if( !Q_stricmp( token, "$framerate" )) { Cmd_Framerate(); } else if( !Q_stricmp( token, "$resample" )) { Cmd_Resample(); } else if( !Q_stricmp( token, "$frame" )) { Cmd_Frame(); frames[groupframe].numgroupframes++; } else if( !Q_stricmp( token, "$load" )) { Cmd_Load( angled ? "[^3angled frame^7]" : "[^2group frame^7]" ); } else { while( TryToken( )); // skip unknown commands } } if( depth != 0 ) COM_FatalError( "missing }\n" ); if( frames[groupframe].numgroupframes == 0 ) { // don't create blank groups, rewind frames MsgDev( D_WARN, "Cmd_Group: remove blank group\n" ); sprite.numframes--; framecount--; } else if( angled && frames[groupframe].numgroupframes != 8 ) { // don't create blank groups, rewind frames MsgDev(D_WARN, "Cmd_Group: remove angled group with invalid framecount\n" ); sprite.numframes--; framecount--; } // back to single frames, invalidate resample resample_w = resample_h = 0; need_resample = false; } /* =============== Cmd_GroupStart syntax: $groupstart $load fire01.bmp $frame xoffset yoffset width height $load fire02.bmp $frame xoffset yoffset width height " $load fire03.bmp $frame xoffset yoffset width height $groupend =============== */ void Cmd_GroupStart( void ) { int groupframe; groupframe = framecount++; resample_w = resample_h = 0; // invalidate resample for group frames[groupframe].type = FRAME_GROUP; frames[groupframe].numgroupframes = 0; need_resample = false; while( 1 ) { GetToken( true ); if( endofscript ) COM_FatalError( "end of file during group\n" ); if( !Q_stricmp( token, "$frame" )) { Cmd_Frame(); frames[groupframe].numgroupframes++; } else if( !Q_stricmp( token, "$load" )) { Cmd_Load( "[^2group frame^7]" ); } else if( !Q_stricmp( token, "$framerate" )) { Cmd_Framerate(); } else if( !Q_stricmp( token, "$resample" )) { Cmd_Resample(); } else if( !Q_stricmp( token, "$groupend" )) { break; } else { while( TryToken( )); // skip unknown commands } } if( frames[groupframe].numgroupframes == 0 ) { // don't create blank groups, rewind frames MsgDev( D_WARN, "remove blank group\n" ); sprite.numframes--; framecount--; } // back to single frames, invalidate resample resample_w = resample_h = 0; need_resample = false; } /* =============== ParseScript =============== */ void ParseScript( void ) { while( 1 ) { GetToken( true ); if( endofscript ) break; if( !Q_stricmp( token, "$load" )) { Cmd_Load ( "[^1frame^7]" ); } else if( !Q_stricmp( token, "$spritename" )) { Cmd_Spritename (); } else if( !Q_stricmp( token, "$type" )) { Cmd_Type (); } else if( !Q_stricmp( token, "$texture" )) { Cmd_Texture (); } else if( !Q_stricmp( token, "$beamlength" )) { Cmd_Beamlength (); } else if( !Q_stricmp( token, "$origin" )) { Cmd_Origin(); } else if( !Q_stricmp( token, "$sync" )) { sprite.synctype = ST_SYNC; } else if( !Q_stricmp( token, "$frame" )) { Cmd_Frame(); sprite.numframes++; } else if( !Q_stricmp( token, "$group" )) { Cmd_Group( false ); sprite.numframes++; } else if( !Q_stricmp( token, "$angled" )) { Cmd_Group( true ); sprite.numframes++; } else if( !Q_stricmp( token, "$groupstart" )) { Cmd_GroupStart(); sprite.numframes++; } else if( !Q_stricmp( token, "$noresample" )) { Cmd_NoResample(); } else if( !Q_stricmp( token, "$resample" )) { Cmd_Resample(); } else { while( TryToken( )); // skip unknown commands } } } /* ============== main ============== */ int main( int argc, char **argv ) { int i; char path[1024]; bool log_append = false; bool log_exist = false; atexit( Sys_CloseLog ); COM_InitCmdlib( argv, argc ); if( argc == 1 ) { Msg( " P2:Savior Sprite Model Compiler\n" ); Msg( " XashXT Group 2018(^1c^7)\n\n\n" ); Msg( "usage: spritegen file.qc\n" "\nlist options:\n" "^2-a^7 - append logfile\n" "^2-dev^7 - shows developer messages\n" "\n\t\tPress any key to exit" ); system( "pause>nul" ); return 1; } for( i = 1; i < argc - 1; i++ ) { if( argv[i][0] == '-' ) { if( !Q_stricmp( argv[i], "-dev" )) { SetDeveloperLevel( verify_atoi( argv[i+1] )); i++; continue; } } switch( argv[i][1] ) { case 'a': log_append = true; break; } } if( !argv[i] ) return 1; log_exist = COM_FileExists( "spritegen.log" ); if( log_append ) Sys_InitLogAppend( "spritegen.log" ); else Sys_InitLog( "spritegen.log" ); if( !log_exist || !log_append ) { Msg( " P2:Savior Sprite Model Compiler\n" ); Msg( " XashXT Group 2018(^1c^7)\n\n" ); } // load the script Q_strncpy( path, argv[i], sizeof( path )); COM_ExtractFilePath( path, spritedir ); COM_DefaultExtension( path, ".qc" ); Msg( "parse: %s\n", path ); LoadScriptFile( path ); // parse it ParseScript (); WriteSPRFile (); Mem_Free( lumpbuffer ); SetDeveloperLevel( D_REPORT ); Mem_Check(); // report leaks if( log_append ) Msg( "\n" ); return 0; }