#include "lc_global.h" #include "lc_library.h" #include "lc_assetloader.h" #include "lc_thumbnailmanager.h" #include "lc_zipfile.h" #include "lc_file.h" #include "lc_piecedata.h" #include "lc_piecemanager.h" #include "lc_colors.h" #include "lc_texture.h" #include "lc_texturemanager.h" #include "lc_category.h" #include "lc_application.h" #include "lc_context.h" #include "lc_glextensions.h" #include "lc_synth.h" #include "lc_traintrack.h" #include "project.h" #include "lc_profile.h" #include "lc_meshloader.h" #include "lc_model.h" #include "lc_view.h" #include "lc_string.h" // Cache payloads use Qt compression with checksum validation. constexpr quint32 LC_LIBRARY_CACHE_VERSION = 0x0114; constexpr qint32 LC_LIBRARY_MESH_CACHE_VERSION = 2; enum class lcLibrarySourceType { Library, StudStyle }; struct lcLibrarySource { lcLibrarySource() = default; lcLibrarySource(const lcLibrarySource&) = delete; lcLibrarySource(lcLibrarySource&&) = delete; lcLibrarySource& operator=(const lcLibrarySource&) = delete; lcLibrarySource& operator=(lcLibrarySource&&) = delete; lcLibrarySourceType Type; std::map> Primitives; }; lcLibraryPrimitive::lcLibraryPrimitive(QString&& FileName, const char* Name, lcZipFileType ZipFileType, quint32 ZipFileIndex, bool Stud, bool StudStyle, bool SubFile) : mFileName(std::move(FileName)) { lcstrcpy(mName, Name); mZipFileType = ZipFileType; mZipFileIndex = ZipFileIndex; mState = lcPrimitiveState::NotLoaded; mStud = Stud; mStudStyle = StudStyle; mSubFile = SubFile; } lcPiecesLibrary::lcPiecesLibrary(lcStudStyle StudStyle, bool StudCylinderColorEnabled) : mStudStyle(StudStyle), mStudCylinderColorEnabled(StudCylinderColorEnabled) { mPieceManager = std::make_unique(); mTextureManager = std::make_unique(); mAssetLoader = std::unique_ptr(new lcAssetLoader(this)); mThumbnailManager = std::unique_ptr(new lcThumbnailManager(this)); mLoadingMesh = std::unique_ptr(new lcMesh); mLoadingMesh->CreateBox(); QStringList cachePathList = QStandardPaths::standardLocations(QStandardPaths::CacheLocation); mCachePath = cachePathList.first(); QDir Dir; Dir.mkpath(mCachePath); mBuffersDirty = false; mHasUnofficial = false; } lcPiecesLibrary::~lcPiecesLibrary() { mThumbnailManager.reset(); mCancelLoading.store(true); mAssetLoader.reset(); Unload(); ReleaseBuffers(); } void lcPiecesLibrary::Unload() { Q_ASSERT(QThread::currentThread() == thread()); const bool HadLoader = static_cast(mAssetLoader); if (HadLoader && mThumbnailManager) mThumbnailManager->Clear(); mAssetLoader.reset(); mSynthPieces.clear(); mPieceManager->Clear(); mSources.clear(); mTextureManager->Clear(); mTextureDiskEntries.clear(); for (std::map& Entries : mTextureArchiveEntries) Entries.clear(); mOfficialPieceCount = 0; for (std::unique_ptr& ZipFile : mZipFiles) ZipFile.reset(); if (HadLoader) mAssetLoader = std::unique_ptr(new lcAssetLoader(this)); } void lcPiecesLibrary::RemoveTemporaryPieceData() { mPieceManager->RemoveTemporaryPieceData(); } void lcPiecesLibrary::ReleaseProjectPieceData(Project* OwnerProject) { mPieceManager->ReleaseProjectPieceData(OwnerProject); } void lcPiecesLibrary::TransferProjectPieceData(Project* Source, Project* Destination) { mPieceManager->TransferProjectPieceData(Source, Destination); } bool lcPiecesLibrary::RenameModelPieceData(lcPieceData* PieceData, const char* NewName) { if (!mPieceManager->RenameModelPieceData(PieceData, NewName)) return false; if (PieceData->mState != lcAssetState::Failed) ClearPieceDataLoadError(PieceData); return true; } std::string lcPiecesLibrary::NormalizePieceDataName(const char* PieceName) { return lcPieceManager::NormalizePieceDataName(PieceName); } lcPieceDataRef lcPiecesLibrary::AcquireModelPieceData(const char* PieceName, Project* Project, bool& Reused) { lcPieceData* PieceData = mPieceManager->AcquireModelPieceData(PieceName, Project, Reused); lcPieceDataRef PieceDataRef = lcPieceDataRef::Adopt(PieceData, this); if (Reused) InvalidatePieceDataLoad(PieceData); return PieceDataRef; } lcResult lcPiecesLibrary::LoadExternalProject(const QFileInfo& FileInfo, bool Preview, bool DeferModelMeshRequests) { static thread_local std::vector LoadingPaths; const QString Path = FileInfo.canonicalFilePath(); if (std::find(LoadingPaths.begin(), LoadingPaths.end(), Path) != LoadingPaths.end()) return lcUnexpected(tr("External project include cycle at '%1'.").arg(Path)); LoadingPaths.push_back(Path); Project* ExternalProject = new Project(Preview); ExternalProject->SetDeferModelMeshRequests(DeferModelMeshRequests); const bool Loaded = ExternalProject->Load(FileInfo.absoluteFilePath(), false); LoadingPaths.pop_back(); if (!Loaded) { delete ExternalProject; return lcUnexpected(tr("Could not load external project '%1'.").arg(FileInfo.absoluteFilePath())); } return ExternalProject; } lcPieceData* lcPiecesLibrary::FindPieceData(const char* PieceName, Project* CurrentProject) { const std::string Name = NormalizePieceDataName(PieceName); return mPieceManager->FindPieceData(Name, CurrentProject); } lcPieceDataRef lcPiecesLibrary::AcquirePieceData(const char* PieceName, Project* CurrentProject, bool CreateMissing, bool SearchProjectFolder) { Q_ASSERT(QThread::currentThread() == thread()); QString ProjectPath; QString LoadError; if (lcPieceData* PieceData = FindPieceData(PieceName, CurrentProject)) return lcPieceDataRef(PieceData, this); if (CurrentProject && SearchProjectFolder && !CurrentProject->GetFileName().isEmpty()) ProjectPath = QFileInfo(CurrentProject->GetFileName()).absolutePath(); if (!ProjectPath.isEmpty()) { QFileInfo ProjectFile = QFileInfo(ProjectPath + QDir::separator() + PieceName); if (ProjectFile.isFile()) { const lcResult ExternalProject = LoadExternalProject(ProjectFile, CurrentProject && CurrentProject->IsPreview(), CurrentProject && CurrentProject->DefersModelMeshRequests()); if (ExternalProject) { lcPieceData* PieceData = mPieceManager->CreatePieceData(PieceName, CurrentProject); PieceData->CreateProject(ExternalProject.value(), PieceName); return lcPieceDataRef(PieceData, this); } else LoadError = ExternalProject.error(); } } if (CreateMissing) { lcPieceData* PieceData = mPieceManager->CreatePieceData(PieceName, CurrentProject); PieceData->SetFailed(LoadError.isEmpty() ? tr("Could not find piece '%1'.").arg(QString::fromLatin1(PieceName)) : LoadError); return lcPieceDataRef(PieceData, this); } return lcPieceDataRef(); } bool lcPiecesLibrary::RemapProjectPieceData(lcPieceData* PieceData, const QString& ProjectDirectory, bool IsPreview) { const QByteArray PieceName(PieceData->mFileName); const QFileInfo Candidate(QDir(ProjectDirectory).absoluteFilePath(QString::fromLatin1(PieceName.constData()))); if (PieceData->IsProject() && Candidate.isFile() && QFileInfo(PieceData->GetProject()->GetFileName()).canonicalFilePath() == Candidate.canonicalFilePath()) return false; if (!PieceData->IsProject() && !Candidate.isFile()) return false; InvalidatePieceDataLoad(PieceData); if (Candidate.isFile()) { const lcResult ExternalProject = LoadExternalProject(Candidate, IsPreview, true); if (ExternalProject) PieceData->CreateProject(ExternalProject.value(), PieceName.constData()); else { PieceData->DetachContainer(); PieceData->SetFailed(ExternalProject.error()); } } else { PieceData->DetachContainer(); PieceData->SetFailed(tr("Could not find external project '%1'.").arg(Candidate.absoluteFilePath())); } NotifyAssetStatusChanged(); return true; } lcTextureRef lcPiecesLibrary::AcquireTexture(const char* TextureName, Project* CurrentProject, bool SearchProjectFolder) { QString ProjectPath; if (SearchProjectFolder && CurrentProject) { QString FileName = CurrentProject->GetFileName(); if (!FileName.isEmpty()) ProjectPath = QFileInfo(FileName).absolutePath(); } std::map> EmbeddedData; if (CurrentProject) EmbeddedData = CurrentProject->GetEmbeddedDataLookup(); lcFileSource Source; return AcquireTexture(TextureName, std::vector{ ProjectPath }, EmbeddedData, Source); } QString lcPiecesLibrary::FindProjectTextureFile(const QString& ProjectPath, const QString& TextureName) { QDir Directory(ProjectPath); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto SkipEmptyParts = Qt::SkipEmptyParts; #else const auto SkipEmptyParts = QString::SplitBehavior::SkipEmptyParts; #endif const QStringList PathComponents = (TextureName + QLatin1String(".png")).split(QLatin1Char('/'), SkipEmptyParts); for (int ComponentIdx = 0; ComponentIdx < PathComponents.size(); ComponentIdx++) { const QString& Component = PathComponents[ComponentIdx]; const bool LastComponent = ComponentIdx == PathComponents.size() - 1; if (Component == QLatin1String(".") || Component == QLatin1String("..")) { if (LastComponent || !Directory.cd(Component)) return QString(); continue; } QFileInfo Entry(Directory, Component); if (LastComponent ? !Entry.isFile() : !Entry.isDir()) { Entry = QFileInfo(); for (const QFileInfo& Candidate : Directory.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) { if (Candidate.fileName().compare(Component, Qt::CaseInsensitive) == 0 && (LastComponent ? Candidate.isFile() : Candidate.isDir())) { Entry = Candidate; break; } } } if (LastComponent) return Entry.isFile() ? Entry.absoluteFilePath() : QString(); if (!Entry.isDir()) return QString(); Directory.setPath(Entry.absoluteFilePath()); } return QString(); } lcFileSource lcPiecesLibrary::ResolveTextureSource(const char* TextureName, const std::vector& SearchDirectories, const std::map>& EmbeddedData, QString& ProjectPath) { Q_ASSERT(QThread::currentThread() == thread()); ProjectPath.clear(); if (!TextureName[0] || strlen(TextureName) >= LC_TEXTURE_NAME_LEN) return lcFileSource(); const bool ArchiveLibrary = static_cast(mZipFiles[static_cast(lcZipFileType::Official)]); lcFileSource Source; const QString Name = QString::fromLatin1(TextureName); const QString Candidates[] = { QLatin1String("textures/") + Name, Name }; const QString Folders[] = { QStringLiteral("p/"), QStringLiteral("parts/"), QStringLiteral("models/"), QStringLiteral("unofficial/p/"), QStringLiteral("unofficial/parts/") }; // Complete the prefixed search before considering any unprefixed location. for (const QString& Candidate : Candidates) { QString EmbeddedName = Candidate + QLatin1String(".png"); EmbeddedName.replace(QLatin1Char('\\'), QLatin1Char('/')); const auto Embedded = EmbeddedData.find(QDir::cleanPath(EmbeddedName).toUpper()); if (Embedded != EmbeddedData.end()) { Source.EmbeddedData = Embedded->second; break; } for (const QString& Directory : SearchDirectories) { if (Directory.isEmpty()) continue; Source.FilePath = FindProjectTextureFile(Directory, Candidate); if (!Source.FilePath.isEmpty()) { ProjectPath = Directory; break; } } if (!Source.FilePath.isEmpty()) break; for (const QString& Folder : Folders) { if (ArchiveLibrary) { const bool Unofficial = Folder.startsWith(QLatin1String("unofficial/")); const int Type = static_cast(Unofficial ? lcZipFileType::Unofficial : lcZipFileType::Official); QString EntryName; if (Unofficial) EntryName = Folder.mid(11) + Candidate + QLatin1String(".png"); else EntryName = QLatin1String("ldraw/") + Folder + Candidate + QLatin1String(".png"); const std::map& Entries = mTextureArchiveEntries[Type]; const auto Entry = Entries.find(QDir::cleanPath(EntryName).toUpper()); if (Entry != Entries.end()) { Source.ArchiveType = Type; Source.ArchiveIndex = Entry->second; break; } } else { const QString EntryName = QDir::cleanPath(Folder + Candidate + QLatin1String(".png")).toUpper(); const auto Entry = mTextureDiskEntries.find(EntryName); if (Entry != mTextureDiskEntries.end()) { Source.FilePath = Entry->second; break; } } } if (!Source.FilePath.isEmpty() || Source.ArchiveIndex != -1) break; } if (!Source.FilePath.isEmpty()) { const QString CanonicalPath = QFileInfo(Source.FilePath).canonicalFilePath(); Source.FilePath = CanonicalPath.isEmpty() ? QFileInfo(Source.FilePath).absoluteFilePath() : CanonicalPath; } return Source; } lcTextureRef lcPiecesLibrary::AcquireTexture(const char* TextureName, const std::vector& SearchDirectories, const std::map>& EmbeddedData, lcFileSource& Source) { QString ProjectPath; Source = ResolveTextureSource(TextureName, SearchDirectories, EmbeddedData, ProjectPath); return lcTextureRef::Adopt(mTextureManager->AcquireTexture(TextureName, Source, ProjectPath), this); } void lcPiecesLibrary::RequestTextureLoad(lcTexture* Texture, lcLoadPriority LoadPriority) { Q_ASSERT(QThread::currentThread() == thread()); mAssetLoader->RequestTextureLoad(Texture, Texture->mSource, LoadPriority); } bool lcPiecesLibrary::WaitForTexture(const lcTextureRef& TextureRef) { Q_ASSERT(QThread::currentThread() == thread()); return mAssetLoader->WaitForTexture(TextureRef.Get(), TextureRef->mSource); } lcTextureBuildResult lcPiecesLibrary::BuildTextureData(const lcFileSource& Source) { const lcResult> File = OpenFile(Source); if (!File) { lcTextureBuildResult Result; Result.Error = lcTextureLoadError::DecodeFailed; Result.ErrorDetails = File.error(); return Result; } lcTextureBuildResult Result = mTextureManager->BuildTextureData(*File.value()); if (Result.DecodedImage.isNull() && Source.EmbeddedData) Result.ErrorDetails = tr("Could not decode embedded texture '%1'.").arg(Source.EmbeddedData->FileName); return Result; } lcResult> lcPiecesLibrary::OpenFile(const lcFileSource& Source) { if (Source.EmbeddedData) { const lcEmbeddedData& Data = *Source.EmbeddedData; if (!Data.Error.isEmpty()) return lcUnexpected(Data.Error); if (!Data.HasValidEncoding()) return lcUnexpected(tr("Invalid Base64 data for embedded file '%1'.").arg(Data.FileName)); const QByteArray Bytes = QByteArray::fromBase64(Data.EncodedData); std::unique_ptr File(new lcMemFile); if (File->WriteBuffer(Bytes.constData(), Bytes.size()) != static_cast(Bytes.size())) return lcUnexpected(tr("Could not read embedded file '%1'.").arg(Data.FileName)); File->Seek(0, SEEK_SET); return std::unique_ptr(std::move(File)); } if (!Source.FilePath.isEmpty()) { std::unique_ptr File(new lcDiskFile(Source.FilePath)); if (!File->Open(QIODevice::ReadOnly)) return lcUnexpected(tr("Could not open file '%1'.").arg(Source.FilePath)); return std::unique_ptr(std::move(File)); } if (Source.ArchiveType < 0 || Source.ArchiveType >= static_cast(lcZipFileType::Count) || Source.ArchiveIndex < 0) return lcUnexpected(tr("Invalid archive file source.")); std::unique_ptr File(new lcMemFile); // Source archives are stable until workers are drained; extraction locks // the selected archive internally without serializing unrelated archives. const std::unique_ptr& Archive = mZipFiles[Source.ArchiveType]; if (!Archive || static_cast(Source.ArchiveIndex) >= Archive->mFiles.size()) return lcUnexpected(tr("Could not read archive entry %1.").arg(Source.ArchiveIndex)); if (!Archive->ExtractFile(Source.ArchiveIndex, *File)) return lcUnexpected(tr("Could not read archive file '%1'.").arg(QString::fromLatin1(Archive->mFiles[Source.ArchiveIndex].file_name))); File->Seek(0, SEEK_SET); return std::unique_ptr(std::move(File)); } bool lcPiecesLibrary::Load(const QString& LibraryPath, bool ShowProgress, bool OfficialLibrary) { Unload(); if (OpenArchive(LibraryPath, lcZipFileType::Official)) { if (OfficialLibrary) mOfficialPieceCount = static_cast(mPieceManager->GetLibraryPieceDataCount()); LoadColors(); mLibraryDir = QFileInfo(LibraryPath).absoluteDir(); QString UnofficialFileName = mLibraryDir.absoluteFilePath(QLatin1String("ldrawunf.zip")); if (!OpenArchive(UnofficialFileName, lcZipFileType::Unofficial)) UnofficialFileName.clear(); ReadArchiveDescriptions(LibraryPath, UnofficialFileName); } else { mLibraryDir.setPath(LibraryPath); if (OpenDirectory(mLibraryDir, ShowProgress)) LoadColors(); else return false; } UpdateStudStyleSource(); lcLoadDefaultCategories(); lcSynthInit(); lcTrainTrackInfo::Initialize(this); return true; } void lcPiecesLibrary::LoadColors() { QString CustomColorsPath = lcGetProfileString(LC_PROFILE_COLOR_CONFIG); if (!CustomColorsPath.isEmpty()) { lcDiskFile ColorFile(CustomColorsPath); if (ColorFile.Open(QIODevice::ReadOnly) && lcLoadColorFile(ColorFile, mStudStyle)) { UpdateLoadingMeshColors(); emit ColorsLoaded(); return; } } if (mZipFiles[static_cast(lcZipFileType::Official)]) { lcMemFile ColorFile; if (!mZipFiles[static_cast(lcZipFileType::Official)]->ExtractFile("ldraw/ldconfig.ldr", ColorFile) || !lcLoadColorFile(ColorFile, mStudStyle)) lcLoadDefaultColors(mStudStyle); } else { lcDiskFile ColorFile(mLibraryDir.absoluteFilePath(QLatin1String("ldconfig.ldr"))); if (!ColorFile.Open(QIODevice::ReadOnly) || !lcLoadColorFile(ColorFile, mStudStyle)) { ColorFile.SetFileName(mLibraryDir.absoluteFilePath(QLatin1String("LDConfig.ldr"))); if (!ColorFile.Open(QIODevice::ReadOnly) || !lcLoadColorFile(ColorFile, mStudStyle)) lcLoadDefaultColors(mStudStyle); } } UpdateLoadingMeshColors(); emit ColorsLoaded(); } void lcPiecesLibrary::UpdateLoadingMeshColors() { lcMeshSection* Sections = mLoadingMesh->mLods[LC_MESH_LOD_HIGH].Sections; Sections[0].ColorIndex = gDefaultColor; Sections[1].ColorIndex = gEdgeColor; } bool lcPiecesLibrary::IsStudPrimitive(const char* FileName) { return memcmp(FileName, "STU", 3) == 0; } bool lcPiecesLibrary::IsStudStylePrimitive(const char* FileName) { constexpr std::array StudStylePrimitives = { "2-4STUD4.DAT", "STUD.DAT", "STUD2.DAT", "STUD2A.DAT", "STUD3.DAT", "STUD4.DAT", "STUD4A.DAT", "STUD4H.DAT", "8/STUD.DAT", "8/STUD2.DAT", "8/STUD2A.DAT", "8/STUD3.DAT", "8/STUD4.DAT", "8/STUD4A.DAT", "8/STUD4H.DAT" }; for (const char* StudStylePrimitive : StudStylePrimitives) if (!strcmp(StudStylePrimitive, FileName)) return true; return false; } qint32 lcPiecesLibrary::MeshCacheSettingsKey(lcStudStyle StudStyle, bool StudCylinderColorEnabled) { return (LC_LIBRARY_MESH_CACHE_VERSION << 16) | (static_cast(StudStyle) << 1) | static_cast(StudCylinderColorEnabled); } void lcPiecesLibrary::UpdateStudStyleSource() { if (!mSources.empty() && mSources.front()->Type == lcLibrarySourceType::StudStyle) mSources.erase(mSources.begin()); mZipFiles[static_cast(lcZipFileType::StudStyle)].reset(); if (mStudStyle == lcStudStyle::Plain || (mStudStyle >= lcStudStyle::HighContrast && !mStudCylinderColorEnabled)) return; const QLatin1String FileNames[] = { QLatin1String(""), // Plain QLatin1String(":/resources/studlogo1.zip"), // ThinLinesLogo QLatin1String(":/resources/studlogo2.zip"), // OutlineLogo QLatin1String(":/resources/studlogo3.zip"), // SharpTopLogo QLatin1String(":/resources/studlogo4.zip"), // RoundedTopLogo QLatin1String(":/resources/studlogo5.zip"), // FlattenedLogo QLatin1String(":/resources/studslegostyle1.zip"), // HighContrast QLatin1String(":/resources/studslegostyle2.zip") // HighContrastLogo }; LC_ARRAY_SIZE_CHECK(FileNames, lcStudStyle::Count); std::unique_ptr StudStyleFile(new lcDiskFile(FileNames[static_cast(mStudStyle)])); if (StudStyleFile->Open(QIODevice::ReadOnly)) OpenArchive(std::move(StudStyleFile), lcZipFileType::StudStyle); } bool lcPiecesLibrary::OpenArchive(const QString& FileName, lcZipFileType ZipFileType) { std::unique_ptr File(new lcDiskFile(FileName)); if (!File->Open(QIODevice::ReadOnly)) return false; return OpenArchive(std::move(File), ZipFileType); } bool lcPiecesLibrary::OpenArchive(std::unique_ptr File, lcZipFileType ZipFileType) { std::unique_ptr ZipFile(new lcZipFile()); if (!ZipFile->OpenRead(std::move(File))) return false; mTextureArchiveEntries[static_cast(ZipFileType)].clear(); std::unique_ptr Source(new lcLibrarySource); Source->Type = ZipFileType != lcZipFileType::StudStyle ? lcLibrarySourceType::Library : lcLibrarySourceType::StudStyle; for (quint32 FileIdx = 0; FileIdx < ZipFile->mFiles.size(); FileIdx++) { lcZipFileInfo& FileInfo = ZipFile->mFiles[FileIdx]; const std::string NormalizedName = NormalizePieceDataName(FileInfo.file_name); const char* Name = NormalizedName.c_str(); if (NormalizedName.size() <= 4) continue; const char* Extension = Name + NormalizedName.size() - 4; if (memcmp(Extension, ".DAT", 4)) { if (!memcmp(Extension, ".PNG", 4)) { if (ZipFileType == lcZipFileType::Official || ZipFileType == lcZipFileType::Unofficial) mTextureArchiveEntries[static_cast(ZipFileType)].emplace(QDir::cleanPath(QString::fromLatin1(Name)).toUpper(), FileIdx); } continue; } if (ZipFileType == lcZipFileType::Official) { if (strncmp(Name, "LDRAW/", 6)) continue; Name += 6; } if (!strncmp(Name, "PARTS/", 6)) { Name += 6; if (strncmp(Name, "S/", 2)) { lcPieceData* PieceData = FindPieceData(Name, nullptr); // Unofficial additions must not replace registered official parts. if (PieceData && ZipFileType == lcZipFileType::Unofficial && PieceData->mZipFileType == lcZipFileType::Official) continue; if (!PieceData) { PieceData = mPieceManager->CreateLibraryPieceData(Name, FileInfo.file_name + (Name - NormalizedName.c_str())); } PieceData->SetZipFile(ZipFileType, FileIdx); } else Source->Primitives[Name] = std::make_unique(QString(), FileInfo.file_name + (Name - NormalizedName.c_str()), ZipFileType, FileIdx, false, false, true); } else if (!strncmp(Name, "P/", 2)) { Name += 2; Source->Primitives[Name] = std::make_unique(QString(), FileInfo.file_name + (Name - NormalizedName.c_str()), ZipFileType, FileIdx, IsStudPrimitive(Name), IsStudStylePrimitive(Name), false); } } mZipFiles[static_cast(ZipFileType)] = std::move(ZipFile); if (ZipFileType != lcZipFileType::StudStyle) mSources.emplace_back(std::move(Source)); else mSources.insert(mSources.begin(), std::move(Source)); return true; } void lcPiecesLibrary::ReadArchiveDescriptions(const QString& OfficialFileName, const QString& UnofficialFileName) { QFileInfo OfficialInfo(OfficialFileName); QFileInfo UnofficialInfo(UnofficialFileName); mArchiveCheckSum[0] = OfficialInfo.size(); mArchiveCheckSum[1] = OfficialInfo.lastModified().toMSecsSinceEpoch(); if (!UnofficialFileName.isEmpty()) { mArchiveCheckSum[2] = UnofficialInfo.size(); mArchiveCheckSum[3] = UnofficialInfo.lastModified().toMSecsSinceEpoch(); } else { mArchiveCheckSum[2] = 0; mArchiveCheckSum[3] = 0; } QString IndexFileName = QFileInfo(QDir(mCachePath), QLatin1String("index")).absoluteFilePath(); if (!LoadCacheIndex(IndexFileName)) { lcMemFile PieceFile; for (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData()) { mZipFiles[static_cast(PieceData->mZipFileType)]->ExtractFile(PieceData->mZipFileIndex, PieceFile, 256); PieceFile.Seek(0, SEEK_END); PieceFile.WriteU8(0); char* Src = (char*)PieceFile.mBuffer + 2; char* Dst = PieceData->m_strDescription; for (;;) { if (*Src != '\r' && *Src != '\n' && *Src && Dst - PieceData->m_strDescription < (int)sizeof(PieceData->m_strDescription) - 1) { *Dst++ = *Src++; continue; } *Dst = 0; break; } } ClassifyPieces(DirectoryFilePaths()); SaveArchiveCacheIndex(IndexFileName); } } bool lcPiecesLibrary::OpenDirectory(const QDir& LibraryDir, bool ShowProgress) { mTextureDiskEntries.clear(); const QLatin1String BaseFolders[] = { QLatin1String(""), QLatin1String("unofficial/") }; constexpr int NumBaseFolders = LC_ARRAY_COUNT(BaseFolders); QFileInfoList FileLists[NumBaseFolders]; for (unsigned int BaseFolderIdx = 0; BaseFolderIdx < NumBaseFolders; BaseFolderIdx++) { QString ParstPath = QDir(LibraryDir.absoluteFilePath(BaseFolders[BaseFolderIdx])).absoluteFilePath(QLatin1String("parts/")); QDir Dir = QDir(ParstPath, QLatin1String("*.dat"), QDir::SortFlags(QDir::Name | QDir::IgnoreCase), QDir::Files | QDir::Hidden | QDir::Readable); FileLists[BaseFolderIdx] = Dir.entryInfoList(); } if (FileLists[static_cast(lcLibraryFolderType::Official)].isEmpty()) return false; mHasUnofficial = !FileLists[static_cast(lcLibraryFolderType::Unofficial)].isEmpty(); ReadDirectoryDescriptions(FileLists, ShowProgress); constexpr std::pair PrimitiveDirectories[] = { { "p/", false }, { "parts/s/", true } }; for (unsigned int BaseFolderIdx = 0; BaseFolderIdx < LC_ARRAY_COUNT(BaseFolders); BaseFolderIdx++) { std::unique_ptr Source(new lcLibrarySource); Source->Type = lcLibrarySourceType::Library; QDir BaseDir(LibraryDir.absoluteFilePath(QLatin1String(BaseFolders[BaseFolderIdx]))); for (const auto& [Path, SubFile] : PrimitiveDirectories) { QString ChildPath = BaseDir.absoluteFilePath(QLatin1String(Path)); QDirIterator DirIterator(ChildPath, QStringList() << QLatin1String("*.dat"), QDir::Files | QDir::Hidden | QDir::Readable, QDirIterator::Subdirectories); while (DirIterator.hasNext()) { QString FileName = DirIterator.next(); QByteArray FileString = BaseDir.relativeFilePath(FileName).toLatin1(); const char* RelativeName = strchr(FileString.constData(), '/') + 1; const std::string Name = NormalizePieceDataName(RelativeName); if (Name.size() <= 4 || Name.compare(Name.size() - 4, 4, ".DAT")) continue; if (BaseFolderIdx > 0 && FindPrimitive(Name.c_str())) continue; if (BaseFolderIdx == static_cast(lcLibraryFolderType::Unofficial)) mHasUnofficial = true; Source->Primitives[Name] = std::make_unique(std::move(FileName), RelativeName, lcZipFileType::Count, 0, !SubFile && IsStudPrimitive(Name.c_str()), IsStudStylePrimitive(Name.c_str()), SubFile); } } mSources.emplace_back(std::move(Source)); // Index PNG paths once; UI-thread texture lookup must not enumerate library folders. const char* TextureDirectories[] = { "p/", "parts/", "models/" }; for (const char* TextureDirectory : TextureDirectories) { const QString ChildPath = BaseDir.absoluteFilePath(QLatin1String(TextureDirectory)); QDirIterator DirIterator(ChildPath, QStringList() << QLatin1String("*.png"), QDir::Files | QDir::Hidden | QDir::Readable, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); while (DirIterator.hasNext()) { const QString FilePath = DirIterator.next(); const QString Name = QDir::cleanPath(LibraryDir.relativeFilePath(FilePath)).toUpper(); mTextureDiskEntries.emplace(Name, FilePath); } } } return true; } void lcPiecesLibrary::ReadDirectoryDescriptions(const QFileInfoList (&FileLists)[static_cast(lcLibraryFolderType::Count)], bool ShowProgress) { QString IndexFileName = QFileInfo(QDir(mCachePath), QLatin1String("index")).absoluteFilePath(); lcMemFile IndexFile; struct CachedDescription { lcPieceData::CacheData CacheData; quint8 FolderType; quint64 FileTime; }; std::map CachedDescriptions; if (ReadCacheFile(IndexFileName, IndexFile, CacheType::Directory) && IndexFile.ReadQString() == mLibraryDir.absolutePath()) { quint32 Count; if (IndexFile.ReadU32(&Count, 1) == 1) { for (quint32 Index = 0; Index < Count; Index++) { std::string FileName; CachedDescription Entry; if (!ReadIndexString(IndexFile, FileName) || FileName.empty() || !ReadIndexString(IndexFile, Entry.CacheData.Description) || !ReadIndexString(IndexFile, Entry.CacheData.PatternParentId) || IndexFile.ReadU8(&Entry.CacheData.Flags, 1) != 1 || IndexFile.ReadU8(&Entry.FolderType, 1) != 1 || IndexFile.ReadU64(&Entry.FileTime, 1) != 1 || Entry.FolderType >= static_cast(lcLibraryFolderType::Count) || !CachedDescriptions.emplace(std::move(FileName), std::move(Entry)).second) { CachedDescriptions.clear(); break; } } } } struct DescriptionJob { lcPieceData* PieceData; QFileInfo FileInfo; lcLibraryFolderType FolderType; bool Modified = false; }; std::vector Jobs; for (int FolderIdx = 0; FolderIdx < static_cast(lcLibraryFolderType::Count); FolderIdx++) { const QFileInfoList& FileList = FileLists[FolderIdx]; for (int FileIdx = 0; FileIdx < FileList.size(); FileIdx++) { QByteArray FileString = FileList[FileIdx].fileName().toLatin1(); const std::string Name = NormalizePieceDataName(FileString.constData()); if (FolderIdx > 0 && mPieceManager->FindPieceData(Name, nullptr)) continue; lcPieceData* PieceData = mPieceManager->CreateLibraryPieceData(Name, FileString.constData()); Jobs.push_back({ PieceData, FileList[FileIdx], static_cast(FolderIdx) }); } } // Only process entries retained by the normalized registry after discovery. Jobs.erase(std::remove_if(Jobs.begin(), Jobs.end(), [this](const DescriptionJob& Job) { return FindPieceData(Job.FileInfo.fileName().toLatin1().constData(), nullptr) != Job.PieceData; }), Jobs.end()); QAtomicInt FilesLoaded; auto ReadDescriptions = [&CachedDescriptions, &FilesLoaded](DescriptionJob& Job) { FilesLoaded.ref(); lcPieceData* PieceData = Job.PieceData; lcDiskFile PieceFile(Job.FileInfo.absoluteFilePath()); char Line[1024]; const auto CachedIt = CachedDescriptions.find(PieceData->mFileName); const quint64 FileTime = Job.FileInfo.lastModified().toMSecsSinceEpoch(); if (CachedIt != CachedDescriptions.end() && CachedIt->second.FileTime == FileTime && CachedIt->second.FolderType == static_cast(Job.FolderType) && PieceData->InitializeFromCache(CachedIt->second.CacheData)) return; Job.Modified = true; if (!PieceFile.Open(QIODevice::ReadOnly) || !PieceFile.ReadLine(Line, sizeof(Line))) { lcstrcpy(PieceData->m_strDescription, "Unknown"); return; } const char* Src = Line + 2; char* Dst = PieceData->m_strDescription; for (;;) { if (*Src != '\r' && *Src != '\n' && *Src && Dst - PieceData->m_strDescription < (int)sizeof(PieceData->m_strDescription) - 1) { *Dst++ = *Src++; continue; } *Dst = 0; break; } }; QProgressDialog* ProgressDialog = new QProgressDialog(nullptr); ProgressDialog->setWindowFlags(ProgressDialog->windowFlags() & ~Qt::WindowCloseButtonHint); ProgressDialog->setWindowTitle(tr("Initializing")); ProgressDialog->setLabelText(tr("Loading Parts Library")); ProgressDialog->setMaximum(static_cast(Jobs.size())); ProgressDialog->setMinimum(0); ProgressDialog->setValue(0); ProgressDialog->setCancelButton(nullptr); ProgressDialog->setAutoReset(false); if (ShowProgress) ProgressDialog->show(); QFuture LoadFuture = QtConcurrent::map(Jobs, ReadDescriptions); while (!LoadFuture.isFinished()) { ProgressDialog->setValue(FilesLoaded); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } ProgressDialog->setValue(FilesLoaded); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); ProgressDialog->deleteLater(); // A target's change, addition or removal can invalidate otherwise unchanged aliases. const bool Modified = std::any_of(Jobs.begin(), Jobs.end(), [](const DescriptionJob& Job) { return Job.Modified; }); if (Modified || CachedDescriptions.size() != Jobs.size()) { DirectoryFilePaths FilePaths; for (const DescriptionJob& Job : Jobs) FilePaths.emplace(Job.PieceData, Job.FileInfo.absoluteFilePath()); ClassifyPieces(FilePaths); lcMemFile NewIndexFile; NewIndexFile.WriteQString(mLibraryDir.absolutePath()); NewIndexFile.WriteU32(static_cast(Jobs.size())); for (const DescriptionJob& Job : Jobs) { const lcPieceData* PieceData = Job.PieceData; const lcPieceData::CacheData CacheData = PieceData->GetCacheData(); if (!WriteIndexString(NewIndexFile, PieceData->mFileName) || !WriteIndexString(NewIndexFile, CacheData.Description) || !WriteIndexString(NewIndexFile, CacheData.PatternParentId)) return; NewIndexFile.WriteU8(CacheData.Flags); NewIndexFile.WriteU8(static_cast(Job.FolderType)); const quint64 FileTime = Job.FileInfo.lastModified().toMSecsSinceEpoch(); NewIndexFile.WriteU64(FileTime); } WriteCacheFile(IndexFileName, NewIndexFile, CacheType::Directory); } } bool lcPiecesLibrary::ReadCacheFile(const QString& FileName, lcMemFile& CacheFile, CacheType Type) { QFile File(FileName); if (!File.open(QIODevice::ReadOnly)) return false; quint32 CacheVersion, CacheFlags; if (File.read(reinterpret_cast(&CacheVersion), sizeof(CacheVersion)) != sizeof(CacheVersion) || CacheVersion != LC_LIBRARY_CACHE_VERSION) return false; if (File.read(reinterpret_cast(&CacheFlags), sizeof(CacheFlags)) != sizeof(CacheFlags) || CacheFlags != static_cast(Type)) return false; if (Type == CacheType::Archive) { qint64 CacheCheckSum[4]; if (File.read(reinterpret_cast(CacheCheckSum), sizeof(CacheCheckSum)) != sizeof(CacheCheckSum) || memcmp(CacheCheckSum, mArchiveCheckSum, sizeof(CacheCheckSum))) return false; } quint32 UncompressedSize; if (File.read(reinterpret_cast(&UncompressedSize), sizeof(UncompressedSize)) != sizeof(UncompressedSize)) return false; const QByteArray Data = qUncompress(File.readAll()); if (Data.isEmpty() || static_cast(Data.size()) != UncompressedSize) return false; CacheFile.SetLength(Data.size()); CacheFile.Seek(0, SEEK_SET); if (!CacheFile.mBuffer) return false; CacheFile.WriteBuffer(Data.constData(), Data.size()); CacheFile.Seek(0, SEEK_SET); return true; } bool lcPiecesLibrary::WriteCacheFile(const QString& FileName, const lcMemFile& CacheFile, CacheType Type) { if (CacheFile.GetLength() > static_cast(std::numeric_limits::max())) return false; QFile File(FileName); if (!File.open(QIODevice::WriteOnly)) return false; constexpr quint32 CacheVersion = LC_LIBRARY_CACHE_VERSION; const quint32 CacheFlags = static_cast(Type); if (File.write(reinterpret_cast(&CacheVersion), sizeof(CacheVersion)) != sizeof(CacheVersion) || File.write(reinterpret_cast(&CacheFlags), sizeof(CacheFlags)) != sizeof(CacheFlags)) return false; if (Type == CacheType::Archive && File.write(reinterpret_cast(mArchiveCheckSum), sizeof(mArchiveCheckSum)) != sizeof(mArchiveCheckSum)) return false; const quint32 UncompressedSize = static_cast(CacheFile.GetLength()); if (File.write(reinterpret_cast(&UncompressedSize), sizeof(UncompressedSize)) != sizeof(UncompressedSize)) return false; const QByteArray CompressedData = qCompress(CacheFile.mBuffer, static_cast(CacheFile.GetLength())); return !CompressedData.isEmpty() && File.write(CompressedData) == CompressedData.size(); } bool lcPiecesLibrary::ReadIndexString(lcMemFile& File, std::string& String) { quint8 Length; if (File.ReadU8(&Length, 1) != 1) return false; String.resize(Length); return File.ReadBuffer(String.data(), Length) == Length; } bool lcPiecesLibrary::WriteIndexString(lcMemFile& File, const std::string& String) { if (String.size() > 255) return false; const quint8 Length = static_cast(String.size()); return File.WriteU8(&Length, 1) == 1 && File.WriteBuffer(String.data(), Length) == Length; } std::string lcPiecesLibrary::ReadAliasTarget(lcFile& File) { std::string Target; char Buffer[1024]; while (File.ReadLine(Buffer, sizeof(Buffer))) { QString Line = QString::fromLatin1(Buffer).trimmed(); if (Line.isEmpty()) continue; QTextStream Stream(&Line, QIODevice::ReadOnly); int LineType; Stream >> LineType; if (Stream.status() != QTextStream::Ok) return std::string(); if (LineType == 0) continue; // Aliases contain exactly one reference and no direct geometry. if (LineType != 1 || !Target.empty()) return std::string(); int Color; Stream >> Color; for (int ValueIndex = 0; ValueIndex < 12; ValueIndex++) { float Value; Stream >> Value; } if (Stream.status() != QTextStream::Ok) return std::string(); const QByteArray Name = Stream.readAll().trimmed().toLatin1(); if (Name.isEmpty() || Name.size() >= LC_PIECE_NAME_LEN) return std::string(); Target = NormalizePieceDataName(Name.constData()); } return Target; } void lcPiecesLibrary::ClassifyPieces(const DirectoryFilePaths& FilePaths) { const std::vector Parts = mPieceManager->GetLibraryPieceData(); std::unordered_map AliasTargets; for (lcPieceData* PieceData : Parts) { const std::string Name = NormalizePieceDataName(PieceData->mFileName); const std::vector ParentIds = PieceData->Classify(Name); if (PieceData->IsAlias()) { lcFileSource Source; if (PieceData->mZipFileType != lcZipFileType::Count) { Source.ArchiveType = static_cast(PieceData->mZipFileType); Source.ArchiveIndex = PieceData->mZipFileIndex; } else { const auto FileIt = FilePaths.find(PieceData); if (FileIt == FilePaths.end()) { AliasTargets.emplace(PieceData, nullptr); continue; } Source.FilePath = FileIt->second; } const lcResult> File = OpenFile(Source); lcPieceData* Target = nullptr; if (File) { const std::string TargetId = ReadAliasTarget(*File.value()); if (!TargetId.empty()) Target = FindPieceData(TargetId.c_str(), nullptr); } AliasTargets.emplace(PieceData, Target); continue; } for (const std::string& ParentId : ParentIds) { const lcPieceData* Parent = FindPieceData(ParentId.c_str(), nullptr); if (Parent && Parent->IsLibraryPiece()) { PieceData->SetPatternParentId(ParentId); break; } } } std::unordered_set ResolvedAliases; for (const auto& [Alias, Target] : AliasTargets) { if (ResolvedAliases.find(Alias) != ResolvedAliases.end()) continue; std::vector Chain; lcPieceData* Current = Alias; bool Cycle = false; while (Current) { const auto TargetIt = AliasTargets.find(Current); if (TargetIt == AliasTargets.end() || ResolvedAliases.find(Current) != ResolvedAliases.end()) break; if (std::find(Chain.begin(), Chain.end(), Current) != Chain.end()) { Cycle = true; break; } Chain.push_back(Current); Current = TargetIt->second; } // Only the referencing alias inherits decoration from its target. for (auto PieceIt = Chain.rbegin(); PieceIt != Chain.rend(); PieceIt++) { lcPieceData* PieceData = *PieceIt; PieceData->SetAliasTarget(Cycle ? nullptr : Current); Current = PieceData; ResolvedAliases.insert(PieceData); } } } bool lcPiecesLibrary::LoadCacheIndex(const QString& FileName) { lcMemFile IndexFile; if (!ReadCacheFile(FileName, IndexFile, CacheType::Archive)) return false; quint32 NumFiles; if (IndexFile.ReadU32(&NumFiles, 1) != 1 || NumFiles != mPieceManager->GetLibraryPieceDataCount()) return false; for (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData()) { lcPieceData::CacheData CacheData; if (!ReadIndexString(IndexFile, CacheData.Description) || !ReadIndexString(IndexFile, CacheData.PatternParentId) || IndexFile.ReadU8(&CacheData.Flags, 1) != 1 || !PieceData->InitializeFromCache(CacheData)) return false; } return true; } bool lcPiecesLibrary::SaveArchiveCacheIndex(const QString& FileName) { lcMemFile IndexFile; const quint32 NumFiles = (quint32)mPieceManager->GetLibraryPieceDataCount(); if (IndexFile.WriteBuffer((char*)&NumFiles, sizeof(NumFiles)) == 0) return false; for (const lcPieceData* PieceData : mPieceManager->GetLibraryPieceData()) { const lcPieceData::CacheData CacheData = PieceData->GetCacheData(); if (!WriteIndexString(IndexFile, CacheData.Description) || !WriteIndexString(IndexFile, CacheData.PatternParentId)) return false; IndexFile.WriteU8(CacheData.Flags); } return WriteCacheFile(FileName, IndexFile, CacheType::Archive); } void lcPiecesLibrary::RequestPieceDataLoad(lcPieceData* PieceData, lcLoadPriority LoadPriority) { Q_ASSERT(QThread::currentThread() == thread()); if (PieceData->IsProject() || (PieceData->IsModel() && !PieceData->GetModel()->HasDirectGeometry())) { PieceData->mState = lcAssetState::Loaded; ClearPieceDataLoadError(PieceData); return; } if (PieceData->mState == lcAssetState::Unloaded) { PieceData->mState = lcAssetState::Loading; ClearPieceDataLoadError(PieceData); const lcBoundingBox& Box = GetLoadingMesh()->mBoundingBox; if (!PieceData->IsModel()) PieceData->SetBoundingBox(Box.Min, Box.Max); else { const lcBoundingBox& ModelBox = PieceData->GetBoundingBox(); PieceData->SetBoundingBox(lcMin(ModelBox.Min, Box.Min), lcMax(ModelBox.Max, Box.Max)); } } if (PieceData->IsLoading()) mAssetLoader->RequestPieceLoad(PieceData, LoadPriority); } void lcPiecesLibrary::PieceDataLoadCancelled(lcPieceData* PieceData) { Q_ASSERT(QThread::currentThread() == thread()); PieceData->mState = lcAssetState::Unloaded; ClearPieceDataLoadError(PieceData); } void lcPiecesLibrary::NotifyAssetStatusChanged() { emit AssetStatusChanged(); } bool lcPiecesLibrary::EnsurePieceDataAssetsReady(const lcPieceDataRef& PieceData) { const bool PieceReady = WaitForPieceData(PieceData); if (PieceData->IsModel()) return static_cast(PieceData->GetModel()->EnsureAssetsReady()) && PieceReady; if (PieceData->IsProject()) { const bool ProjectReady = static_cast(PieceData->GetProject()->EnsureAssetsReady()); PieceData->GetProject()->UpdatePieceData(PieceData.Get()); return ProjectReady && PieceReady; } return PieceReady; } bool lcPiecesLibrary::WaitForPieceData(const lcPieceDataRef& PieceDataRef) { Q_ASSERT(QThread::currentThread() == thread()); if (PieceDataRef->mState == lcAssetState::Unloaded) RequestPieceDataLoad(PieceDataRef.Get(), lcLoadPriority::Blocking); const bool RequestsSucceeded = mAssetLoader->WaitForPieces({ PieceDataRef.Get() }); return RequestsSucceeded && PieceDataRef->mState == lcAssetState::Loaded; } bool lcPiecesLibrary::WaitForPieceData(const std::vector& PieceDataRefs) { Q_ASSERT(QThread::currentThread() == thread()); std::vector Pieces; Pieces.reserve(PieceDataRefs.size()); for (const lcPieceDataRef& PieceDataRef : PieceDataRefs) { if (PieceDataRef->mState == lcAssetState::Unloaded) RequestPieceDataLoad(PieceDataRef.Get(), lcLoadPriority::Blocking); Pieces.push_back(PieceDataRef.Get()); } const bool RequestsSucceeded = mAssetLoader->WaitForPieces(Pieces); for (const lcPieceDataRef& PieceDataRef : PieceDataRefs) if (PieceDataRef->mState != lcAssetState::Loaded) return false; return RequestsSucceeded; } void lcPiecesLibrary::RequestSynthMesh(lcPiece* Piece) { Q_ASSERT(QThread::currentThread() == thread()); lcPieceData* PieceData = Piece->mPieceData.Get(); std::shared_ptr SynthInfo = PieceData ? PieceData->GetSynthInfoShared() : nullptr; if (!SynthInfo) { CancelSynthMesh(Piece); return; } mSynthPieces.insert_or_assign(Piece, QString()); mAssetLoader->RequestSynthMesh(Piece, PieceData, std::move(SynthInfo), Piece->GetControlPoints()); } void lcPiecesLibrary::CancelSynthMesh(lcPiece* Piece) { Q_ASSERT(QThread::currentThread() == thread()); mSynthPieces.erase(Piece); if (mAssetLoader) mAssetLoader->CancelSynthMesh(Piece); } bool lcPiecesLibrary::WaitForSynthMeshes(const std::vector& Pieces) { Q_ASSERT(QThread::currentThread() == thread()); const bool RequestsSucceeded = mAssetLoader->WaitForSynthMeshes(Pieces); for (const lcPiece* Piece : Pieces) if (Piece->mPieceData && Piece->mPieceData->GetSynthInfo() && !Piece->HasGeneratedMesh()) return false; return RequestsSucceeded; } QString lcPiecesLibrary::GetSynthMeshError(const lcPiece* Piece) const { Q_ASSERT(QThread::currentThread() == thread()); const auto It = mSynthPieces.find(const_cast(Piece)); return It != mSynthPieces.end() ? It->second : QString(); } bool lcPiecesLibrary::PublishPieceDataMesh(lcPieceData* PieceData, std::unique_ptr Mesh, QString Error) { Q_ASSERT(QThread::currentThread() == thread()); const bool Loaded = Error.isEmpty(); if (Loaded) { if (Mesh) PieceData->SetMesh(Mesh.release()); PieceData->mState = lcAssetState::Loaded; ClearPieceDataLoadError(PieceData); } else PieceData->SetFailed(std::move(Error)); ScheduleBufferRepack(); return Loaded; } bool lcPiecesLibrary::PublishSynthMesh(lcPiece* Piece, std::unique_ptr Mesh, QString Error) { Q_ASSERT(QThread::currentThread() == thread()); const bool Loaded = Error.isEmpty() && Mesh; if (!Loaded && Error.isEmpty()) Error = tr("Could not generate geometry for piece '%1'.").arg(QString::fromLatin1(Piece->mPieceData->mFileName)); Piece->SetGeneratedMesh(Loaded ? Mesh.release() : nullptr); const auto It = mSynthPieces.find(Piece); if (It != mSynthPieces.end()) It->second = Loaded ? QString() : std::move(Error); return Loaded; } void lcPiecesLibrary::PublishTexture(lcTexture* Texture, lcTextureBuildResult Result) { Q_ASSERT(QThread::currentThread() == thread()); if (!Result.DecodedImage.isNull()) Texture->SetImage(std::move(Result.DecodedImage), LC_TEXTURE_MIPMAPS); else Texture->SetFailed(Result.Error, std::move(Result.ErrorDetails)); } void lcPiecesLibrary::RebuildModelPieceData(lcPieceData* PieceData) { Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(PieceData->IsModel()); InvalidatePieceDataLoad(PieceData); PieceData->ReleaseMesh(); RequestPieceDataLoad(PieceData, lcLoadPriority::Normal); } void lcPiecesLibrary::InvalidatePieceDataLoad(lcPieceData* PieceData) { Q_ASSERT(QThread::currentThread() == thread()); PieceData->mState = lcAssetState::Unloaded; ClearPieceDataLoadError(PieceData); if (mAssetLoader) mAssetLoader->InvalidatePieceLoad(PieceData); } void lcPiecesLibrary::ReleasePieceData(lcPieceData* PieceData) { const int Remaining = mPieceManager->ReleasePieceData(PieceData); if (Remaining == 1 && mAssetLoader) mAssetLoader->CancelPieceLoad(PieceData); } void lcPiecesLibrary::AddPieceDataReference(lcPieceData* PieceData) { mPieceManager->AddPieceDataReference(PieceData); } void lcPiecesLibrary::SetPieceDataLoadError(const lcPieceData* PieceData, QString Error) { mPieceManager->SetPieceDataLoadError(PieceData, std::move(Error)); } QString lcPiecesLibrary::GetPieceDataLoadError(const lcPieceData* PieceData) const { return mPieceManager->GetPieceDataLoadError(PieceData); } void lcPiecesLibrary::ClearPieceDataLoadError(const lcPieceData* PieceData) { mPieceManager->ClearPieceDataLoadError(PieceData); } lcPieceSourceSnapshot lcPiecesLibrary::SnapshotPieceDataSource(const lcPieceData* PieceData) const { lcPieceSourceSnapshot Source; Source.FileName = QString::fromLatin1(PieceData->mFileName); Source.LibraryDirectory = mLibraryDir.absolutePath(); Source.CachePath = QFileInfo(QDir(mCachePath), Source.FileName).absoluteFilePath(); Source.ZipFileType = PieceData->mZipFileType; Source.ZipFileIndex = PieceData->mZipFileIndex; Source.StudStyle = mStudStyle; Source.StudCylinderColorEnabled = mStudCylinderColorEnabled; if (PieceData->IsModel()) { Source.InlineModel = true; Source.InlineMeshLines = PieceData->GetModel()->GetFileLines(); const lcModel* Model = PieceData->GetModel(); Source.TextureSearchDirectories = Model->GetAssetSearchDirectories(); if (Model->GetProject()) Source.EmbeddedData = Model->GetProject()->GetEmbeddedDataLookup(); } return Source; } lcPieceBuildResult lcPiecesLibrary::BuildPieceData(const lcPieceSourceSnapshot& Source) { lcPieceBuildResult Result; if (mCancelLoading.load()) { Result.Error = tr("Library loading was cancelled."); return Result; } if (!Source.InlineModel && Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast(Source.ZipFileType)]) { lcMemFile CacheData; if (ReadCacheFile(Source.CachePath, CacheData, CacheType::Archive)) { qint32 Flags; if (CacheData.ReadBuffer(&Flags, sizeof(Flags)) == sizeof(Flags) && Flags == MeshCacheSettingsKey(Source.StudStyle, Source.StudCylinderColorEnabled)) { Result.Mesh.reset(new lcMesh); if (Result.Mesh->FileLoad(CacheData)) { Result.FromCache = true; return Result; } Result.Mesh.reset(); } } } lcLibraryMeshData MeshData; lcMeshLoader MeshLoader(MeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes); if (Source.InlineModel) { lcMemFile PieceFile; for (const QString& Line : Source.InlineMeshLines) { const QByteArray Buffer = Line.toLatin1(); PieceFile.WriteBuffer(Buffer.constData(), Buffer.size()); PieceFile.WriteBuffer("\r\n", 2); } PieceFile.Seek(0, SEEK_SET); if (!MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED)) { Result.Error = tr("Could not load model geometry %1.").arg(Source.FileName); if (!MeshLoader.GetError().isEmpty()) Result.Error += QLatin1Char('\n') + MeshLoader.GetError(); return Result; } if (MeshData.IsEmpty()) { Result.EmptyGeometry = true; return Result; } } else { bool Loaded = false; if (Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast(Source.ZipFileType)]) { lcFileSource FileSource; FileSource.ArchiveType = static_cast(Source.ZipFileType); FileSource.ArchiveIndex = Source.ZipFileIndex; const lcResult> File = OpenFile(FileSource); if (File) Loaded = MeshLoader.LoadMesh(*File.value(), LC_MESHDATA_SHARED); } else { lcFileSource FileSource; FileSource.FilePath = QDir(Source.LibraryDirectory).absoluteFilePath(QStringLiteral("parts/") + Source.FileName); const lcResult> File = OpenFile(FileSource); if (File) Loaded = MeshLoader.LoadMesh(*File.value(), LC_MESHDATA_SHARED); // Directory loading retries unofficial geometry after an open or parse failure. if (mHasUnofficial && !Loaded) { MeshData.Clear(); FileSource.FilePath = QDir(Source.LibraryDirectory).absoluteFilePath(QStringLiteral("unofficial/parts/") + Source.FileName); const lcResult> UnofficialFile = OpenFile(FileSource); if (UnofficialFile) Loaded = MeshLoader.LoadMesh(*UnofficialFile.value(), LC_MESHDATA_SHARED); } } if (mCancelLoading.load()) Result.Error = tr("Library loading was cancelled."); else if (!Loaded) { Result.Error = tr("Could not load part %1.").arg(Source.FileName); if (!MeshLoader.GetError().isEmpty()) Result.Error += QLatin1Char('\n') + MeshLoader.GetError(); } if (!Result.Error.isEmpty()) return Result; } Result.Mesh.reset(MeshData.CreateMesh(nullptr)); if (!Result.Mesh) Result.Error = tr("Could not prepare piece %1 for rendering.").arg(Source.FileName); return Result; } void lcPiecesLibrary::SaveBuiltPieceDataCache(const lcPieceSourceSnapshot& Source, lcMesh& Mesh) { if (Source.ZipFileType == lcZipFileType::Count) return; lcMemFile MeshData; const qint32 Flags = MeshCacheSettingsKey(Source.StudStyle, Source.StudCylinderColorEnabled); if (MeshData.WriteBuffer((char*)&Flags, sizeof(Flags)) != sizeof(Flags) || !Mesh.FileSave(MeshData)) return; WriteCacheFile(Source.CachePath, MeshData, CacheType::Archive); } lcResult> lcPiecesLibrary::OpenPrimitiveFile(const lcLibraryPrimitive* Primitive) { lcFileSource Source; if (Primitive->mZipFileType != lcZipFileType::Count) { Source.ArchiveType = static_cast(Primitive->mZipFileType); Source.ArchiveIndex = Primitive->mZipFileIndex; } else Source.FilePath = Primitive->mFileName; return OpenFile(Source); } lcResult> lcPiecesLibrary::OpenPieceDataFile(const char* PieceName) { const std::optional FileInfo = mPieceManager->GetPieceDataFileInfo(PieceName); if (mZipFiles[static_cast(lcZipFileType::Official)]) { if (FileInfo && FileInfo->ZipFileType != lcZipFileType::Count) { lcFileSource Source; Source.ArchiveType = static_cast(FileInfo->ZipFileType); Source.ArchiveIndex = FileInfo->ZipFileIndex; return OpenFile(Source); } if (!FileInfo) { for (const lcZipFileType Type : { lcZipFileType::Official, lcZipFileType::Unofficial }) { const std::unique_ptr& Archive = mZipFiles[static_cast(Type)]; if (!Archive) continue; const QString Prefix = Type == lcZipFileType::Official ? QStringLiteral("ldraw/") : QString(); for (const QString& Folder : { QStringLiteral("parts/"), QStringLiteral("p/") }) { const QByteArray FileName = (Prefix + Folder + QString::fromLatin1(PieceName)).toLatin1(); const std::optional FileIndex = Archive->FindFileIndex(FileName.constData()); if (!FileIndex) continue; lcFileSource Source; Source.ArchiveType = static_cast(Type); Source.ArchiveIndex = *FileIndex; if (lcResult> File = OpenFile(Source)) return File; } } return lcUnexpected(tr("Could not open included file %1.").arg(QString::fromLatin1(PieceName))); } } const QString Name = QString::fromLatin1(FileInfo ? FileInfo->FileName.c_str() : PieceName); std::vector Folders(1, QStringLiteral("parts/")); if (!FileInfo) Folders.emplace_back(QStringLiteral("p/")); if (mHasUnofficial) { Folders.emplace_back(QStringLiteral("unofficial/parts/")); if (!FileInfo) Folders.emplace_back(QStringLiteral("unofficial/p/")); } for (const QString& Folder : Folders) { const QString FileName = Folder + Name; lcFileSource Source; Source.FilePath = mLibraryDir.absoluteFilePath(FileName); if (lcResult> File = OpenFile(Source)) return File; #if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) if (!FileInfo) { Source.FilePath = mLibraryDir.absoluteFilePath(FileName.toLower()); if (lcResult> File = OpenFile(Source)) return File; } #endif } return lcUnexpected(tr("Could not open included file %1.").arg(QString::fromLatin1(PieceName))); } void lcPiecesLibrary::ReleaseBuffers() { mBuffersDirty = true; // Library-only loading can finish without creating a renderer or shared buffers. if (!mVertexBuffer.IsValid() && !mIndexBuffer.IsValid()) return; lcContext* Context = lcContext::GetGlobalOffscreenContext(); Context->MakeCurrent(); Context->DestroyVertexBuffer(mVertexBuffer); Context->DestroyIndexBuffer(mIndexBuffer); } void lcPiecesLibrary::UpdateBuffers(lcContext* Context) { if (!gSupportsVertexBufferObject || !mBuffersDirty) return; int VertexDataSize = 0; int IndexDataSize = 0; std::vector Meshes; const auto AddMesh = [&VertexDataSize, &IndexDataSize, &Meshes](lcPieceData* PieceData) { lcMesh* Mesh = PieceData->GetMesh(); if (!Mesh) return; if (Mesh->mVertexDataSize < 0 || Mesh->mIndexDataSize < 0 || Mesh->mVertexDataSize > 16 * 1024 * 1024 || Mesh->mIndexDataSize > 16 * 1024 * 1024 || (Mesh->mVertexDataSize && !Mesh->mVertexData) || (Mesh->mIndexDataSize && !Mesh->mIndexData) || VertexDataSize > std::numeric_limits::max() - Mesh->mVertexDataSize || IndexDataSize > std::numeric_limits::max() - Mesh->mIndexDataSize) { Mesh->mVertexCacheOffset = -1; Mesh->mIndexCacheOffset = -1; return; } VertexDataSize += Mesh->mVertexDataSize; IndexDataSize += Mesh->mIndexDataSize; Meshes.push_back(Mesh); }; for (lcPieceData* PieceData : mPieceManager->GetAllPieceData()) AddMesh(PieceData); if (!VertexDataSize) { Context->DestroyVertexBuffer(mVertexBuffer); Context->DestroyIndexBuffer(mIndexBuffer); mBuffersDirty = false; return; } void* VertexData = malloc(VertexDataSize); void* IndexData = IndexDataSize ? malloc(IndexDataSize) : nullptr; if (!VertexData || (IndexDataSize && !IndexData)) { free(VertexData); free(IndexData); return; } VertexDataSize = 0; IndexDataSize = 0; for (lcMesh* Mesh : Meshes) { if (Mesh->mIndexDataSize && !IndexData) { for (lcMesh* CachedMesh : Meshes) { CachedMesh->mVertexCacheOffset = -1; CachedMesh->mIndexCacheOffset = -1; } free(VertexData); free(IndexData); return; } Mesh->mVertexCacheOffset = VertexDataSize; Mesh->mIndexCacheOffset = IndexDataSize; if (Mesh->mVertexDataSize) memcpy((char*)VertexData + VertexDataSize, Mesh->mVertexData, Mesh->mVertexDataSize); if (Mesh->mIndexDataSize) memcpy((char*)IndexData + IndexDataSize, Mesh->mIndexData, Mesh->mIndexDataSize); VertexDataSize += Mesh->mVertexDataSize; IndexDataSize += Mesh->mIndexDataSize; } Context->DestroyVertexBuffer(mVertexBuffer); Context->DestroyIndexBuffer(mIndexBuffer); mVertexBuffer = Context->CreateVertexBuffer(VertexDataSize, VertexData); if (IndexDataSize) mIndexBuffer = Context->CreateIndexBuffer(IndexDataSize, IndexData); mBuffersDirty = false; free(VertexData); free(IndexData); } void lcPiecesLibrary::ScheduleBufferRepack() { mStreamingBuffersPending = true; if (mBufferRepackScheduled) return; mBufferRepackScheduled = true; QTimer::singleShot(120, this, [this]() { mBufferRepackScheduled = false; if (!mStreamingBuffersPending) return; if (mAssetLoader && mAssetLoader->HasPendingWork()) { ScheduleBufferRepack(); return; } mStreamingBuffersPending = false; mBuffersDirty = true; lcView::UpdateAllViews(); }); } lcMesh* lcPiecesLibrary::GetLoadingMesh() const { return mLoadingMesh.get(); } void lcPiecesLibrary::AddTextureReference(lcTexture* Texture) { mTextureManager->AddTextureReference(Texture); } void lcPiecesLibrary::ReleaseTexture(lcTexture* Texture) { mTextureManager->ReleaseTexture(Texture); } void lcPiecesLibrary::UpdateStudStyleAndColors(lcStudStyle StudStyle, bool StudCylinderColorEnabled) { Q_ASSERT(QThread::currentThread() == thread()); if (mStudStyle == StudStyle && mStudCylinderColorEnabled == StudCylinderColorEnabled) return; // Finish running workers before changing shared primitive data and colors. // Queued requests resume with the new settings afterward. mAssetLoader->PauseQueuedWork(); mStudStyle = StudStyle; mStudCylinderColorEnabled = StudCylinderColorEnabled; LoadColors(); UpdateStudStyleSource(); for (const std::unique_ptr& Source : mSources) { for (const auto& PrimitiveIt : Source->Primitives) { lcLibraryPrimitive* Primitive = PrimitiveIt.second.get(); if (Primitive->mStudStyle || Primitive->mMeshData.mHasStyleStud) Primitive->Unload(); } } const auto ReloadPiece = [this](lcPieceData* PieceData) { if (PieceData->mState != lcAssetState::Loaded || !PieceData->GetMesh() || !(PieceData->GetMesh()->mFlags & lcMeshFlag::HasStyleStud)) return; if (PieceData->IsModel()) { PieceData->ReleaseMesh(); PieceData->mState = lcAssetState::Unloaded; RequestPieceDataLoad(PieceData, lcLoadPriority::Low); } else if (!PieceData->IsProject()) { PieceData->Unload(); RequestPieceDataLoad(PieceData, lcLoadPriority::Low); } mBuffersDirty = true; }; for (lcPieceData* PieceData : mPieceManager->GetAllPieceData()) ReloadPiece(PieceData); std::vector SynthPieces; SynthPieces.reserve(mSynthPieces.size()); for (const auto& Entry : mSynthPieces) SynthPieces.push_back(Entry.first); for (lcPiece* Piece : SynthPieces) Piece->RefreshGeneratedMesh(); mAssetLoader->ResumeQueuedWork(); } lcLibraryPrimitive* lcPiecesLibrary::FindPrimitive(const char* Name) const { for (const std::unique_ptr& Source : mSources) { const auto PrimitiveIt = Source->Primitives.find(Name); if (PrimitiveIt != Source->Primitives.end()) return PrimitiveIt->second.get(); } return nullptr; } lcResult lcPiecesLibrary::LoadPrimitive(lcLibraryPrimitive* Primitive) { QThread* Thread = QThread::currentThread(); lcLibraryPrimitive* Parent = nullptr; mPrimitiveMutex.lock(); if (ShouldCancelLoading()) { mPrimitiveMutex.unlock(); return lcUnexpected(tr("Library loading was cancelled.")); } if (Primitive->mState == lcPrimitiveState::Loaded) { mPrimitiveMutex.unlock(); return lcResult(); } std::vector& Stack = mPrimitiveLoadStacks[Thread]; if (!Stack.empty()) { Parent = Stack.back(); for (lcLibraryPrimitive* Dependency = Primitive; Dependency;) { if (Dependency == Parent) { mPrimitiveMutex.unlock(); return lcUnexpected(tr("Primitive include cycle at '%1'.").arg(QString::fromLatin1(Primitive->mName))); } const auto It = mPrimitiveDependencies.find(Dependency); Dependency = It != mPrimitiveDependencies.end() ? It->second : nullptr; } mPrimitiveDependencies[Parent] = Primitive; } Stack.push_back(Primitive); mPrimitiveMutex.unlock(); lcResult Result = LoadPrimitiveData(Primitive); mPrimitiveMutex.lock(); if (Parent) mPrimitiveDependencies.erase(Parent); std::vector& CompletedStack = mPrimitiveLoadStacks[Thread]; CompletedStack.pop_back(); if (CompletedStack.empty()) mPrimitiveLoadStacks.erase(Thread); mPrimitiveMutex.unlock(); return Result; } lcResult lcPiecesLibrary::LoadPrimitiveData(lcLibraryPrimitive* Primitive) { mPrimitiveMutex.lock(); if (ShouldCancelLoading()) { mPrimitiveMutex.unlock(); return lcUnexpected(tr("Library loading was cancelled.")); } if (Primitive->mState == lcPrimitiveState::NotLoaded) Primitive->mState = lcPrimitiveState::Loading; else { while (Primitive->mState == lcPrimitiveState::Loading) { if (ShouldCancelLoading()) { mPrimitiveMutex.unlock(); return lcUnexpected(tr("Library loading was cancelled.")); } mPrimitiveLoaded.wait(&mPrimitiveMutex, 10); } const bool Loaded = Primitive->mState == lcPrimitiveState::Loaded; const QString Error = Primitive->mLoadError; mPrimitiveMutex.unlock(); if (Loaded) return lcResult(); return lcUnexpected(Error); } mPrimitiveMutex.unlock(); lcMeshLoader MeshLoader(Primitive->mMeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes); const auto LoadFailed = [this, Primitive](QString Error) -> lcResult { Primitive->mMeshData.Clear(); if (Error.isEmpty()) Error = tr("Could not load included file %1.").arg(QString::fromLatin1(Primitive->mName)); QMutexLocker LoadLock(&mPrimitiveMutex); Primitive->mLoadError = Error; Primitive->mState = lcPrimitiveState::NotLoaded; mPrimitiveLoaded.wakeAll(); return lcUnexpected(std::move(Error)); }; lcLibraryPrimitive* LowPrimitive = nullptr; if (mZipFiles[static_cast(lcZipFileType::Official)] && Primitive->mStud && !Primitive->mStudStyle) { if (strncmp(Primitive->mName, "8/", 2)) // todo: low LOD for loose file libraries { char Name[LC_PIECE_NAME_LEN]; lcstrcpy(Name, "8/"); lcstrcat(Name, Primitive->mName); lcstrupr(Name); LowPrimitive = FindPrimitive(Name); // todo: low primitives don't work with studlogo } } const lcResult> File = OpenPrimitiveFile(Primitive); if (!File) return LoadFailed(File.error()); if (!MeshLoader.LoadMesh(*File.value(), LowPrimitive ? LC_MESHDATA_HIGH : LC_MESHDATA_SHARED)) return LoadFailed(MeshLoader.GetError()); if (LowPrimitive) { const lcResult> LowFile = OpenPrimitiveFile(LowPrimitive); if (!LowFile) return LoadFailed(LowFile.error()); if (!MeshLoader.LoadMesh(*LowFile.value(), LC_MESHDATA_LOW)) return LoadFailed(MeshLoader.GetError()); } mPrimitiveMutex.lock(); Primitive->mLoadError.clear(); Primitive->mState = lcPrimitiveState::Loaded; mPrimitiveLoaded.wakeAll(); mPrimitiveMutex.unlock(); return lcResult(); } bool lcPiecesLibrary::PieceDataInCategory(lcPieceData* PieceData, const char* CategoryKeywords) const { if (!PieceData->IsLibraryPiece()) return false; const char* PieceName; if (PieceData->m_strDescription[0] == '~' || PieceData->m_strDescription[0] == '_') PieceName = PieceData->m_strDescription + 1; else PieceName = PieceData->m_strDescription; return lcMatchCategory(PieceName, CategoryKeywords); } void lcPiecesLibrary::GetCategoryEntries(int CategoryIndex, bool GroupPieces, std::vector& SinglePieces, std::vector& GroupedPieces) { if (CategoryIndex >= 0 && CategoryIndex < static_cast(gCategories.size())) GetCategoryEntries(gCategories[CategoryIndex].Keywords.constData(), GroupPieces, SinglePieces, GroupedPieces); } void lcPiecesLibrary::GetCategoryEntries(const char* CategoryKeywords, bool GroupPieces, std::vector& SinglePieces, std::vector& GroupedPieces) { SinglePieces.clear(); GroupedPieces.clear(); for (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData()) { if (!PieceDataInCategory(PieceData, CategoryKeywords)) continue; if (!GroupPieces) { SinglePieces.emplace_back(PieceData); continue; } // Check if it's a patterned piece. if (PieceData->IsDecorated()) { lcPieceData* Parent = FindPieceData(PieceData->GetPatternParentId().c_str(), nullptr); if (Parent) { // Check if the parent was added as a single piece. auto ParentIt = std::find(SinglePieces.begin(), SinglePieces.end(), Parent); if (ParentIt != SinglePieces.end()) SinglePieces.erase(ParentIt); if (std::find(GroupedPieces.begin(), GroupedPieces.end(), Parent) == GroupedPieces.end()) GroupedPieces.emplace_back(Parent); } else { // Decorations without a plain parent remain individual entries. SinglePieces.emplace_back(PieceData); } } else { // Check if this piece has already been added to this category by one of its children. if (std::find(GroupedPieces.begin(), GroupedPieces.end(), PieceData) == GroupedPieces.end()) SinglePieces.emplace_back(PieceData); } } } std::vector lcPiecesLibrary::GetParts() const { return mPieceManager->GetLibraryPieceData(); } std::vector lcPiecesLibrary::GetVisibleTrainTrackParts(const lcTrainTrackConnectionType& ConnectionType) const { std::vector Parts; for (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData()) { lcTrainTrackInfo* TrainTrackInfo = PieceData->GetTrainTrackInfo(); if (TrainTrackInfo && TrainTrackInfo->IsVisible() && TrainTrackInfo->CanConnectTo(ConnectionType, true)) Parts.emplace_back(PieceData); } return Parts; } std::vector lcPiecesLibrary::GetPartsFromSet(const std::vector& PartIds) const { std::vector Parts; Parts.reserve(PartIds.size()); for (const std::string& PartId : PartIds) { if (lcPieceData* PieceData = mPieceManager->FindPieceData(PartId, nullptr)) Parts.push_back(PieceData); } return Parts; } std::string lcPiecesLibrary::GetPartId(const lcPieceData* PieceData) const { return mPieceManager->GetLibraryPieceDataName(PieceData); } bool lcPiecesLibrary::LoadBuiltinPieceLibrary() { std::unique_ptr File(new lcDiskFile(":/resources/library.zip")); if (!File->Open(QIODevice::ReadOnly) || !OpenArchive(std::move(File), lcZipFileType::Official)) return false; lcMemFile PieceFile; for (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData()) { mZipFiles[static_cast(PieceData->mZipFileType)]->ExtractFile(PieceData->mZipFileIndex, PieceFile, 256); PieceFile.Seek(0, SEEK_END); PieceFile.WriteU8(0); char* Src = (char*)PieceFile.mBuffer + 2; char* Dst = PieceData->m_strDescription; for (;;) { if (*Src != '\r' && *Src != '\n' && *Src && Dst - PieceData->m_strDescription < (int)sizeof(PieceData->m_strDescription) - 1) { *Dst++ = *Src++; continue; } *Dst = 0; break; } } ClassifyPieces(DirectoryFilePaths()); lcLoadDefaultColors(mStudStyle); UpdateLoadingMeshColors(); UpdateStudStyleSource(); lcLoadDefaultCategories(true); lcSynthInit(); lcTrainTrackInfo::Initialize(this); return true; }