creator / scripts /functions.js
K1Z3M1112's picture
Update scripts/functions.js
4eb9319 verified
Raw History Blame Contribute Delete
27.3 kB
/**
* Author: Peter Dragicevic [peter@petschko.org]
* Authors-Website: http://petschko.org/
* Date: 30.03.2017
* Time: 23:05
*/
// Globals
var zip;
/**
* Try to Reads the Encryption-Code and insert it to the given input element
*
* @param {string} systemFileElId - File-Picker-Id for the System.json-File
* @param {string} codeTextElId - Text-Input-Id for the Encryption-Key
* @param {string} headerLenId - Text-Input for the Header-Length
*/
function getCode(systemFileElId, codeTextElId, headerLenId) {
var systemFileEl = document.getElementById(systemFileElId);
var codeTextEl = document.getElementById(codeTextElId);
var headerLen = document.getElementById(headerLenId);
if(systemFileEl.files.length < 1) {
alert(lang.strings['error.detect.noFile']
.replace('{0}', 'System(.json)')
.replace('{1}', '.rpgmvp / .png_'));
return;
}
Decrypter.detectEncryptionCode(new RPGFile(systemFileEl.files[0], null), parseInt(headerLen.value), function(key) {
if(key === null) {
alert(lang.strings['error.detect.invalidJsonFile'].replace('{0}', '.json/.rpgmvp/.png_'));
return;
}
if(typeof key === 'string' && key.length > 0) {
codeTextEl.value = key;
codeTextEl.className = removeValidationCssClasses(codeTextEl.className);
codeTextEl.className = addCssClass(codeTextEl.className, 'valid');
alert(lang.strings['info.detect.keyFound'].replace('{0}', key));
} else
window.prompt(
lang.strings['error.detect.keyNotFound.1'] + '\n\n' +
lang.strings['error.detect.keyNotFound.2'].replace('{0}', '.rpgmvp / .png_') + '\n' +
'-------------------------------------\n' +
lang.strings['error.detect.keyNotFound.3'] + '\n' +
'1. ' + lang.strings['error.detect.keyNotFound.3.1'] + '\n' +
'2. ' + lang.strings['error.detect.keyNotFound.3.2'].replace('{0}', './www/js/rpg_core(.js)') + '\n' +
'3. ' + lang.strings['error.detect.keyNotFound.3.3'] + '\n' +
'4. ' + lang.strings['error.detect.keyNotFound.3.4'] + '\n\n',
'https://pastebin.com/nCrzCpzD'
);
});
}
/**
* Removes a CSS-Class
*
* @param {string} elementClasses - Element CSS-Classes
* @param {string} removeClass - Class to remove
* @returns {string} - New CSS-Classes
*/
function removeCssClass(elementClasses, removeClass) {
var newClassNames = '';
var classes = elementClasses.split(' ');
// Check if element is in array else exit
if(classes.indexOf(removeClass) === -1)
return elementClasses;
for(var i = 0; i < classes.length; i++) {
if(classes[i] !== removeClass)
newClassNames += classes[i] + ' ';
}
return newClassNames.trim();
}
/**
* Adds a CSS-Class, it never adds an existing class twice
*
* @param {string} elementClasses - Element CSS-Classes
* @param {string} addClass - Class to add
* @returns {string} - New CSS-Classes
*/
function addCssClass(elementClasses, addClass) {
var classes = elementClasses.split(' ');
if(classes.indexOf(addClass) !== -1)
return elementClasses;
return (elementClasses + ' ' + addClass).trim();
}
/**
* Check if a the searched Class is within this class-string
*
* @param {string} elementClasses - Element CSS-Classes
* @param {string} searchClass - Class to check
* @returns {boolean} - true if the class exist else false
*/
function hasCssClass(elementClasses, searchClass) {
var classes = elementClasses.split(' ');
return (classes.indexOf(searchClass) !== -1);
}
/**
* Get the Value of a Radio-Button-Group
*
* @param {string} radioButtonGroupName - Radio-Button-Group-Name
* @param {string} fallback - Fallback-Value
* @returns {string} - Current Value or Fallback-Value if group doesn't exists or nothing is selected
*/
function getRadioButtonValue(radioButtonGroupName, fallback) {
var radioButtons = document.getElementsByName(radioButtonGroupName);
var currentValue = fallback;
for(var i = 0; i < radioButtons.length; i++) {
if(radioButtons[i].checked) {
currentValue = radioButtons[i].value;
break;
}
}
return currentValue;
}
/**
* Removes all Validation-Classes
*
* @param {string} currentClasses - Current Element CSS-Classes
* @returns {string} - CSS-Classes without validation ones
*/
function removeValidationCssClasses(currentClasses) {
currentClasses = removeCssClass(currentClasses, 'invalid');
currentClasses = removeCssClass(currentClasses, 'manualChange');
currentClasses = removeCssClass(currentClasses, 'valid');
return currentClasses;
}
/**
* Changes the validation class to manualChange
*
* @param {string} elementId - Element-Id to where to change
*/
function manualChange(elementId) {
var element = document.getElementById(elementId);
element.className = removeValidationCssClasses(element.className);
element.className = addCssClass(element.className, 'manualChange');
}
/**
* Shows/Hides a Spoiler-Element
*
* @param {string} spoilerTextElId - Element-Id of the Spoiler-Text
* @param {string} spoilerText - Text of the Spoiler-Text-Element (without show/hide)
* @param {string} spoilerElId - Spoiler-Element-Id
*/
function spoiler(spoilerTextElId, spoilerText, spoilerElId) {
var spoilerTextEl = document.getElementById(spoilerTextElId);
var spoilerEl = document.getElementById(spoilerElId);
var showHideSpan = spoilerTextEl.getElementsByTagName('span')[0];
if(hasCssClass(spoilerEl.className, 'hidden')) {
spoilerEl.className = removeCssClass(spoilerEl.className, 'hidden');
showHideSpan.setAttribute(lang.translateableText, 'enDecrypt.button.headerValues.show.hide');
showHideSpan.innerHTML = html(lang.strings['enDecrypt.button.headerValues.show.hide']);
} else {
spoilerEl.className = addCssClass(spoilerEl.className, 'hidden');
showHideSpan.setAttribute(lang.translateableText, 'enDecrypt.button.headerValues.show.show');
showHideSpan.innerHTML = html(lang.strings['enDecrypt.button.headerValues.show.show']);
}
}
/**
* Adds all Action-Listener
*/
function init() {
var addMethod = window.addEventListener ? 'addEventListener' : 'attachEvent';
// Get Elements
var detectButton = document.getElementById('detectButton');
var inputCode = document.getElementById('decryptCode');
var decryptButton = document.getElementById('decrypt');
var encryptButtonMv = document.getElementById('encrypt-mv');
var encryptButtonMz = document.getElementById('encrypt-mz');
var restoreButton = document.getElementById('restoreEncryptedImages');
var fullGameZipButton = document.getElementById('decryptFullGameZip');
var spoilerHeader = document.getElementById('spoilerHeaderInfoText');
var headerRadioButtons = document.getElementsByName('checkFakeHeader');
var headerAreaEl = document.getElementById('headerValuesEditArea');
var headerResetButton = document.getElementById('resetHeader');
var zipSaveButtons = document.getElementsByClassName('zipSave');
var clearFileListButtons = document.getElementsByClassName('clearFileList');
// Language Init
lang = new Language(languageList);
// Prepare stuff
if(! parseInt(getRadioButtonValue('checkFakeHeader', '1')))
headerAreaEl.className = addCssClass(headerAreaEl.className, 'hidden');
setHeaderDefaultValues(false);
// Add Listener
headerRadioButtons[0][addMethod](window.addEventListener ? 'click' : 'onclick', function() {
// Yes Button
headerAreaEl.className = removeCssClass(headerAreaEl.className, 'hidden');
}, false);
headerRadioButtons[1][addMethod](window.addEventListener ? 'click' : 'onclick', function() {
// No Button
headerAreaEl.className = addCssClass(headerAreaEl.className, 'hidden');
}, false);
headerResetButton[addMethod](window.addEventListener ? 'click' : 'onclick', function() {
setHeaderDefaultValues(true);
}, false);
spoilerHeader[addMethod](window.addEventListener ? 'click' : 'onclick', function() {
spoiler('spoilerHeaderInfoText', 'Header-Values', 'headerInfo');
}, false);
detectButton[addMethod](window.addEventListener ? 'click' : 'onclick', function() {
getCode('systemFile', 'decryptCode', 'headerLen');
}, false);
decryptButton[addMethod](window.addEventListener ? 'click' : 'onclick', function() {
processFiles(
'encryptedFiles',
'decryptCode',
'blob-list',
true,
!! parseInt(getRadioButtonValue('checkFakeHeader', '0')),
'headerLen',
'signature',
'version',
'remain',
RPGFile.typeMV
);
}, false);
encryptButtonMv[addMethod](window.addEventListener ? 'click' : 'onclick', function() {
processFiles(
'encryptedFiles',
'decryptCode',
'blob-list',
false,
true,
'headerLen',
'signature',
'version',
'remain',
RPGFile.typeMV
);
}, false);
encryptButtonMz[addMethod](window.addEventListener ? 'click' : 'onclick', function() {
processFiles(
'encryptedFiles',
'decryptCode',
'blob-list',
false,
true,
'headerLen',
'signature',
'version',
'remain',
RPGFile.typeMZ
);
}, false);
inputCode[addMethod](window.addEventListener ? 'change' : 'onchange', function() {
manualChange('decryptCode');
}, false);
restoreButton[addMethod](window.addEventListener ? 'click' : 'onclick', function() {
processRestoreFiles('encryptedImg', 'blob-list');
}, false);
if(fullGameZipButton) {
fullGameZipButton[addMethod](window.addEventListener ? 'click' : 'onclick', function() {
processFullGameZip();
}, false);
}
// File-List Listener
var i;
for(i = 0; i < zipSaveButtons.length; i++) {
zipSaveButtons[i][addMethod](window.addEventListener ? 'click' : 'onclick', function() {
saveZip();
}, false);
}
for(i = 0; i < clearFileListButtons.length; i++) {
clearFileListButtons[i][addMethod](window.addEventListener ? 'click' : 'onclick', function() {
clearFileList('blob-list', 'zipSave', 'clearFileList');
}, false);
}
zip = new ZIP();
}
document.body[window.addEventListener ? 'addEventListener' : 'attachEvent'](
window.addEventListener ? 'load' : 'onload', init(), false);
/**
* Set all Values to default within the Header-Values-Area
*
* @param {boolean} confirmDialog - Ask before reset
*/
function setHeaderDefaultValues(confirmDialog) {
if(confirmDialog) {
if(! confirm(lang.strings['confirm.resetHeaderValues']))
return;
}
// Get all Elements
var headerLenEl = document.getElementById('headerLen');
var headerSigEl = document.getElementById('signature');
var headerVerEl = document.getElementById('version');
var headerRemainEl = document.getElementById('remain');
// Get a dummy Decrypter Class
var decrypter = new Decrypter('000000');
// Set Values
headerLenEl.value = decrypter.defaultHeaderLen;
headerLenEl.placeholder = decrypter.defaultHeaderLen;
headerSigEl.value = decrypter.defaultSignature;
headerSigEl.placeholder = decrypter.defaultSignature;
headerVerEl.value = decrypter.defaultVersion;
headerVerEl.placeholder = decrypter.defaultVersion;
headerRemainEl.value = decrypter.defaultRemain;
headerRemainEl.placeholder = decrypter.defaultRemain;
}
/**
* Restores images from MV-Encrypted-Files
*
* @param {string} fileUrlElId - Element-Id of the File(s)-Picker
* @param {string} outputElClass - Output-Element-Class
*/
function processRestoreFiles(fileUrlElId, outputElClass) {
var outputEls = document.getElementsByClassName(outputElClass);
var fileUrlEl = document.getElementById(fileUrlElId);
// Check if at least 1 File is given
if(fileUrlEl.files.length < 1) {
alert(lang.strings['error.restore.noFileSelected']);
return;
}
var decrypter = new Decrypter(null);
decrypter.headerLen = 16;
// Process all Files
var buttonsEnabled = false; // Just trigger that event one time this loop
for(var i = 0; i < fileUrlEl.files.length; i++) {
var rpgFile = new RPGFile(fileUrlEl.files[i], null);
decrypter.restoreHeader(rpgFile, function(rpgFile, exception) {
// Output Decrypted file
for(var n = 0; n < outputEls.length; n++) {
if(exception !== null)
outputEls[n].appendChild(rpgFile.createOutPut(exception.toString()));
else {
rpgFile.convertExtension(true);
outputEls[n].appendChild(rpgFile.createOutPut(null));
zip.addFile(rpgFile);
}
}
if(! buttonsEnabled && exception === null) {
enableFileButtons('clearFileList', 'zipSave');
buttonsEnabled = true;
}
});
}
}
/**
* Decrypt a bunch of MV-Encrypted-Files
*
* @param {string} fileUrlElId - Element-Id of the File(s)-Picker
* @param {string} decryptCodeElId - Element-Id of the Decryption-Code Input Field
* @param {string} outputElClass - Output-Element-Class
* @param {boolean} decrypt - Decrypt (true decrypts false encrypts)
* @param {boolean} verifyHeader - Verify Header
* @param {string} headerLenElId - Element-Id of the Header-Length
* @param {string} signatureElId - Element-Id of the Signature
* @param {string} versionElId - Element-Id of the Version
* @param {string} remainElId - Element-Id of the Remain
* @param {int} rpgMakerType - Type of the RPG-Maker
*/
function processFiles(
fileUrlElId,
decryptCodeElId,
outputElClass,
decrypt,
verifyHeader,
headerLenElId,
signatureElId,
versionElId,
remainElId,
rpgMakerType
) {
var outputEls = document.getElementsByClassName(outputElClass);
var fileUrlEl = document.getElementById(fileUrlElId);
var encryptCodeEl = document.getElementById(decryptCodeElId);
var encryptionCode = encryptCodeEl.value;
var headerLen = null;
var signature = null;
var version = null;
var remain = null;
// On encryption verify-header is req
if(! decrypt)
verifyHeader = true;
if(verifyHeader) {
var headerLenEl = document.getElementById(headerLenElId);
var signatureEl = document.getElementById(signatureElId);
var versionEl = document.getElementById(versionElId);
var remainEl = document.getElementById(remainElId);
if(headerLenEl)
headerLen = headerLenEl.value;
if(signatureEl)
signature = signatureEl.value;
if(versionEl)
version = versionEl.value;
if(remainEl)
remain = remainEl.value;
}
// Check if all required stuff is given
if(! encryptionCode) {
alert(lang.strings['error.enDecrypt.noCode']);
encryptCodeEl.className = removeValidationCssClasses(encryptCodeEl.className);
encryptCodeEl.className = addCssClass(encryptCodeEl.className, 'invalid');
return;
}
// Check if code just contain HEX-Chars
if(! Decrypter.checkHexChars(encryptionCode)) {
alert(lang.strings['error.enDecrypt.invalidCode'].replace('{0}', '0-9 & A-F & a-f'));
encryptCodeEl.className = removeValidationCssClasses(encryptCodeEl.className);
encryptCodeEl.className = addCssClass(encryptCodeEl.className, 'invalid');
return;
}
// Set valid encryption class
encryptCodeEl.className = removeValidationCssClasses(encryptCodeEl.className);
encryptCodeEl.className = addCssClass(encryptCodeEl.className, 'valid');
// Check if at least 1 File is given
if(fileUrlEl.files.length < 1) {
alert(lang.strings['error.enDecrypt.noFileSelected']);
return;
}
var decrypter = new Decrypter(encryptionCode);
decrypter.ignoreFakeHeader = ! verifyHeader;
if(verifyHeader) {
// Handle Header details
headerLenEl.className = removeValidationCssClasses(headerLenEl.className);
if(! isNaN(headerLen) && Math.floor(headerLen) > 0)
decrypter.headerLen = Math.floor(headerLen);
else if(headerLen) {
headerLenEl.className = addCssClass(headerLenEl.className, 'invalid');
alert(lang.strings['warn.enDecrypt.header.invalidLength'].replace('{0}', decrypter.defaultHeaderLen));
}
signatureEl.className = removeValidationCssClasses(signatureEl.className);
if(Decrypter.checkHexChars(signature))
decrypter.signature = signature;
else if(signature) {
signatureEl.className = addCssClass(signatureEl.className, 'invalid');
alert(lang.strings['warn.enDecrypt.header.invalidSignature']
.replace('{0}', '0-9 & A-F & a-f')
.replace('{1}', decrypter.defaultSignature));
}
versionEl.className = removeValidationCssClasses(versionEl.className);
if(Decrypter.checkHexChars(version))
decrypter.version = version;
else if(version) {
versionEl.className = addCssClass(versionEl.className, 'invalid');
alert(lang.strings['warn.enDecrypt.header.invalidVersion']
.replace('{0}', '0-9 & A-F & a-f')
.replace('{1}', decrypter.defaultVersion));
}
remainEl.className = removeValidationCssClasses(remainEl.className);
if(Decrypter.checkHexChars(remain))
decrypter.remain = remain;
else if(remain) {
remainEl.className = addCssClass(remainEl.className, 'invalid');
alert(lang.strings['warn.enDecrypt.header.invalidRemain']
.replace('{0}', '0-9 & A-F & a-f')
.replace('{1}', decrypter.defaultRemain));
}
}
// Process all Files
var buttonsEnabled = false; // Just trigger that event one time this loop
for(var i = 0; i < fileUrlEl.files.length; i++) {
var rpgFile = new RPGFile(fileUrlEl.files[i], null);
rpgFile.rpgMakerMz = rpgMakerType === RPGFile.typeMZ;
if(decrypt) {
decrypter.decryptFile(rpgFile, function(rpgFile, exception) {
// Output Decrypted file
for(var n = 0; n < outputEls.length; n++) {
if(exception !== null)
outputEls[n].appendChild(rpgFile.createOutPut(exception.toString()));
else {
rpgFile.convertExtension(true);
outputEls[n].appendChild(rpgFile.createOutPut(null));
zip.addFile(rpgFile);
}
}
if(! buttonsEnabled && exception === null) {
enableFileButtons('clearFileList', 'zipSave');
buttonsEnabled = true;
}
});
} else {
decrypter.encryptFile(rpgFile, function(rpgFile, exception) {
// Output Encrypted file
for(var n = 0; n < outputEls.length; n++) {
if(exception !== null)
outputEls[n].appendChild(rpgFile.createOutPut(exception.toString()));
else {
rpgFile.convertExtension(false);
outputEls[n].appendChild(rpgFile.createOutPut(null));
zip.addFile(rpgFile);
}
}
if(! buttonsEnabled && exception === null) {
enableFileButtons('clearFileList', 'zipSave');
buttonsEnabled = true;
}
});
}
}
}
/**
* Clears the File-List & disables the ZIP-Save-Button
*
* @param {string} fileListClass - Class-Name of the File-List
* @param {string} zipSaveButtonClass - Class-Name of the ZIP-Save Button
* @param {string} clearFileListButtonClass - Class-Name of the Clear-File-List Button
*/
function clearFileList(fileListClass, zipSaveButtonClass, clearFileListButtonClass) {
var fileListEls = document.getElementsByClassName(fileListClass);
var zipSaveButtonEls = document.getElementsByClassName(zipSaveButtonClass);
var clearFileListButtonEls = document.getElementsByClassName(clearFileListButtonClass);
var i;
for(i = 0; i < fileListEls.length; i++)
fileListEls[i].innerHTML = '';
for(i = 0; i < zipSaveButtonEls.length; i++)
zipSaveButtonEls[i].disabled = 'disabled';
for(i = 0; i < clearFileListButtonEls.length; i++)
clearFileListButtonEls[i].disabled = 'disabled';
// Dispose old ZIP Object (Clear Memory)
zip.dispose();
}
/**
* ReEnables the Buttons for the File-List
*
* @param {string} clearFileListButtonClass - Class-Name of the Clear-File-List Button
* @param {string} zipSaveButtonClass - Class-Name of the ZIP-Save Button
*/
function enableFileButtons(clearFileListButtonClass, zipSaveButtonClass) {
var clearFileListButtonEls = document.getElementsByClassName(clearFileListButtonClass);
var zipSaveButtonEls = document.getElementsByClassName(zipSaveButtonClass);
var i;
for(i = 0; i < clearFileListButtonEls.length; i++)
clearFileListButtonEls[i].disabled = '';
for(i = 0; i < zipSaveButtonEls.length; i++)
zipSaveButtonEls[i].disabled = '';
}
/**
* Encodes text HTML-Special Characters
*
* @param {string} s - Unencoded String
* @returns {string} - Encoded String
*/
function html(s) {
var el = document.createElement("div");
el.innerText = el.textContent = s;
s = el.innerHTML;
return s.replaceAll(/"/g, '&quot;')
.replaceAll(/'/g, '&apos;')
.replaceAll('#', '&num;');
}
/**
* Escapes Titles/Alt-Tags
*
* @param {string} s - Enencoded String
* @returns {string} - Encoded String
*/
function escapeTitle(s) {
s = html(s);
return s.replaceAll('&quot;', '\'\'')
.replaceAll('&apos;', '\'')
.replaceAll('&num;', '#')
.replaceAll('&amp;', '&');
}
/**
* Saves the current Files as ZIP
*/
function saveZip() {
zip.save();
}
/**
* Full-game ZIP workflow.
* Reads a complete RPG Maker game ZIP, detects the encryption key, decrypts all
* encrypted resources while preserving their original directory paths, removes
* the encrypted entries, and writes the decrypted resources back into the same
* paths (with normal extensions) before generating one output ZIP.
*/
function processFullGameZip() {
var input = document.getElementById('fullGameZip');
var status = document.getElementById('fullGameZipStatus');
var button = document.getElementById('decryptFullGameZip');
if(!input || !input.files || input.files.length < 1) {
alert('Please select a full game ZIP file.');
return;
}
var file = input.files[0];
if(!/\.zip$/i.test(file.name)) {
alert('The selected file is not a ZIP archive.');
return;
}
var setStatus = function(message) {
if(status) status.textContent = message;
};
var oldText = button ? button.textContent : '';
if(button) {
button.disabled = true;
button.textContent = 'Processing...';
}
var finish = function() {
if(button) {
button.disabled = false;
button.textContent = oldText || 'Decrypt Full Game ZIP';
}
};
setStatus('กำลังอ่าน ZIP เกม...');
JSZip.loadAsync(file).then(function(gameZip) {
var entries = Object.keys(gameZip.files);
var systemPath = findGameZipFile(entries, /(^|\/)system\.json$/i);
var headerLen = parseInt(document.getElementById('headerLen') ? document.getElementById('headerLen').value : '16');
if(isNaN(headerLen) || headerLen < 1) headerLen = 16;
// systemPath looks like "<root>/data/System.json" (MZ) or "<root>/www/data/System.json" (MV).
// The game root is the folder that contains data/, img/, audio/, js/ as SIBLINGS —
// i.e. one level above the "data" folder, not the data folder itself.
var gameRoot = systemPath ? systemPath.substring(0, systemPath.lastIndexOf('/') + 1).replace(/[^\/]+\/$/, '') : '';
return detectFullGameZipKey(gameZip, entries, systemPath, headerLen).then(function(keyInfo) {
if(!keyInfo || !keyInfo.key) {
throw new Error('ไม่พบ encryptionKey ในเกม ZIP (ลองตรวจว่าเป็น RPG Maker MV/MZ ที่เข้ารหัสหรือไม่)');
}
var decrypter = new Decrypter(keyInfo.key);
decrypter.headerLen = headerLen;
decrypter.ignoreFakeHeader = !parseInt(getRadioButtonValue('checkFakeHeader', '0'));
var signatureEl = document.getElementById('signature');
var versionEl = document.getElementById('version');
var remainEl = document.getElementById('remain');
if(parseInt(getRadioButtonValue('checkFakeHeader', '0'))) {
if(signatureEl && Decrypter.checkHexChars(signatureEl.value)) decrypter.signature = signatureEl.value;
if(versionEl && Decrypter.checkHexChars(versionEl.value)) decrypter.version = versionEl.value;
if(remainEl && Decrypter.checkHexChars(remainEl.value)) decrypter.remain = remainEl.value;
}
var encrypted = entries.filter(function(path) {
return !gameZip.files[path].dir && isFullGameEncryptedPath(path) && isWithinGameRoot(path, gameRoot);
});
if(encrypted.length === 0)
throw new Error('ไม่พบไฟล์เข้ารหัส (.rpgmvp/.rpgmvm/.rpgmvo/.png_/.ogg_/.m4a_) ใน ZIP');
setStatus('พบไฟล์เข้ารหัส ' + encrypted.length + ' ไฟล์ กำลังถอดรหัส...');
var output = {};
var completed = 0;
// Keep every original entry except encrypted resources. Encrypted
// resources are inserted later using their original directory + normal extension.
entries.forEach(function(path) {
if(gameZip.files[path].dir) {
output[path] = {dir:true};
} else if(!isFullGameEncryptedPath(path) || !isWithinGameRoot(path, gameRoot)) {
output[path] = {zipObject:gameZip.files[path]};
}
});
var tasks = encrypted.map(function(path) {
return gameZip.files[path].async('arraybuffer').then(function(buffer) {
var decrypted = decrypter.decrypt(buffer);
var outputPath = fullGameNormalPath(path);
output[outputPath] = {data:decrypted};
completed++;
setStatus('ถอดรหัส ' + completed + '/' + encrypted.length + ' ไฟล์...');
});
});
return Promise.all(tasks).then(function() {
var resultZip = new JSZip();
Object.keys(output).forEach(function(path) {
var item = output[path];
if(item.dir) resultZip.folder(path.replace(/\/$/, ''));
else if(item.data) resultZip.file(path, item.data);
else resultZip.file(path, item.zipObject.async('arraybuffer'));
});
setStatus('ถอดรหัสเสร็จ กำลังสร้าง ZIP เกมเต็ม...');
return resultZip.generateAsync({type:'blob', compression:'DEFLATE'}).then(function(blob) {
var outputName = file.name.replace(/\.zip$/i, '') + '_decrypted.zip';
saveAs(blob, outputName);
setStatus('เสร็จแล้ว: ' + outputName + ' — ถอดรหัส ' + encrypted.length + ' ไฟล์ และคงโครงสร้างโฟลเดอร์เดิม');
});
});
});
}).catch(function(error) {
console.error(error);
setStatus('ผิดพลาด: ' + (error && error.message ? error.message : error));
alert('ถอดรหัส ZIP ไม่สำเร็จ: ' + (error && error.message ? error.message : error));
}).then(finish, finish);
}
function findGameZipFile(entries, regex) {
var matches = entries.filter(function(path) { return regex.test(path); });
if(matches.length === 0) return null;
// Prefer the shallowest System.json (normal MV: www/data/System.json, MZ: data/System.json).
matches.sort(function(a,b) { return a.split('/').length - b.split('/').length; });
return matches[0];
}
function detectFullGameZipKey(gameZip, entries, systemPath, headerLen) {
var makeFile = function(path, data, type) {
return data.then(function(buffer) {
return new File([buffer], path.split('/').pop(), {type:type || 'application/octet-stream'});
});
};
if(systemPath) {
return makeFile(systemPath, gameZip.files[systemPath].async('arraybuffer'), 'application/json').then(function(file) {
return new Promise(function(resolve) {
Decrypter.detectEncryptionCode(new RPGFile(file, null), headerLen, function(key) {
resolve({key:key, source:systemPath});
});
});
});
}
var imagePath = findGameZipFile(entries, /\.(rpgmvp|png_)$/i);
if(imagePath) {
return gameZip.files[imagePath].async('arraybuffer').then(function(buffer) {
return {key:Decrypter.getKeyFromPNG(headerLen, buffer), source:imagePath};
});
}
return Promise.resolve(null);
}
function isWithinGameRoot(path, root) {
if(!root) return true;
return path.indexOf(root) === 0;
}
function isFullGameEncryptedPath(path) {
return /\.(rpgmvp|rpgmvm|rpgmvo|png_|ogg_|m4a_)$/i.test(path);
}
function fullGameNormalPath(path) {
return path.replace(/\.(rpgmvp|png_)$/i, '.png')
.replace(/\.(rpgmvm|m4a_)$/i, '.m4a')
.replace(/\.(rpgmvo|ogg_)$/i, '.ogg');
}