/* ** 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 . */ /*********************************************************************************************** *** 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/EditorSaveLoad.cpp $* * * * Author:: Patrick Smith * * * * $Modtime:: 3/05/02 3:30p $* * * * $Revision:: 37 $* * * *---------------------------------------------------------------------------------------------* * Functions: * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #include "stdafx.h" #include "leveleditdoc.h" #include "editorsaveload.h" #include "persist.h" #include "persistfactory.h" #include "preset.h" #include "utils.h" #include "definition.h" #include "editorchunkids.h" #include "physstaticsavesystem.h" #include "chunkio.h" #include "cameramgr.h" #include "filemgr.h" #include "matrix3d.h" #include "rawfile.h" #include "nodemgr.h" #include "sceneeditor.h" #include "node.h" #include "lightambientform.h" #include "soundscene.h" #include "backgroundmgr.h" #include "conversationmgr.h" #include "weathermgr.h" #include "combat.h" #include "mapmgr.h" #include "pathfind.h" #include "lightsolvesavesystem.h" #include "heightfieldmgr.h" /////////////////////////////////////////////////////////////////////// // Global singleton instance /////////////////////////////////////////////////////////////////////// EditorSaveLoadClass _TheEditorSaveLoadSubsystem; PathfindImportExportSaveLoadClass _ThePathfindImporterExporter; /////////////////////////////////////////////////////////////////////// // Static member initialization /////////////////////////////////////////////////////////////////////// bool EditorSaveLoadClass::m_LoadedValidVis = true; /////////////////////////////////////////////////////////////////////// // Constants /////////////////////////////////////////////////////////////////////// enum { CHUNKID_MICRO_CHUNKS = 0x10140738, CHUNKID_OBSOLETE, CHUNKID_PATHFIND_DATA }; enum { VARID_INCLUDE_FILE = 0x01, VARID_CAMERA_TM, VARID_OBSOLETE_0, VARID_BACK_MUSIC, VARID_AMBIENT_LIGHT, VARID_OBSOLETE_1, VARID_OBSOLETE_2, VARID_FAR_CLIP_PLANE_DOUBLE, VARID_FOG_COLOR, VARID_FOG_PLANES, VARID_FOG_ENABLED, VARID_FAR_CLIP_PLANE, VARID_RESTART_SCRIPT_NAME, VARID_RESPAWN_SCRIPT_NAME, }; enum { CHUNKID_LVL_DATA = 304021447, CHUNKID_LIGHT_SOLVE, }; /////////////////////////////////////////////////////////////////////// // // Chunk_ID // /////////////////////////////////////////////////////////////////////// uint32 EditorSaveLoadClass::Chunk_ID (void) const { return CHUNKID_EDITOR_SAVELOAD; } /////////////////////////////////////////////////////////////////////// // // Contains_Data // /////////////////////////////////////////////////////////////////////// bool EditorSaveLoadClass::Contains_Data (void) const { return true; } /////////////////////////////////////////////////////////////////////// // // Save // /////////////////////////////////////////////////////////////////////// bool EditorSaveLoadClass::Save (ChunkSaveClass &csave) { bool retval = true; int index; csave.Begin_Chunk (CHUNKID_MICRO_CHUNKS); // // Write the camera transform to the chunk // Matrix3D camera_tm = ::Get_Camera_Mgr ()->Get_Camera ()->Get_Transform (); WRITE_MICRO_CHUNK (csave, VARID_CAMERA_TM, camera_tm); // // Write the miscellaneous level settings to the chunk // Vector3 ambient_light = ::Get_Scene_Editor ()->Get_Ambient_Light (); WRITE_MICRO_CHUNK (csave, VARID_AMBIENT_LIGHT, ambient_light); // // Save the fog settings // bool fog_enabled = false; struct { float z_near; float z_far; } fog_planes = { 50.0F, 100.0F }; Vector3 fog_color (0, 0, 0); ::Get_Scene_Editor ()->Get_Fog_Range (&fog_planes.z_near, &fog_planes.z_far); fog_color = ::Get_Scene_Editor ()->Get_Fog_Color (); fog_enabled = ::Get_Scene_Editor ()->Get_Fog_Enable (); WRITE_MICRO_CHUNK (csave, VARID_FOG_ENABLED, fog_enabled); WRITE_MICRO_CHUNK (csave, VARID_FOG_COLOR, fog_color); WRITE_MICRO_CHUNK (csave, VARID_FOG_PLANES, fog_planes); // // Save the restart and respawn script names // StringClass restart_script_name = CombatManager::Get_Start_Script (); StringClass respawn_script_name = CombatManager::Get_Respawn_Script (); WRITE_MICRO_CHUNK_WWSTRING (csave, VARID_RESTART_SCRIPT_NAME, restart_script_name); WRITE_MICRO_CHUNK_WWSTRING (csave, VARID_RESPAWN_SCRIPT_NAME, respawn_script_name); // // Save the far clip plane... // float znear = 0; float zfar = 0; ::Get_Camera_Mgr ()->Get_Camera ()->Get_Clip_Planes (znear, zfar); WRITE_MICRO_CHUNK (csave, VARID_FAR_CLIP_PLANE, zfar); // // Write the background music filename out to the chunk // CString filename = ::Get_Scene_Editor ()->Get_Background_Music_Filename (); CString path = ::Get_File_Mgr ()->Make_Relative_Path (filename); WRITE_MICRO_CHUNK_STRING (csave, VARID_BACK_MUSIC, (LPCTSTR)path); // // Write the list of include files for the level to the chunk. // STRING_LIST &include_list = ::Get_File_Mgr ()->Get_Include_File_List (); for (index = 0; index < include_list.Count (); index ++) { StringClass filename = (LPCTSTR)include_list[index]; WRITE_MICRO_CHUNK_WWSTRING (csave, VARID_INCLUDE_FILE, filename); } csave.End_Chunk (); return retval; } /////////////////////////////////////////////////////////////////////// // // Load // /////////////////////////////////////////////////////////////////////// bool EditorSaveLoadClass::Load (ChunkLoadClass &cload) { bool retval = true; while (cload.Open_Chunk ()) { switch (cload.Cur_Chunk_ID ()) { // // Load all the presets from this chunk // case CHUNKID_MICRO_CHUNKS: retval &= Load_Micro_Chunks (cload); break; } cload.Close_Chunk (); } SaveLoadSystemClass::Register_Post_Load_Callback (this); return retval; } /////////////////////////////////////////////////////////////////////// // // Load_Micro_Chunks // /////////////////////////////////////////////////////////////////////// bool EditorSaveLoadClass::Load_Micro_Chunks (ChunkLoadClass &cload) { bool retval = true; Vector3 fog_color (0, 0, 0); bool fog_enabled = false; StringClass restart_script_name; StringClass respawn_script_name; while (cload.Open_Micro_Chunk ()) { switch (cload.Cur_Micro_Chunk_ID ()) { // // Read the camera's transformation matrix from the chunk // and pass it onto the camera. // case VARID_CAMERA_TM: { Matrix3D camera_tm; cload.Read(&camera_tm,sizeof (camera_tm)); ::Get_Camera_Mgr ()->Get_Camera ()->Set_Transform (camera_tm); } break; // // Read the include file from the chunk and add it to the list // of level include files. // case VARID_INCLUDE_FILE: { StringClass filename; cload.Read(filename.Get_Buffer(cload.Cur_Micro_Chunk_Length()),cload.Cur_Micro_Chunk_Length()); ::Get_File_Mgr ()->Get_Include_File_List ().Add ((LPCTSTR)filename); } break; case VARID_FAR_CLIP_PLANE_DOUBLE: { float znear = 0; float zfar = 0; ::Get_Camera_Mgr ()->Get_Camera ()->Get_Clip_Planes (znear, zfar); double double_zfar = 0; cload.Read (&double_zfar, sizeof (double_zfar)); zfar = double_zfar; ::Get_Camera_Mgr ()->Get_Camera ()->Set_Clip_Planes (znear, zfar); } break; case VARID_FAR_CLIP_PLANE: { float znear = 0; float zfar = 0; ::Get_Camera_Mgr ()->Get_Camera ()->Get_Clip_Planes (znear, zfar); cload.Read (&zfar, sizeof (zfar)); ::Get_Camera_Mgr ()->Get_Camera ()->Set_Clip_Planes (znear, zfar); } break; case VARID_AMBIENT_LIGHT: { Vector3 ambient_light; cload.Read (&ambient_light, sizeof (ambient_light)); ::Get_Scene_Editor ()->Set_Ambient_Light (ambient_light); ::Get_Scene_Editor ()->Update_Lighting (); LightAmbientFormClass *light_form = Get_Ambient_Light_Form (); light_form->Update_Settings (); } break; READ_MICRO_CHUNK (cload, VARID_FOG_ENABLED, fog_enabled); READ_MICRO_CHUNK (cload, VARID_FOG_COLOR, fog_color); READ_MICRO_CHUNK_WWSTRING (cload, VARID_RESTART_SCRIPT_NAME, restart_script_name); READ_MICRO_CHUNK_WWSTRING (cload, VARID_RESPAWN_SCRIPT_NAME, respawn_script_name); case VARID_FOG_PLANES: { struct { float z_near; float z_far; } fog_planes = { 50.0F, 100.0F }; cload.Read (&fog_planes, sizeof (fog_planes)); ::Get_Scene_Editor ()->Set_Fog_Range (fog_planes.z_near, fog_planes.z_far); } break; case VARID_BACK_MUSIC: { StringClass filename; cload.Read(filename.Get_Buffer(cload.Cur_Micro_Chunk_Length()),cload.Cur_Micro_Chunk_Length()); ::Get_Scene_Editor ()->Set_Background_Music (filename); } break; } cload.Close_Micro_Chunk (); } // // Apply the script settings // CombatManager::Set_Start_Script (restart_script_name); CombatManager::Set_Respawn_Script (respawn_script_name); // // Apply the fog settings // ::Get_Scene_Editor ()->Set_Fog_Color (fog_color); ::Get_Scene_Editor ()->Set_Fog_Enable (fog_enabled); return retval; } /////////////////////////////////////////////////////////////////////// // // Save_Level // /////////////////////////////////////////////////////////////////////// void EditorSaveLoadClass::Save_Level (LPCTSTR filename) { // // Create the file // HANDLE hfile = ::CreateFile (filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0L, NULL); ASSERT (hfile != INVALID_HANDLE_VALUE); if (hfile != INVALID_HANDLE_VALUE) { RawFileClass file_obj; file_obj.Attach (hfile); ChunkSaveClass chunk_save (&file_obj); // // (gth) March 4, 2002 - modifying this function to embed two "save files" into this // file. The first chunk will contain an entire save for the normal level data. The second // chunk will contain the lighting save // chunk_save.Begin_Chunk(CHUNKID_LVL_DATA); Save_Level_Data(chunk_save); chunk_save.End_Chunk(); chunk_save.Begin_Chunk(CHUNKID_LIGHT_SOLVE); Save_Light_Solve(chunk_save); chunk_save.End_Chunk(); // // Remember that we are now up-to-date // Set_Modified (false); } return ; } /////////////////////////////////////////////////////////////////////// // // Save_Level_Data - saves the level data for an LVL file // /////////////////////////////////////////////////////////////////////// void EditorSaveLoadClass::Save_Level_Data(ChunkSaveClass & chunk_save) { // // Repartition the static culling systems // (gth) can't re-partition the object culling systems or // we'll lose hierarchical vis // ::Get_Scene_Editor ()->Re_Partition_Static_Lights (); ::Get_Scene_Editor ()->Re_Partition_Audio_System (); // // Basically just save the list of nodes and some other // miscellaneous information (camera, sky, etc). // SaveLoadSystemClass::Save (chunk_save, _PhysStaticDataSaveSystem); SaveLoadSystemClass::Save (chunk_save, _TheNodeMgr); SaveLoadSystemClass::Save (chunk_save, _TheEditorSaveLoadSubsystem); SaveLoadSystemClass::Save (chunk_save, _TheBackgroundMgr); SaveLoadSystemClass::Save (chunk_save, _TheWeatherMgr); SaveLoadSystemClass::Save (chunk_save, _TheMapMgrSaveLoadSubsystem); SaveLoadSystemClass::Save (chunk_save, _TheHeightfieldMgrSaveLoadSubsystem); // // Save the level-specific conversations // _ConversationMgrSaveLoad.Set_Category_To_Save (ConversationMgrClass::CATEGORY_LEVEL); SaveLoadSystemClass::Save (chunk_save, _ConversationMgrSaveLoad); return ; } /////////////////////////////////////////////////////////////////////// // // Save_Light_Solve - saves the light-solve data for an LVL file // /////////////////////////////////////////////////////////////////////// void EditorSaveLoadClass::Save_Light_Solve(ChunkSaveClass & chunk_save) { SaveLoadSystemClass::Save( chunk_save, _TheLightSolveSaveSystem); return ; } /////////////////////////////////////////////////////////////////////// // // Load_Level // /////////////////////////////////////////////////////////////////////// void EditorSaveLoadClass::Load_Level (LPCTSTR filename) { // // Create the file // HANDLE hfile = ::CreateFile (filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0L, NULL); ASSERT (hfile != INVALID_HANDLE_VALUE); if (hfile != INVALID_HANDLE_VALUE) { RawFileClass file_obj; file_obj.Attach (hfile); ChunkLoadClass chunk_load (&file_obj); SoundSceneClass *sound_scene = WWAudioClass::Get_Instance ()->Get_Sound_Scene (); if (sound_scene != NULL) { sound_scene->Set_Batch_Mode (true); } // // Get the save-load subsystem to load // m_LoadedValidVis = true; NodeMgrClass::Free_Nodes (); // // (gth) March 4, 2002 - LVL files are now two independent save-load operations, // one for the normal level data and one for the light solve data. Here, we // detect whether we are loading a file that was created before this change. // uint32 id,size; if (chunk_load.Peek_Next_Chunk(&id,&size) && (id == CHUNKID_LVL_DATA)) { // Current file format, multiple saves embedded into this file while (chunk_load.Open_Chunk()) { switch (chunk_load.Cur_Chunk_ID()) { case CHUNKID_LVL_DATA: case CHUNKID_LIGHT_SOLVE: SaveLoadSystemClass::Load (chunk_load); break; default: break; } chunk_load.Close_Chunk(); } } else { // Legacy file format support SaveLoadSystemClass::Load (chunk_load); } // // Repartition the static culling systems // (gth) can't re-partition the object culling systems or // we'll lose hierarchical vis // ::Get_Scene_Editor ()->Re_Partition_Static_Lights (); ::Get_Scene_Editor ()->Re_Partition_Audio_System (); // // Now create any embedded nodes (nodes that are embedded inside // the preset of any nodes that are currently in the level). // NodeMgrClass::Create_All_Embedded_Nodes (); // // Validate VIS information if necessary // if (m_LoadedValidVis) { ::Get_Scene_Editor ()->Validate_Vis (); ::Get_Scene_Editor ()->Update_Culling_System_Bounding_Boxes (); } else { ::MessageBox (NULL, "Static geometry has changed since the last time this level was loaded. All previously generated VIS data has been discarded.", "Vis Discarded", MB_ICONEXCLAMATION | MB_OK); ::Get_Scene_Editor ()->Discard_Vis (); } if (sound_scene != NULL) { sound_scene->Set_Batch_Mode (false); } // // Remember that we are now up-to-date // Set_Modified (false); } return ; } /////////////////////////////////////////////////////////////////////// // // Export_Dynamic_Objects // /////////////////////////////////////////////////////////////////////// void EditorSaveLoadClass::Export_Dynamic_Objects (LPCTSTR filename) { // // Create the file // HANDLE hfile = ::CreateFile (filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0L, NULL); ASSERT (hfile != INVALID_HANDLE_VALUE); if (hfile != INVALID_HANDLE_VALUE) { RawFileClass file_obj; file_obj.Attach (hfile); ChunkSaveClass chunk_save (&file_obj); // // Remove the static objects from the level, save the nodes, // then put the static objects back // NODE_LIST static_obj_list; NodeMgrClass::Remove_Static_Objects (static_obj_list); SaveLoadSystemClass::Save (chunk_save, _TheNodeMgr); NodeMgrClass::Put_Objects_Back (static_obj_list); } return ; } /////////////////////////////////////////////////////////////////////// // // Import_Dynamic_Objects // /////////////////////////////////////////////////////////////////////// void EditorSaveLoadClass::Import_Dynamic_Objects (LPCTSTR filename) { // // Create the file // HANDLE hfile = ::CreateFile (filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0L, NULL); ASSERT (hfile != INVALID_HANDLE_VALUE); if (hfile != INVALID_HANDLE_VALUE) { RawFileClass file_obj; file_obj.Attach (hfile); ChunkLoadClass chunk_load (&file_obj); // // Find the largest used ID // uint32 start_id = (NodeMgrClass::Get_Max_Used_ID () + 1); // // Remove the dynamic objects from the level and load the new objects. // NODE_LIST dynamic_obj_list; NodeMgrClass::Remove_Dynamic_Objects (dynamic_obj_list); SaveLoadSystemClass::Load (chunk_load); ::Output_Message ("Resetting existing static node IDs.\r\n"); // // Re-assign IDs to the existing static nodes // for ( NodeClass *node = NodeMgrClass::Get_First (); node != NULL; node = NodeMgrClass::Get_Next (node)) { if (node->Is_Static ()) { node->Set_ID (start_id++); } } // // Check all the dynamic objects for ID collision // CString id_collision_msg = "The following dynamic object IDs collided on import:\n\n"; bool show_msg = false; DynamicVectorClass bad_node_list; for (int index = 0; index < dynamic_obj_list.Count (); index ++) { NodeClass *node = dynamic_obj_list[index]; if (NodeMgrClass::Find_Node (node->Get_ID ()) != NULL) { CString entry; entry.Format ("Object %d\n", node->Get_ID ()); id_collision_msg += entry; bad_node_list.Add (node); show_msg = true; } } // // Put the original objects back // NodeMgrClass::Put_Objects_Back (dynamic_obj_list); NodeMgrClass::Reset_New_ID (); // // Let the user know we had ID collision (if necessary) // if (show_msg) { ::MessageBox (NULL, id_collision_msg, "ID Collision", MB_ICONERROR | MB_OK); // // Ask the user if we should fix the collisions or not // if (::MessageBox (NULL, "Would you like to repair the newly imported objects with ambiguous IDs?", "ID Collision", MB_ICONQUESTION | MB_YESNO) == IDYES) { // // Repair all collisions // for (int index = 0; index < bad_node_list.Count (); index ++) { NodeClass *node = bad_node_list[index]; if (node != NULL) { node->Set_ID (NodeMgrClass::Get_Node_ID (node->Get_Type ())); } } } } } return ; } /////////////////////////////////////////////////////////////////////// // // On_Post_Load // /////////////////////////////////////////////////////////////////////// void EditorSaveLoadClass::On_Post_Load (void) { } /////////////////////////////////////////////////////////////////////// // // Export_Pathfind // /////////////////////////////////////////////////////////////////////// void PathfindImportExportSaveLoadClass::Export_Pathfind (LPCTSTR filename) { // // Create the file // HANDLE hfile = ::CreateFile (filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0L, NULL); ASSERT (hfile != INVALID_HANDLE_VALUE); if (hfile != INVALID_HANDLE_VALUE) { RawFileClass file_obj; file_obj.Attach (hfile); ChunkSaveClass chunk_save (&file_obj); // // // Basically just save the list of nodes and some other // miscellaneous information (camera, sky, etc). // SaveLoadSystemClass::Save (chunk_save, _ThePathfindImporterExporter); } return ; } /////////////////////////////////////////////////////////////////////// // // Import_Pathfind // /////////////////////////////////////////////////////////////////////// void PathfindImportExportSaveLoadClass::Import_Pathfind (LPCTSTR filename) { // // Create the file // HANDLE hfile = ::CreateFile (filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0L, NULL); ASSERT (hfile != INVALID_HANDLE_VALUE); if (hfile != INVALID_HANDLE_VALUE) { RawFileClass file_obj; file_obj.Attach (hfile); ChunkLoadClass chunk_load (&file_obj); // // Get the save-load subsystem to load // SaveLoadSystemClass::Load (chunk_load); } return ; } /////////////////////////////////////////////////////////////////////// // // Save // /////////////////////////////////////////////////////////////////////// bool PathfindImportExportSaveLoadClass::Save (ChunkSaveClass &csave) { // // Save the pathfind data // csave.Begin_Chunk (CHUNKID_PATHFIND_DATA); PathfindClass::Get_Instance()->Save (csave); csave.End_Chunk (); return true; } /////////////////////////////////////////////////////////////////////// // // Load // /////////////////////////////////////////////////////////////////////// bool PathfindImportExportSaveLoadClass::Load (ChunkLoadClass &cload) { while (cload.Open_Chunk ()) { switch (cload.Cur_Chunk_ID ()) { // // Import the pathfind data... // case CHUNKID_PATHFIND_DATA: PathfindClass::Get_Instance()->Load (cload); break; } cload.Close_Chunk (); } return true; }