// 图片文件读取模块 // 负责处理所有图片文件相关的逻辑 const fs = require('fs'); const path = require('path'); class TextureReader { constructor(serverDir) { this.serverDir = serverDir; this.sequencesPath = path.join(serverDir, 'disk_data'); } /** * 获取可用的文件夹列表 * @returns {Promise} 文件夹名称数组 */ async getAvailableFolders() { return new Promise((resolve, reject) => { // 检查目录是否存在 fs.access(this.sequencesPath, fs.constants.F_OK, (accessErr) => { if (accessErr) { // 目录不存在,返回空数组而不是错误 console.log(`[TextureReader] 目录不存在: ${this.sequencesPath},返回空列表`); resolve([]); return; } fs.readdir(this.sequencesPath, { withFileTypes: true }, (err, files) => { if (err) { // 读取失败,返回空数组而不是错误 console.error(`[TextureReader] 读取目录失败: ${this.sequencesPath}`, err); resolve([]); return; } const folders = files .filter(dirent => dirent.isDirectory() && dirent.name.startsWith('player_')) .map(dirent => dirent.name) .sort(); resolve(folders); }); }); }); } /** * 获取指定文件夹中的帧文件列表 * @param {string} folderName - 文件夹名称 * @returns {Promise<{frames: number[], maxFrame: number}>} 帧文件信息 */ async getFrameFiles(folderName) { return new Promise((resolve, reject) => { // 处理URL编码和空格 const decodedFolderName = decodeURIComponent(folderName).replace(/%20/g, ' '); const folderPath = path.join(this.sequencesPath, decodedFolderName); console.log(`[TextureReader] 请求帧列表: folderName=${folderName}, decoded=${decodedFolderName}, folderPath=${folderPath}`); // 检查目录是否存在 fs.access(folderPath, fs.constants.F_OK, (accessErr) => { if (accessErr) { console.error(`[TextureReader] 文件夹不存在: ${folderPath}`); reject(new Error('Folder not found')); return; } fs.readdir(folderPath, (err, files) => { if (err) { console.error(`[TextureReader] 读取文件夹失败: ${err.message}, folderPath=${folderPath}`); reject(err); return; } // 过滤出 PNG 文件,并按数字排序 // 支持两种格式:1) 纯数字.png (如 00.png) 2) 任意名称+数字.png (如 动画00.png) const pngFiles = files .filter(file => file.toLowerCase().endsWith('.png')) .map(file => { let frameNum = null; // 先尝试匹配纯数字格式:00.png, 01.png let match = /^(\d+)\.png$/i.exec(file); if (match) { frameNum = parseInt(match[1], 10); } else { // 再尝试匹配文件名末尾的数字格式:动画00.png, 生成白色背景长矛刺击动画00.png match = /(\d+)\.png$/i.exec(file); if (match) { frameNum = parseInt(match[1], 10); } } if (frameNum !== null) { return { frameNum: frameNum, fileName: file }; } return null; }) .filter(item => item !== null) .sort((a, b) => a.frameNum - b.frameNum); const result = { frames: pngFiles.map(item => item.frameNum), fileNames: pngFiles.map(item => item.fileName), maxFrame: pngFiles.length > 0 ? Math.max(...pngFiles.map(item => item.frameNum)) : 0 }; resolve(result); }); }); }); } /** * 处理获取文件夹列表的 API 请求 * @param {http.ServerResponse} res - HTTP 响应对象 */ handleGetFolders(res) { this.getAvailableFolders() .then(folders => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(folders)); }) .catch(err => { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to read sequences directory', details: err.message })); }); } /** * 处理获取帧文件列表的 API 请求 * @param {string} folderName - 文件夹名称 * @param {http.ServerResponse} res - HTTP 响应对象 */ handleGetFrames(folderName, res) { this.getFrameFiles(folderName) .then(result => { // 检查是否有图片 if (!result.frames || result.frames.length === 0) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: '该文件夹中没有图片', frames: [] })); return; } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result)); }) .catch(err => { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Folder not found', details: err.message })); }); } } module.exports = TextureReader;