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
JavaScript
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