Skip to content
This repository has been archived by the owner on Oct 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #4 from igor725/dev
Browse files Browse the repository at this point in the history
Trophies system, languages and bugfixes
  • Loading branch information
igor725 authored May 13, 2024
2 parents ce0ff71 + ead38b3 commit 5a51d78
Show file tree
Hide file tree
Showing 39 changed files with 1,504 additions and 623 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: build

on:
push:
branches: [ main ]
branches: [ main, dev ]

jobs:
package:
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,35 @@ This is a launcher for [psOff, PlayStation 4 emulator](https://github.com/SysRay
+ Automatic updates for emulator releases and nightly builds
+ List filtering by title name/id
+ Game's background music
+ Trophies system

## Planned features
+ Controllers/Keybind cofiguration
+ Gamepad control support
+ Audio device selection
+ Trophies system
+ Custom themes

## How can I download it?

Glad you asked! Just hit the [Actions](<https://github.com/igor725/adv-launch/actions>) button and download the latest available build.
Glad you asked! Just hit the [Actions](<https://github.com/igor725/adv-launch/actions?query=branch%3Amain>) button and download the latest available build.

## Contributing

Every contribution are welcome! The launcher's code is not the cleanest in the world but it's manageable.
Some parts are hacky as hell and will be reworked later (probably).

## Other languages

This launcher supports multiple languages. The language of displaying text depends on emulator's language settings. If selected langauge is not available, then it will fallback to English.

### What should I do to add support for my language?

Well, first of all you should go to `webroot/langs/`, clone `en.json` and rename it to your short language code. Now you can update the file contents and translate these strings to your language. When translation is done, open `js/lang.js` and look for `avail_langs` array, find the line with corresponding to your language name comment and change the `null` value to the actual short language code. Now you can test your changes and open some [PR](<https://github.com/igor725/adv-launch/pulls>) if everything's ok :)

### I don't see my language name in avail_langs comments, what should I do?

Sadly, nothing. This is impossible to add new language since we stick to [PS4 supported languages list](<https://www.psdevwiki.com/ps4/Languages>).

## LICENSE

The launcher released under MIT license, but some its parts are licensed under other licenses, see `bin/*-license.txt` for more info.
37 changes: 25 additions & 12 deletions settings.js → libs/settings.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');

module.exports.Config = class Config {
#cfgfile = path.join(__dirname, '/config.json');
#cfgfile = path.join(__dirname, '../config.json');
#default = {
update_channel: 'release',
update_freq: 'weekly',
Expand All @@ -13,10 +13,10 @@ module.exports.Config = class Config {
};
#emuconfpath = null;
#emuconf = {
controls: null,
graphics: null,
general: null,
audio: null
controls: {},
graphics: {},
general: {},
audio: {}
};
#unsaved = {
launcher: false,
Expand Down Expand Up @@ -82,6 +82,11 @@ module.exports.Config = class Config {
return this.#emuconf.general.systemlang ?? 1;
};

getTrophyKey = () => {
if (this.#emuconf.general === null) return '';
return this.#emuconf.general.trophyKey ?? '';
};

markLaunch = () => {
this.#data.first_launch = false;
this.#unsaved.launcher = true;
Expand All @@ -97,19 +102,27 @@ module.exports.Config = class Config {

try {
this.#emuconf.controls = JSON.parse(fs.readFileSync(this.#emuconfpath.controls));
} catch (e) { }
} catch (e) {
console.error('Failed to parse emulator controls config: ', e.toString());
}

try {
this.#emuconf.general = JSON.parse(fs.readFileSync(this.#emuconfpath.general));
} catch (e) { }
} catch (e) {
console.error('Failed to parse emulator general config: ', e.toString());
}

try {
this.#emuconf.audio = JSON.parse(fs.readFileSync(this.#emuconfpath.audio));
} catch (e) { }
} catch (e) {
console.error('Failed to parse emulator audio config: ', e.toString());
}

try {
this.#emuconf.graphics = JSON.parse(fs.readFileSync(this.#emuconfpath.graphics));
} catch (e) { }
} catch (e) {
console.error('Failed to parse emulator graphics config: ', e.toString());
}
};

getFullConfig = () => {
Expand Down Expand Up @@ -141,7 +154,7 @@ module.exports.Config = class Config {
const fac = this.#emuconf[facility];
Object.assign(fac, values);
this.#unsaved.emulator[facility] = true;
for (const [key, value] in Object.entries(values)) {
for (const [key, value] of Object.entries(values)) {
this.runCallback(`emu.${facility}`, key, value);
}
}
Expand Down
209 changes: 209 additions & 0 deletions libs/trophies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
const fs = require('node:fs');
const crypto = require('node:crypto');
const xparser = require('xml-js');
const CRYPTO_METHOD = 'aes-128-cbc';

module.exports.TrophySharedConfig = class TrophySharedConfig {
static #keygen_erk = Buffer.alloc(16, 0);
static #keygen_iv = Buffer.alloc(16, 0);
static #keygen_set = false;
#key = null;
#commid = -1;

constructor(commid) {
this.setNetCommID(commid);
}

static setERK(key) {
TrophySharedConfig.#keygen_erk.fill(key, 0, 16, 'hex');
this.#keygen_set = true;
}

getNetCommKey() {
return this.#key;
}

getNetCommID() {
return this.#commid;
}

setNetCommID(id) {
if (!TrophySharedConfig.#keygen_set) throw new Error('No trophy ERK set');
if (id < 0) {
this.#key = null;
this.#commid = -1;
return null;
}

const key_ciph = crypto.createCipheriv(CRYPTO_METHOD, TrophySharedConfig.#keygen_erk, TrophySharedConfig.#keygen_iv);
this.#key = key_ciph.update(Buffer.from(`NPWR${String(id).padStart(5, '0')}_00\0\0\0\0`));
this.#commid = id;
key_ciph.destroy();

return this.#key;
}
};

class TrophyFile {
static #validhdr = Buffer.from('<!--Sce-Np-Trophy');
#tsc = null;
#name = null;
#bdata = null;
#isEnc = false;

constructor(tsc, name, data, flags) {
this.#isEnc = Boolean(flags & 3 === 3);
this.#name = name;
this.#bdata = data;
this.#tsc = tsc;
}

isImage() {
return this.#name.endsWith('.PNG');
}

isXML() {
return this.#name.endsWith('.ESFM');
}

getData() {
if (this.#isEnc) {
const buf = this.#bdata;
const iv = buf.subarray(0, 16);
const tsc = this.#tsc;

let key = tsc.getNetCommKey();

if (key === null) {
const vh = TrophyFile.#validhdr;

for (let i = 0; i < 99999; ++i) {
key = tsc.setNetCommID(i);

const test_deciph = crypto.createDecipheriv(CRYPTO_METHOD, key, iv);
if (vh.compare(test_deciph.update(buf.subarray(16, 64)), 0, vh.length) === 0) {
break;
}

key = tsc.setNetCommID(-1);
}

if (key === null) throw new Error('Failed to guess netcommid');
}

const data_deciph = crypto.createDecipheriv(CRYPTO_METHOD, key, iv);
const final = Buffer.concat([data_deciph.update(buf.subarray(16)), data_deciph.final()]);
data_deciph.destroy();

return final;
}

return this.#bdata;
}

toString() {
return `TrophyFile {name: ${this.#name}, encrypted: ${this.#isEnc}}`;
}
};

module.exports.TrophyDataReader = class TrophyDataReader {
#trops = [];

constructor(tf) {
if (tf instanceof TrophyFile) {
if (!tf.isXML()) throw new Error('Not a XML file');
const xdata = xparser.xml2js(tf.getData());
const trophyroot = xdata.elements.find((el) => el.name === 'trophyconf' && el.type === 'element');

if (trophyroot) {
trophyroot.elements.forEach((el) => {
if (el.name !== 'trophy') return;

const trophy = {
id: parseInt(el.attributes.id),
/* Unused for now, so there is no reason to actually send this data */
// pid: parseInt(el.attributes.pid ?? -1),
// gid: parseInt(el.attributes.gid ?? -1),
hidden: el.attributes.hidden === 'yes',
grade: el.attributes.ttype
};

if (el.elements) {
trophy.name = el.elements.find((el) => el.name === 'name').elements[0].text;
trophy.detail = el.elements.find((el) => el.name === 'detail').elements[0].text;
}

this.#trops.push(trophy);
});
}

return;
}

throw new Error('Not a TrophyFile');
}

addImages(tp) {
if (tp instanceof module.exports.Trophies) {
for (const trop of this.#trops) {
const pngf = tp.findFile(`TROP${String(trop.id).padStart(3, '0')}.PNG`);
if (pngf) trop.icon = `data:image/png;base64,${pngf.getData().toString('base64')}`;
}
return;
}

throw new Error('Not a Trophies');
}

get array() {
return this.#trops;
}

get length() {
return this.#trops.length;
}

toString() {
return `TrophyDataReader ${JSON.stringify(this.#trops, null, 2)}`;
}
};

module.exports.Trophies = class Trophies {
static #entries_offset = 96;
static #entry_size = 64;
#tsc = null;

constructor(fpath, commid = -1) {
this.#tsc = new module.exports.TrophySharedConfig(commid);
const buf = this.buffer = fs.readFileSync(fpath);

if (buf.readUint32BE(0) !== 3701624064) throw new Error('Invalid magic');
if (buf.readUint32BE(4) !== 3) throw new Error('Invalid trp version');

this.entries_num = buf.readUint32BE(16);
if (buf.readUint32BE(20) !== Trophies.#entry_size) throw new Error('Invalid entry size');
}

resetNetCommID() {
this.#tsc.setNetCommID(-1);
}

findFile(fname) {
if (fname.length > 32) throw new Error('File name is too length');
fname = fname.toUpperCase();
const buf = this.buffer;

for (let i = 0; i < this.entries_num; ++i) {
const base = (Trophies.#entries_offset + (i * Trophies.#entry_size));
if (buf.toString('utf-8', base, base + fname.length) !== fname) continue;

const pos = Number(buf.readBigUint64BE(base + 32));
const end = pos + Number(buf.readBigUint64BE(base + 32 + 8));
const flags = buf.readUint32BE(base + 32 + 16);

return new TrophyFile(this.#tsc, fname, buf.subarray(pos, end), flags);
}

return null;
}
};
Loading

0 comments on commit 5a51d78

Please sign in to comment.