|
"use strict"; |
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
const path_1 = require("path"); |
|
const promises_1 = require("fs/promises"); |
|
const isSystem = /System(\.json)?$/i; |
|
const isMvFile = /\.(?:(rpgmv[pmo])|(?:(png|m4a|ogg)_?)|)$/i; |
|
const ops = { |
|
hasArgs: false, |
|
decrypt: false, |
|
encrypt: false, |
|
verbose: true, |
|
}; |
|
const argv = process.argv.slice(1); |
|
if (argv.includes("--decrypt")) { |
|
ops.hasArgs = true; |
|
ops.decrypt = true; |
|
} |
|
if (argv.includes("--encrypt")) { |
|
ops.hasArgs = true; |
|
ops.encrypt = true; |
|
} |
|
if (argv.includes("--quiet")) { |
|
ops.verbose = false; |
|
} |
|
// prettier-ignore |
|
const mvHeader = Uint8Array.from([ |
|
0x52, 0x50, 0x47, 0x4d, |
|
0x56, 0x00, 0x00, 0x00, |
|
0x00, 0x03, 0x01, 0x00, |
|
0x00, 0x00, 0x00, 0x00, |
|
]); |
|
// prettier-ignore |
|
const pngHeader = Uint8Array.from([ |
|
0x89, 0x50, 0x4e, 0x47, |
|
0x0d, 0x0a, 0x1a, 0x0a, |
|
0x00, 0x00, 0x00, 0x0d, |
|
0x49, 0x48, 0x44, 0x52 |
|
]); |
|
const HEADER_LENGTH = 16; |
|
function getKey(key) { |
|
if (!key) |
|
return null; |
|
return Uint8Array.from(key |
|
.split(/([0-9a-f]{2})/i) |
|
.filter(Boolean) |
|
.map((x) => parseInt(x, 16))); |
|
} |
|
function verifyMvHeader(buf) { |
|
for (let i = 0; i < mvHeader.length; ++i) { |
|
if (buf[i] !== mvHeader[i]) |
|
return false; |
|
} |
|
return true; |
|
} |
|
async function pngDecrypt(path) { |
|
const buf = await (0, promises_1.readFile)(path); |
|
if (!verifyMvHeader(buf)) |
|
throw new Error("Not an RPG Maker MV file."); |
|
const res = new Uint8Array(buf.length - mvHeader.length); |
|
res.set(buf.slice(HEADER_LENGTH)); |
|
res.set(pngHeader, 0); |
|
return res; |
|
} |
|
async function decrypt(key, path) { |
|
const buf = await (0, promises_1.readFile)(path); |
|
if (!verifyMvHeader(buf)) |
|
throw new Error("Not an RPG Maker MV file."); |
|
const res = new Uint8Array(buf.length - mvHeader.length); |
|
res.set(buf.slice(HEADER_LENGTH)); |
|
for (let i = 0; i < HEADER_LENGTH; ++i) { |
|
res[i] = res[i] ^ key[i]; |
|
} |
|
return res; |
|
} |
|
async function encrypt(key, path) { |
|
const buf = await (0, promises_1.readFile)(path); |
|
const res = new Uint8Array(buf.length + mvHeader.length); |
|
res.set(buf, mvHeader.length); |
|
res.set(mvHeader, 0); |
|
for (let i = 0; i < HEADER_LENGTH; ++i) { |
|
res[i + HEADER_LENGTH] = res[i + HEADER_LENGTH] ^ key[i]; |
|
} |
|
return res; |
|
} |
|
async function getPaths(input, basePath, ret = []) { |
|
for (const p of input) { |
|
const path = (0, path_1.join)(basePath, p); |
|
const stats = await (0, promises_1.stat)(path); |
|
if (stats.isDirectory()) |
|
await getPaths(await (0, promises_1.readdir)(path), path, ret); |
|
else |
|
ret.push(path); |
|
} |
|
return ret; |
|
} |
|
function pad(input, len = 5, char = " ", right = false) { |
|
input = "" + input; |
|
while (input.length < len) { |
|
input = right ? input + char : char + input; |
|
} |
|
return input; |
|
} |
|
let logFile = ""; |
|
function log(...input) { |
|
for (let i = 0; i < input.length; ++i) { |
|
logFile += input[i].toString() + (i < input.length - 1 ? " " : "\n"); |
|
} |
|
console.log(...input); |
|
} |
|
function strip(path, base) { |
|
return path.replace(base, ""); |
|
} |
|
async function main() { |
|
let sysFile = ""; |
|
let sysData = { |
|
encryptionKey: "", |
|
hasEncryptedImages: true, |
|
hasEncryptedAudio: true, |
|
}; |
|
const cwd = (0, path_1.join)(process.cwd()); |
|
const basePath = (0, path_1.join)(cwd, "www"); |
|
const dirs = await (0, promises_1.readdir)(basePath); |
|
if (dirs.includes("data")) { |
|
const sys = await (0, promises_1.readdir)((0, path_1.join)(basePath, "data")); |
|
for (const p of sys) { |
|
if (isSystem.test(p)) { |
|
sysFile = p; |
|
break; |
|
} |
|
} |
|
} |
|
const sysPath = (0, path_1.join)(basePath, "data", sysFile); |
|
if (sysFile) { |
|
sysData = JSON.parse(await (0, promises_1.readFile)(sysPath, "utf8")); |
|
} |
|
const key = getKey(sysData.encryptionKey); |
|
if (!ops.hasArgs) { |
|
if (sysData.hasEncryptedAudio || sysData.hasEncryptedImages) { |
|
ops.decrypt = true; |
|
} |
|
if (!sysData.hasEncryptedAudio && !sysData.hasEncryptedImages) { |
|
ops.encrypt = true; |
|
} |
|
} |
|
// log("encryptionKey:", key, "\nBase path:", basePath); |
|
let ignore = []; |
|
const ignorePath = (0, path_1.join)(cwd, "rpgmvcryptignore.txt"); |
|
try { |
|
const ignoreList = await (0, promises_1.readFile)(ignorePath, "utf8"); |
|
ignore = ignore.concat(ignoreList.split(/\r?\n|\r/).map(p => (0, path_1.join)(cwd, strip((0, path_1.normalize)(p), cwd)))); |
|
} |
|
catch (e) { |
|
// ignorefile doesn't exist |
|
} |
|
const buildIgnore = !ignore.length; |
|
const files = await getPaths(dirs, basePath); |
|
const stats = { |
|
files: 0, |
|
encrypt: { |
|
png: 0, |
|
m4a: 0, |
|
ogg: 0, |
|
}, |
|
decrypt: { |
|
png: 0, |
|
m4a: 0, |
|
ogg: 0, |
|
}, |
|
ignore: { |
|
png: 0, |
|
m4a: 0, |
|
ogg: 0, |
|
}, |
|
}; |
|
log("+--------------------------------------+"); |
|
log("| RPGMaker MV File Decrypter/Encrypter |"); |
|
log("| v1.0 - Made by Vizmute (@vizmute) |"); |
|
log("+--------------------------------------+"); |
|
const op = []; |
|
if (ops.decrypt) |
|
op.push("decrypt"); |
|
if (ops.encrypt) |
|
op.push("encrypt"); |
|
log(`| ${pad(`Running ${op.join(", ")}...`, 36, " ", true)} |`); |
|
log("+------------+------------+------------+"); |
|
for (const path of files) { |
|
const m = path.match(isMvFile); |
|
if (!m) |
|
continue; |
|
let ext = "."; |
|
let type = "png"; |
|
if (m[1] === "rpgmvo" || m[2] === "ogg_") { |
|
ext = ".ogg"; |
|
type = "ogg"; |
|
} |
|
if (m[1] === "rpgmvm" || m[2] === "m4a_") { |
|
ext = ".m4a"; |
|
type = "m4a"; |
|
} |
|
if (m[1] === "rpgmvp" || m[2] === "png_") { |
|
ext = ".png"; |
|
type = "png"; |
|
} |
|
if (m[2] === "ogg") { |
|
ext = ".rpgmvo"; |
|
type = "ogg"; |
|
} |
|
if (m[2] === "m4a") { |
|
ext = ".rpgmvm"; |
|
type = "m4a"; |
|
} |
|
if (m[2] === "png") { |
|
ext = ".rpgmvp"; |
|
type = "png"; |
|
} |
|
try { |
|
let file = null; |
|
switch (ext) { |
|
case ".png": { |
|
if (!ops.decrypt || ignore.includes(path)) { |
|
++stats.ignore.png; |
|
break; |
|
} |
|
if (ops.verbose) |
|
log(`+ Decrypting ${type}: ${strip(path, cwd)}`); |
|
++stats.decrypt.png; |
|
file = await pngDecrypt(path); |
|
break; |
|
} |
|
case ".rpgmvo": |
|
case ".rpgmvm": |
|
case ".rpgmvp": { |
|
if (!key) |
|
break; |
|
if (ops.decrypt && !ops.encrypt && buildIgnore) { |
|
ignore.push(strip(path, cwd)); |
|
} |
|
if (!ops.encrypt || ignore.includes(path)) { |
|
++stats.ignore[type]; |
|
break; |
|
} |
|
if (ops.verbose) |
|
log(`+ Encrypting ${type}: ${strip(path, cwd)}`); |
|
++stats.encrypt[type]; |
|
file = await encrypt(key, path); |
|
break; |
|
} |
|
default: { |
|
if (!key) |
|
break; |
|
if (!ops.decrypt || ignore.includes(path)) { |
|
++stats.ignore[type]; |
|
break; |
|
} |
|
if (ops.verbose) |
|
log(`+ Decrypting ${type}: ${strip(path, cwd)}`); |
|
++stats.decrypt[type]; |
|
file = await decrypt(key, path); |
|
break; |
|
} |
|
} |
|
if (!file) |
|
continue; |
|
++stats.files; |
|
await (0, promises_1.writeFile)(path.replace(m[0], ext), file); |
|
await (0, promises_1.unlink)(path); |
|
} |
|
catch (e) { |
|
log(e); |
|
} |
|
} |
|
if (sysFile) { |
|
let write = ""; |
|
if (ops.decrypt && !ops.encrypt) { |
|
sysData.hasEncryptedAudio = false; |
|
sysData.hasEncryptedImages = false; |
|
write = "decrypt"; |
|
} |
|
if (ops.encrypt && !ops.decrypt) { |
|
sysData.hasEncryptedAudio = true; |
|
sysData.hasEncryptedImages = true; |
|
write = "encrypt"; |
|
} |
|
if (write) { |
|
// log(`Running file ${write}...`); |
|
// log("----------"); |
|
await (0, promises_1.writeFile)(sysPath, JSON.stringify(sysData), "utf8"); |
|
} |
|
} |
|
if (buildIgnore && ignore.length) { |
|
await (0, promises_1.writeFile)(ignorePath, ignore.join("\n")); |
|
} |
|
if (ops.verbose) |
|
log("+------------+------------+------------+"); |
|
log("| Decrypted | Encrypted | Ignored |"); |
|
log("+------------+------------+------------+"); |
|
log(`| PNG: ${pad(stats.decrypt.png)} | PNG: ${pad(stats.encrypt.png)} | PNG: ${pad(stats.ignore.png)} |`); |
|
log(`| OGG: ${pad(stats.decrypt.ogg)} | OGG: ${pad(stats.encrypt.ogg)} | OGG: ${pad(stats.ignore.ogg)} |`); |
|
log(`| M4A: ${pad(stats.decrypt.m4a)} | M4A: ${pad(stats.encrypt.m4a)} | M4A: ${pad(stats.ignore.m4a)} |`); |
|
log(`| ALL: ${pad(stats.decrypt.png + stats.decrypt.ogg + stats.decrypt.m4a)} | ALL: ${pad(stats.encrypt.png + stats.encrypt.ogg + stats.encrypt.m4a)} | ALL: ${pad(stats.ignore.png + stats.ignore.ogg + stats.ignore.m4a)} |`); |
|
log("+------------+------------+------------+"); |
|
log(`| TOTAL: ${pad(stats.files, 29)} |`); |
|
log("+--------------------------------------+"); |
|
await (0, promises_1.writeFile)((0, path_1.join)(process.cwd(), "rpgmvcrypt.log"), logFile, "utf8"); |
|
} |
|
main(); |