1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010 Winch Gate Property Limited
4 // This source file has been modified by the following contributors:
5 // Copyright (C) 2011-2013 Matt RAYKOWSKI (sfb) <matt.raykowski@gmail.com>
6 // Copyright (C) 2014 Matthew LAGOE (Botanic) <cyberempires@gmail.com>
7 // Copyright (C) 2014-2020 Jan BOON (Kaetemi) <jan.boon@kaetemi.be>
9 // This program is free software: you can redistribute it and/or modify
10 // it under the terms of the GNU Affero General Public License as
11 // published by the Free Software Foundation, either version 3 of the
12 // License, or (at your option) any later version.
14 // This program is distributed in the hope that it will be useful,
15 // but WITHOUT ANY WARRANTY; without even the implied warranty of
16 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 // GNU Affero General Public License for more details.
19 // You should have received a copy of the GNU Affero General Public License
20 // along with this program. If not, see <http://www.gnu.org/licenses/>.
22 //-----------------------------------------------------------------------------
24 //-----------------------------------------------------------------------------
29 #include "game_share/bnp_patch.h"
30 #include "nel/misc/path.h"
31 #include "nel/misc/file.h"
32 #include "nel/misc/command.h"
33 #include "nel/misc/sstring.h"
34 #include "nel/misc/seven_zip.h"
35 #include "game_share/singleton_registry.h"
38 using namespace NLMISC
;
40 #define PERSISTENT_TOKEN_FAMILY RyzomTokenFamily
43 //-----------------------------------------------------------------------------
44 // Handy utility functions
45 //-----------------------------------------------------------------------------
47 void normalisePackageDescriptionFileName(std::string
& fileName
)
50 fileName
="package_description";
51 if (NLMISC::CFile::getExtension(fileName
).empty() && fileName
[fileName
.size()-1]!='.')
55 void GeneratePatch(const std::string
& srcFileName
,const std::string
& destFileName
,const std::string
& patchFileName
)
57 std::string cmd
= toString("xdelta delta %s %s %s", srcFileName
.c_str(), destFileName
.c_str(), patchFileName
.c_str());
59 nlinfo("Executing system command: %s", cmd
.c_str());
62 _spawnlp(_P_WAIT
, "xdelta.exe", "xdelta.exe", "delta", srcFileName
.c_str(), destFileName
.c_str(), patchFileName
.c_str(), NULL
);
63 #else // NL_OS_WINDOWS
64 // xdelta-1.x behaves like "diff" and returns 0 for identical files, 1 for different files, 2 for errors
65 sint error
= system (cmd
.c_str());
68 nlwarning("'%s' failed with error code %d", cmd
.c_str(), error
);
69 #endif // NL_OS_WINDOWS
72 void ApplyPatch(const std::string
& srcFileName
,const std::string
& destFileName
,const std::string
& patchFileName
=std::string())
74 std::string cmd
= toString("xdelta patch %s %s %s", patchFileName
.c_str(), srcFileName
.c_str(), destFileName
.c_str());
76 nlinfo("Executing system command: %s", cmd
.c_str());
79 _spawnlp(_P_WAIT
, "xdelta.exe", "xdelta.exe", "patch",patchFileName
.c_str(), srcFileName
.c_str(), destFileName
.c_str(), NULL
);
80 #else // NL_OS_WINDOWS
81 // xdelta-1.x behaves like "diff" and returns 0 for identical files, 1 for different files, 2 for errors
82 sint error
= system (cmd
.c_str());
85 nlwarning("'%s' failed with error code %d", cmd
.c_str(), error
);
86 #endif // NL_OS_WINDOWS
89 void GenerateLZMA(const std::string
&sourceFile
, const std::string
&outputFile
)
92 nlinfo("Compressing %s to %s using LZMA...", sourceFile
.c_str(), outputFile
.c_str());
95 if (!packLZMA(sourceFile
, outputFile
))
97 nlwarning("LZMA compress failed");
102 //-----------------------------------------------------------------------------
103 // class CPackageDescription
104 //-----------------------------------------------------------------------------
106 class CPackageDescription
: public IVersionNumberGenerator
109 DECLARE_PERSISTENCE_METHODS
112 CPackageDescription();
116 void setup(const std::string
& packageName
);
117 void storeToPdr(CPersistentDataRecord
& pdr
) const;
119 void readIndex(CBNPFileSet
& packageIndex
) const;
120 void writeIndex(const CBNPFileSet
& packageIndex
) const;
122 void getCategories(CPersistentDataRecord
&pdr
) const;
124 void updateIndexFileList(CBNPFileSet
& packageIndex
) const;
125 void generateClientIndex(CProductDescriptionForClient
& theClientPackage
,const CBNPFileSet
& packageIndex
) const;
126 void addVersion(CBNPFileSet
& packageIndex
);
127 void generatePatches(CBNPFileSet
& packageIndex
) const;
128 void createDirectories() const;
129 void buildDefaultFileList();
131 void updatePatchSizes(CBNPFileSet
& packageIndex
) const;
133 // specialisation of IVersionNumberGenerator
134 void grabVersionNumber();
135 uint32
getPackageVersionNumber();
138 CBNPCategorySet _Categories
;
139 std::string _IndexFileName
;
140 std::string _ClientIndexFileName
;
141 std::string _RootDirectory
;
142 std::string _PatchDirectory
;
143 std::string _BnpDirectory
;
144 std::string _RefDirectory
;
145 std::string _NextVersionFile
;
147 uint32 _NextVersionNumber
;
148 bool _VersionNumberReserved
;
152 //-----------------------------------------------------------------------------
153 // methods CPackageDescription
154 //-----------------------------------------------------------------------------
156 CPackageDescription::CPackageDescription()
161 void CPackageDescription::clear()
163 _NextVersionNumber
= std::numeric_limits
<uint32
>::max();
164 _VersionNumberReserved
= false;
166 _IndexFileName
.clear();
167 _ClientIndexFileName
.clear();
168 _PatchDirectory
.clear();
169 _BnpDirectory
.clear();
170 _RefDirectory
.clear();
173 void CPackageDescription::setup(const std::string
& packageName
)
175 nlinfo("Reading package description: %s ...",packageName
.c_str());
177 // clear out old contents before reading from input file
180 // read new contents from input file
181 static CPersistentDataRecord pdr
;
183 pdr
.readFromTxtFile(packageName
);
187 if (_RootDirectory
.empty())
188 _RootDirectory
= NLMISC::CFile::getPath(packageName
);
189 _RootDirectory
= NLMISC::CPath::standardizePath(_RootDirectory
, true);
192 if (_PatchDirectory
.empty())
193 _PatchDirectory
= _RootDirectory
+ "patch";
194 _PatchDirectory
= NLMISC::CPath::standardizePath(_PatchDirectory
, true);
197 if (_BnpDirectory
.empty())
198 _BnpDirectory
= _RootDirectory
+"bnp";
199 _BnpDirectory
= NLMISC::CPath::standardizePath(_BnpDirectory
, true);
202 if (_RefDirectory
.empty())
203 _RefDirectory
= _RootDirectory
+"ref";
204 _RefDirectory
= NLMISC::CPath::standardizePath(_RefDirectory
, true);
207 if (_ClientIndexFileName
.empty())
208 _ClientIndexFileName
= NLMISC::CFile::getFilenameWithoutExtension(packageName
)+".idx";
211 if (_IndexFileName
.empty())
212 _IndexFileName
= NLMISC::CFile::getFilenameWithoutExtension(_ClientIndexFileName
)+".hist";
215 void CPackageDescription::storeToPdr(CPersistentDataRecord
& pdr
) const
221 void CPackageDescription::readIndex(CBNPFileSet
& packageIndex
) const
223 std::string indexPath
= _RootDirectory
+ _IndexFileName
;
225 nlinfo("Reading history file: %s ...", indexPath
.c_str());
227 // clear out old contents before reading from input file
228 packageIndex
.clear();
230 // read new contents from input file
231 if (NLMISC::CFile::fileExists(indexPath
))
233 static CPersistentDataRecord pdr
;
235 pdr
.readFromTxtFile(indexPath
);
236 packageIndex
.apply(pdr
);
240 void CPackageDescription::writeIndex(const CBNPFileSet
& packageIndex
) const
242 std::string indexPath
= _RootDirectory
+ _IndexFileName
;
244 nlinfo("Writing history file: %s ...", indexPath
.c_str());
246 // write contents to output file
247 static CPersistentDataRecordRyzomStore pdr
;
249 packageIndex
.store(pdr
);
250 pdr
.writeToTxtFile(indexPath
);
253 void CPackageDescription::getCategories(CPersistentDataRecord
&pdr
) const
256 _Categories
.store(pdr
);
259 void CPackageDescription::updateIndexFileList(CBNPFileSet
& packageIndex
) const
261 nlinfo("Updating file list from package categories (%d files) ...",_Categories
.fileCount());
262 for (uint32 i
=_Categories
.fileCount();i
--;)
264 const std::string
& fileName
= _Categories
.getFile(i
);
265 packageIndex
.addFile(fileName
,_Categories
.isFileIncremental(fileName
));
267 // if the file is flagged as non-incremental then we need to add its refference file too
268 if (!_Categories
.isFileIncremental(fileName
))
270 std::string refName
= NLMISC::CFile::getFilenameWithoutExtension(_Categories
.getFile(i
))+"_.ref";
271 packageIndex
.addFile(refName
);
273 // if the ref file doesn't exist then create it by copying the original
274 if (NLMISC::CFile::fileExists(_BnpDirectory
+fileName
) && !NLMISC::CFile::fileExists(_BnpDirectory
+refName
))
276 NLMISC::CFile::copyFile(_BnpDirectory
+refName
,_BnpDirectory
+fileName
);
277 nlassert(NLMISC::CFile::getFileSize(_BnpDirectory
+refName
)== NLMISC::CFile::getFileSize(_BnpDirectory
+fileName
));
283 void CPackageDescription::generateClientIndex(CProductDescriptionForClient
& theClientPackage
, const CBNPFileSet
& packageIndex
) const
285 std::string patchNumber
= toString("%05u", packageIndex
.getVersionNumber());
286 std::string patchDirectory
= _PatchDirectory
+ patchNumber
;
287 std::string patchFile
= patchDirectory
+ "/" + _ClientIndexFileName
;
289 nlinfo("Generating client index: %s...", patchFile
.c_str());
291 // make sure the version sub directory exist
292 CFile::createDirectory(patchDirectory
);
294 // clear out the client package before we start
295 theClientPackage
.clear();
297 // copy the categories using a pdr record
298 static CPersistentDataRecordRyzomStore pdr
;
300 _Categories
.store(pdr
);
301 theClientPackage
.setCategories(pdr
);
303 // copy the files using a pdr record
305 packageIndex
.store(pdr
);
306 theClientPackage
.setFiles(pdr
);
308 // create the output file
310 theClientPackage
.store(pdr
);
312 std::string newName
= patchDirectory
+ "/" + NLMISC::CFile::getFilenameWithoutExtension(_ClientIndexFileName
) + "_" + patchNumber
;
314 pdr
.writeToBinFile(newName
+ ".idx");
315 pdr
.writeToTxtFile(newName
+ "_debug.xml");
318 void CPackageDescription::addVersion(CBNPFileSet
& packageIndex
)
320 // calculate the last version number in the index file
321 // nlinfo("Calculating package version number...");
322 // uint32 versionNumber= packageIndex.getVersionNumber();
323 // nlinfo("Last version number = %d",versionNumber);
324 // uint32 newVersionNumber= packageIndex.addVersion(_BnpDirectory,versionNumber+1);
325 // nlinfo("New version number = %d",newVersionNumber);
327 // setup the default next version number by scanning the package index for the highest existing version number
328 nlinfo("Calculating package version number...");
329 _NextVersionNumber
= packageIndex
.getVersionNumber();
330 nlinfo("Last version number = %u",_NextVersionNumber
);
331 ++_NextVersionNumber
;
333 // have the package index check its file list to see if a new version is required
334 uint32 newVersionNumber
= packageIndex
.addVersion(_BnpDirectory
,_RefDirectory
,*this);
335 nlinfo("Added files for version: %u",newVersionNumber
);
338 void CPackageDescription::generatePatches(CBNPFileSet
& packageIndex
) const
340 nlinfo("Generating patches ...");
342 for (uint32 i
= packageIndex
.fileCount(); i
--;)
344 bool deleteRefAfterDelta
= true;
345 bool usingTemporaryFile
= false;
346 // generate file name root
347 std::string bnpFileName
= _BnpDirectory
+ packageIndex
.getFile(i
).getFileName();
348 std::string refNameRoot
= _RefDirectory
+ NLMISC::CFile::getFilenameWithoutExtension(bnpFileName
);
349 std::string patchNameRoot
= _PatchDirectory
+ NLMISC::CFile::getFilenameWithoutExtension(bnpFileName
);
351 // if the file has no versions then skip on to the next file
352 if (packageIndex
.getFile(i
).versionCount()==0)
355 // get the last version number and the related file name
356 const CBNPFileVersion
& curVersion
= packageIndex
.getFile(i
).getVersion(packageIndex
.getFile(i
).versionCount()-1);
357 std::string curVersionFileName
= refNameRoot
+NLMISC::toString("_%05u.%s",curVersion
.getVersionNumber(),NLMISC::CFile::getExtension(bnpFileName
).c_str());
358 // std::string patchFileName= patchNameRoot+NLMISC::toString("_%05d.patch",curVersion.getVersionNumber());
359 std::string patchFileName
= _PatchDirectory
+ toString("%05u/",curVersion
.getVersionNumber())+NLMISC::CFile::getFilenameWithoutExtension(bnpFileName
)+toString("_%05u",curVersion
.getVersionNumber())+".patch";
361 // get the second last version number and the related file name
362 std::string prevVersionFileName
;
363 if (packageIndex
.getFile(i
).versionCount()==1)
365 prevVersionFileName
= _RootDirectory
+ "empty";
366 CFile::createEmptyFile(prevVersionFileName
);
367 usingTemporaryFile
= true;
368 deleteRefAfterDelta
= false;
372 const CBNPFileVersion
& prevVersion
= packageIndex
.getFile(i
).getVersion(packageIndex
.getFile(i
).versionCount()-2);
373 prevVersionFileName
= refNameRoot
+NLMISC::toString("_%05u.%s",prevVersion
.getVersionNumber(),NLMISC::CFile::getExtension(bnpFileName
).c_str());
375 std::string refVersionFileName
= prevVersionFileName
;
377 // create the subdirectory for this patch number
378 string versionSubDir
= _PatchDirectory
+ toString("%05u/", curVersion
.getVersionNumber());
379 CFile::createDirectory(versionSubDir
);
381 // generate the lzma packed version of the bnp if needed (lzma file are slow to generate)
382 string lzmaFile
= versionSubDir
+CFile::getFilename(bnpFileName
)+".lzma";
383 if (!CFile::fileExists(lzmaFile
))
385 // build the lzma compression in a temp file (avoid leaving dirty file if the
386 // process cannot terminate)
387 GenerateLZMA(bnpFileName
, lzmaFile
+".tmp");
388 // rename the tmp file
389 CFile::moveFile(lzmaFile
, lzmaFile
+".tmp");
392 // store the lzma file size in the descriptor
393 packageIndex
.getFile(i
).getVersion(packageIndex
.getFile(i
).versionCount()-1).set7ZipFileSize(CFile::getFileSize(lzmaFile
));
395 // if we need to generate a new patch then do it and create the new ref file
396 if (!NLMISC::CFile::fileExists(curVersionFileName
))
398 nlinfo("- Creating patch: %s",patchFileName
.c_str());
400 // in the case where we compress against a ref file...
401 if (!_Categories
.isFileIncremental(NLMISC::CFile::getFilename(bnpFileName
)))
403 // setup the name of the reference file to patch against
404 refVersionFileName
= _BnpDirectory
+NLMISC::CFile::getFilenameWithoutExtension(bnpFileName
)+"_.ref";
406 // delete the previous patch - because we only need the latest patch for non-incremental files
407 std::string lastPatch
= _PatchDirectory
+ NLMISC::CFile::getFilenameWithoutExtension(prevVersionFileName
)+".patch";
408 if (NLMISC::CFile::fileExists(lastPatch
.c_str()))
409 NLMISC::CFile::deleteFile(lastPatch
.c_str());
412 // call xdelta to generate the patch
413 GeneratePatch(refVersionFileName
, bnpFileName
, patchFileName
);
414 nlassert(NLMISC::CFile::fileExists(patchFileName
));
416 uint32 nPatchSize
= NLMISC::CFile::getFileSize(patchFileName
);
417 packageIndex
.getFile(i
).getVersion(packageIndex
.getFile(i
).versionCount()-1).setPatchSize(nPatchSize
);
419 // apply the incremental patch to the old ref file to create the new ref file
420 // and ensure that the new ref file matches the BNP
421 ApplyPatch(refVersionFileName
, curVersionFileName
, patchFileName
);
422 nlassert(NLMISC::CFile::fileExists(curVersionFileName
));
423 nlassert(NLMISC::CFile::thoroughFileCompare(bnpFileName
, curVersionFileName
));
426 // if we have a ref file still hanging about from the previous patch then delete it
427 if (NLMISC::CFile::fileExists(prevVersionFileName
))
429 NLMISC::CFile::deleteFile(prevVersionFileName
);
434 void CPackageDescription::createDirectories() const
436 NLMISC::CFile::createDirectoryTree(_RootDirectory
);
437 NLMISC::CFile::createDirectoryTree(_PatchDirectory
);
438 NLMISC::CFile::createDirectoryTree(_BnpDirectory
);
439 NLMISC::CFile::createDirectoryTree(_RefDirectory
);
442 void CPackageDescription::buildDefaultFileList()
444 // make sure the default categories exist
445 CBNPCategory
* packedCategory
= _Categories
.getCategory("main", true);
446 packedCategory
->setOptional(false);
447 packedCategory
->setIncremental(true);
449 CBNPCategory
* unpackedCategory
= _Categories
.getCategory("unpacked", true);
450 unpackedCategory
->setUnpackTo("./");
451 unpackedCategory
->setOptional(false);
452 unpackedCategory
->setIncremental(false);
454 CBNPCategory
* optionCategory
= _Categories
.getCategory("optional", true);
455 optionCategory
->setOptional(true);
456 optionCategory
->setIncremental(true);
458 // look for BNP files in the BNP directry and add them to the main category
459 std::vector
<std::string
> fileList
;
460 NLMISC::CPath::getPathContent(_BnpDirectory
,false,false,true,fileList
);
461 for (uint32 i
=0;i
<fileList
.size();++i
)
462 if (NLMISC::toLowerAscii(NLMISC::CFile::getExtension(fileList
[i
]))=="bnp"
463 || NLMISC::toLowerAscii(NLMISC::CFile::getExtension(fileList
[i
]))=="snp")
464 _Categories
.addFile("main",NLMISC::toLowerAscii(NLMISC::CFile::getFilename(fileList
[i
])));
466 _Categories
.addFile("unpacked","root.bnp");
469 void CPackageDescription::updatePatchSizes(CBNPFileSet
& packageIndex
) const
473 void CPackageDescription::grabVersionNumber()
475 // if we've already grabbed the next version number then just return
476 if (_VersionNumberReserved
)
479 // if we don't have a version file to deal with then we're done
480 if (_NextVersionFile
.empty())
483 // read the version number from the '_NextVersion' file
484 nlassert(NLMISC::CFile::fileExists(_NextVersionFile
));
485 NLMISC::CSString fileContents
;
486 fileContents
.readFromFile(_NextVersionFile
);
487 uint32 versionFromFile
= fileContents
.atoui();
488 nlinfo("Version number read from file (%s) = %u",_NextVersionFile
.c_str(),versionFromFile
);
490 // select the higher of the 2 version numbers
491 _NextVersionNumber
= std::max(_NextVersionNumber
,versionFromFile
);
492 nlinfo("New version number = %u",_NextVersionNumber
);
494 // write the result +1 back to the '_NextVersion' file
495 (NLMISC::CSString()<<(_NextVersionNumber
+1)).writeToFile(_NextVersionFile
);
496 fileContents
.readFromFile(_NextVersionFile
);
497 versionFromFile
= fileContents
.atoui();
498 nlassert( versionFromFile
== (_NextVersionNumber
+1) );
500 // success so flag the version number as reserved
501 _VersionNumberReserved
= true;
504 uint32
CPackageDescription::getPackageVersionNumber()
506 return _NextVersionNumber
;
510 //-----------------------------------------------------------------------------
511 // Persistent data for CPackageDescription
512 //-----------------------------------------------------------------------------
514 #define PERSISTENT_CLASS CPackageDescription
515 #define PERSISTENT_DATA\
517 PROP(std::string,_IndexFileName)\
518 PROP(std::string,_PatchDirectory)\
519 PROP(std::string,_BnpDirectory)\
520 PROP(std::string,_RefDirectory)\
521 PROP(std::string,_NextVersionFile)\
523 //#pragma message( PERSISTENT_GENERATION_MESSAGE )
524 #include "game_share/persistent_data_template.h"
526 #undef PERSISTENT_CLASS
527 #undef PERSISTENT_DATA
530 //-----------------------------------------------------------------------------
532 //-----------------------------------------------------------------------------
534 static bool createNewProduct(std::string fileName
)
536 // normalise the file name (and path)
537 normalisePackageDescriptionFileName(fileName
);
539 // make sure the file doesn't exist
540 BOMB_IF(NLMISC::CFile::fileExists(fileName
),("Failed to careate new package because file already exists: "+fileName
).c_str(),return false);
542 // create the directory tree required for the file
543 NLMISC::CFile::createDirectoryTree(NLMISC::CFile::getPath(fileName
));
545 // create a new package, store it to a persistent data record and write the latter to a file
546 CPackageDescription package
;
547 static CPersistentDataRecordRyzomStore pdr
;
549 package
.storeToPdr(pdr
);
550 pdr
.writeToTxtFile(fileName
);
551 package
.setup(fileName
);
552 package
.createDirectories();
553 package
.buildDefaultFileList();
554 package
.storeToPdr(pdr
);
555 pdr
.writeToTxtFile(fileName
);
557 BOMB_IF(!NLMISC::CFile::fileExists(fileName
),("Failed to create new package file: "+fileName
).c_str(),return false);
558 nlinfo("New package description file created successfully: %s", fileName
.c_str());
563 static bool updateProduct(std::string fileName
)
565 // normalise the file name (and path)
566 normalisePackageDescriptionFileName(fileName
);
568 // make sure the file exists
569 BOMB_IF(!NLMISC::CFile::fileExists(fileName
),("Failed to process package because file not found: "+fileName
).c_str(),return false);
571 // read the package description file
572 CPackageDescription thePackage
;
573 thePackage
.setup(fileName
);
575 // read the index file for the package
576 CBNPFileSet packageIndex
;
577 thePackage
.readIndex(packageIndex
);
579 // update the files list in the index
580 thePackage
.updateIndexFileList(packageIndex
);
582 // update the index for the package
583 thePackage
.addVersion(packageIndex
);
585 // save the updated index file
586 thePackage
.writeIndex(packageIndex
);
588 // generate patches as required
589 thePackage
.generatePatches(packageIndex
);
591 // add patch sizes to index file
592 thePackage
.updatePatchSizes(packageIndex
);
594 // save the updated index file
595 thePackage
.writeIndex(packageIndex
);
597 // generate client index file
598 CProductDescriptionForClient theClientPackage
;
599 thePackage
.generateClientIndex(theClientPackage
,packageIndex
);
605 //-----------------------------------------------------------------------------
607 //-----------------------------------------------------------------------------
609 NLMISC_COMMAND(createNewProduct
,"create a new package description file","<package description file name>")
614 createNewProduct(args
[0]);
619 NLMISC_COMMAND(updateProduct
,"process a package","<package description file name>")
624 updateProduct(args
[0]);
629 NLMISC_COMMAND(go
,"perform a 'createNewProduct' if required and 'updateProduct' on patch_test/test0/test_package.xml","")
634 if (!NLMISC::CFile::fileExists("patch_test/test0/test_package.xml"))
635 createNewProduct("patch_test/test0/test_package.xml");
636 updateProduct("patch_test/test0/test_package.xml");