UNPKG

pw-guild-icon-parser

Version:

Parser for Perfect World guild icon lists - converts PNG icons to DDS atlas format with DXT5 compression

126 lines 5.7 kB
import { parseIconList, updateIconList, ensureDefaultIcon, calculateIconPosition } from './parser.js'; import { validateAndConvertPNG } from './image/converter.js'; import { writeDDS } from './dds/writer.js'; import { createEmptyAtlas, placeIconInAtlas } from './atlas/builder.js'; import { patchDDSIcon } from './dds/patcher.js'; import { existsSync } from 'fs'; import { join, dirname } from 'path'; /** * Add a new icon to the guild icon list * * @param options - Configuration options * @param options.fid - Faction ID * @param options.serverId - Server ID * @param options.pngPath - Path to the 16x16 PNG file (valid PNG format) * @param options.txtPath - Path to the iconlist_guild.txt file * @param options.ddsPath - Path to the iconlist_guild.dds file (will be created/updated) * @param options.iconsDir - Optional directory where PNG icons are stored (default: same as txtPath/../icones) * * @throws Error if PNG is invalid, dimensions are wrong, or file operations fail */ export async function addIcon(options) { const { fid, serverId, pngPath, txtPath, ddsPath, iconsDir } = options; // Validate inputs if (!existsSync(pngPath)) { throw new Error(`PNG file not found: ${pngPath}`); } // Create TXT file if it doesn't exist if (!existsSync(txtPath)) { const { writeFile } = await import('fs/promises'); // Create default file: 16x16 icons, 62x62 grid (Windows line endings CRLF) await writeFile(txtPath, '16\r\n16\r\n62\r\n62\r\n', 'utf-8'); } // Ensure default icon 0_0 exists await ensureDefaultIcon(txtPath); // Parse icon list (will be updated if needed) const config = await parseIconList(txtPath); // Validate PNG and convert to RGBA const iconData = await validateAndConvertPNG(pngPath); // Get icon name (format: serverid_fid.dds) const iconName = `${serverId}_${fid}.dds`; const iconsDirPath = iconsDir || join(dirname(txtPath), 'icones'); // Copy PNG to icons directory first (using same naming: serverid_fid.png) const { copyFile } = await import('fs/promises'); const targetPngPath = join(iconsDirPath, iconName.replace('.dds', '.png')); await copyFile(pngPath, targetPngPath); // Check if icon already exists in TXT const iconExists = config.icons.includes(iconName); let iconIndex; if (iconExists) { // Icon already exists - find its index iconIndex = config.icons.indexOf(iconName); } else { // Icon doesn't exist - add it // Check if we have space in the grid const maxIcons = config.gridWidth * config.gridHeight; if (config.icons.length >= maxIcons) { throw new Error(`Grid is full. Maximum ${maxIcons} icons supported (${config.gridWidth}x${config.gridHeight})`); } // Update TXT file with new icon name await updateIconList(txtPath, iconName); // Re-parse to get updated config const updatedConfig = await parseIconList(txtPath); iconIndex = updatedConfig.icons.indexOf(iconName); } // Calculate atlas dimensions const atlasWidth = config.iconWidth * config.gridWidth; const atlasHeight = config.iconHeight * config.gridHeight; // Try fast path: patch existing DDS file if (existsSync(ddsPath)) { try { // Calculate position for this icon const position = calculateIconPosition(iconIndex, config.gridWidth); // Patch the DDS file directly (FAST - only updates the specific icon blocks) await patchDDSIcon(ddsPath, iconData.data, config.iconWidth, config.iconHeight, position, atlasWidth); return; // Done! } catch (error) { // If patching fails, fall back to full rebuild console.warn(`Fast path failed, rebuilding atlas: ${error}`); } } // Slow path: rebuild entire DDS (only when file doesn't exist or is invalid) await rebuildAtlasFromTxt(txtPath, ddsPath, iconsDirPath); } /** * Rebuild the complete DDS atlas from the TXT file (only when necessary) * Returns the atlas data instead of reading it back from disk */ async function rebuildAtlasFromTxt(txtPath, ddsPath, iconsDir) { const config = await parseIconList(txtPath); // Create empty atlas const atlas = createEmptyAtlas(config); // Load and place each icon in order for (let i = 0; i < config.icons.length; i++) { const iconName = config.icons[i]; const iconPngPath = join(iconsDir, iconName.replace('.dds', '.png')); if (!existsSync(iconPngPath)) { console.warn(`Warning: PNG file not found for ${iconName}, skipping`); continue; } try { // Load PNG and convert to RGBA const iconData = await validateAndConvertPNG(iconPngPath); // Calculate position based on index const position = calculateIconPosition(i, config.gridWidth); // Place icon in atlas placeIconInAtlas(atlas, iconData.data, config.iconWidth, config.iconHeight, position); } catch (error) { console.warn(`Warning: Failed to load icon ${iconName}: ${error}`); } } // Write complete DDS file await writeDDS(ddsPath, atlas.width, atlas.height, atlas.rgbaData); // Return atlas data directly (no need to read back from disk) return { width: atlas.width, height: atlas.height, rgbaData: atlas.rgbaData }; } export * from './types.js'; export { parseIconList, calculateIconPosition } from './parser.js'; export { validateAndConvertPNG } from './image/converter.js'; //# sourceMappingURL=index.js.map