bool C4GameSave::SaveDesc(C4Group &hToGroup)
{
	// Unfortunately, there's no way to prealloc the buffer in an appropriate size
	StdStrBuf sBuffer;

	// Header
	sBuffer.AppendFormat("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1031{\\fonttbl {\\f0\\fnil\\fcharset%d Times New Roman;}}", 0 /*FIXME: a number for UTF-8 here*/);
	sBuffer.Append(LineFeed);

	// Scenario title
	sBuffer.AppendFormat("\\uc1\\pard\\ulnone\\b\\f0\\fs20 %s\\par",Game.ScenarioTitle.getData());
	sBuffer.Append(LineFeed "\\b0\\fs16\\par" LineFeed);

	// OK; each specializations has its own desc format
	WriteDesc(sBuffer);

	// End of file
	sBuffer.Append(LineFeed "}" LineFeed);

	// Generate Filename
	StdStrBuf sFilename; char szLang[3];
	SCopyUntil(Config.General.Language, szLang, ',', 2);
	sFilename.Format(C4CFN_ScenarioDesc,szLang);

	// Save to file
	return !!hToGroup.Add(sFilename.getData(),sBuffer,false,true);
}
Beispiel #2
0
StdStrBuf C4Shader::Build(const ShaderSliceList &Slices, bool fDebug)
{

	// At the start of the shader set the #version and number of
	// available uniforms
	StdStrBuf Buf;
#ifndef USE_CONSOLE
	GLint iMaxFrags = 0, iMaxVerts = 0;
	glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB, &iMaxFrags);
	glGetIntegerv(GL_MAX_VERTEX_UNIFORM_COMPONENTS_ARB, &iMaxVerts);
#else
	int iMaxFrags = INT_MAX, iMaxVerts = INT_MAX;
#endif
	Buf.Format("#version %d\n"
			   "#define MAX_FRAGMENT_UNIFORM_COMPONENTS %d\n"
			   "#define MAX_VERTEX_UNIFORM_COMPONENTS %d\n",
			   C4Shader_Version, iMaxFrags, iMaxVerts);

	// Put slices
	int iPos = -1, iNextPos = -1;
	do
	{
		iPos = iNextPos; iNextPos = C4Shader_LastPosition+1;
		// Add all slices at the current level
		if (fDebug && iPos > 0)
			Buf.AppendFormat("\t// Position %d:\n", iPos);
		for (ShaderSliceList::const_iterator pSlice = Slices.begin(); pSlice != Slices.end(); pSlice++)
		{
			if (pSlice->Position < iPos) continue;
			if (pSlice->Position > iPos)
			{
				iNextPos = Min(iNextPos, pSlice->Position);
				continue;
			}
			// Same position - add slice!
			if (fDebug)
			{
				if (pSlice->Source.getLength())
					Buf.AppendFormat("\t// Slice from %s:\n", pSlice->Source.getData());
				else
					Buf.Append("\t// Built-in slice:\n");
				}
			Buf.Append(pSlice->Text);
			if (Buf[Buf.getLength()-1] != '\n')
				Buf.AppendChar('\n');
		}
		// Add seperator - only priority (-1) is top-level
		if (iPos == -1) {
			Buf.Append("void main() {\n");
		}
	}
	while (iNextPos <= C4Shader_LastPosition);

	// Terminate
	Buf.Append("}\n");
	return Buf;
}
Beispiel #3
0
void C4GameSave::WriteDescPlayers(StdStrBuf &sBuf, bool fByTeam, int32_t idTeam)
{
	// write out all players; only if they match the given team if specified
	C4PlayerInfo *pPlr; bool fAnyPlrWritten = false;
	for (int i = 0; (pPlr = Game.PlayerInfos.GetPlayerInfoByIndex(i)); i++)
		if (pPlr->HasJoined() && !pPlr->IsRemoved() && !pPlr->IsInvisible())
		{
			if (fByTeam)
			{
				if (idTeam)
				{
					// match team
					if (pPlr->GetTeam() != idTeam) continue;
				}
				else
				{
					// must be in no known team
					if (Game.Teams.GetTeamByID(pPlr->GetTeam())) continue;
				}
			}
			if (fAnyPlrWritten)
				sBuf.Append(", ");
			else if (fByTeam && idTeam)
			{
				C4Team *pTeam = Game.Teams.GetTeamByID(idTeam);
				if (pTeam) sBuf.AppendFormat("%s: ", pTeam->GetName());
			}
			sBuf.Append(pPlr->GetName());
			fAnyPlrWritten = true;
		}
	if (fAnyPlrWritten) WriteDescLineFeed(sBuf);
}
Beispiel #4
0
bool C4TextureMap::SaveMap(C4Group &hGroup, const char *szEntryName)
	{
#ifdef C4ENGINE
	// build file in memory
	StdStrBuf sTexMapFile;
	// add desc
	sTexMapFile.Append("# Automatically generated texture map" LineFeed);
	sTexMapFile.Append("# Contains material-texture-combinations added at runtime" LineFeed);
	// add overload-entries
	if (fOverloadMaterials) sTexMapFile.Append("# Import materials from global file as well" LineFeed "OverloadMaterials" LineFeed);
	if (fOverloadTextures) sTexMapFile.Append("# Import textures from global file as well" LineFeed "OverloadTextures" LineFeed);
	sTexMapFile.Append(LineFeed);
	// add entries
	for (int32_t i = 0; i < C4M_MaxTexIndex; i++)
		if (!Entry[i].isNull())
			{
			// compose line
			sTexMapFile.AppendFormat("%d=%s-%s" LineFeed, i, Entry[i].GetMaterialName(), Entry[i].GetTextureName());
			}
	// create new buffer allocated with new [], because C4Group cannot handle StdStrBuf-buffers
	size_t iBufSize = sTexMapFile.getLength();
	BYTE *pBuf = new BYTE[iBufSize];
	memcpy(pBuf, sTexMapFile.getData(), iBufSize);
	// add to group
	bool fSuccess = !!hGroup.Add(szEntryName, pBuf, iBufSize, false, true);
	if (!fSuccess) delete [] pBuf;
	// done
	return fSuccess;
#else
	return FALSE;
#endif
	}
