345 lines
11 KiB
C++
345 lines
11 KiB
C++
/*
|
|
** Command & Conquer Renegade(tm)
|
|
** Copyright 2025 Electronic Arts Inc.
|
|
**
|
|
** 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.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/***********************************************************************************************
|
|
*** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S ***
|
|
***********************************************************************************************
|
|
* *
|
|
* Project Name : LevelEdit *
|
|
* *
|
|
* $Archive:: /Commando/Code/Tools/LevelEdit/editormixfile.cpp $*
|
|
* *
|
|
* Author:: Patrick Smith *
|
|
* *
|
|
* $Modtime:: 9/12/01 12:38p $*
|
|
* *
|
|
* $Revision:: 7 $*
|
|
* *
|
|
*---------------------------------------------------------------------------------------------*
|
|
* Functions: *
|
|
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
|
|
|
#include "stdafx.h"
|
|
#include "filemgr.h"
|
|
#include "editormixfile.h"
|
|
#include "utils.h"
|
|
#include "mixfile.h"
|
|
#include "TGAToDXT.h"
|
|
#include "LevelEdit.h"
|
|
#include "RegKeys.h"
|
|
#include <stdlib.h>
|
|
#include <winbase.h>
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// EditorMixFileCreator
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////
|
|
EditorMixFileCreator::EditorMixFileCreator()
|
|
{
|
|
// OPTIMIZATION: Store state of texture compression for quick access.
|
|
TexturesCompressed = Are_Textures_Compressed();
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Add_File
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////
|
|
void
|
|
EditorMixFileCreator::Add_File (const char *full_path, const char *name)
|
|
{
|
|
char substitutefullpath [MAX_PATH];
|
|
char substitutename [MAX_PATH];
|
|
|
|
// See if another file needs to be substituted for the input file.
|
|
Substitute_File (full_path, name, substitutefullpath, substitutename);
|
|
|
|
StringClass lower_name = substitutename;
|
|
::strlwr (lower_name.Peek_Buffer ());
|
|
|
|
//
|
|
// Is this file already in the system?
|
|
//
|
|
EditorMixFileEntry *stored_path = FilenameHash.Get (lower_name);
|
|
if (stored_path != NULL) {
|
|
|
|
//
|
|
// Don't store the new file unless its newer then the one already
|
|
// in the hash
|
|
//
|
|
if (::Quick_Compare_Files (stored_path->Get_Path (), substitutefullpath) > 0) {
|
|
|
|
//
|
|
// Remap this entry to the new path
|
|
//
|
|
stored_path->Set_Path (substitutefullpath);
|
|
}
|
|
|
|
} else {
|
|
|
|
//
|
|
// Allocate a new entry...
|
|
//
|
|
stored_path = new EditorMixFileEntry;
|
|
stored_path->Set_Name (lower_name);
|
|
stored_path->Set_Path (substitutefullpath);
|
|
|
|
//
|
|
// Now add this entry to the hash
|
|
//
|
|
FilenameHash.Insert (lower_name, stored_path);
|
|
}
|
|
|
|
return ;
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Substitute_File
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////
|
|
void EditorMixFileCreator::Substitute_File (const char *fullpath, const char *name, char *substitutefullpath, char *substitutename)
|
|
{
|
|
bool substitutefile;
|
|
char filename [_MAX_FNAME];
|
|
char extension [_MAX_EXT];
|
|
CString cachedfullpath;
|
|
CString cachedname;
|
|
|
|
// Assume that another file will not be substituted for the original file.
|
|
substitutefile = false;
|
|
|
|
// Has compression been enabled by user?
|
|
if (TexturesCompressed) {
|
|
|
|
// Is this file a .TGA? If so use a compressed (.DDS) version stored in the texture cache.
|
|
_splitpath (fullpath, NULL, NULL, filename, extension);
|
|
if (_stricmp (extension, ".tga") == 0) {
|
|
|
|
const char *exclusionstring0 = "bump_";
|
|
const char *exclusionstring1 = "font";
|
|
|
|
// Does this filename not start with one of the exclusion strings?
|
|
// NOTE 0: bump_*.* files are bump maps and cannot be compressed because the pixel data is interpreted as bump data.
|
|
// NOTE 1: font*.* files are font files that are not processed by the run-time compressed texture loading system and, therefore, must be excluded.
|
|
if ((_strnicmp (filename, exclusionstring0, strlen (exclusionstring0)) != 0) &&
|
|
(_strnicmp (filename, exclusionstring1, strlen (exclusionstring1)) != 0)) {
|
|
|
|
const char *ddsextension = ".dds";
|
|
const char *failedtoopentext = "Failed to open %s\r\n";
|
|
|
|
char mangleddirectory [_MAX_DIR];
|
|
char mangledname [_MAX_FNAME];
|
|
HANDLE tgafile;
|
|
HANDLE ddsfile;
|
|
char *s;
|
|
|
|
// OPTIMIZATION: Has the .DDS file already been created and stored in the texture cache?
|
|
|
|
// NOTE: If the name contains a subdirectory mangle it with the filename. This will ensure that the filename is unique.
|
|
_splitpath (name, NULL, mangleddirectory, mangledname, NULL);
|
|
|
|
// NOTE: The cached name must be the same as the original name but with .DDS extension.
|
|
cachedname = mangleddirectory;
|
|
cachedname += mangledname;
|
|
cachedname += ddsextension;
|
|
|
|
// Replace backslashes in the mangled directory with dots to 'flatten' it.
|
|
s = mangleddirectory;
|
|
while (*s != '\0') {
|
|
if ((*s == '\\') || (*s == '/')) {
|
|
*s = '.';
|
|
}
|
|
s++;
|
|
}
|
|
|
|
cachedfullpath = ::Get_File_Mgr()->Get_Texture_Cache_Path();
|
|
cachedfullpath += "\\";
|
|
cachedfullpath += mangleddirectory;
|
|
cachedfullpath += filename;
|
|
cachedfullpath += ddsextension;
|
|
|
|
// Does the cached .DDS file exist?
|
|
tgafile = CreateFile (fullpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0L, NULL);
|
|
ddsfile = CreateFile (cachedfullpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0L, NULL);
|
|
if (tgafile != INVALID_HANDLE_VALUE) {
|
|
|
|
FILETIME tgawritetime;
|
|
|
|
GetFileTime (tgafile, NULL, NULL, &tgawritetime);
|
|
if (ddsfile != INVALID_HANDLE_VALUE) {
|
|
|
|
FILETIME ddswritetime;
|
|
|
|
// Does the cached .DDS file have the same time stamp as the .TGA file?
|
|
GetFileTime (ddsfile, NULL, NULL, &ddswritetime);
|
|
CloseHandle (tgafile);
|
|
CloseHandle (ddsfile);
|
|
if ((tgawritetime.dwLowDateTime == ddswritetime.dwLowDateTime) && (tgawritetime.dwHighDateTime == ddswritetime.dwHighDateTime)) {
|
|
|
|
// Files have the same time stamp. Substitute the .DDS file.
|
|
substitutefile = true;
|
|
|
|
} else {
|
|
|
|
// Files have a different time stamp (and therefore it is assumed that the .TGA file has been modified since the .DDS file was created).
|
|
// Create a new .DDS file from the .TGA file and give it the same time stamp as the .TGA file.
|
|
substitutefile = Convert_File (fullpath, cachedfullpath, &tgawritetime);
|
|
}
|
|
|
|
} else {
|
|
|
|
CloseHandle (tgafile);
|
|
|
|
// Create a new .DDS file from the .TGA file and give it the same time stamp as the .TGA file.
|
|
substitutefile = Convert_File (fullpath, cachedfullpath, &tgawritetime);
|
|
}
|
|
|
|
} else {
|
|
|
|
CString message;
|
|
|
|
// Cannot open the input file!
|
|
message.Format ("Failed to open: %s\r\n", fullpath);
|
|
::Output_Message (message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (substitutefile) {
|
|
|
|
// Substitute the cached file.
|
|
strcpy (substitutefullpath, cachedfullpath);
|
|
strcpy (substitutename, cachedname);
|
|
|
|
} else {
|
|
|
|
// Refer the caller back to the original input file ie. no file is substituted.
|
|
strcpy (substitutefullpath, fullpath);
|
|
strcpy (substitutename, name);
|
|
}
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Generate_Mix_File
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////
|
|
bool EditorMixFileCreator::Convert_File (const char *fullpath, const char *cachedfullpath, FILETIME *tgawritetimeptr)
|
|
{
|
|
const char *failedtocompresstext = "Failed to compress %s\r\n";
|
|
const char *removedalphatext = "Removed alpha channel in %s\r\n";
|
|
|
|
bool success, alpharemoved;
|
|
CString message;
|
|
|
|
success = _TGAToDXTConverter.Convert (fullpath, cachedfullpath, tgawritetimeptr, alpharemoved);
|
|
if (!success) {
|
|
message.Format (failedtocompresstext, fullpath);
|
|
::Output_Message (message);
|
|
}
|
|
if (alpharemoved) {
|
|
message.Format (removedalphatext, fullpath);
|
|
::Output_Message (message);
|
|
}
|
|
|
|
return (success);
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Generate_Mix_File
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////
|
|
void
|
|
EditorMixFileCreator::Generate_Mix_File (const char *full_path)
|
|
{
|
|
MixFileCreator mix_file (full_path);
|
|
|
|
//
|
|
// Loop over all the entries in hash table
|
|
//
|
|
HashTemplateIterator<StringClass, EditorMixFileEntry *> iterator (FilenameHash);
|
|
for (iterator.First (); iterator.Is_Done () == false; iterator.Next ()) {
|
|
EditorMixFileEntry *entry = iterator.Peek_Value ();
|
|
|
|
//
|
|
// Add this file to the mix
|
|
//
|
|
mix_file.Add_File (entry->Get_Path (), entry->Get_Name ());
|
|
}
|
|
|
|
return ;
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Flush
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////
|
|
void
|
|
EditorMixFileCreator::Flush (void)
|
|
{
|
|
//
|
|
// Loop over all the entries in hash table
|
|
//
|
|
HashTemplateIterator<StringClass, EditorMixFileEntry *> iterator (FilenameHash);
|
|
for (iterator.First (); iterator.Is_Done () == false; iterator.Next ()) {
|
|
|
|
//
|
|
// Free this entry
|
|
//
|
|
EditorMixFileEntry *entry = iterator.Peek_Value ();
|
|
delete entry;
|
|
}
|
|
|
|
FilenameHash.Remove_All ();
|
|
return ;
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Set_Texture_Compression
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////
|
|
void EditorMixFileCreator::Set_Texture_Compression (bool onoff)
|
|
{
|
|
// Update the registry.
|
|
theApp.WriteProfileInt (CONFIG_KEY, TEXTURE_COMPRESSION_VALUE, onoff);
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Are_Textures_Compressed
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////
|
|
bool EditorMixFileCreator::Are_Textures_Compressed()
|
|
{
|
|
// Read from the registry.
|
|
return (theApp.GetProfileInt (CONFIG_KEY, TEXTURE_COMPRESSION_VALUE, 0) == 1);
|
|
}
|