add cpk support
This commit is contained in:
parent
735fe414ca
commit
abb4cd24cb
@ -15,6 +15,7 @@ node index.js <Command> <Options> <Path>...
|
||||
```shell
|
||||
acb2hcas [-d] [-k <key>] [-t <type>] [-o <outputDir>] [-s] <acbPath>...
|
||||
acb2wavs [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <acbPath>...
|
||||
acb_mix [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <acbPath>...
|
||||
awb2hcas [-d] [-k <key>] [-w <awbKey>] [-t <type>] [-o <outputDir>] [-s] <awbPath>...
|
||||
awb2wavs [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <awbPath>...
|
||||
hca2wav [-k <key>] [-o <outputFile>] [-v <volume>] [-m <mode>] <hcaPath>...
|
||||
@ -22,7 +23,7 @@ view_utf [-o <outputFile>] <acbPath/acfPath>...
|
||||
decrypt_acb [-k <key>] [-t <type>] <acbPath>...
|
||||
decrypt_awb [-k <key>] [-t <type>] <awbPath>...
|
||||
decrypt_hca [-k <key>] [-w <awbKey>] [-t <type>] <hcaPath>...
|
||||
mix_acb [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <acbPath>...
|
||||
extract_cpk [-o <outputDir>] <cpkPath>...
|
||||
```
|
||||
|
||||
## Options
|
||||
@ -43,6 +44,8 @@ acb2hcas - Extract acb file to hca files (with/without decrypt)
|
||||
|
||||
acb2wavs - Extract acb file and convert hca files To wav files
|
||||
|
||||
acb_mix - Experimental. Convert acb file (and awb files) to mixed wav files
|
||||
|
||||
awb2hcas - Extract awb file to hca files (with/without decrypt)
|
||||
|
||||
awb2wavs - Extract awb file and convert hca files To wav files
|
||||
@ -57,7 +60,7 @@ decrypt_awb - Decrypt awb file (Warning: will overwrite orignal file)
|
||||
|
||||
decrypt_hca - Decrypt hca file (Warning: will overwrite orignal file)
|
||||
|
||||
mix_acb - Experimental. Convert acb file (and awb files) to mixed wav files
|
||||
extract_cpk - Extract cpk file
|
||||
|
||||
## Tips
|
||||
|
||||
|
3
acb2hcas.bat
Normal file
3
acb2hcas.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
node.exe "%~dp0src\index.js" acb2hcas %*
|
||||
pause
|
@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0index.js" acb2hcas -d -k %key% %*
|
||||
pause
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0src\index.js" acb2hcas -d -k %key% %*
|
||||
pause
|
@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0index.js" mix_acb -k %key% %*
|
||||
pause
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0src\index.js" acb2wavs -k %key% %*
|
||||
pause
|
@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0index.js" awb2wavs -k %key% %*
|
||||
pause
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0src\index.js" acb_mix -k %key% %*
|
||||
pause
|
3
awb2hcas.bat
Normal file
3
awb2hcas.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
node.exe "%~dp0src\index.js" awb2hcas %*
|
||||
pause
|
5
awb2hcas_decrypt.bat
Normal file
5
awb2hcas_decrypt.bat
Normal file
@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0src\index.js" awb2hcas -d -k %key% %*
|
||||
pause
|
@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0index.js" acb2wavs -k %key% %*
|
||||
pause
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0src\index.js" awb2wavs -k %key% %*
|
||||
pause
|
5
decrypt_acb.bat
Normal file
5
decrypt_acb.bat
Normal file
@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0src\index.js" decrypt_acb -k %key% %*
|
||||
pause
|
5
decrypt_awb.bat
Normal file
5
decrypt_awb.bat
Normal file
@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0src\index.js" decrypt_awb -k %key% %*
|
||||
pause
|
@ -1,7 +1,7 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
set /P awbKey=Please input awbKey and press Enter:
|
||||
if "%awbKey%"=="" set awbKey=0
|
||||
node.exe "%~dp0index.js" decrypt_hca -k %key% -w %awbKey% %*
|
||||
pause
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
set /P awbKey=Please input awbKey and press Enter:
|
||||
if "%awbKey%"=="" set awbKey=0
|
||||
node.exe "%~dp0src\index.js" decrypt_hca -k %key% -w %awbKey% %*
|
||||
pause
|
3
extract_cpk.bat
Normal file
3
extract_cpk.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
node.exe "%~dp0src\index.js" extract_cpk %*
|
||||
pause
|
@ -1,7 +1,7 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
set /P awbKey=Please input awbKey and press Enter:
|
||||
if "%awbKey%"=="" set awbKey=0
|
||||
node.exe "%~dp0index.js" hca2wav -k %key% -w %awbKey% %*
|
||||
pause
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
set /P awbKey=Please input awbKey and press Enter:
|
||||
if "%awbKey%"=="" set awbKey=0
|
||||
node.exe "%~dp0src\index.js" hca2wav -k %key% -w %awbKey% %*
|
||||
pause
|
3
keys.txt
3
keys.txt
@ -61,3 +61,6 @@ ONE PIECE DANCE BATTLE
|
||||
|
||||
アイドルマスター ミリオンライブ! シアターデイズ
|
||||
765765765765765
|
||||
|
||||
ハイスクール・フリート 艦隊バトルでピンチ!
|
||||
43472919336422565
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "critools",
|
||||
"version": "1.0.0",
|
||||
"description": "JavaScript tools for extract audio from game file",
|
||||
"main": "index.js",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
@ -1,383 +1,287 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const utf = require('./utf');
|
||||
const afs2 = require('./afs2');
|
||||
const hca = require('./hca');
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const mkdir = util.promisify(fs.mkdir);
|
||||
|
||||
function findZero(buffer, start) {
|
||||
while (buffer[start] !== 0x0) start++;
|
||||
return start;
|
||||
}
|
||||
|
||||
function parseUtf(buffer, toString) {
|
||||
if (!buffer || buffer.length < 4) return null;
|
||||
let pos = 0;
|
||||
const config = {};
|
||||
config.magic = buffer.slice(pos, 4).toString(); pos += 4;
|
||||
if (config.magic !== '@UTF') return null;
|
||||
config.dataSize = buffer.readUInt32BE(pos); pos += 4;
|
||||
buffer = buffer.slice(pos);
|
||||
pos = 0;
|
||||
config.unknown = buffer.readUInt16BE(pos); pos += 2;
|
||||
if (config.unknown !== 1) debugger;
|
||||
config.valueOffset = buffer.readUInt16BE(pos); pos += 2;
|
||||
config.stringOffset = buffer.readUInt32BE(pos); pos += 4;
|
||||
config.dataOffset = buffer.readUInt32BE(pos); pos += 4;
|
||||
config.nameOffset = buffer.readUInt32BE(pos); pos += 4;
|
||||
config.elementCount = buffer.readUInt16BE(pos); pos += 2;
|
||||
config.valueSize = buffer.readUInt16BE(pos); pos += 2;
|
||||
config.pageCount = buffer.readUInt32BE(pos); pos += 4;
|
||||
let stringEnd = findZero(buffer, config.stringOffset);
|
||||
config.name = buffer.slice(config.stringOffset, stringEnd).toString();
|
||||
let valuePos = config.valueOffset;
|
||||
const pages = [];
|
||||
config.types = [];
|
||||
let firstPos = pos;
|
||||
for (let i = 0; i < config.pageCount; i++) {
|
||||
let page = {};
|
||||
pos = firstPos;
|
||||
for (let j = 0; j < config.elementCount; j++) {
|
||||
const type = buffer.readUInt8(pos); pos = pos + 1;
|
||||
if (i === 0) config.types[j] = type;
|
||||
let stringOffset = config.stringOffset + buffer.readUInt32BE(pos); pos += 4;
|
||||
stringEnd = findZero(buffer, stringOffset);
|
||||
const key = buffer.slice(stringOffset, stringEnd).toString();
|
||||
let offset = 0;
|
||||
switch (type >>> 5) {
|
||||
case 0: debugger; break;
|
||||
case 1: offset = pos; break;
|
||||
case 2: offset = valuePos; break;
|
||||
}
|
||||
let value;
|
||||
switch (type & 0x1F) {
|
||||
case 0x10: value = buffer.readInt8(offset); offset += 1; break;
|
||||
case 0x11: value = buffer.readUInt8(offset); offset += 1; break;
|
||||
case 0x12: value = buffer.readInt16BE(offset); offset += 2; break;
|
||||
case 0x13: value = buffer.readUInt16BE(offset); offset += 2; break;
|
||||
case 0x14: value = buffer.readInt32BE(offset); offset += 4; break;
|
||||
case 0x15: value = buffer.readUInt32BE(offset); offset += 4; break;
|
||||
case 0x16: value = buffer.readBigInt64BE(offset); offset += 8; break;
|
||||
case 0x17: value = buffer.readBigUInt64BE(offset); offset += 8; break;
|
||||
case 0x18: value = buffer.readFloatBE(offset); offset += 4; break;
|
||||
case 0x19: debugger; value = buffer.readDoubleBE(offset); offset += 8; break;
|
||||
case 0x1A:
|
||||
stringOffset = config.stringOffset + buffer.readUInt32BE(offset); offset += 4;
|
||||
stringEnd = findZero(buffer, stringOffset);
|
||||
value = buffer.slice(stringOffset, stringEnd).toString();
|
||||
break;
|
||||
case 0x1B:
|
||||
const bufferStart = config.dataOffset + buffer.readUInt32BE(offset); offset += 4;
|
||||
const bufferLen = buffer.readUInt32BE(offset); offset += 4;
|
||||
value = buffer.slice(bufferStart, bufferStart + bufferLen);
|
||||
let temp = parseUtf(value, toString);
|
||||
if (temp) value = temp; else if (toString) value = buffer.slice(bufferStart, bufferStart + bufferLen).toString('hex');
|
||||
break;
|
||||
}
|
||||
switch (type >>> 5) {
|
||||
case 0: debugger; break;
|
||||
case 1: pos = offset; break;
|
||||
case 2: valuePos = offset; break;
|
||||
}
|
||||
page[key] = value;
|
||||
}
|
||||
pages.push(page);
|
||||
}
|
||||
pages.config = config;
|
||||
return pages;
|
||||
}
|
||||
exports.parse = parseUtf;
|
||||
|
||||
async function parseAcb(acbPath) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
const buffer = await readFile(acbPath);
|
||||
const utfs = utf.parse(buffer);
|
||||
if (!utfs) throw new Error(`NOT ACB FILE`);
|
||||
if (utfs.length !== 1) debugger;
|
||||
const acb = utfs[0];
|
||||
acb.buffer = buffer;
|
||||
acb.memoryHcas = await afs2.parse(acb.AwbFile);
|
||||
acb.streamHcas = [];
|
||||
for (let i = 0; i < acb.StreamAwbHash.length; i++) {
|
||||
const StreamAwb = acb.StreamAwbHash[i];
|
||||
const awbPath = path.join(pathInfo.dir, StreamAwb.Name + '.awb');
|
||||
if (fs.existsSync(awbPath)) {
|
||||
const obj = await afs2.parse(awbPath);
|
||||
acb.streamHcas.push(obj);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < acb.WaveformTable.length; i++) {
|
||||
const Waveform = acb.WaveformTable[i];
|
||||
const isMemory = Waveform.Streaming === 0;
|
||||
if (!isMemory) {
|
||||
if (!acb.streamHcas[Waveform.StreamAwbPortNo]) {
|
||||
throw new Error(`MISSING ${acb.StreamAwbHash[i].Name}.awb`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return acb;
|
||||
}
|
||||
|
||||
async function parseCommand(acb, command, key) {
|
||||
let samplingRate = 0, channelCount = 0;
|
||||
let k = 0;
|
||||
const commands = [];
|
||||
while (k < command.length) {
|
||||
const cmd = command.readUInt16BE(k); k += 2;
|
||||
const len = command.readUInt8(k); k += 1;
|
||||
if (len !== 4 && len !== 0) debugger;
|
||||
let file, u16;
|
||||
switch (cmd) {
|
||||
case 0x0000:
|
||||
k = command.length;
|
||||
break;
|
||||
case 0x07d0: // Start Waveform
|
||||
u16 = command.readUInt16BE(k); k += 2;
|
||||
if (u16 !== 0x0002) debugger;
|
||||
const SynthIndex = command.readUInt16BE(k); k += 2;
|
||||
const Synth = acb.SynthTable[SynthIndex];
|
||||
u16 = Synth.ReferenceItems.readUInt16BE(0);
|
||||
if (u16 !== 0x0001) debugger;
|
||||
const WaveformIndex = Synth.ReferenceItems.readUInt16BE(2);
|
||||
const Waveform = acb.WaveformTable[WaveformIndex];
|
||||
const isMemory = Waveform.Streaming === 0;
|
||||
if (Waveform.EncodeType === 2) {
|
||||
file = isMemory ? acb.memoryHcas[Waveform.MemoryAwbId] : acb.streamHcas[Waveform.StreamAwbPortNo][Waveform.StreamAwbId];
|
||||
if (Buffer.isBuffer(file)) {
|
||||
const awbKey = isMemory ? acb.memoryHcas.config.key : acb.streamHcas[Waveform.StreamAwbPortNo].config.key;
|
||||
file = await hca.decode(file, key, awbKey);
|
||||
if (isMemory) acb.memoryHcas[Waveform.MemoryAwbId] = file; else acb.streamHcas[Waveform.StreamAwbPortNo][Waveform.StreamAwbId] = file;
|
||||
if (samplingRate === 0) samplingRate = file.samplingRate; else if (samplingRate !== file.samplingRate) throw new Error(`SamplingRate Different`);
|
||||
if (channelCount === 0) channelCount = file.channelCount; else if (channelCount !== file.channelCount) throw new Error(`ChannelCount Different`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Not HCA File`);
|
||||
}
|
||||
commands.push({ type: 0, pcmData: file.pcmData });
|
||||
break;
|
||||
case 0x07d1: // Set Position
|
||||
const StartOffset = command.readUInt32BE(k); k += 4;
|
||||
if (StartOffset > 3600000) debugger;
|
||||
commands.push({ type: 1, offset: StartOffset });
|
||||
break;
|
||||
default:
|
||||
debugger;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { commands, samplingRate, channelCount };
|
||||
}
|
||||
|
||||
async function mixAcb(acbPath, key, wavDir, mode, skip) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const acb = await parseAcb(acbPath);
|
||||
if (wavDir === undefined) wavDir = path.join(pathInfo.dir, acb.Name);
|
||||
if (!fs.existsSync(wavDir)) {
|
||||
await mkdir(wavDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
console.log(`Mixing ${pathInfo.base}...`);
|
||||
const cueNameMap = {};
|
||||
for (let i = 0; i < acb.CueNameTable.length; i++) {
|
||||
const cueName = acb.CueNameTable[i];
|
||||
cueNameMap[cueName.CueIndex] = cueName.CueName;
|
||||
}
|
||||
for (let i = 0; i < acb.CueTable.length; i++) {
|
||||
const Cue = acb.CueTable[i];
|
||||
let samplingRate = 0, channelCount = 0;
|
||||
if (Cue.ReferenceType !== 3) debugger;
|
||||
const Sequence = acb.SequenceTable[Cue.ReferenceIndex];
|
||||
// Sequence.Type: 0 - Polyphonic, 1 - Sequential, Random, Random No Repeat, Switch, Shuffle Cue, Combo Sequential, Track Transition by Selector
|
||||
const timeline = [];
|
||||
let size = 0;
|
||||
for (let j = 0; j < Sequence.NumTracks; j++) {
|
||||
const index = Sequence.TrackIndex.readUInt16BE(j * 2);
|
||||
const Track = acb.TrackTable[index];
|
||||
const TrackEvent = acb.TrackEventTable[Track.EventIndex];
|
||||
const track = await parseCommand(acb, TrackEvent.Command, key);
|
||||
if (track.samplingRate) {
|
||||
if (samplingRate === 0) samplingRate = track.samplingRate; else if (track.samplingRate !== samplingRate) throw new Error(`SamplingRate Different`);
|
||||
}
|
||||
if (track.channelCount) {
|
||||
if (channelCount === 0) channelCount = track.channelCount; else if (track.channelCount !== channelCount) throw new Error(`ChannelCount Different`);
|
||||
}
|
||||
let time = 0;
|
||||
for (let k = 0; k < track.commands.length; k++) {
|
||||
const command = track.commands[k];
|
||||
switch (command.type) {
|
||||
case 0:
|
||||
let m = 0;
|
||||
while (m < timeline.length && time > timeline[m].time) m++;
|
||||
let offset = Math.round(time * samplingRate * channelCount / 1000);
|
||||
if (offset % channelCount !== 0) offset += channelCount - offset % channelCount;
|
||||
if (m == timeline.length) timeline.push({ time, offset, pcmDatas: [] });
|
||||
const last = timeline[m].offset + command.pcmData.length;
|
||||
if (last > size) size = last;
|
||||
timeline[m].pcmDatas.push(command.pcmData);
|
||||
break;
|
||||
case 1:
|
||||
time += command.offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (size === 0) continue;
|
||||
const pcmData = new Float32Array(size);
|
||||
if (timeline.length === 0) continue;
|
||||
timeline.push({ offset: 0xFFFFFFFF, pcmDatas: [] });
|
||||
const runnings = [];
|
||||
let now = timeline[0].offset;
|
||||
for (let i = 0; i < timeline.length; i++) {
|
||||
const wave = timeline[i];
|
||||
const len = wave.offset - now;
|
||||
const pcmDatas = [];
|
||||
let k = 0;
|
||||
while (k < runnings.length) {
|
||||
const running = runnings[k];
|
||||
let end = running.offset + len;
|
||||
if (end >= running.pcmData.length) {
|
||||
pcmDatas.push(running.pcmData.slice(running.offset));
|
||||
runnings.splice(k, 1);
|
||||
} else {
|
||||
pcmDatas.push(running.pcmData.slice(running.offset, end));
|
||||
running.offset = end;
|
||||
k++;
|
||||
}
|
||||
}
|
||||
for (let j = 0; j < wave.pcmDatas.length; j++) {
|
||||
runnings.push({
|
||||
pcmData: wave.pcmDatas[j],
|
||||
offset: 0
|
||||
});
|
||||
}
|
||||
k = now;
|
||||
if (pcmDatas.length > 0) {
|
||||
let max = 0;
|
||||
for (let j = 1; j < pcmDatas.length; j++) if (pcmDatas[j].length > max) max = j;
|
||||
for (let j = 0; j < pcmDatas[max].length; j++) {
|
||||
let f = 0;
|
||||
for (let m = 0; m < pcmDatas.length; m++) {
|
||||
if (j < pcmDatas[m].length) f += pcmDatas[m][j];
|
||||
}
|
||||
if (f > 1.0) f = 1.0;
|
||||
if (f < -1.0) f = -1.0;
|
||||
pcmData[k++] = f;
|
||||
}
|
||||
}
|
||||
now = wave.offset;
|
||||
}
|
||||
const wavPath = path.join(wavDir, cueNameMap[i] + '.wav');
|
||||
console.log(`Writing ${cueNameMap[i] + '.wav'}...`);
|
||||
await hca.writeWavFile(wavPath, mode, channelCount, samplingRate, pcmData);
|
||||
}
|
||||
}
|
||||
exports.mixAcb = mixAcb;
|
||||
|
||||
async function acb2hcas(acbPath, key, hcaDir, type, skip) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const acb = await parseAcb(acbPath);
|
||||
if (hcaDir === undefined) hcaDir = path.join(pathInfo.dir, acb.Name);
|
||||
if (!fs.existsSync(hcaDir)) {
|
||||
await mkdir(hcaDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
console.log(`Extracting ${pathInfo.base}...`);
|
||||
let memory = 0, stream = 0;
|
||||
for (let i = 0; i < acb.WaveformTable.length; i++) {
|
||||
const Waveform = acb.WaveformTable[i];
|
||||
const isMemory = Waveform.Streaming === 0;
|
||||
const hcaBuffer = isMemory ? acb.memoryHcas[Waveform.MemoryAwbId] : acb.streamHcas[Waveform.StreamAwbPortNo][Waveform.StreamAwbId];
|
||||
const awbKey = isMemory ? acb.memoryHcas.config.key : acb.streamHcas[Waveform.StreamAwbPortNo].config.key;
|
||||
const name = isMemory ? `memory_${++memory}.hca` : `stream_${++stream}.hca`;
|
||||
const hcaPath = path.join(hcaDir, name);
|
||||
if (key !== undefined) {
|
||||
console.log(`Decrypting ${name}...`);
|
||||
await hca.decrypt(hcaBuffer, key, awbKey, type);
|
||||
}
|
||||
console.log(`Writing ${name}...`);
|
||||
await writeFile(hcaPath, hcaBuffer);
|
||||
}
|
||||
}
|
||||
exports.acb2hcas = acb2hcas;
|
||||
|
||||
async function acb2wavs(acbPath, key, wavDir, volume, mode, skip) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const acb = await parseAcb(acbPath);
|
||||
if (wavDir === undefined) wavDir = path.join(pathInfo.dir, acb.Name);
|
||||
if (!fs.existsSync(wavDir)) {
|
||||
await mkdir(wavDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
console.log(`Extracting ${pathInfo.base}...`);
|
||||
let memory = 0, stream = 0;
|
||||
for (let i = 0; i < acb.WaveformTable.length; i++) {
|
||||
const Waveform = acb.WaveformTable[i];
|
||||
const isMemory = Waveform.Streaming === 0;
|
||||
const hcaBuffer = isMemory ? acb.memoryHcas[Waveform.MemoryAwbId] : acb.streamHcas[Waveform.StreamAwbPortNo][Waveform.StreamAwbId];
|
||||
const awbKey = isMemory ? acb.memoryHcas.config.key : acb.streamHcas[Waveform.StreamAwbPortNo].config.key;
|
||||
const name = isMemory ? `memory_${++memory}.wav` : `stream_${++stream}.wav`;
|
||||
const wavPath = path.join(wavDir, name);
|
||||
console.log(`Writing ${name}...`);
|
||||
await hca.decodeToWav(hcaBuffer, key, awbKey, wavPath, volume, mode);
|
||||
}
|
||||
}
|
||||
exports.acb2wavs = acb2wavs;
|
||||
|
||||
async function decryptAcb(acbPath, key, type) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const acb = await parseAcb(acbPath);
|
||||
console.log(`Decrypting ${pathInfo.base}...`);
|
||||
if (acb.memoryHcas) {
|
||||
for (let i = 0; i < acb.memoryHcas.length; i++) {
|
||||
await hca.decrypt(acb.memoryHcas[i], key, acb.memoryHcas.config.key, type);
|
||||
}
|
||||
acb.memoryHcas.config.buffer.writeUInt16BE(0, 0xE);
|
||||
}
|
||||
for (let i = 0; i < acb.StreamAwbHash.length; i++) {
|
||||
for (let j = 0; j < acb.streamHcas[i].length; j++) {
|
||||
await hca.decrypt(acb.streamHcas[i][j], key, acb.streamHcas[i].config.key, type);
|
||||
}
|
||||
const buffer = acb.streamHcas[i].config.buffer;
|
||||
buffer.writeUInt16BE(0, 0xE);
|
||||
const md5 = crypto.createHash('md5');
|
||||
md5.update(buffer);
|
||||
const hash = md5.digest();
|
||||
const awb = acb.StreamAwbHash[i];
|
||||
hash.copy(awb.Hash);
|
||||
await writeFile(path.join(pathInfo.dir, awb.Name + '.awb'), buffer);
|
||||
if (acb.StreamAwbAfs2Header) {
|
||||
const Header = acb.StreamAwbAfs2Header[i].Header;
|
||||
buffer.copy(Header, 0, 0, Header.length);
|
||||
}
|
||||
}
|
||||
await writeFile(acbPath, acb.buffer);
|
||||
}
|
||||
exports.decryptAcb = decryptAcb;
|
||||
|
||||
async function viewUtf(acbPath, outputPath) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
if (outputPath === undefined) outputPath = path.join(pathInfo.dir, pathInfo.name + '.json');
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const buffer = await readFile(acbPath);
|
||||
const utf = parseUtf(buffer, true);
|
||||
if (utf && utf.AwbFile && utf.AwbFile.length > 0x20) utf.AwbFile = utf.AwbFile.substring(0, 0x20);
|
||||
console.log(`Writing ${path.parse(outputPath).base}...`);
|
||||
await writeFile(outputPath, JSON.stringify(utf, null, 2));
|
||||
}
|
||||
exports.view = viewUtf;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const afs2 = require('./afs2');
|
||||
const hca = require('./hca');
|
||||
const utf = require('./utf');
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const mkdir = util.promisify(fs.mkdir);
|
||||
|
||||
async function parseAcb(acbPath) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
const buffer = await readFile(acbPath);
|
||||
const utfs = utf.parseUtf(buffer);
|
||||
if (!utfs) throw new Error(`NOT ACB FILE`);
|
||||
if (utfs.length !== 1) debugger;
|
||||
const acb = utfs[0];
|
||||
acb.buffer = buffer;
|
||||
acb.memoryHcas = await afs2.parseAFS2(acb.AwbFile);
|
||||
acb.streamHcas = [];
|
||||
for (let i = 0; i < acb.StreamAwbHash.length; i++) {
|
||||
const StreamAwb = acb.StreamAwbHash[i];
|
||||
const awbPath = path.join(pathInfo.dir, StreamAwb.Name + '.awb');
|
||||
if (fs.existsSync(awbPath)) {
|
||||
const obj = await afs2.parseAFS2(awbPath);
|
||||
acb.streamHcas.push(obj);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < acb.WaveformTable.length; i++) {
|
||||
const Waveform = acb.WaveformTable[i];
|
||||
const isMemory = Waveform.Streaming === 0;
|
||||
if (!isMemory) {
|
||||
if (!acb.streamHcas[Waveform.StreamAwbPortNo]) {
|
||||
throw new Error(`MISSING ${acb.StreamAwbHash[i].Name}.awb`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return acb;
|
||||
}
|
||||
|
||||
async function parseCommand(acb, command, key) {
|
||||
let samplingRate = 0, channelCount = 0;
|
||||
let k = 0;
|
||||
const commands = [];
|
||||
while (k < command.length) {
|
||||
const cmd = command.readUInt16BE(k); k += 2;
|
||||
const len = command.readUInt8(k); k += 1;
|
||||
if (len !== 4 && len !== 0) debugger;
|
||||
let file, u16;
|
||||
switch (cmd) {
|
||||
case 0x0000:
|
||||
k = command.length;
|
||||
break;
|
||||
case 0x07d0: // Start Waveform
|
||||
u16 = command.readUInt16BE(k); k += 2;
|
||||
if (u16 !== 0x0002) debugger;
|
||||
const SynthIndex = command.readUInt16BE(k); k += 2;
|
||||
const Synth = acb.SynthTable[SynthIndex];
|
||||
u16 = Synth.ReferenceItems.readUInt16BE(0);
|
||||
if (u16 !== 0x0001) debugger;
|
||||
const WaveformIndex = Synth.ReferenceItems.readUInt16BE(2);
|
||||
const Waveform = acb.WaveformTable[WaveformIndex];
|
||||
const isMemory = Waveform.Streaming === 0;
|
||||
if (Waveform.EncodeType === 2) {
|
||||
file = isMemory ? acb.memoryHcas[Waveform.MemoryAwbId] : acb.streamHcas[Waveform.StreamAwbPortNo][Waveform.StreamAwbId];
|
||||
if (Buffer.isBuffer(file)) {
|
||||
const awbKey = isMemory ? acb.memoryHcas.config.key : acb.streamHcas[Waveform.StreamAwbPortNo].config.key;
|
||||
file = await hca.decodeHca(file, key, awbKey);
|
||||
if (isMemory) acb.memoryHcas[Waveform.MemoryAwbId] = file; else acb.streamHcas[Waveform.StreamAwbPortNo][Waveform.StreamAwbId] = file;
|
||||
if (samplingRate === 0) samplingRate = file.samplingRate; else if (samplingRate !== file.samplingRate) throw new Error(`SamplingRate Different`);
|
||||
if (channelCount === 0) channelCount = file.channelCount; else if (channelCount !== file.channelCount) throw new Error(`ChannelCount Different`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Not HCA File`);
|
||||
}
|
||||
commands.push({ type: 0, pcmData: file.pcmData });
|
||||
break;
|
||||
case 0x07d1: // Set Position
|
||||
const StartOffset = command.readUInt32BE(k); k += 4;
|
||||
if (StartOffset > 3600000) debugger;
|
||||
commands.push({ type: 1, offset: StartOffset });
|
||||
break;
|
||||
default:
|
||||
debugger;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { commands, samplingRate, channelCount };
|
||||
}
|
||||
|
||||
async function mixAcb(acbPath, key, wavDir, mode, skip) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const acb = await parseAcb(acbPath);
|
||||
if (wavDir === undefined) wavDir = path.join(pathInfo.dir, acb.Name);
|
||||
if (!fs.existsSync(wavDir)) {
|
||||
await mkdir(wavDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
console.log(`Mixing ${pathInfo.base}...`);
|
||||
const cueNameMap = {};
|
||||
for (let i = 0; i < acb.CueNameTable.length; i++) {
|
||||
const cueName = acb.CueNameTable[i];
|
||||
cueNameMap[cueName.CueIndex] = cueName.CueName;
|
||||
}
|
||||
for (let i = 0; i < acb.CueTable.length; i++) {
|
||||
const Cue = acb.CueTable[i];
|
||||
let samplingRate = 0, channelCount = 0;
|
||||
if (Cue.ReferenceType !== 3) debugger;
|
||||
const Sequence = acb.SequenceTable[Cue.ReferenceIndex];
|
||||
// Sequence.Type: 0 - Polyphonic, 1 - Sequential, Random, Random No Repeat, Switch, Shuffle Cue, Combo Sequential, Track Transition by Selector
|
||||
const timeline = [];
|
||||
let size = 0;
|
||||
for (let j = 0; j < Sequence.NumTracks; j++) {
|
||||
const index = Sequence.TrackIndex.readUInt16BE(j * 2);
|
||||
const Track = acb.TrackTable[index];
|
||||
const TrackEvent = acb.TrackEventTable[Track.EventIndex];
|
||||
const track = await parseCommand(acb, TrackEvent.Command, key);
|
||||
if (track.samplingRate) {
|
||||
if (samplingRate === 0) samplingRate = track.samplingRate; else if (track.samplingRate !== samplingRate) throw new Error(`SamplingRate Different`);
|
||||
}
|
||||
if (track.channelCount) {
|
||||
if (channelCount === 0) channelCount = track.channelCount; else if (track.channelCount !== channelCount) throw new Error(`ChannelCount Different`);
|
||||
}
|
||||
let time = 0;
|
||||
for (let k = 0; k < track.commands.length; k++) {
|
||||
const command = track.commands[k];
|
||||
switch (command.type) {
|
||||
case 0:
|
||||
let m = 0;
|
||||
while (m < timeline.length && time > timeline[m].time) m++;
|
||||
let offset = Math.round(time * samplingRate * channelCount / 1000);
|
||||
if (offset % channelCount !== 0) offset += channelCount - offset % channelCount;
|
||||
if (m == timeline.length) timeline.push({ time, offset, pcmDatas: [] });
|
||||
const last = timeline[m].offset + command.pcmData.length;
|
||||
if (last > size) size = last;
|
||||
timeline[m].pcmDatas.push(command.pcmData);
|
||||
break;
|
||||
case 1:
|
||||
time += command.offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (size === 0) continue;
|
||||
const pcmData = new Float32Array(size);
|
||||
if (timeline.length === 0) continue;
|
||||
timeline.push({ offset: 0xFFFFFFFF, pcmDatas: [] });
|
||||
const runnings = [];
|
||||
let now = timeline[0].offset;
|
||||
for (let i = 0; i < timeline.length; i++) {
|
||||
const wave = timeline[i];
|
||||
const len = wave.offset - now;
|
||||
const pcmDatas = [];
|
||||
let k = 0;
|
||||
while (k < runnings.length) {
|
||||
const running = runnings[k];
|
||||
let end = running.offset + len;
|
||||
if (end >= running.pcmData.length) {
|
||||
pcmDatas.push(running.pcmData.slice(running.offset));
|
||||
runnings.splice(k, 1);
|
||||
} else {
|
||||
pcmDatas.push(running.pcmData.slice(running.offset, end));
|
||||
running.offset = end;
|
||||
k++;
|
||||
}
|
||||
}
|
||||
for (let j = 0; j < wave.pcmDatas.length; j++) {
|
||||
runnings.push({
|
||||
pcmData: wave.pcmDatas[j],
|
||||
offset: 0
|
||||
});
|
||||
}
|
||||
k = now;
|
||||
if (pcmDatas.length > 0) {
|
||||
let max = 0;
|
||||
for (let j = 1; j < pcmDatas.length; j++) if (pcmDatas[j].length > max) max = j;
|
||||
for (let j = 0; j < pcmDatas[max].length; j++) {
|
||||
let f = 0;
|
||||
for (let m = 0; m < pcmDatas.length; m++) {
|
||||
if (j < pcmDatas[m].length) f += pcmDatas[m][j];
|
||||
}
|
||||
if (f > 1.0) f = 1.0;
|
||||
if (f < -1.0) f = -1.0;
|
||||
pcmData[k++] = f;
|
||||
}
|
||||
}
|
||||
now = wave.offset;
|
||||
}
|
||||
const wavPath = path.join(wavDir, cueNameMap[i] + '.wav');
|
||||
console.log(`Writing ${cueNameMap[i] + '.wav'}...`);
|
||||
await hca.writeWavFile(wavPath, mode, channelCount, samplingRate, pcmData);
|
||||
}
|
||||
}
|
||||
exports.mixAcb = mixAcb;
|
||||
|
||||
async function acb2hcas(acbPath, key, hcaDir, type, skip) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const acb = await parseAcb(acbPath);
|
||||
if (hcaDir === undefined) hcaDir = path.join(pathInfo.dir, acb.Name);
|
||||
if (!fs.existsSync(hcaDir)) {
|
||||
await mkdir(hcaDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
console.log(`Extracting ${pathInfo.base}...`);
|
||||
let memory = 0, stream = 0;
|
||||
for (let i = 0; i < acb.WaveformTable.length; i++) {
|
||||
const Waveform = acb.WaveformTable[i];
|
||||
const isMemory = Waveform.Streaming === 0;
|
||||
const hcaBuffer = isMemory ? acb.memoryHcas[Waveform.MemoryAwbId] : acb.streamHcas[Waveform.StreamAwbPortNo][Waveform.StreamAwbId];
|
||||
const awbKey = isMemory ? acb.memoryHcas.config.key : acb.streamHcas[Waveform.StreamAwbPortNo].config.key;
|
||||
const name = isMemory ? `memory_${++memory}.hca` : `stream_${++stream}.hca`;
|
||||
const hcaPath = path.join(hcaDir, name);
|
||||
if (key !== undefined) {
|
||||
console.log(`Decrypting ${name}...`);
|
||||
await hca.decryptHca(hcaBuffer, key, awbKey, type);
|
||||
}
|
||||
console.log(`Writing ${name}...`);
|
||||
await writeFile(hcaPath, hcaBuffer);
|
||||
}
|
||||
}
|
||||
exports.acb2hcas = acb2hcas;
|
||||
|
||||
async function acb2wavs(acbPath, key, wavDir, volume, mode, skip) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const acb = await parseAcb(acbPath);
|
||||
if (wavDir === undefined) wavDir = path.join(pathInfo.dir, acb.Name);
|
||||
if (!fs.existsSync(wavDir)) {
|
||||
await mkdir(wavDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
console.log(`Extracting ${pathInfo.base}...`);
|
||||
let memory = 0, stream = 0;
|
||||
for (let i = 0; i < acb.WaveformTable.length; i++) {
|
||||
const Waveform = acb.WaveformTable[i];
|
||||
const isMemory = Waveform.Streaming === 0;
|
||||
const hcaBuffer = isMemory ? acb.memoryHcas[Waveform.MemoryAwbId] : acb.streamHcas[Waveform.StreamAwbPortNo][Waveform.StreamAwbId];
|
||||
const awbKey = isMemory ? acb.memoryHcas.config.key : acb.streamHcas[Waveform.StreamAwbPortNo].config.key;
|
||||
const name = isMemory ? `memory_${++memory}.wav` : `stream_${++stream}.wav`;
|
||||
const wavPath = path.join(wavDir, name);
|
||||
await hca.decodeHcaToWav(hcaBuffer, key, awbKey, wavPath, volume, mode);
|
||||
}
|
||||
}
|
||||
exports.acb2wavs = acb2wavs;
|
||||
|
||||
async function decryptAcb(acbPath, key, type) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const acb = await parseAcb(acbPath);
|
||||
console.log(`Decrypting ${pathInfo.base}...`);
|
||||
if (acb.memoryHcas) {
|
||||
for (let i = 0; i < acb.memoryHcas.length; i++) {
|
||||
await hca.decryptHca(acb.memoryHcas[i], key, acb.memoryHcas.config.key, type);
|
||||
}
|
||||
acb.memoryHcas.config.buffer.writeUInt16BE(0, 0xE);
|
||||
}
|
||||
for (let i = 0; i < acb.StreamAwbHash.length; i++) {
|
||||
for (let j = 0; j < acb.streamHcas[i].length; j++) {
|
||||
await hca.decryptHca(acb.streamHcas[i][j], key, acb.streamHcas[i].config.key, type);
|
||||
}
|
||||
const buffer = acb.streamHcas[i].config.buffer;
|
||||
buffer.writeUInt16BE(0, 0xE);
|
||||
const md5 = crypto.createHash('md5');
|
||||
md5.update(buffer);
|
||||
const hash = md5.digest();
|
||||
const awb = acb.StreamAwbHash[i];
|
||||
hash.copy(awb.Hash);
|
||||
await writeFile(path.join(pathInfo.dir, awb.Name + '.awb'), buffer);
|
||||
if (acb.StreamAwbAfs2Header) {
|
||||
const Header = acb.StreamAwbAfs2Header[i].Header;
|
||||
buffer.copy(Header, 0, 0, Header.length);
|
||||
}
|
||||
}
|
||||
await writeFile(acbPath, acb.buffer);
|
||||
}
|
||||
exports.decryptAcb = decryptAcb;
|
@ -1,121 +1,120 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
const hca = require('./hca');
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const mkdir = util.promisify(fs.mkdir);
|
||||
|
||||
async function parseAFS2(buffer) {
|
||||
if (typeof(buffer) === 'string') buffer = await readFile(buffer);
|
||||
if (!buffer || buffer.length < 4) return null;
|
||||
let pos = 0;
|
||||
const config = {};
|
||||
config.buffer = buffer;
|
||||
config.magic = buffer.slice(pos, 4).toString(); pos += 4;
|
||||
if (config.magic !== 'AFS2') return null;
|
||||
config.unknown1 = buffer.readUInt8(pos); pos += 1;
|
||||
config.sizeLen = buffer.readUInt8(pos); pos += 1;
|
||||
config.unknown2 = buffer.readUInt8(pos); pos += 1;
|
||||
config.unknown3 = buffer.readUInt8(pos); pos += 1;
|
||||
config.fileCount = buffer.readUInt32LE(pos); pos += 4;
|
||||
config.align = buffer.readUInt16LE(pos); pos += 2;
|
||||
config.key = buffer.readUInt16LE(pos); pos += 2;
|
||||
config.fileIds = [];
|
||||
for (let i = 0; i < config.fileCount; i++) {
|
||||
const fileId = buffer.readUInt16LE(pos); pos += 2;
|
||||
config.fileIds.push(fileId);
|
||||
}
|
||||
const files = [];
|
||||
let start;
|
||||
if (config.sizeLen === 2) {
|
||||
start = buffer.readUInt16LE(pos); pos += 2;
|
||||
} else if (config.sizeLen === 4) {
|
||||
start = buffer.readUInt32LE(pos); pos += 4;
|
||||
} else debugger;
|
||||
let mod = start % config.align;
|
||||
if (mod != 0) start += config.align - mod;
|
||||
for (let i = 0; i < config.fileCount; i++) {
|
||||
let end;
|
||||
if (config.sizeLen === 2) {
|
||||
end = buffer.readUInt16LE(pos); pos += 2;
|
||||
} else if (config.sizeLen === 4) {
|
||||
end = buffer.readUInt32LE(pos); pos += 4;
|
||||
} else debugger;
|
||||
files.push(buffer.slice(start, end));
|
||||
start = end;
|
||||
mod = start % config.align;
|
||||
if (mod != 0) start += config.align - mod;
|
||||
}
|
||||
files.config = config;
|
||||
return files;
|
||||
}
|
||||
exports.parse = parseAFS2;
|
||||
|
||||
async function awb2hcas(awbPath, key, hcaDir, type, skip) {
|
||||
const pathInfo = path.parse(awbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const list = await parseAFS2(awbPath);
|
||||
if (hcaDir === undefined) hcaDir = path.join(pathInfo.dir, pathInfo.name);
|
||||
if (!fs.existsSync(hcaDir)) {
|
||||
await mkdir(hcaDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
const len = ('' + list.length).length;
|
||||
console.log(`Extracting ${pathInfo.base}...`);
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const hcaBuff = list[i];
|
||||
let name = '' + (i + 1);
|
||||
while (name.length < len) name = '0' + name;
|
||||
if (key !== undefined) {
|
||||
console.log(`Decrypting ${name}.hca...`);
|
||||
await hca.decrypt(hcaBuff, key, list.config.key, type);
|
||||
}
|
||||
console.log(`Writing ${name}.hca...`);
|
||||
await writeFile(path.join(hcaDir, name + '.hca'), hcaBuff);
|
||||
}
|
||||
}
|
||||
exports.awb2hcas = awb2hcas;
|
||||
|
||||
async function awb2wavs(awbPath, key, wavDir, volume, mode, skip) {
|
||||
const pathInfo = path.parse(awbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const list = await parseAFS2(awbPath);
|
||||
if (wavDir === undefined) wavDir = path.join(pathInfo.dir, pathInfo.name);
|
||||
if (!fs.existsSync(wavDir)) {
|
||||
await mkdir(wavDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
const len = ('' + list.length).length;
|
||||
console.log(`Extracting ${pathInfo.base}...`);
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const hcaBuff = list[i];
|
||||
let name = '' + (i + 1);
|
||||
while (name.length < len) name = '0' + name;
|
||||
console.log(`Writing ${name}.wav...`);
|
||||
const wavPath = path.join(wavDir, name + '.wav');
|
||||
await hca.decodeToWav(hcaBuff, key, list.config.key, wavPath, volume, mode);
|
||||
}
|
||||
}
|
||||
exports.awb2wavs = awb2wavs;
|
||||
|
||||
async function decryptAwb(awbPath, key, type) {
|
||||
const pathInfo = path.parse(awbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const list = await parseAFS2(awbPath);
|
||||
console.log(`Decrypting ${pathInfo.base}...`);
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
await hca.decrypt(list[i], key, list.config.key, type);
|
||||
}
|
||||
const buffer = list.config.buffer;
|
||||
buffer.writeUInt16BE(0, 0xE);
|
||||
console.log(`Writing ${pathInfo.base}...`);
|
||||
await writeFile(awbPath, buffer);
|
||||
}
|
||||
exports.decryptAwb = decryptAwb;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
const hca = require('./hca');
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const mkdir = util.promisify(fs.mkdir);
|
||||
|
||||
async function parseAFS2(buffer) {
|
||||
if (typeof(buffer) === 'string') buffer = await readFile(buffer);
|
||||
if (!buffer || buffer.length < 4) return null;
|
||||
let pos = 0;
|
||||
const config = {};
|
||||
config.buffer = buffer;
|
||||
config.magic = buffer.slice(pos, 4).toString(); pos += 4;
|
||||
if (config.magic !== 'AFS2') return null;
|
||||
config.unknown1 = buffer.readUInt8(pos); pos += 1;
|
||||
config.sizeLen = buffer.readUInt8(pos); pos += 1;
|
||||
config.unknown2 = buffer.readUInt8(pos); pos += 1;
|
||||
config.unknown3 = buffer.readUInt8(pos); pos += 1;
|
||||
config.fileCount = buffer.readUInt32LE(pos); pos += 4;
|
||||
config.align = buffer.readUInt16LE(pos); pos += 2;
|
||||
config.key = buffer.readUInt16LE(pos); pos += 2;
|
||||
config.fileIds = [];
|
||||
for (let i = 0; i < config.fileCount; i++) {
|
||||
const fileId = buffer.readUInt16LE(pos); pos += 2;
|
||||
config.fileIds.push(fileId);
|
||||
}
|
||||
const files = [];
|
||||
let start;
|
||||
if (config.sizeLen === 2) {
|
||||
start = buffer.readUInt16LE(pos); pos += 2;
|
||||
} else if (config.sizeLen === 4) {
|
||||
start = buffer.readUInt32LE(pos); pos += 4;
|
||||
} else debugger;
|
||||
let mod = start % config.align;
|
||||
if (mod != 0) start += config.align - mod;
|
||||
for (let i = 0; i < config.fileCount; i++) {
|
||||
let end;
|
||||
if (config.sizeLen === 2) {
|
||||
end = buffer.readUInt16LE(pos); pos += 2;
|
||||
} else if (config.sizeLen === 4) {
|
||||
end = buffer.readUInt32LE(pos); pos += 4;
|
||||
} else debugger;
|
||||
files.push(buffer.slice(start, end));
|
||||
start = end;
|
||||
mod = start % config.align;
|
||||
if (mod != 0) start += config.align - mod;
|
||||
}
|
||||
files.config = config;
|
||||
return files;
|
||||
}
|
||||
exports.parseAFS2 = parseAFS2;
|
||||
|
||||
async function awb2hcas(awbPath, key, hcaDir, type, skip) {
|
||||
const pathInfo = path.parse(awbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const list = await parseAFS2(awbPath);
|
||||
if (hcaDir === undefined) hcaDir = path.join(pathInfo.dir, pathInfo.name);
|
||||
if (!fs.existsSync(hcaDir)) {
|
||||
await mkdir(hcaDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
const len = ('' + list.length).length;
|
||||
console.log(`Extracting ${pathInfo.base}...`);
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const hcaBuff = list[i];
|
||||
let name = '' + (i + 1);
|
||||
while (name.length < len) name = '0' + name;
|
||||
if (key !== undefined) {
|
||||
console.log(`Decrypting ${name}.hca...`);
|
||||
await hca.decryptHca(hcaBuff, key, list.config.key, type);
|
||||
}
|
||||
console.log(`Writing ${name}.hca...`);
|
||||
await writeFile(path.join(hcaDir, name + '.hca'), hcaBuff);
|
||||
}
|
||||
}
|
||||
exports.awb2hcas = awb2hcas;
|
||||
|
||||
async function awb2wavs(awbPath, key, wavDir, volume, mode, skip) {
|
||||
const pathInfo = path.parse(awbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const list = await parseAFS2(awbPath);
|
||||
if (wavDir === undefined) wavDir = path.join(pathInfo.dir, pathInfo.name);
|
||||
if (!fs.existsSync(wavDir)) {
|
||||
await mkdir(wavDir, { recursive: true });
|
||||
} else if (skip) {
|
||||
console.log(`Skipped ${pathInfo.base}...`);
|
||||
return;
|
||||
}
|
||||
const len = ('' + list.length).length;
|
||||
console.log(`Extracting ${pathInfo.base}...`);
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const hcaBuff = list[i];
|
||||
let name = '' + (i + 1);
|
||||
while (name.length < len) name = '0' + name;
|
||||
const wavPath = path.join(wavDir, name + '.wav');
|
||||
await hca.decodeHcaToWav(hcaBuff, key, list.config.key, wavPath, volume, mode);
|
||||
}
|
||||
}
|
||||
exports.awb2wavs = awb2wavs;
|
||||
|
||||
async function decryptAwb(awbPath, key, type) {
|
||||
const pathInfo = path.parse(awbPath);
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const list = await parseAFS2(awbPath);
|
||||
console.log(`Decrypting ${pathInfo.base}...`);
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
await hca.decryptHca(list[i], key, list.config.key, type);
|
||||
}
|
||||
const buffer = list.config.buffer;
|
||||
buffer.writeUInt16BE(0, 0xE);
|
||||
console.log(`Writing ${pathInfo.base}...`);
|
||||
await writeFile(awbPath, buffer);
|
||||
}
|
||||
exports.decryptAwb = decryptAwb;
|
128
src/cpk.js
Normal file
128
src/cpk.js
Normal file
@ -0,0 +1,128 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
const utf = require('./utf');
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const mkdir = util.promisify(fs.mkdir);
|
||||
|
||||
function parseTag(buffer, tag) {
|
||||
if (tag !== buffer.slice(0, 4).toString()) return null;
|
||||
const size = buffer.readUInt32LE(0x8);
|
||||
if (!size) return null;
|
||||
const offset = 0x10;
|
||||
return utf.parseUtf(buffer.slice(offset, offset + size));
|
||||
}
|
||||
|
||||
async function parseCpk(cpkPath) {
|
||||
const buffer = await readFile(cpkPath);
|
||||
let utfs = parseTag(buffer, 'CPK ');
|
||||
if (!utfs || utfs.length !== 1) return null;
|
||||
const cpk = { buffer };
|
||||
cpk.info = utfs[0];
|
||||
let offset, size;
|
||||
// HTOC
|
||||
offset = (Number)(cpk.info.HtocOffset);
|
||||
size = (Number)(cpk.info.HtocSize);
|
||||
if (offset && size) cpk.htoc = parseTag(buffer.slice(offset, offset + size), 'HTOC');
|
||||
// TOC
|
||||
offset = (Number)(cpk.info.TocOffset);
|
||||
size = (Number)(cpk.info.TocSize);
|
||||
if (offset && size) cpk.toc = parseTag(buffer.slice(offset, offset + size), 'TOC ');
|
||||
// ETOC
|
||||
offset = (Number)(cpk.info.EtocOffset);
|
||||
size = (Number)(cpk.info.EtocSize);
|
||||
if (offset && size) cpk.etoc = parseTag(buffer.slice(offset, offset + size), 'ETOC');
|
||||
return cpk;
|
||||
}
|
||||
|
||||
async function extractCpk(cpkPath, output) {
|
||||
const cpk = await parseCpk(cpkPath);
|
||||
if (!cpk) return;
|
||||
if (output === undefined) output = path.parse(cpkPath).dir;
|
||||
for (let i = 0; i < cpk.toc.length; i++) {
|
||||
const item = cpk.toc[i];
|
||||
let buffer = cpk.buffer;
|
||||
const offset = (Number)(cpk.info.TocOffset + item.FileOffset);
|
||||
let fileBuffer = buffer.slice(offset, offset + item.FileSize);
|
||||
fileBuffer = extract(fileBuffer);
|
||||
const dir = path.join(output, item.DirName);
|
||||
if (!fs.existsSync(dir)) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
await writeFile(path.join(dir, item.FileName), fileBuffer);
|
||||
}
|
||||
}
|
||||
exports.extractCpk = extractCpk;
|
||||
|
||||
function extract(buffer) {
|
||||
if ('CRILAYLA' !== buffer.slice(0, 0x8).toString()) return buffer;
|
||||
const uncompressSize = buffer.readUInt32LE(0x8);
|
||||
const headerOffset = buffer.readUInt32LE(0xC);
|
||||
const result = Buffer.allocUnsafe(uncompressSize + 0x100);
|
||||
for (let i = 0; i < 0x100; i++) result[i] = buffer[0x10 + headerOffset + i];
|
||||
let output = 0;
|
||||
const end = 0x100 + uncompressSize - 1;
|
||||
const lens = [ 2, 3, 5, 8 ];
|
||||
const reader = new BitReader(buffer.slice(0, buffer.length - 0x100));
|
||||
while (output < uncompressSize) {
|
||||
if (reader.getBits(1) > 0) {
|
||||
let offset = end - output + reader.getBits(13) + 3;
|
||||
let length = 3;
|
||||
let level;
|
||||
for (level = 0; level < lens.length; level++) {
|
||||
const lv = reader.getBits(lens[level]);
|
||||
length += lv;
|
||||
if (lv != ((1 << lens[level]) - 1)) break;
|
||||
}
|
||||
if (level === lens.length) {
|
||||
let lv;
|
||||
do {
|
||||
lv = reader.getBits(8);
|
||||
length += lv;
|
||||
} while (lv === 0xFF);
|
||||
}
|
||||
for (let i = 0; i < length; i++) {
|
||||
result[end - output] = result[offset--];
|
||||
output++;
|
||||
}
|
||||
} else {
|
||||
result[end - output] = reader.getBits(8);
|
||||
output++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class BitReader {
|
||||
constructor(buffer) {
|
||||
this.buffer = buffer;
|
||||
this.offset = buffer.length - 1;
|
||||
this.pool = 0;
|
||||
this.left = 0;
|
||||
}
|
||||
getBits(count) {
|
||||
let result = 0;
|
||||
let produced = 0;
|
||||
let round;
|
||||
while (produced < count) {
|
||||
if (this.left == 0) {
|
||||
this.pool = this.buffer[this.offset];
|
||||
this.left = 8;
|
||||
this.offset--;
|
||||
}
|
||||
if (this.left > (count - produced)) {
|
||||
round = count - produced;
|
||||
} else {
|
||||
round = this.left;
|
||||
}
|
||||
result <<= round;
|
||||
result |= ((this.pool >>> (this.left - round)) & ((1 << round) - 1));
|
||||
this.left -= round;
|
||||
produced += round;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
1852
hca.js → src/hca.js
1852
hca.js → src/hca.js
File diff suppressed because it is too large
Load Diff
@ -1,151 +1,158 @@
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
const path = require('path');
|
||||
const utf = require('./utf');
|
||||
const afs2 = require('./afs2');
|
||||
const hca = require('./hca');
|
||||
|
||||
const lstat = util.promisify(fs.lstat);
|
||||
const readdir = util.promisify(fs.readdir);
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage: node index.js <Command> <Options> <Path>...`);
|
||||
console.log(`Command:`);
|
||||
console.log(`\tacb2hcas [-d] [-k <key>] [-t <type>] [-o <outputDir>] [-s] <acbPath>...`);
|
||||
console.log(`\tacb2wavs [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <acbPath>...`);
|
||||
console.log(`\tawb2hcas [-d] [-k <key>] [-t <type>] [-o <outputDir>] [-s] <awbPath>...`);
|
||||
console.log(`\tawb2wavs [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <awbPath>...`);
|
||||
console.log(`\thca2wav [-k <key>] [-w <awbKey>] [-o <outputFile>] [-v <volume>] [-m <mode>] <hcaPath>...`);
|
||||
console.log(`\tview_utf [-o <outputFile>] <acbPath/acfPath>...`);
|
||||
console.log(`\tdecrypt_acb [-k <key>] [-t <type>] <acbPath>...`);
|
||||
console.log(`\tdecrypt_awb [-k <key>] [-t <type>] <awbPath>...`);
|
||||
console.log(`\tdecrypt_hca [-k <key>] [-w <awbKey>] [-t <type>] <hcaPath>...`);
|
||||
console.log(`\tmix_acb [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <acbPath>...`);
|
||||
console.log(`Options:`);
|
||||
console.log(`\t-d / --decrypt Decrypt hca files`);
|
||||
console.log(`\t-k / --key <key> Decrypt key`);
|
||||
console.log(`\t-w / --awbKey <awbKey> Decrypt key In Awb File`);
|
||||
console.log(`\t-t / --type <type> Hca Encrypt Type (1 / 0) (Default: 1)`);
|
||||
console.log(`\t-o / --output <output> Output Directory / File`);
|
||||
console.log(`\t-v / --volume <volume> Wav Volume (Default: 1.0)`);
|
||||
console.log(`\t-m / --mode <mode> Wav Bit Mode (Default: 16)`);
|
||||
console.log(`\t-s / --skip Skip exists files`);
|
||||
}
|
||||
|
||||
async function handlePathes(pathes, ext) {
|
||||
let i = 0;
|
||||
while (i < pathes.length) {
|
||||
const path1 = pathes[i];
|
||||
if (fs.existsSync(path1)) {
|
||||
const stats1 = await lstat(path1);
|
||||
if (stats1.isDirectory()) {
|
||||
pathes.splice(i, 1);
|
||||
const files = await readdir(path1);
|
||||
for (let j = 0; j < files.length; j++) {
|
||||
const base = files[j];
|
||||
const path2 = path.join(path1, base);
|
||||
const stats2 = await lstat(path2);
|
||||
if (path.parse(base).ext === ext || stats2.isDirectory()) {
|
||||
pathes.push(path2);
|
||||
}
|
||||
}
|
||||
} else if (ext && path.parse(path1).ext !== ext) {
|
||||
pathes.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
pathes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const argv = process.argv;
|
||||
if (argv.length < 3) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
let decrypt = false, key = undefined, awbKey = undefined, output = undefined, volume = 1, mode = 16, type = 1, skip = false;
|
||||
let i = 3;
|
||||
const pathes = [];
|
||||
while (i < argv.length) {
|
||||
const arg = argv.splice(i, 1)[0];
|
||||
if (arg === '-d' || arg === '--decrypt') {
|
||||
decrypt = true;
|
||||
} else if (arg === '-k' || arg === '--key') {
|
||||
key = argv.splice(i, 1)[0];
|
||||
} else if (arg === '-w' || arg === '--awbKey') {
|
||||
awbKey = parseInt(argv.splice(i, 1)[0], 10);
|
||||
} else if (arg === '-o' || arg === '--output') {
|
||||
output = argv.splice(i, 1)[0];
|
||||
} else if (arg === '-v' || arg === '--volume') {
|
||||
volume = parseFloat(argv.splice(i, 1)[0], 10);
|
||||
} else if (arg === '-m' || arg === '--mode') {
|
||||
mode = parseInt(argv.splice(i, 1)[0], 10);
|
||||
} else if (arg === '-t' || arg === '--type') {
|
||||
type = parseInt(argv.splice(i, 1)[0], 10);
|
||||
} else if (arg === '-s' || arg === '--skip') {
|
||||
skip = true;
|
||||
} else {
|
||||
pathes.push(arg);
|
||||
}
|
||||
}
|
||||
if (pathes.length === 0) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
switch (argv[2]) {
|
||||
case 'acb2hcas':
|
||||
if (!decrypt) key = undefined;
|
||||
await handlePathes(pathes, '.acb');
|
||||
for (let i = 0; i < pathes.length; i++) await utf.acb2hcas(pathes[i], key, output, type, skip);
|
||||
break;
|
||||
case 'acb2wavs':
|
||||
await handlePathes(pathes, '.acb');
|
||||
for (let i = 0; i < pathes.length; i++) await utf.acb2wavs(pathes[i], key, output, volume, mode, skip);
|
||||
break;
|
||||
case 'awb2hcas':
|
||||
if (!decrypt) key = undefined;
|
||||
await handlePathes(pathes, '.awb');
|
||||
for (let i = 0; i < pathes.length; i++) await afs2.awb2hcas(pathes[i], key, output, type, skip);
|
||||
break;
|
||||
case 'awb2wavs':
|
||||
await handlePathes(pathes, '.awb');
|
||||
for (let i = 0; i < pathes.length; i++) await afs2.awb2wavs(pathes[i], key, output, volume, mode, skip);
|
||||
break;
|
||||
case 'hca2wav':
|
||||
await handlePathes(pathes, '.hca');
|
||||
for (let i = 0; i < pathes.length; i++) await hca.decodeToWav(pathes[i], key, awbKey, output, volume, mode);
|
||||
break;
|
||||
case 'view_utf':
|
||||
await handlePathes(pathes);
|
||||
for (let i = 0; i < pathes.length; i++) await utf.view(pathes[i], output);
|
||||
break;
|
||||
case 'decrypt_acb':
|
||||
await handlePathes(pathes, '.acb');
|
||||
for (let i = 0; i < pathes.length; i++) await utf.decryptAcb(pathes[i], key, type);
|
||||
break;
|
||||
case 'decrypt_awb':
|
||||
await handlePathes(pathes, '.awb');
|
||||
for (let i = 0; i < pathes.length; i++) await afs2.decryptAwb(pathes[i], key, type);
|
||||
break;
|
||||
case 'decrypt_hca':
|
||||
await handlePathes(pathes, '.hca');
|
||||
for (let i = 0; i < pathes.length; i++) await hca.decrypt(pathes[i], key, awbKey, type, pathes[i]);
|
||||
break;
|
||||
case 'mix_acb':
|
||||
await handlePathes(pathes, '.acb');
|
||||
for (let i = 0; i < pathes.length; i++) await utf.mixAcb(pathes[i], key, output, mode, skip);
|
||||
break;
|
||||
default:
|
||||
usage();
|
||||
break;
|
||||
}
|
||||
console.log('FINISH!');
|
||||
} catch (e) {
|
||||
console.error(`ERROR: ${e.message}`);
|
||||
debugger;
|
||||
}
|
||||
})();
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
const path = require('path');
|
||||
const utf = require('./utf');
|
||||
const afs2 = require('./afs2');
|
||||
const acb = require('./acb');
|
||||
const hca = require('./hca');
|
||||
const cpk = require('./cpk');
|
||||
|
||||
const lstat = util.promisify(fs.lstat);
|
||||
const readdir = util.promisify(fs.readdir);
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage: node index.js <Command> <Options> <Path>...`);
|
||||
console.log(`Command:`);
|
||||
console.log(`\tacb2hcas [-d] [-k <key>] [-t <type>] [-o <outputDir>] [-s] <acbPath>...`);
|
||||
console.log(`\tacb2wavs [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <acbPath>...`);
|
||||
console.log(`\tawb2hcas [-d] [-k <key>] [-t <type>] [-o <outputDir>] [-s] <awbPath>...`);
|
||||
console.log(`\tawb2wavs [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <awbPath>...`);
|
||||
console.log(`\thca2wav [-k <key>] [-w <awbKey>] [-o <outputFile>] [-v <volume>] [-m <mode>] <hcaPath>...`);
|
||||
console.log(`\tview_utf [-o <outputFile>] <acbPath/acfPath>...`);
|
||||
console.log(`\tdecrypt_acb [-k <key>] [-t <type>] <acbPath>...`);
|
||||
console.log(`\tdecrypt_awb [-k <key>] [-t <type>] <awbPath>...`);
|
||||
console.log(`\tdecrypt_hca [-k <key>] [-w <awbKey>] [-t <type>] <hcaPath>...`);
|
||||
console.log(`\tacb_mix [-k <key>] [-o <outputDir>] [-v <volume>] [-m <mode>] [-s] <acbPath>...`);
|
||||
console.log(`\textract_cpk [-o <outputDir>] <cpkPath>...`);
|
||||
console.log(`Options:`);
|
||||
console.log(`\t-d / --decrypt Decrypt hca files`);
|
||||
console.log(`\t-k / --key <key> Decrypt key`);
|
||||
console.log(`\t-w / --awbKey <awbKey> Decrypt key In Awb File`);
|
||||
console.log(`\t-t / --type <type> Hca Encrypt Type (1 / 0) (Default: 1)`);
|
||||
console.log(`\t-o / --output <output> Output Directory / File`);
|
||||
console.log(`\t-v / --volume <volume> Wav Volume (Default: 1.0)`);
|
||||
console.log(`\t-m / --mode <mode> Wav Bit Mode (Default: 16)`);
|
||||
console.log(`\t-s / --skip Skip exists files`);
|
||||
}
|
||||
|
||||
async function handlePathes(pathes, ext) {
|
||||
let i = 0;
|
||||
while (i < pathes.length) {
|
||||
const path1 = pathes[i];
|
||||
if (fs.existsSync(path1)) {
|
||||
const stats1 = await lstat(path1);
|
||||
if (stats1.isDirectory()) {
|
||||
pathes.splice(i, 1);
|
||||
const files = await readdir(path1);
|
||||
for (let j = 0; j < files.length; j++) {
|
||||
const base = files[j];
|
||||
const path2 = path.join(path1, base);
|
||||
const stats2 = await lstat(path2);
|
||||
if (path.parse(base).ext === ext || stats2.isDirectory()) {
|
||||
pathes.push(path2);
|
||||
}
|
||||
}
|
||||
} else if (ext && path.parse(path1).ext !== ext) {
|
||||
pathes.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
pathes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const argv = process.argv;
|
||||
if (argv.length < 3) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
let decrypt = false, key = undefined, awbKey = undefined, output = undefined, volume = 1, mode = 16, type = 1, skip = false;
|
||||
let i = 3;
|
||||
const pathes = [];
|
||||
while (i < argv.length) {
|
||||
const arg = argv.splice(i, 1)[0];
|
||||
if (arg === '-d' || arg === '--decrypt') {
|
||||
decrypt = true;
|
||||
} else if (arg === '-k' || arg === '--key') {
|
||||
key = argv.splice(i, 1)[0];
|
||||
} else if (arg === '-w' || arg === '--awbKey') {
|
||||
awbKey = parseInt(argv.splice(i, 1)[0], 10);
|
||||
} else if (arg === '-o' || arg === '--output') {
|
||||
output = argv.splice(i, 1)[0];
|
||||
} else if (arg === '-v' || arg === '--volume') {
|
||||
volume = parseFloat(argv.splice(i, 1)[0], 10);
|
||||
} else if (arg === '-m' || arg === '--mode') {
|
||||
mode = parseInt(argv.splice(i, 1)[0], 10);
|
||||
} else if (arg === '-t' || arg === '--type') {
|
||||
type = parseInt(argv.splice(i, 1)[0], 10);
|
||||
} else if (arg === '-s' || arg === '--skip') {
|
||||
skip = true;
|
||||
} else {
|
||||
pathes.push(arg);
|
||||
}
|
||||
}
|
||||
if (pathes.length === 0) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
switch (argv[2]) {
|
||||
case 'acb2hcas':
|
||||
if (!decrypt) key = undefined;
|
||||
await handlePathes(pathes, '.acb');
|
||||
for (let i = 0; i < pathes.length; i++) await acb.acb2hcas(pathes[i], key, output, type, skip);
|
||||
break;
|
||||
case 'acb2wavs':
|
||||
await handlePathes(pathes, '.acb');
|
||||
for (let i = 0; i < pathes.length; i++) await acb.acb2wavs(pathes[i], key, output, volume, mode, skip);
|
||||
break;
|
||||
case 'awb2hcas':
|
||||
if (!decrypt) key = undefined;
|
||||
await handlePathes(pathes, '.awb');
|
||||
for (let i = 0; i < pathes.length; i++) await afs2.awb2hcas(pathes[i], key, output, type, skip);
|
||||
break;
|
||||
case 'awb2wavs':
|
||||
await handlePathes(pathes, '.awb');
|
||||
for (let i = 0; i < pathes.length; i++) await afs2.awb2wavs(pathes[i], key, output, volume, mode, skip);
|
||||
break;
|
||||
case 'hca2wav':
|
||||
await handlePathes(pathes, '.hca');
|
||||
for (let i = 0; i < pathes.length; i++) await hca.decodeHcaToWav(pathes[i], key, awbKey, output, volume, mode);
|
||||
break;
|
||||
case 'view_utf':
|
||||
await handlePathes(pathes);
|
||||
for (let i = 0; i < pathes.length; i++) await utf.viewUtf(pathes[i], output);
|
||||
break;
|
||||
case 'decrypt_acb':
|
||||
await handlePathes(pathes, '.acb');
|
||||
for (let i = 0; i < pathes.length; i++) await acb.decryptAcb(pathes[i], key, type);
|
||||
break;
|
||||
case 'decrypt_awb':
|
||||
await handlePathes(pathes, '.awb');
|
||||
for (let i = 0; i < pathes.length; i++) await afs2.decryptAwb(pathes[i], key, type);
|
||||
break;
|
||||
case 'decrypt_hca':
|
||||
await handlePathes(pathes, '.hca');
|
||||
for (let i = 0; i < pathes.length; i++) await hca.decryptHca(pathes[i], key, awbKey, type, pathes[i]);
|
||||
break;
|
||||
case 'acb_mix':
|
||||
await handlePathes(pathes, '.acb');
|
||||
for (let i = 0; i < pathes.length; i++) await acb.mixAcb(pathes[i], key, output, mode, skip);
|
||||
break;
|
||||
case 'extract_cpk':
|
||||
await handlePathes(pathes, '.cpk');
|
||||
for (let i = 0; i < pathes.length; i++) await cpk.extractCpk(pathes[i], output);
|
||||
break;
|
||||
default:
|
||||
usage();
|
||||
break;
|
||||
}
|
||||
console.log('FINISH!');
|
||||
} catch (e) {
|
||||
console.error(`ERROR: ${e.message}`);
|
||||
debugger;
|
||||
}
|
||||
})();
|
98
src/utf.js
Normal file
98
src/utf.js
Normal file
@ -0,0 +1,98 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
function findZero(buffer, start) {
|
||||
while (buffer[start] !== 0x0) start++;
|
||||
return start;
|
||||
}
|
||||
|
||||
function parseUtf(buffer, toString) {
|
||||
if (!buffer || buffer.length < 4) return null;
|
||||
let pos = 0;
|
||||
const config = {};
|
||||
config.magic = buffer.slice(pos, 4).toString(); pos += 4;
|
||||
if (config.magic !== '@UTF') return null;
|
||||
config.dataSize = buffer.readUInt32BE(pos); pos += 4;
|
||||
buffer = buffer.slice(pos);
|
||||
pos = 0;
|
||||
config.unknown = buffer.readUInt16BE(pos); pos += 2;
|
||||
config.valueOffset = buffer.readUInt16BE(pos); pos += 2;
|
||||
config.stringOffset = buffer.readUInt32BE(pos); pos += 4;
|
||||
config.dataOffset = buffer.readUInt32BE(pos); pos += 4;
|
||||
config.nameOffset = buffer.readUInt32BE(pos); pos += 4;
|
||||
config.elementCount = buffer.readUInt16BE(pos); pos += 2;
|
||||
config.valueSize = buffer.readUInt16BE(pos); pos += 2;
|
||||
config.pageCount = buffer.readUInt32BE(pos); pos += 4;
|
||||
let stringEnd = findZero(buffer, config.stringOffset);
|
||||
config.name = buffer.slice(config.stringOffset, stringEnd).toString();
|
||||
let valuePos = config.valueOffset;
|
||||
const pages = [];
|
||||
config.types = [];
|
||||
let firstPos = pos;
|
||||
for (let i = 0; i < config.pageCount; i++) {
|
||||
let page = {};
|
||||
pos = firstPos;
|
||||
for (let j = 0; j < config.elementCount; j++) {
|
||||
let type = buffer.readUInt8(pos); pos = pos + 1;
|
||||
if (i === 0) config.types[j] = type;
|
||||
let stringOffset = config.stringOffset + buffer.readUInt32BE(pos); pos += 4;
|
||||
stringEnd = findZero(buffer, stringOffset);
|
||||
const key = buffer.slice(stringOffset, stringEnd).toString();
|
||||
const method = type >>> 5;
|
||||
type = type & 0x1F;
|
||||
let value = null;
|
||||
if (method > 0) {
|
||||
let offset = method === 1 ? pos : valuePos;
|
||||
switch (type) {
|
||||
case 0x10: value = buffer.readInt8(offset); offset += 1; break;
|
||||
case 0x11: value = buffer.readUInt8(offset); offset += 1; break;
|
||||
case 0x12: value = buffer.readInt16BE(offset); offset += 2; break;
|
||||
case 0x13: value = buffer.readUInt16BE(offset); offset += 2; break;
|
||||
case 0x14: value = buffer.readInt32BE(offset); offset += 4; break;
|
||||
case 0x15: value = buffer.readUInt32BE(offset); offset += 4; break;
|
||||
case 0x16: value = buffer.readBigInt64BE(offset); offset += 8; break;
|
||||
case 0x17: value = buffer.readBigUInt64BE(offset); offset += 8; break;
|
||||
case 0x18: value = buffer.readFloatBE(offset); offset += 4; break;
|
||||
case 0x19: debugger; value = buffer.readDoubleBE(offset); offset += 8; break;
|
||||
case 0x1A:
|
||||
stringOffset = config.stringOffset + buffer.readUInt32BE(offset); offset += 4;
|
||||
stringEnd = findZero(buffer, stringOffset);
|
||||
value = buffer.slice(stringOffset, stringEnd).toString();
|
||||
break;
|
||||
case 0x1B:
|
||||
const bufferStart = config.dataOffset + buffer.readUInt32BE(offset); offset += 4;
|
||||
const bufferLen = buffer.readUInt32BE(offset); offset += 4;
|
||||
value = buffer.slice(bufferStart, bufferStart + bufferLen);
|
||||
let temp = parseUtf(value, toString);
|
||||
if (temp) value = temp; else if (toString) value = buffer.slice(bufferStart, bufferStart + bufferLen).toString('hex');
|
||||
break;
|
||||
default:
|
||||
console.log(`unknown type: ${type}`);
|
||||
break;
|
||||
}
|
||||
if (method === 1) pos = offset; else valuePos = offset;
|
||||
}
|
||||
page[key] = value;
|
||||
}
|
||||
pages.push(page);
|
||||
}
|
||||
pages.config = config;
|
||||
return pages;
|
||||
}
|
||||
exports.parseUtf = parseUtf;
|
||||
|
||||
async function viewUtf(acbPath, outputPath) {
|
||||
const pathInfo = path.parse(acbPath);
|
||||
if (outputPath === undefined) outputPath = path.join(pathInfo.dir, pathInfo.name + '.json');
|
||||
console.log(`Parsing ${pathInfo.base}...`);
|
||||
const buffer = await readFile(acbPath);
|
||||
const obj = parseUtf(buffer, true);
|
||||
if (obj && obj.AwbFile && obj.AwbFile.length > 0x20) obj.AwbFile = obj.AwbFile.substring(0, 0x20);
|
||||
console.log(`Writing ${path.parse(outputPath).base}...`);
|
||||
await writeFile(outputPath, JSON.stringify(obj, null, 2));
|
||||
}
|
||||
exports.viewUtf = viewUtf;
|
3
view_utf.bat
Normal file
3
view_utf.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
node.exe "%~dp0src\index.js" view_utf %*
|
||||
pause
|
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
node.exe "%~dp0index.js" acb2hcas %*
|
||||
pause
|
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
node.exe "%~dp0index.js" awb2hcas %*
|
||||
pause
|
@ -1,5 +0,0 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0index.js" awb2hcas -d -k %key% %*
|
||||
pause
|
@ -1,5 +0,0 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0index.js" decrypt_acb -k %key% %*
|
||||
pause
|
@ -1,5 +0,0 @@
|
||||
@echo off
|
||||
set /P key=Please input key and press Enter:
|
||||
if "%key%"=="" set key=0
|
||||
node.exe "%~dp0index.js" decrypt_awb -k %key% %*
|
||||
pause
|
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
node.exe "%~dp0index.js" view_utf %*
|
||||
pause
|
Loading…
Reference in New Issue
Block a user