Beispiel #5
0
void C4GameSave::WriteDescLeague(StdStrBuf &sBuf, bool fLeague, const char *strLeagueName)
{
	if (fLeague)
	{
		sBuf.AppendFormat(LoadResStr("IDS_PRC_LEAGUE"), strLeagueName);
		WriteDescLineFeed(sBuf);
	}
}
Beispiel #6
0
void C4GameSave::WriteDescGameTime(StdStrBuf &sBuf)
{
	// Write game duration
	if (Game.Time)
	{
		sBuf.AppendFormat(LoadResStr("IDS_DESC_DURATION"),
		                  Game.Time/3600,(Game.Time%3600)/60,Game.Time%60);
		WriteDescLineFeed(sBuf);
	}
}
Beispiel #7
0
void C4GameSave::WriteDescDate(StdStrBuf &sBuf, bool fRecord)
{
	// write local time/date
	time_t tTime; time(&tTime);
	struct tm *pLocalTime;
	pLocalTime=localtime(&tTime);
	sBuf.AppendFormat(LoadResStr(fRecord ? "IDS_DESC_DATEREC" : (::Network.isEnabled() ? "IDS_DESC_DATENET" : "IDS_DESC_DATE")),
	                  pLocalTime->tm_mday,
	                  pLocalTime->tm_mon+1,
	                  pLocalTime->tm_year+1900,
	                  pLocalTime->tm_hour,
	                  pLocalTime->tm_min);
	WriteDescLineFeed(sBuf);
}
Beispiel #8
0
StdStrBuf GetDbgRecPktData(C4RecordChunkType eType, const StdBuf &RawData) {
  StdStrBuf r;
  switch (eType) {
    case RCT_AulFunc:
      r.Ref(reinterpret_cast<const char *>(RawData.getData()),
            RawData.getSize() - 1);
      break;
    default:
      for (int i = 0; i < RawData.getSize(); ++i)
        r.AppendFormat("%02x ", (uint32_t)((uint8_t *)RawData.getData())[i]);
      break;
  }
  return r;
}
Beispiel #9
0
StdStrBuf C4ObjectList::GetNameList(C4DefList &rDefs) const
{
	int cpos,idcount;
	C4ID c_id;
	C4Def *cdef;
	StdStrBuf Buf;
	for (cpos=0; (c_id=GetListID(C4D_All,cpos)); cpos++)
		if ((cdef=rDefs.ID2Def(c_id)))
		{
			idcount=ObjectCount(c_id);
			if (cpos>0) Buf.Append(", ");
			Buf.AppendFormat("%dx %s",idcount,cdef->GetName());
		}
	return Buf;
}
StdStrBuf C4MusicFileOgg::GetDebugInfo() const
{
	StdStrBuf result;
	result.Append(FileName);
	result.AppendFormat("[%.0lf]", last_playback_pos_sec);
	result.AppendChar('[');
	bool sec = false;
	for (auto i = categories.cbegin(); i != categories.cend(); ++i)
	{
		if (sec) result.AppendChar(',');
		result.Append(i->getData());
		sec = true;
	}
	result.AppendChar(']');
	return result;
}
void C4GraphicsSystem::DrawHelp()
	{
	if (!ShowHelp) return;
	if (!Application.isFullScreen) return;
	int32_t iX = ViewportArea.X, iY = ViewportArea.Y;
	int32_t iWdt = ViewportArea.Wdt;
	StdStrBuf strText;
	// left coloumn
	strText.AppendFormat("[%s]\n\n", LoadResStr("IDS_CTL_GAMEFUNCTIONS"));
	// main functions
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("ToggleShowHelp").getData(), LoadResStr("IDS_CON_HELP"));
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("MusicToggle").getData(), LoadResStr("IDS_CTL_MUSIC"));
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("NetClientListDlgToggle").getData(), LoadResStr("IDS_DLG_NETWORK"));
	// messages
	StdCopyStrBuf strAltChatKey(GetKeyboardInputName("ChatOpen", false, 0));
	strText.AppendFormat("\n<c ffff00>%s/%s</c> - %s\n", GetKeyboardInputName("ChatOpen", false, 1).getData(), strAltChatKey.getData(), LoadResStr("IDS_CTL_SENDMESSAGE"));
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("MsgBoardScrollUp").getData(), LoadResStr("IDS_CTL_MESSAGEBOARDBACK"));
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("MsgBoardScrollDown").getData(), LoadResStr("IDS_CTL_MESSAGEBOARDFORWARD"));
	// irc chat
	strText.AppendFormat("\n<c ffff00>%s</c> - %s\n", GetKeyboardInputName("ToggleChat").getData(), LoadResStr("IDS_CTL_IRCCHAT"));
	// scoreboard
	strText.AppendFormat("\n<c ffff00>%s</c> - %s\n", GetKeyboardInputName("ScoreboardToggle").getData(), LoadResStr("IDS_CTL_SCOREBOARD"));
	// screenshots
	strText.AppendFormat("\n<c ffff00>%s</c> - %s\n", GetKeyboardInputName("Screenshot").getData(), LoadResStr("IDS_CTL_SCREENSHOT"));
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("ScreenshotEx").getData(), LoadResStr("IDS_CTL_SCREENSHOTEX"));

	Application.DDraw->TextOut(strText.getData(), Game.GraphicsResource.FontRegular, 1.0, Application.DDraw->lpBack,
														 iX + 128, iY + 64, CStdDDraw::DEFAULT_MESSAGE_COLOR, ALeft);

	// right coloumn
	strText.Clear();
	// game speed
	strText.AppendFormat("\n\n<c ffff00>%s</c> - %s\n", GetKeyboardInputName("GameSpeedUp").getData(), LoadResStr("IDS_CTL_GAMESPEEDUP"));
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("GameSlowDown").getData(), LoadResStr("IDS_CTL_GAMESPEEDDOWN"));
	// debug
	strText.AppendFormat("\n\n[%s]\n\n", "Debug");
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("DbgModeToggle").getData(), LoadResStr("IDS_CTL_DEBUGMODE"));
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("DbgShowVtxToggle").getData(), "Entrance+Vertices");
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("DbgShowActionToggle").getData(), "Actions/Commands/Pathfinder");
	strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("DbgShowSolidMaskToggle").getData(), "SolidMasks");
	Application.DDraw->TextOut(strText.getData(), Game.GraphicsResource.FontRegular, 1.0, Application.DDraw->lpBack,
														 iX + iWdt/2 + 64, iY + 64, CStdDDraw::DEFAULT_MESSAGE_COLOR, ALeft);
	}
