/* * Descent 3 * Copyright (C) 2024 Parallax Software * * 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 . --- HISTORICAL COMMENTS FOLLOW --- * $Logfile: /DescentIII/Main/editor/Group.cpp $ * $Revision: 1.1.1.1 $ * $Date: 2003-08-26 03:57:38 $ * $Author: kevinb $ * * Functions for copying, pasting, etc. of groups * * $Log: not supported by cvs2svn $ * * 35 9/15/99 1:56p Matt * Added the option to allow rooms or groups placed on the terrain to * either align with the terrain or with gravity. * * 34 8/30/99 1:01p Gwar * use the "true" center of faces for determining place position of a * group * * 33 8/17/99 6:12p Gwar * handle NEWEDITOR generic object management in AttachGroup * * 32 8/17/99 6:46a Gwar * added group support to NEWEDITOR * * 31 5/08/99 1:39a Matt * Added a function to delete all objects of a certain type, and support * for placing and attaching groups to the terrain. * * 30 4/21/99 10:19p Matt * DeleteGroup() wasn't setting the world changed flag * * 29 3/15/99 1:29p Matt * Added objects & triggers to save/load for groups. * * 28 1/29/99 12:48p Matt * Rewrote the doorway system * * 27 1/26/99 6:02p Matt * Fixed (I hope) a problem with pasted object having the wrong * orientation. * * 26 1/21/99 11:15p Jeff * pulled out some structs and defines from header files and moved them * into separate header files so that multiplayer dlls don't require major * game headers, just those new headers. Side effect is a shorter build * time. Also cleaned up some header file #includes that weren't needed. * This affected polymodel.h, object.h, player.h, vecmat.h, room.h, * manage.h and multi.h * * 25 1/21/99 11:34a Matt * Got rid of portal triggers. Since we don't have multi-face portals, a * face trigger works fine for a portal. Also fixed a few editor/trigger * bugs. * * 24 1/14/99 11:06a Matt * Added names to triggers * * 23 1/08/99 2:56p Samir * Ripped out OSIRIS1. * * 22 12/01/98 11:17p Matt * Made copy/paste work with doors. * * 21 10/08/98 4:23p Kevin * Changed code to comply with memory library usage. Always use mem_malloc * , mem_free and mem_strdup * * 20 9/24/98 5:00p Matt * Improved error checking for running out of rooms. * * 19 9/02/98 5:57p Matt * Added code to make faces match exactly when forming a portal. This * should get rid of all remaining cracking problems. * * 18 9/01/98 12:04p Matt * Ripped out multi-face portal code * * 17 8/15/98 10:50p Matt * When copying a portal, copy its flags * * 16 6/23/98 2:43p Matt * Changed calls to OutrageMessageBox() & Debug_MessageBox() to deal with * int return value (instead of bool). * * 15 4/21/98 2:43p Matt * Added system to disable render updates while deleting large numbers of * rooms, since the mine is not stable until this operation is done. * * 14 2/09/98 1:38p Matt * When pasting a group, ask the user if he also wants to paste objects * and triggers * * 13 2/04/98 6:23p Matt * Changed object room number to indicate a terrain cell via a flag. Got * rid of the object flag which used to indicate terrain. * * 12 1/29/98 5:50p Matt * Changed old camera object type to be viewer object (for editor), and * now camera objects will just be for game cameras. * * 11 1/20/98 4:12p Samir * New script housekeeping system. * * 10 1/15/98 7:34p Matt * Revamped error checking when computing face normals * * 9 12/23/97 11:06a Samir * Added pserror.h * * 8 10/13/97 6:20p Matt * Fixed stupid bug copying objects in group * * 7 10/03/97 3:37p Matt * Fixed bug copying multiple objects in a room, and changed code to not * copy viewer or player objects * * 6 9/24/97 6:18p Samir * Use script names instead of script id values to identify scripts. * * 5 9/22/97 2:31p Matt * Made group code copy & paste objects & triggers * * 4 9/08/97 4:05p Samir * Fixed some warnings and took out extranneous includes to windows.h in * important headers. * * 3 9/02/97 6:42p Matt * Got paste & group save/load working * * 2 8/29/97 5:45p Matt * Converted group code to work with rooms (unfinished) * * 11 6/16/97 6:40p Matt * Fixed bug caused when Selected_segments[] was changed while * DeleteGroup() was indexing through it. * * 10 6/12/97 7:17p Matt * Deleted now-unused RotateWorld() function * * 9 5/23/97 5:46p Matt * Added SetMatrixBasedOnSide() functionality to ExtractMatrixFromSeg(). * * 8 5/13/97 11:58a Matt * Made group file code save & load the level file version number to use * when reading segments * * 7 5/08/97 5:07p Matt * When rotating world, rotate the editor's wireframe window view vars * * 6 5/08/97 4:43p Matt * Fixed bug when rotating objects in RotateWorld() * * 5 5/06/97 2:26p Jason * fixed bug in rotate world * * 4 5/05/97 3:57p Matt * Added code to rotate the mine (& the objects in it) to join with the * terrain. * * 3 3/31/97 5:58p Matt * Revamped mine update flags * * 2 3/12/97 3:25p Matt * Added funcs for cut, copy, paste, & delete, and to save and load * groups. * * 1 3/11/97 9:34p Matt * * $NoKeywords: $ */ #include "group.h" #ifndef NEWEDITOR #include "d3edit.h" #else #include "globals.h" #endif #include "selectedroom.h" #include "room.h" #include "hroom.h" #include "erooms.h" #include "loadlevel.h" #ifndef NEWEDITOR #include "moveworld.h" #endif #include "object.h" #include "trigger.h" #include "pserror.h" #include "doorway.h" #include "mem.h" #include #include "door.h" // Free a group. // Parameters: g - the group to be freed void FreeGroup(group *g) { for (int r = 0; r < g->nrooms; r++) FreeRoom(&g->rooms[r]); mem_free(g->rooms); if (g->nobjects) mem_free(g->objects); if (g->ntriggers) mem_free(g->triggers); mem_free(g); } // Copy the given list of rooms to a group // Parameters: nrooms - the number of rooms in list // roomnums - pointer to list of room numbers // attachroom, attachface - where group attaches when pasted // Returns: pointer to group group *CopyGroup(int nrooms, int *roomnums, int attachroom, int attachface) { group *g; int r; int room_xlate[MAX_ROOMS]; int n_objects = 0, n_triggers = 0, n_doors = 0; // Allocate group & room arrays g = (group *)mem_malloc(sizeof(*g)); ASSERT(g != NULL); g->nrooms = nrooms; g->rooms = (room *)mem_malloc(sizeof(*g->rooms) * nrooms); ASSERT(g->rooms != NULL); // Initialize xlate list for (r = 0; r < MAX_ROOMS; r++) room_xlate[r] = -1; // Copy the rooms & build xlate list. Count objects while we're at it for (r = 0; r < nrooms; r++) { CopyRoom(&g->rooms[r], &Rooms[roomnums[r]]); room_xlate[roomnums[r]] = r; for (int o = Rooms[roomnums[r]].objects; o != -1; o = Objects[o].next) if ((Objects[o].type != OBJ_VIEWER) && (Objects[o].type != OBJ_CAMERA) && (Objects[o].type != OBJ_PLAYER)) { n_objects++; if (Objects[o].type == OBJ_DOOR) n_doors++; } } // Add portals for rooms in the group for (r = 0; r < nrooms; r++) for (int p = 0; p < Rooms[roomnums[r]].num_portals; p++) { portal *pp = &Rooms[roomnums[r]].portals[p]; if ((pp->croom != -1) && IsRoomSelected(pp->croom)) { // link the two rooms if (r < room_xlate[pp->croom]) // only link lower to higher, so we don't double link LinkRoomsSimple(g->rooms, r, pp->portal_face, room_xlate[pp->croom], Rooms[pp->croom].portals[pp->cportal].portal_face); // Copy the portal flags int new_portal_num = g->rooms[r].faces[pp->portal_face].portal_num; g->rooms[r].portals[new_portal_num].flags = pp->flags; } } // Fill in attach fields g->attachroom = room_xlate[attachroom]; g->attachface = attachface; // Copy objects g->nobjects = n_objects; g->ndoors = n_doors; if (n_objects == 0) g->objects = NULL; else { int objnum = 0; // Get memory for objects g->objects = (object *)mem_malloc(sizeof(*g->objects) * n_objects); // Go through list of rooms & copy the objects for (r = 0; r < nrooms; r++) { for (int o = Rooms[roomnums[r]].objects; o != -1; o = Objects[o].next) { object *dest = &g->objects[objnum], *src = &Objects[o]; if ((src->type == OBJ_VIEWER) || (src->type == OBJ_CAMERA) || (src->type == OBJ_PLAYER)) continue; // don't copy cameras // Copy some selected data dest->type = src->type; dest->id = src->id; dest->flags = src->flags; dest->pos = src->pos; dest->orient = src->orient; dest->lifeleft = src->lifeleft; dest->contains_type = src->contains_type; dest->contains_id = src->contains_id; dest->contains_count = src->contains_count; if (src->control_type == CT_POWERUP) dest->ctype.powerup_info.count = src->ctype.powerup_info.count; if (src->type == OBJ_DOOR) dest->shields = src->shields; dest->roomnum = r; objnum++; } } } // Count the triggers in the group for (int t = 0; t < Num_triggers; t++) if (room_xlate[Triggers[t].roomnum] != -1) n_triggers++; // Copy triggers g->ntriggers = n_triggers; if (n_triggers == 0) g->triggers = NULL; else { int trignum = 0; // Get memory for triggers g->triggers = (trigger *)mem_malloc(sizeof(*g->triggers) * n_triggers); for (int t = 0; t < Num_triggers; t++) if (room_xlate[Triggers[t].roomnum] != -1) { g->triggers[trignum] = Triggers[t]; g->triggers[trignum].roomnum = room_xlate[Triggers[t].roomnum]; trignum++; } } // Return the new group return g; } extern bool Disable_editor_rendering; // Delete the given list of rooms // Parameters: nrooms - the number of rooms in list // roomnums - pointer to list of room numbers void DeleteGroup(int nrooms, int *roomnums) { int i; int *troomnums; bool save_disable_flag = Disable_editor_rendering; Disable_editor_rendering = 1; // Make copy of list of rooms to delete. We must do this because roomnums may be a pointer // to the global Selected_list, the contents of which may be changed by DeleteRoomFromMine() troomnums = (int *)mem_malloc(sizeof(*troomnums) * nrooms); for (i = 0; i < nrooms; i++) troomnums[i] = roomnums[i]; // Now delete the rooms for (i = 0; i < nrooms; i++) DeleteRoomFromMine(&Rooms[troomnums[i]]); // Free up our array mem_free(troomnums); // Regenerate external object rooms CreateRoomObjects(); // Restore old flag Disable_editor_rendering = save_disable_flag; // We're done! Update flags and leave World_changed = 1; } // Place the given group at the specified room face // The function merely causes the group to be drawn in the editor, allowing the user to line it up // before attaching it. The function AttachGroup() must be called to do the actual attachment. // Parameters: destroomp, destside - where to place the group // g - the group to place void PlaceGroup(room *destroomp, int destface, group *g) { // Clear the placed room, if one exists Placed_room = -1; ASSERT(destroomp->faces[destface].portal_num == -1); // Set globals Placed_group = g; Placed_room_orient.fvec = destroomp->faces[destface].normal; Placed_room_angle = 0; Placed_baseroomp = destroomp; Placed_baseface = destface; // Compute attach points on each face #ifndef NEWEDITOR ComputeCenterPointOnFace(&Placed_room_attachpoint, destroomp, destface); ComputeCenterPointOnFace(&Placed_room_origin, &g->rooms[g->attachroom], g->attachface); #else float ComputeFaceBoundingCircle(vector * center, room * rp, int facenum); ComputeFaceBoundingCircle(&Placed_room_attachpoint, destroomp, destface); ComputeFaceBoundingCircle(&Placed_room_origin, &g->rooms[g->attachroom], g->attachface); #endif // Compute initial orientation matrix ComputePlacedRoomMatrix(); // Set the flag State_changed = 1; } #include "terrain.h" // Place the given group at the specified terrain cell // The function merely causes the group to be drawn in the editor, allowing the user to line it up // before attaching it. The function AttachGroup() must be called to do the actual attachment. // Parameters: cellnum - where to place the group // g - the group to place void PlaceGroupTerrain(int cellnum, group *g, bool align_to_terrain) { // Clear the placed room, if one exists Placed_room = -1; // Set globals Placed_group = g; Placed_room_angle = 0; Placed_baseroomp = NULL; if (align_to_terrain) Placed_room_orient.fvec = TerrainNormals[MAX_TERRAIN_LOD - 1][cellnum].normal1; else Placed_room_orient.fvec = Identity_matrix.uvec; // Compute attach point on terrain ComputeTerrainSegmentCenter(&Placed_room_attachpoint, cellnum); // Compute attach points on face ComputeCenterPointOnFace(&Placed_room_origin, &g->rooms[g->attachroom], g->attachface); // Compute initial orientation matrix ComputePlacedRoomMatrix(); // Set the flag State_changed = 1; } // Attach the already-placed group void AttachGroup() { vector basecenter, attcenter; room *baseroomp, *attroomp; int baseface, attface; int room_xlate[MAX_ROOMS]; int r; ASSERT(Placed_group != NULL); // Set some vars baseroomp = Placed_baseroomp; baseface = Placed_baseface; attroomp = &Placed_group->rooms[Placed_group->attachroom]; attface = Placed_group->attachface; attcenter = Placed_room_origin; basecenter = Placed_room_attachpoint; // Copy the rooms into the main room list, build the xlate table, and rotate the verts for (r = 0; r < Placed_group->nrooms; r++) { int roomnum; room *newroomp; // Get a new room & copy into it roomnum = GetFreeRoom(0); if (roomnum == -1) { OutrageMessageBox("Cannot attach group: Not enough free rooms."); for (int t = 0; t < r; t++) // delete the rooms we just created DeleteRoomFromMine(&Rooms[room_xlate[t]]); return; } newroomp = &Rooms[roomnum]; CopyRoom(newroomp, &Placed_group->rooms[r]); room_xlate[r] = roomnum; // Rotate verts in the new rooms for (int i = 0; i < newroomp->num_verts; i++) newroomp->verts[i] = ((newroomp->verts[i] - attcenter) * Placed_room_rotmat) + basecenter; // Recompute normals for the faces if (!ResetRoomFaceNormals(newroomp)) Int3(); // Get Matt } // Add portals for new rooms for (r = 0; r < Placed_group->nrooms; r++) for (int p = 0; p < Placed_group->rooms[r].num_portals; p++) { portal *pp = &Placed_group->rooms[r].portals[p]; if (r < pp->croom) // only link lower to higher, so we don't double link LinkRoomsSimple(Rooms, room_xlate[r], pp->portal_face, room_xlate[pp->croom], Placed_group->rooms[pp->croom].portals[pp->cportal].portal_face); // Copy the portal flags int new_portal_num = Rooms[room_xlate[r]].faces[pp->portal_face].portal_num; Rooms[room_xlate[r]].portals[new_portal_num].flags = pp->flags; } // Get pointer to the placed attachroom room *newroomp = &Rooms[room_xlate[Placed_group->attachroom]]; if (baseroomp) { // Clip the connecting faces against each other if (!ClipFacePair(newroomp, attface, baseroomp, baseface)) { OutrageMessageBox("Error making portal -- faces probably don't overlap."); for (r = 0; r < Placed_group->nrooms; r++) FreeRoom(&Rooms[room_xlate[r]]); return; } // Make the two faces match exactly MatchPortalFaces(baseroomp, baseface, newroomp, attface); // Link the attach room to the base room LinkRoomsSimple(Rooms, ROOMNUM(baseroomp), baseface, ROOMNUM(newroomp), attface); } // Paste objects if (Placed_group->nobjects) { bool place_all = 0; // Ask if the user wants the objects pasted if (Placed_group->nobjects > Placed_group->ndoors) place_all = (OutrageMessageBox(MBOX_YESNO, "The group you are pasting has %d (non-door) object(s). Do you wish to paste it/them?", Placed_group->nobjects - Placed_group->ndoors) == IDYES); // Add the objects (either just doors or all objects) for (int o = 0; o < Placed_group->nobjects; o++) { object *src = &Placed_group->objects[o], *dest; if (place_all || (src->type == OBJ_DOOR)) { vector pos; matrix orient; int objnum; pos = ((src->pos - attcenter) * Placed_room_rotmat) + basecenter; orient = ~Placed_room_rotmat * src->orient; objnum = ObjCreate(src->type, src->id, room_xlate[src->roomnum], &pos, &orient); #ifdef NEWEDITOR if (IS_GENERIC(src->type)) LevelObjIncrementObject(src->id); #endif dest = &Objects[objnum]; dest->flags = src->flags; dest->lifeleft = src->lifeleft; dest->contains_type = src->contains_type; dest->contains_id = src->contains_id; dest->contains_count = src->contains_count; if (dest->control_type == CT_POWERUP) dest->ctype.powerup_info.count = src->ctype.powerup_info.count; if (src->type == OBJ_DOOR) dest->shields = src->shields; } } } // Paste triggers if (Placed_group->ntriggers) { // Ask if the user wants the triggers if (OutrageMessageBox(MBOX_YESNO, "The group you are pasting has %d trigger(s). Do you wish to paste it/them?", Placed_group->ntriggers) == IDYES) { // Add the triggers for (int t = 0; t < Placed_group->ntriggers; t++) { trigger *tp = &Placed_group->triggers[t]; int trignum; trignum = AddTrigger("", room_xlate[tp->roomnum], tp->facenum, tp->activator, NULL); ASSERT(trignum != -1); Triggers[trignum].flags = tp->flags; } } else { // Triggers not being pasted, so must delete any floating trigger faces for (r = 0; r < Placed_group->nrooms; r++) { room *rp = &Rooms[room_xlate[r]]; for (int f = 0; f < rp->num_faces; f++) { if (rp->faces[f].flags & FF_FLOATING_TRIG) DeleteRoomFace(rp, f); } } } } // Un-place the now-attached group Placed_group = NULL; // We're done! Update flags and leave World_changed = 1; } #define GROUP_FILE_TAG "D3GP" #define GROUP_FILE_VERSION 3 // Version number changes: // 0 -> 1 Save the level file version to use when reading objects, rooms, etc. // 1 -> 2 Now works for room engine // 2 -> 3 Save objects, doors, & triggers #define cf_ReadVector(f, v) \ do { \ (v)->x = cf_ReadFloat(f); \ (v)->y = cf_ReadFloat(f); \ (v)->z = cf_ReadFloat(f); \ } while (0) #define cf_WriteVector(f, v) \ do { \ cf_WriteFloat((f), (v)->x); \ cf_WriteFloat((f), (v)->y); \ cf_WriteFloat((f), (v)->z); \ } while (0) #define cf_WriteMatrix(f, m) \ do { \ cf_WriteVector((f), &(m)->rvec); \ cf_WriteVector((f), &(m)->uvec); \ cf_WriteVector((f), &(m)->fvec); \ } while (0) #define cf_ReadMatrix(f, m) \ do { \ cf_ReadVector((f), &(m)->rvec); \ cf_ReadVector((f), &(m)->uvec); \ cf_ReadVector((f), &(m)->fvec); \ } while (0) // Saves an object for a group void WriteGroupObject(CFILE *ofile, object *objp) { cf_WriteByte(ofile, objp->type); cf_WriteShort(ofile, objp->id); cf_WriteInt(ofile, objp->flags); if (objp->type == OBJ_DOOR) cf_WriteShort(ofile, objp->shields); cf_WriteInt(ofile, objp->roomnum); cf_WriteVector(ofile, &objp->pos); cf_WriteMatrix(ofile, &objp->orient); cf_WriteByte(ofile, objp->contains_type); cf_WriteByte(ofile, objp->contains_id); cf_WriteByte(ofile, objp->contains_count); cf_WriteFloat(ofile, objp->lifeleft); } void WriteGroupTrigger(CFILE *ofile, trigger *tp) { cf_WriteShort(ofile, tp->roomnum); cf_WriteShort(ofile, tp->facenum); cf_WriteShort(ofile, tp->flags); cf_WriteShort(ofile, tp->activator); } // Saves a group to disk in the given filename void SaveGroup(char *filename, group *g) { CFILE *ifile; int i; ifile = cfopen(filename, "wb"); if (!ifile) return; // Write tag & version number cf_WriteBytes((uint8_t *)GROUP_FILE_TAG, 4, ifile); cf_WriteInt(ifile, GROUP_FILE_VERSION); cf_WriteInt(ifile, LEVEL_FILE_VERSION); // Write the current list of textures WriteTextureList(ifile); // Write group info cf_WriteInt(ifile, g->nrooms); cf_WriteInt(ifile, g->attachroom); cf_WriteInt(ifile, g->attachface); cf_WriteInt(ifile, g->nobjects); cf_WriteInt(ifile, g->ndoors); cf_WriteInt(ifile, g->ntriggers); // Write rooms for (i = 0; i < g->nrooms; i++) WriteRoom(ifile, &g->rooms[i]); // Write objects for (i = 0; i < g->nobjects; i++) WriteGroupObject(ifile, &g->objects[i]); // Write triggers for (i = 0; i < g->ntriggers; i++) WriteGroupTrigger(ifile, &g->triggers[i]); // Close the file cfclose(ifile); } // Reads an object for a group void ReadGroupObject(CFILE *ifile, object *objp) { objp->type = cf_ReadByte(ifile); objp->id = cf_ReadShort(ifile); objp->flags = cf_ReadInt(ifile); if (objp->type == OBJ_DOOR) objp->shields = cf_ReadShort(ifile); objp->roomnum = cf_ReadInt(ifile); cf_ReadVector(ifile, &objp->pos); cf_ReadMatrix(ifile, &objp->orient); objp->contains_type = cf_ReadByte(ifile); objp->contains_id = cf_ReadByte(ifile); objp->contains_count = cf_ReadByte(ifile); objp->lifeleft = cf_ReadFloat(ifile); } void ReadGroupTrigger(CFILE *ifile, trigger *tp) { tp->roomnum = cf_ReadShort(ifile); tp->facenum = cf_ReadShort(ifile); tp->flags = cf_ReadShort(ifile); tp->activator = cf_ReadShort(ifile); } // Loads a group from disk group *LoadGroup(char *filename) { CFILE *ifile; char tag[4]; int version, level_version; group *g; int i; ifile = cfopen(filename, "rb"); if (!ifile) return NULL; // Read & check tag cf_ReadBytes((uint8_t *)tag, 4, ifile); if (strncmp(tag, GROUP_FILE_TAG, 4)) { cfclose(ifile); return NULL; } // Read & check version number version = cf_ReadInt(ifile); if (version > GROUP_FILE_VERSION) { cfclose(ifile); return NULL; } // Check for out-of-date groups if (version < 2) { OutrageMessageBox("Sorry - segment groups no longer supported."); cfclose(ifile); return NULL; } // Read & check level file version number level_version = cf_ReadInt(ifile); if (level_version > LEVEL_FILE_VERSION) { cfclose(ifile); return NULL; } // Read the textures & build xlate table ReadTextureList(ifile); // Allocate group g = (group *)mem_malloc(sizeof(*g)); // Read group info g->nrooms = cf_ReadInt(ifile); g->attachroom = cf_ReadInt(ifile); g->attachface = cf_ReadInt(ifile); // Read object, door, & trigger info if (version >= 3) { g->nobjects = cf_ReadInt(ifile); g->ndoors = cf_ReadInt(ifile); g->ntriggers = cf_ReadInt(ifile); } else g->nobjects = g->ndoors = g->ntriggers = 0; // Allocate room and vertex arrays g->rooms = (room *)mem_malloc(sizeof(*g->rooms) * g->nrooms); // Read rooms for (i = 0; i < g->nrooms; i++) { g->rooms[i].used = 0; // ReadRoom() wants the used flag to be clear ReadRoom(ifile, &g->rooms[i], level_version); } // Read objects & triggers g->triggers = NULL; g->objects = NULL; if (version >= 3) { // Allocate objects array if (g->nobjects) g->objects = (object *)mem_malloc(sizeof(*g->objects) * g->nobjects); // Allocate triggers array if (g->ntriggers) g->triggers = (trigger *)mem_malloc(sizeof(*g->triggers) * g->ntriggers); // Read objects for (i = 0; i < g->nobjects; i++) ReadGroupObject(ifile, &g->objects[i]); // Read triggers for (i = 0; i < g->ntriggers; i++) ReadGroupTrigger(ifile, &g->triggers[i]); } // Close the file cfclose(ifile); return g; }