/* EntryOperations::convertTextures * Converts multiple TEXTURE1/2 entries to a single ZDoom text-based * TEXTURES entry *******************************************************************/ bool EntryOperations::convertTextures(vector<ArchiveEntry*> entries) { // Check any entries were given if (entries.size() == 0) return false; // Get parent archive of entries Archive* parent = entries[0]->getParent(); // Can't do anything if entry isn't in an archive if (!parent) return false; // Find patch table in parent archive Archive::search_options_t opt; opt.match_type = EntryType::getType("pnames"); ArchiveEntry* pnames = parent->findLast(opt); // Check it exists if (!pnames) return false; // Load patch table PatchTable ptable; ptable.loadPNAMES(pnames); // Read all texture entries to a single list TextureXList tx; for (unsigned a = 0; a < entries.size(); a++) tx.readTEXTUREXData(entries[a], ptable, true); // Convert to extended (TEXTURES) format tx.convertToTEXTURES(); // Create new TEXTURES entry and write to it ArchiveEntry* textures = parent->addNewEntry("TEXTURES", parent->entryIndex(entries[0])); if (textures) { bool ok = tx.writeTEXTURESData(textures); EntryType::detectEntryType(textures); textures->setExtensionByType(); return ok; } else return false; }
/* EntryOperations::findTextureErrors * Detect errors in a TEXTUREx entry *******************************************************************/ bool EntryOperations::findTextureErrors(vector<ArchiveEntry*> entries) { // Check any entries were given if (entries.size() == 0) return false; // Get parent archive of entries Archive* parent = entries[0]->getParent(); // Can't do anything if entry isn't in an archive if (!parent) return false; // Find patch table in parent archive Archive::search_options_t opt; opt.match_type = EntryType::getType("pnames"); ArchiveEntry* pnames = parent->findLast(opt); // Check it exists if (!pnames) return false; // Load patch table PatchTable ptable; ptable.loadPNAMES(pnames); // Read all texture entries to a single list TextureXList tx; for (unsigned a = 0; a < entries.size(); a++) tx.readTEXTUREXData(entries[a], ptable, true); // Detect errors tx.findErrors(); return true; }
/* EntryOperations::createTexture * Same as addToPatchTable, but also creates a single-patch texture * from each added patch *******************************************************************/ bool EntryOperations::createTexture(vector<ArchiveEntry*> entries) { // Check any entries were given if (entries.size() == 0) return true; // Get parent archive Archive* parent = entries[0]->getParent(); // Create texture entries if needed if (!TextureXEditor::setupTextureEntries(parent)) return false; // Find texturex entry to add to Archive::search_options_t opt; opt.match_type = EntryType::getType("texturex"); ArchiveEntry* texturex = parent->findFirst(opt); // Check it exists bool zdtextures = false; if (!texturex) { opt.match_type = EntryType::getType("zdtextures"); texturex = parent->findFirst(opt); if (!texturex) return false; else zdtextures = true; } // Find patch table in parent archive ArchiveEntry* pnames = NULL; if (!zdtextures) { opt.match_type = EntryType::getType("pnames"); pnames = parent->findLast(opt); // Check it exists if (!pnames) return false; } // Check entries aren't locked (texture editor open or iwad) if ((pnames && pnames->isLocked()) || texturex->isLocked()) { if (parent->isReadOnly()) wxMessageBox("Cannot perform this action on an IWAD", "Error", wxICON_ERROR); else wxMessageBox("Cannot perform this action because one or more texture related entries is locked. Please close the archive's texture editor if it is open.", "Error", wxICON_ERROR); return false; } TextureXList tx; PatchTable ptable; if (zdtextures) { // Load TEXTURES tx.readTEXTURESData(texturex); } else { // Load patch table ptable.loadPNAMES(pnames); // Load TEXTUREx tx.readTEXTUREXData(texturex, ptable); } // Create textures from entries SImage image; for (unsigned a = 0; a < entries.size(); a++) { // Check entry type if (!(entries[a]->getType()->extraProps().propertyExists("image"))) { wxLogMessage("Entry %s is not a valid image", entries[a]->getName()); continue; } // Check entry name string name = entries[a]->getName(true); if (name.Length() > 8) { wxLogMessage("Entry %s has too long a name to add to the patch table (name must be 8 characters max)", entries[a]->getName()); continue; } // Add to patch table if (!zdtextures) ptable.addPatch(name); // Load patch to temp image Misc::loadImageFromEntry(&image, entries[a]); // Create texture CTexture* ntex = new CTexture(zdtextures); ntex->setName(name); ntex->addPatch(name, 0, 0); ntex->setWidth(image.getWidth()); ntex->setHeight(image.getHeight()); // Setup texture scale if (tx.getFormat() == TXF_TEXTURES) ntex->setScale(1, 1); else ntex->setScale(0, 0); // Add to texture list tx.addTexture(ntex); } if (zdtextures) { // Write texture data back to textures entry tx.writeTEXTURESData(texturex); } else { // Write patch table data back to pnames entry ptable.writePNAMES(pnames); // Write texture data back to texturex entry tx.writeTEXTUREXData(texturex, ptable); } return true; }
void ArchiveOperations::removeUnusedTextures(Archive* archive) { // Check archive was given if (!archive) return; // --- Build list of used textures --- TexUsedMap used_textures; int total_maps = 0; // Get all SIDEDEFS entries Archive::SearchOptions opt; opt.match_type = EntryType::fromId("map_sidedefs"); auto sidedefs = archive->findAll(opt); total_maps += sidedefs.size(); // Go through and add used textures to list DoomMapFormat::SideDef sdef; wxString tex_lower, tex_middle, tex_upper; for (auto& sidedef : sidedefs) { int nsides = sidedef->size() / 30; sidedef->seek(0, SEEK_SET); for (int s = 0; s < nsides; s++) { // Read side data sidedef->read(&sdef, 30); // Get textures tex_lower = wxString::FromAscii(sdef.tex_lower, 8); tex_middle = wxString::FromAscii(sdef.tex_middle, 8); tex_upper = wxString::FromAscii(sdef.tex_upper, 8); // Add to used textures list used_textures[tex_lower].used = true; used_textures[tex_middle].used = true; used_textures[tex_upper].used = true; } } // Get all TEXTMAP entries opt.match_name = "TEXTMAP"; opt.match_type = EntryType::fromId("udmf_textmap"); auto udmfmaps = archive->findAll(opt); total_maps += udmfmaps.size(); // Go through and add used textures to list Tokenizer tz; tz.setSpecialCharacters("{};="); for (auto& udmfmap : udmfmaps) { // Open in tokenizer tz.openMem(udmfmap->data(), "UDMF TEXTMAP"); // Go through text tokens wxString token = tz.getToken(); while (!token.IsEmpty()) { // Check for sidedef definition if (token == "sidedef") { tz.getToken(); // Skip { token = tz.getToken(); while (token != "}") { // Check for texture property if (token == "texturetop" || token == "texturemiddle" || token == "texturebottom") { tz.getToken(); // Skip = used_textures[tz.getToken()].used = true; } token = tz.getToken(); } } // Next token token = tz.getToken(); } } // Check if any maps were found if (total_maps == 0) return; // Find all TEXTUREx entries opt.match_name = ""; opt.match_type = EntryType::fromId("texturex"); auto tx_entries = archive->findAll(opt); // Go through texture lists PatchTable ptable; // Dummy patch table, patch info not needed here wxArrayString unused_tex; for (auto& tx_entrie : tx_entries) { TextureXList txlist; txlist.readTEXTUREXData(tx_entrie, ptable); // Go through textures bool anim = false; for (unsigned t = 1; t < txlist.size(); t++) { wxString texname = txlist.texture(t)->name(); // Check for animation start for (int b = 0; b < n_tex_anim; b++) { if (texname == tex_anim_start[b]) { anim = true; break; } } // Check for animation end bool thisend = false; for (int b = 0; b < n_tex_anim; b++) { if (texname == tex_anim_end[b]) { anim = false; thisend = true; break; } } // Mark if unused and not part of an animation if (!used_textures[texname].used && !anim && !thisend) unused_tex.Add(txlist.texture(t)->name()); } } // Pop up a dialog with a checkbox list of unused textures wxMultiChoiceDialog dialog( theMainWindow, "The following textures are not used in any map,\nselect which textures to delete", "Delete Unused Textures", unused_tex); // Get base resource textures (if any) auto base_resource = App::archiveManager().baseResourceArchive(); vector<ArchiveEntry*> base_tx_entries; if (base_resource) base_tx_entries = base_resource->findAll(opt); PatchTable pt_temp; TextureXList tx; for (auto& texturex : base_tx_entries) tx.readTEXTUREXData(texturex, pt_temp, true); vector<wxString> base_resource_textures; for (unsigned a = 0; a < tx.size(); a++) base_resource_textures.push_back(tx.texture(a)->name()); // Determine which textures to check initially wxArrayInt selection; for (unsigned a = 0; a < unused_tex.size(); a++) { bool swtex = false; // Check for switch texture if (unused_tex[a].StartsWith("SW1")) { // Get counterpart switch name wxString swname = unused_tex[a]; swname.Replace("SW1", "SW2", false); // Check if its counterpart is used if (used_textures[swname].used) swtex = true; } else if (unused_tex[a].StartsWith("SW2")) { // Get counterpart switch name wxString swname = unused_tex[a]; swname.Replace("SW2", "SW1", false); // Check if its counterpart is used if (used_textures[swname].used) swtex = true; } // Check for base resource texture bool br_tex = false; for (auto& texture : base_resource_textures) { if (texture.CmpNoCase(unused_tex[a]) == 0) { Log::info(3, "Texture " + texture + " is in base resource"); br_tex = true; break; } } if (!swtex && !br_tex) selection.Add(a); } dialog.SetSelections(selection); int n_removed = 0; if (dialog.ShowModal() == wxID_OK) { // Get selected textures selection = dialog.GetSelections(); // Go through texture lists for (auto& entry : tx_entries) { TextureXList txlist; txlist.readTEXTUREXData(entry, ptable); // Go through selected textures to delete for (int i : selection) { // Get texture index int index = txlist.textureIndex(WxUtils::strToView(unused_tex[i])); // Delete it from the list (if found) if (index >= 0) { txlist.removeTexture(index); n_removed++; } } // Write texture list data back to entry txlist.writeTEXTUREXData(entry, ptable); } } wxMessageBox(wxString::Format("Removed %d unused textures", n_removed)); }
// ----------------------------------------------------------------------------- // Removes any patches and associated entries from [archive] that are not used // in any texture definitions // ----------------------------------------------------------------------------- bool ArchiveOperations::removeUnusedPatches(Archive* archive) { if (!archive) return false; // Find PNAMES entry Archive::SearchOptions opt; opt.match_type = EntryType::fromId("pnames"); ArchiveEntry* pnames = archive->findLast(opt); // Find TEXTUREx entries opt.match_type = EntryType::fromId("texturex"); vector<ArchiveEntry*> tx_entries = archive->findAll(opt); // Can't do anything without PNAMES/TEXTUREx if (!pnames || tx_entries.size() == 0) return false; // Open patch table PatchTable ptable; ptable.loadPNAMES(pnames, archive); // Open texturex entries to update patch usage vector<TextureXList*> tx_lists; for (unsigned a = 0; a < tx_entries.size(); a++) { TextureXList* texturex = new TextureXList(); texturex->readTEXTUREXData(tx_entries[a], ptable); for (unsigned t = 0; t < texturex->nTextures(); t++) ptable.updatePatchUsage(texturex->getTexture(t)); tx_lists.push_back(texturex); } // Go through patch table unsigned removed = 0; vector<ArchiveEntry*> to_remove; for (unsigned a = 0; a < ptable.nPatches(); a++) { auto& p = ptable.patch(a); // Check if used in any texture if (p.used_in.size() == 0) { // Unused // If its entry is in the archive, flag it to be removed ArchiveEntry* entry = theResourceManager->getPatchEntry(p.name, "patches", archive); if (entry && entry->getParent() == archive) to_remove.push_back(entry); // Update texturex list patch indices for (unsigned t = 0; t < tx_lists.size(); t++) tx_lists[t]->removePatch(p.name); // Remove the patch from the patch table LOG_MESSAGE(1, "Removed patch %s", p.name); removed++; ptable.removePatch(a--); } } // Remove unused patch entries for (unsigned a = 0; a < to_remove.size(); a++) { LOG_MESSAGE(1, "Removed entry %s", to_remove[a]->getName()); archive->removeEntry(to_remove[a]); } // Write PNAMES changes ptable.writePNAMES(pnames); // Write TEXTUREx changes for (unsigned a = 0; a < tx_lists.size(); a++) tx_lists[a]->writeTEXTUREXData(tx_entries[a], ptable); // Cleanup for (unsigned a = 0; a < tx_lists.size(); a++) delete tx_lists[a]; // Notify user wxMessageBox( S_FMT("Removed %d patches and %lu entries. See console log for details.", removed, to_remove.size()), "Removed Unused Patches", wxOK | wxICON_INFORMATION); return true; }
// ----------------------------------------------------------------------------- // Static function to check if an archive has sufficient texture related // entries, and if not, prompts the user to either create or import them. // Returns true if the entries exist, false otherwise // ----------------------------------------------------------------------------- bool TextureXEditor::setupTextureEntries(Archive* archive) { using Format = TextureXList::Format; // Check any archive was given if (!archive) return false; // Search archive for any ZDoom TEXTURES entries Archive::SearchOptions options; options.match_type = EntryType::fromId("zdtextures"); auto entry_tx = archive->findFirst(options); // Find any TEXTURES entry // If it's found, we're done if (entry_tx) return true; // Search archive for any texture-related entries options.match_type = EntryType::fromId("texturex"); entry_tx = archive->findFirst(options); // Find any TEXTUREx entry options.match_type = EntryType::fromId("pnames"); auto entry_pnames = archive->findFirst(options); // Find any PNAMES entry // If both exist, we're done if (entry_tx && entry_pnames) return true; // Todo: accept entry_tx without pnames if the textures are in Jaguar mode // If no TEXTUREx entry exists if (!entry_tx) { // No TEXTUREx entries found, so ask if the user wishes to create one wxMessageDialog dlg( nullptr, "The archive does not contain any texture definitions (TEXTURE1/2 or TEXTURES). " "Do you wish to create or import a texture definition list?", "No Texture Definitions Found", wxYES_NO); if (dlg.ShowModal() == wxID_YES) { CreateTextureXDialog ctxd(nullptr); while (true) { // Check if cancelled if (ctxd.ShowModal() == wxID_CANCEL) return false; if (ctxd.createNewSelected()) { // User selected to create a new TEXTUREx list ArchiveEntry* texturex = nullptr; // Doom or Strife TEXTUREx if (ctxd.getSelectedFormat() == Format::Normal || ctxd.getSelectedFormat() == Format::Strife11) { // Create texture list TextureXList txlist; txlist.setFormat(ctxd.getSelectedFormat()); // Create patch table PatchTable ptt; // Create dummy patch auto dpatch = App::archiveManager().programResourceArchive()->entryAtPath("s3dummy.lmp"); archive->addEntry(dpatch, "patches", true); ptt.addPatch("S3DUMMY"); // Create dummy texture auto dummytex = std::make_unique<CTexture>(); dummytex->setName("S3DUMMY"); dummytex->addPatch("S3DUMMY", 0, 0); dummytex->setWidth(128); dummytex->setHeight(128); dummytex->setScale({ 0., 0. }); // Add dummy texture to list // (this serves two purposes - supplies the special 'invalid' texture by default, // and allows the texturex format to be detected) txlist.addTexture(std::move(dummytex)); // Add empty PNAMES entry to archive entry_pnames = archive->addNewEntry("PNAMES"); ptt.writePNAMES(entry_pnames); entry_pnames->setType(EntryType::fromId("pnames")); entry_pnames->setExtensionByType(); // Add empty TEXTURE1 entry to archive texturex = archive->addNewEntry("TEXTURE1"); txlist.writeTEXTUREXData(texturex, ptt); texturex->setType(EntryType::fromId("texturex")); texturex->setExtensionByType(); } else if (ctxd.getSelectedFormat() == Format::Textures) { // Create texture list TextureXList txlist; txlist.setFormat(Format::Textures); // Add empty TEXTURES entry to archive texturex = archive->addNewEntry("TEXTURES"); texturex->setType(EntryType::fromId("zdtextures")); texturex->setExtensionByType(); return false; } if (!texturex) return false; } else { // User selected to import texture definitions from the base resource archive auto bra = App::archiveManager().baseResourceArchive(); if (!bra) { wxMessageBox( "No Base Resource Archive is opened, please select/open one", "Error", wxICON_ERROR); continue; } // Find all relevant entries in the base resource archive Archive::SearchOptions opt; opt.match_type = EntryType::fromId("texturex"); auto import_tx = bra->findAll(opt); // Find all TEXTUREx entries opt.match_type = EntryType::fromId("pnames"); auto import_pnames = bra->findLast(opt); // Find last PNAMES entry // Check enough entries exist if (import_tx.empty() || !import_pnames) { wxMessageBox( "The selected Base Resource Archive does not contain " "sufficient texture definition entries", "Error", wxICON_ERROR); continue; } // Copy TEXTUREx entries over to current archive for (auto& a : import_tx) { auto texturex = archive->addEntry(a, "global", true); texturex->setType(EntryType::fromId("texturex")); texturex->setExtensionByType(); } // Copy PNAMES entry over to current archive entry_pnames = archive->addEntry(import_pnames, "global", true); entry_pnames->setType(EntryType::fromId("pnames")); entry_pnames->setExtensionByType(); } break; } return true; } // 'No' clicked return false; } else // TEXTUREx entry exists { // TODO: Probably a better idea here to get the user to select an archive to import the patch table from // If no PNAMES entry was found, search resource archives if (!entry_pnames) { Archive::SearchOptions opt; opt.match_type = EntryType::fromId("pnames"); entry_pnames = App::archiveManager().findResourceEntry(opt, archive); } // If no PNAMES entry is found at all, show an error and abort // TODO: ask user to select appropriate base resource archive if (!entry_pnames) { wxMessageBox("PNAMES entry not found!", wxMessageBoxCaptionStr, wxICON_ERROR); return false; } return true; } return false; }