Beispiel #12
0
BOOL C4PropertyDlg::Update() {
  if (!Active) return FALSE;

  StdStrBuf Output;

  idSelectedDef = C4ID_None;

  // Compose info text by selected object(s)
  switch (Selection.ObjectCount()) {
    // No selection
    case 0:
      Output = LoadResStr("IDS_CNS_NOOBJECT");
      break;
    // One selected object
    case 1: {
      C4Object *cobj = Selection.GetObject();
      // Type
      Output.AppendFormat(LoadResStr("IDS_CNS_TYPE"), cobj->GetName(),
                          C4IdText(cobj->Def->id));
      // Owner
      if (ValidPlr(cobj->Owner)) {
        Output.Append(LineFeed);
        Output.AppendFormat(LoadResStr("IDS_CNS_OWNER"),
                            Game.Players.Get(cobj->Owner)->GetName());
      }
      // Contents
      if (cobj->Contents.ObjectCount()) {
        Output.Append(LineFeed);
        Output.Append(LoadResStr("IDS_CNS_CONTENTS"));
        Output.Append(static_cast<const StdStrBuf &>(
            cobj->Contents.GetNameList(Game.Defs)));
      }
      // Action
      if (cobj->Action.Act != ActIdle) {
        Output.Append(LineFeed);
        Output.Append(LoadResStr("IDS_CNS_ACTION"));
        Output.Append(cobj->Def->ActMap[cobj->Action.Act].Name);
      }
      // Locals
      int cnt;
      bool fFirstLocal = true;
      for (cnt = 0; cnt < cobj->Local.GetSize(); cnt++)
        if (!!cobj->Local[cnt]) {
          // Header
          if (fFirstLocal) {
            Output.Append(LineFeed);
            Output.Append(LoadResStr("IDS_CNS_LOCALS"));
            fFirstLocal = false;
          }
          Output.Append(LineFeed);
          // Append id
          Output.AppendFormat(" Local(%d) = ", cnt);
          // write value
          Output.Append(
              static_cast<const StdStrBuf &>(cobj->Local[cnt].GetDataString()));
        }
      // Locals (named)
      for (cnt = 0; cnt < cobj->LocalNamed.GetAnzItems(); cnt++) {
        // Header
        if (fFirstLocal) {
          Output.Append(LineFeed);
          Output.Append(LoadResStr("IDS_CNS_LOCALS"));
          fFirstLocal = false;
        }
        Output.Append(LineFeed);
        // Append name
        Output.AppendFormat(" %s = ", cobj->LocalNamed.pNames->pNames[cnt]);
        // write value
        Output.Append(static_cast<const StdStrBuf &>(
            cobj->LocalNamed.pData[cnt].GetDataString()));
      }
      // Effects
      for (C4Effect *pEffect = cobj->pEffects; pEffect;
           pEffect = pEffect->pNext) {
        // Header
        if (pEffect == cobj->pEffects) {
          Output.Append(LineFeed);
          Output.Append(LoadResStr("IDS_CNS_EFFECTS"));
        }
        Output.Append(LineFeed);
        // Effect name
        Output.AppendFormat(" %s: Interval %d", pEffect->Name,
                            pEffect->iIntervall);
      }
      // Store selected def
      idSelectedDef = cobj->id;
      break;
    }
    // Multiple selected objects
    default:
      Output.Format(LoadResStr("IDS_CNS_MULTIPLEOBJECTS"),
                    Selection.ObjectCount());
      break;
  }
// Update info edit control
#ifdef WITH_DEVELOPER_MODE
  GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
  gtk_text_buffer_set_text(
      buffer, C4Language::IconvUtf8(Output.getData()).getData(), -1);
#endif
  return TRUE;
}
Beispiel #13
0
void C4GameSave::WriteDescEngine(StdStrBuf &sBuf)
{
	char ver[32]; sprintf(ver, "%d.%d", (int)C4XVER1, (int)C4XVER2);
	sBuf.AppendFormat(LoadResStr("IDS_DESC_VERSION"), ver);
	WriteDescLineFeed(sBuf);
}
Beispiel #14
0
BOOL C4Record::Start(bool fInitial) {
  // no double record
  if (fRecording) return FALSE;

  // create demos folder
  if (!Config.General.CreateSaveFolder(Config.General.SaveDemoFolder.getData(),
                                       LoadResStr("IDS_GAME_RECORDSTITLE")))
    return FALSE;

  // various infos
  StdStrBuf sDemoFolder;
  sDemoFolder.Ref(Config.General.SaveDemoFolder);
  char sScenName[_MAX_FNAME + 1];
  SCopy(GetFilenameOnly(Game.Parameters.Scenario.getFile()), sScenName,
        _MAX_FNAME);

  // remove trailing numbers from scenario name (e.g. from savegames) - could we
  // perhaps use C4S.Head.Origin instead...?
  char *pScenNameEnd = sScenName + SLen(sScenName);
  while (Inside<char>(*--pScenNameEnd, '0', '9'))
    if (pScenNameEnd == sScenName) break;
  pScenNameEnd[1] = 0;

  // determine index (by total number of records)
  Index = 1;
  for (DirectoryIterator i(Config.General.SaveDemoFolder.getData()); *i; ++i)
    if (WildcardMatch(C4CFN_ScenarioFiles, *i)) Index++;

  // compose record filename
  sFilename.Format("%s" DirSep "%03i-%s.c4s", sDemoFolder.getData(), Index,
                   sScenName);

  // log
  StdStrBuf sLog;
  sLog.Format(LoadResStr("IDS_PRC_RECORDINGTO"), sFilename.getData());
  if (Game.FrameCounter) sLog.AppendFormat(" (Frame %d)", Game.FrameCounter);
  Log(sLog.getData());

  // save game - this also saves player info list
  C4GameSaveRecord saveRec(fInitial, Index, Game.Parameters.isLeague());
  if (!saveRec.Save(sFilename.getData())) return FALSE;
  saveRec.Close();

  // unpack group, if neccessary
  if (!DirectoryExists(sFilename.getData()) &&
      !C4Group_UnpackDirectory(sFilename.getData()))
    return FALSE;

  // open control record file
  char szCtrlRecFilename[_MAX_PATH + 1 + _MAX_FNAME];
  sprintf(szCtrlRecFilename, "%s" DirSep C4CFN_CtrlRec, sFilename.getData());
  if (!CtrlRec.Create(szCtrlRecFilename)) return FALSE;

  // open record group
  if (!RecordGrp.Open(sFilename.getData())) return FALSE;

  // record go
  fStreaming = false;
  fRecording = true;
  iLastFrame = 0;
  return TRUE;
}
Beispiel #15
0
BOOL C4UpdatePackage::MkUp(C4Group *pGrp1, C4Group *pGrp2, C4GroupEx *pUpGrp,
                           BOOL *fModified) {
    // (CAUTION: pGrp1 may be NULL - that means that there is no counterpart for
    // Grp2
    //           in the base group)

    // compare headers
    if (!pGrp1 || pGrp1->GetCreation() != pGrp2->GetCreation() ||
            pGrp1->GetOriginal() != pGrp2->GetOriginal() ||
            !SEqual(pGrp1->GetMaker(), pGrp2->GetMaker()) ||
            !SEqual(pGrp1->GetPassword(), pGrp2->GetPassword()))
        *fModified = TRUE;
    // set header
    pUpGrp->SetHead(*pGrp2);
    // compare entries
    char strItemName[_MAX_PATH], strItemName2[_MAX_PATH];
    StdStrBuf EntryList;
    strItemName[0] = strItemName2[0] = 0;
    pGrp2->ResetSearch();
    if (!*fModified) pGrp1->ResetSearch();
    int iChangedEntries = 0;
    while (pGrp2->FindNextEntry("*", strItemName, NULL, NULL, !!strItemName[0])) {
        // add to entry list
        if (!!EntryList) EntryList.AppendChar('|');
        EntryList.AppendFormat("%s=%d", strItemName, pGrp2->EntryTime(strItemName));
        // no modification detected yet? then check order
        if (!*fModified) {
            if (!pGrp1->FindNextEntry("*", strItemName2, NULL, NULL,
                                      !!strItemName2[0]))
                *fModified = TRUE;
            else if (!SEqual(strItemName, strItemName2))
                *fModified = TRUE;
        }

        // TODO: write DeleteEntries.txt

        // a child group?
        C4GroupEx ChildGrp2;
        if (ChildGrp2.OpenAsChild(pGrp2, strItemName)) {
            // open in Grp1
            C4Group *pChildGrp1 = new C4GroupEx();
            if (!pGrp1 || !pChildGrp1->OpenAsChild(pGrp1, strItemName)) {
                delete pChildGrp1;
                pChildGrp1 = NULL;
            }
            // open group for update data
            C4GroupEx UpdGroup;
            char strTempGroupName[_MAX_FNAME + 1];
            strTempGroupName[0] = 0;
            if (!UpdGroup.OpenAsChild(pUpGrp, strItemName)) {
                // create new group (may be temporary)
                // SCopy(GetCfg()->AtTempPath("~upd"), strTempGroupName, _MAX_FNAME);
                MakeTempFilename(strTempGroupName);
                if (!UpdGroup.Open(strTempGroupName, TRUE)) {
                    delete pChildGrp1;
                    WriteLog("Error: could not create temp group\n");
                    return FALSE;
                }
            }
            // do nested MkUp-search
            BOOL Modified = FALSE;
            BOOL fSuccess = MkUp(pChildGrp1, &ChildGrp2, &UpdGroup, &Modified);
            // sort & close
            extern const char **C4Group_SortList;
            UpdGroup.SortByList(C4Group_SortList, ChildGrp2.GetName());
            UpdGroup.Close(FALSE);
            // check entry times
            if (!pGrp1 ||
                    (pGrp1->EntryTime(strItemName) != pGrp2->EntryTime(strItemName)))
                Modified = TRUE;
            // add group (if modified)
            if (fSuccess && Modified) {
                if (strTempGroupName[0])
                    if (!pUpGrp->Move(strTempGroupName, strItemName)) {
                        WriteLog("Error: could not add modified group\n");
                        return FALSE;
                    }
                // copy core
                pUpGrp->SaveEntryCore(*pGrp2, strItemName);
                pUpGrp->SetSavedEntryCore(strItemName);
                // got a modification in a subgroup
                *fModified = TRUE;
                iChangedEntries++;
            } else
                // delete group (do not remove groups that existed before!)
                if (strTempGroupName[0])
                    if (remove(strTempGroupName))
                        if (rmdir(strTempGroupName)) {
                            WriteLog("Error: could not delete temporary directory\n");
                            return FALSE;
                        }
            delete pChildGrp1;
        } else {
            // compare them (size & crc32)
            if (!pGrp1 ||
                    pGrp1->EntrySize(strItemName) != pGrp2->EntrySize(strItemName) ||
                    pGrp1->EntryCRC32(strItemName) != pGrp2->EntryCRC32(strItemName)) {
                BOOL fCopied = FALSE;

                // save core (EntryCRC32 might set additional fields)
                pUpGrp->SaveEntryCore(*pGrp2, strItemName);

                // already in update grp?
                if (pUpGrp->EntryTime(strItemName) != pGrp2->EntryTime(strItemName) ||
                        pUpGrp->EntrySize(strItemName) != pGrp2->EntrySize(strItemName) ||
                        pUpGrp->EntryCRC32(strItemName) != pGrp2->EntryCRC32(strItemName)) {
                    // copy it
                    if (!C4Group_CopyEntry(pGrp2, pUpGrp, strItemName)) {
                        WriteLog("Error: could not add changed entry to update group\n");
                        return FALSE;
                    }
                    // set entry core
                    pUpGrp->SetSavedEntryCore(strItemName);
                    // modified...
                    *fModified = TRUE;
                    fCopied = TRUE;
                }
                iChangedEntries++;

                WriteLog("%s\\%s: update%s\n", pGrp2->GetFullName().getData(),
                         strItemName, fCopied ? "" : " (already in group)");
            }
        }
    }
    // write entries list (always)
    if (!pUpGrp->Add(C4CFN_UpdateEntries, EntryList, FALSE, TRUE)) {
        WriteLog("Error: could not save entry list!");
        return FALSE;
    }

    if (iChangedEntries > 0)
        WriteLog("%s: %d/%d changed (%s)\n", pGrp2->GetFullName().getData(),
                 iChangedEntries, pGrp2->EntryCount(),
                 *fModified ? "update" : "skip");

    // success
    return TRUE;
}