Initial commit.
This commit is contained in:
parent
4b837bb771
commit
f93153595c
|
@ -0,0 +1,7 @@
|
||||||
|
[Dolphin]
|
||||||
|
SortRole=type
|
||||||
|
Timestamp=2018,5,12,15,59,59
|
||||||
|
Version=4
|
||||||
|
|
||||||
|
[Settings]
|
||||||
|
HiddenFilesShown=true
|
|
@ -0,0 +1 @@
|
||||||
|
validation/node_modules
|
|
@ -0,0 +1,10 @@
|
||||||
|
language: node_js
|
||||||
|
|
||||||
|
node_js:
|
||||||
|
- "node"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- cd validation
|
||||||
|
- npm install
|
||||||
|
script:
|
||||||
|
- node app.js
|
|
@ -0,0 +1,175 @@
|
||||||
|
# Contributing
|
||||||
|
Contributions to the yuzu Games Wiki are welcomed, as keeping all of the data up to date and accurate is a community effort.
|
||||||
|
|
||||||
|
**Table of Contents**:
|
||||||
|
- [Info About This Wiki](#info-about-this-wiki)
|
||||||
|
- [Angle Brackets](#angle-brackets)
|
||||||
|
- [yuzu Version](#citra-version)
|
||||||
|
- [Dates](#dates)
|
||||||
|
- [GitHub Issues](#github-issues)
|
||||||
|
- [Screenshots](#screenshots)
|
||||||
|
- [Title IDs](#title-ids)
|
||||||
|
- [TOML](#toml)
|
||||||
|
- [Code](#code)
|
||||||
|
- [Metadata](#metadata)
|
||||||
|
- [Icon](#icon)
|
||||||
|
- [Boxart](#boxart)
|
||||||
|
- [Game Screenshots](#game-screenshots)
|
||||||
|
- [Savefiles](#savefiles)
|
||||||
|
- [Save Metadata](#save-metadata)
|
||||||
|
- [Save Data](#save-data)
|
||||||
|
- [Wiki](#wiki)
|
||||||
|
|
||||||
|
## Info About This Wiki
|
||||||
|
|
||||||
|
### Angle Brackets
|
||||||
|
Throughout this guide, code blocks like `<Value>` are used. This means that "Value" should be replaced by something, and the "<>" should be deleted.
|
||||||
|
|
||||||
|
### yuzu Version
|
||||||
|
All data must be collected from the latest official yuzu nightly, downloaded from [here](https://citra-emu.org/download/).
|
||||||
|
|
||||||
|
### Dates
|
||||||
|
All dates follow the format `<4-Digit Year>-<2-Digit Month>-<2-Digit Day>`. For example, June 3rd 2017 would be "2017-06-03".
|
||||||
|
|
||||||
|
### GitHub Issues
|
||||||
|
Game issues can be found [here](https://github.com/yuzu-emu/yuzu/issues). The ID of the issue can be found at the end of the URL. For example, [SNES Virtual Console Games - Crash on Boot](https://github.com/citra-emu/citra/issues/2782)'s ID is 2782.
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
The recommended application for capturing the icon, boxart, and screenshots is [ShareX](https://github.com/ShareX/ShareX). Screenshots can not be compressed, and must be in the PNG format.
|
||||||
|
|
||||||
|
### Title IDs
|
||||||
|
Title IDs can be found near the top of a log when running a game. For example, this is what it looks like for The Legend of Zelda: Ocarina of Time, 0004000000033500: `[ 0.019882] Loader <Info> core/loader/ncch.cpp:Load:340: Program ID: 0004000000033500`.
|
||||||
|
|
||||||
|
### TOML
|
||||||
|
In this repo, DAT files follow the [TOML](https://github.com/toml-lang/toml) syntax, where each line consists of the creation of a piece of data. The simplest form of this is assigning a value to a key (`<Key> = <Value>`). The data types used for these `Value`s in this wiki are:
|
||||||
|
- Booleans, true or false (Example: `true`.)
|
||||||
|
- Integers, numbers (Example: `5`.)
|
||||||
|
- Strings, characters with surrounding quotes (Example: `"Hi!"`.)
|
||||||
|
- Arrays, collection of booleans, integers, or strings (Example for an array of integers: `[33, 2398, 234]`.)
|
||||||
|
|
||||||
|
These key/value pairs can be grouped together using an array of tables (Example: `[[ Stuff ]]`, with the pairs on the next lines.). These can be used more than once in a TOML file.
|
||||||
|
|
||||||
|
## Code
|
||||||
|
The code consists of the actual files in the Github repisitory. To modify them, you have to fork this repo, make your changes, and send a pull request.
|
||||||
|
|
||||||
|
At the root, there's a folder for each game. The names of these folders should follow these specifications:
|
||||||
|
- Only use letters, numbers, and hyphens (No spaces!), because they will be linked to on the site.
|
||||||
|
- Names should be lowercase to ensure consistency.
|
||||||
|
- Have a wiki page with the same name.
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
The metadata for the game is located at `/<Game Name>/game.dat`. This is required info about the game, all feilds are mandatory unless noted otherwise. The DAT values (See: [TOML](#toml)) are:
|
||||||
|
- `title` (String): English title of the game. This doesn't have to match the wiki or folder name, so there can be spaces.
|
||||||
|
- `description` (String): Get these from [Wikipedia](https://en.wikipedia.org/wiki/List_of_Nintendo_3DS_games). Short, 2-3 line description of the game.
|
||||||
|
- `github_issues` (Array of integers): The GitHub issue IDs for the game. See: [GitHub Issues](#github-issues).
|
||||||
|
- `needs_system_files` (Boolean): Whether the game requests the system files or not, regardless of whether it could be played without them (See: [yuzu Version](#citra-version)).
|
||||||
|
- `needs_shared_font` (Boolean): Whether the game requests the shared font or not, regardless of whether it could be played without them (See: [yuzu Version](#citra-version)).
|
||||||
|
- `game_type` (String): Whether the game has a retail release, `"switch"`, is an E-Shop **exclusive**, `"eshop"`, a Virtual Console game, `"vc"`, or DSiWare, `"dsi"`. This line is optional for retail releases.
|
||||||
|
- `releases` (Array of tables): Info about each release of the game. **The USA release should come first.**
|
||||||
|
- `title` (String): Title ID of this release of the game. See: [Title IDs](#title-ids).
|
||||||
|
- `region` (String): Region of the game. Possible values are:
|
||||||
|
- `aus`
|
||||||
|
- `chn`
|
||||||
|
- `eur`
|
||||||
|
- `jpn`
|
||||||
|
- `kor`
|
||||||
|
- `twn`
|
||||||
|
- `usa`
|
||||||
|
- `all` (Don't tag a game released in multiple regions as `all`. This is reserved for specific games released as such.)
|
||||||
|
- `release_date` (String): When the game was released in this region. See: [Dates](#dates).
|
||||||
|
- `title` (String): Title ID of this release of the game which was used during testing. See: [Title IDs](#title-ids).
|
||||||
|
|
||||||
|
An example of a game metadata file is the one for [The Legend of Zelda: Majora's Mask](https://github.com/citra-emu/citra-games-wiki/blob/master/games/legend-of-zelda-majoras-mask/game.dat):
|
||||||
|
```toml
|
||||||
|
title = "The Legend of Zelda: Majora's Mask 3D"
|
||||||
|
description = "The Legend of Zelda: Majora's Mask 3D is an action-adventure video game co-developed by Grezzo and Nintendo for the Nintendo 3DS handheld game console. The game is an enhanced remake of The Legend of Zelda: Majora's Mask, which was originally released for the Nintendo 64 home console in 2000. The game was released worldwide in February 2015"
|
||||||
|
github_issues = [2517]
|
||||||
|
needs_system_files = false
|
||||||
|
needs_shared_font = false
|
||||||
|
|
||||||
|
[[ releases ]]
|
||||||
|
title = "0004000000125500"
|
||||||
|
region = "usa"
|
||||||
|
release_date = "2015-02-13"
|
||||||
|
|
||||||
|
[[ releases ]]
|
||||||
|
title = "0004000000125600"
|
||||||
|
region = "eur"
|
||||||
|
release_date = "2015-02-13"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Icon
|
||||||
|
The icon for a game is located at `/<Game Name>/icon.png` (See: [Screenshots](#screenshots). The suggested process for getting one is:
|
||||||
|
- Make sure the ROM for the game is in your yuzu game directory.
|
||||||
|
- Take a screenshot of yuzu's library listing (See: [yuzu Version](#citra-version)).
|
||||||
|
- Crop out the game icon.
|
||||||
|
- The icon should be `48x48`.
|
||||||
|
|
||||||
|
### Boxart
|
||||||
|
The boxart for the game is located at `/<Game Name>/boxart.png`. The suggested process for getting retail boxart is:
|
||||||
|
- Download a scan from [GameTDB](http://www.gametdb.com/), preferably with the `Nintendo 3DS` logo on the right.
|
||||||
|
- The boxart should be from the USA.
|
||||||
|
- Downsize it to `328x300` using [PicResize](http://www.picresize.com/).
|
||||||
|
- Compress it using [TinyPNG](https://tinypng.com/).
|
||||||
|
|
||||||
|
The required process for getting eShop only boxart is:
|
||||||
|
- Run the game in yuzu (See: [yuzu Version](#citra-version)).
|
||||||
|
- Use 1x internal resolution.
|
||||||
|
- Increase the window size of yuzu to fill most of your monitor.
|
||||||
|
- Screenshot the title screen, which should only be the top screen.
|
||||||
|
- Downsize it to `500x300` using [PicResize](http://www.picresize.com/).
|
||||||
|
- Compress it using [TinyPNG](https://tinypng.com/).
|
||||||
|
- Examples are [Fairune](https://github.com/citra-emu/citra-games-wiki/blob/master/games/fairune/boxart.png) and [Pokémon Picross](https://github.com/citra-emu/citra-games-wiki/blob/master/games/pokemon-picross/boxart.png)
|
||||||
|
|
||||||
|
The required process for getting virtual console boxart is:
|
||||||
|
- Run the game in yuzu (See: [yuzu Version](#citra-version)).
|
||||||
|
- Use 1x internal resolution.
|
||||||
|
- Increase the window size of yuzu to fill most of your monitor.
|
||||||
|
- Screenshot the title screen, which should only be the top screen.
|
||||||
|
- Downsize it to `328x300` using [PicResize](http://www.picresize.com/).
|
||||||
|
- Compress it using [TinyPNG](https://tinypng.com/).
|
||||||
|
- Examples are [Legend of Zelda](https://github.com/citra-emu/citra-games-wiki/blob/master/games/legend-of-zelda/boxart.png) and [Tetris](https://github.com/citra-emu/citra-games-wiki/blob/master/games/tetris/boxart.png)
|
||||||
|
|
||||||
|
### Game Screenshots
|
||||||
|
The screenshots for the game are located in `/<Game Name>/screenshots/` (See: [Screenshots](#screenshots)). Screenshots **must** follow these specifications:
|
||||||
|
- Native resolution.
|
||||||
|
- Smallest window size.
|
||||||
|
- Black background (For the blank space left and right of the bottom screen.). To achieve this, go to the [User Directory](https://citra-emu.org/wiki/user-directory/), and from there navigate to the `config` directory. Open qt-config.ini with a text editor, and set bg_blue, bg_green, and bg_red to 0.
|
||||||
|
|
||||||
|
Additionally, if a game has a rating of 3 or higher, **you must include at least 3 screenshots**, otherwise 1 screenshot is acceptable. The names of the screenshots don't matter.
|
||||||
|
|
||||||
|
### Savefiles
|
||||||
|
#### Save Metadata
|
||||||
|
The metadata for a save is located at `/<Game Name>/savefiles/<Save Name>.dat`. This is info about the save. The DAT values (See: [TOML](#toml)) are:
|
||||||
|
- `title` (String): The location of the save ingame.
|
||||||
|
- `description` (String): A brief explanation about the save.
|
||||||
|
- `author` (String): Your forum account name, if you have one. If you don't, don't include this line.
|
||||||
|
- `title_id` (String): Title ID of the game.
|
||||||
|
|
||||||
|
#### Save Data
|
||||||
|
The save data is located at `/<Game Name>/savefiles/<Save Name>.zip` (See: [yuzu Version](#citra-version)). To make a ZIP file, the process is:
|
||||||
|
- Make sure the ROM for the game is in your yuzu game directory.
|
||||||
|
- Right click on the game and click `Open Save Data Location`. This should open a folder named `data`.
|
||||||
|
- Note the folder that the `data` folder is in. This is the low Title ID. As an example, the low Title ID for The Legend of Zelda: Ocarina of Time is `00033500`.
|
||||||
|
- The folder that the low Title ID folder is in should be named `00040000`, the high Title ID.
|
||||||
|
- Copy the high title ID folder elsewhere.
|
||||||
|
- Delete everything from the high title ID folder except for the low Title ID folder.
|
||||||
|
- Compress the high title ID folder into a ZIP.
|
||||||
|
|
||||||
|
## Wiki
|
||||||
|
The wiki contains info about specific game problems, and can be modified by anyone. They use [Markdown](https://guides.github.com/features/mastering-markdown/) formatting.
|
||||||
|
|
||||||
|
Each page's title should match the game's respective folder in the code section, except with hyphens in the code changed to spaces on the wiki. **Don't use the following characters in your wiki page's titles: \ / : * ? " < > |.**
|
||||||
|
|
||||||
|
The format of each page is as follows:
|
||||||
|
- H2 header with text saying `Summary`.
|
||||||
|
- Brief summary of how the game performs: graphically, auditorily, and frame rate (with general hardware comparison - see MK7 example). See: [yuzu Version](#citra-version).
|
||||||
|
|
||||||
|
An example of a game wiki page is the one for [Mario Kart 7](https://github.com/citra-emu/citra-games-wiki/wiki/Mario-Kart-7):
|
||||||
|
```markdown
|
||||||
|
## Summary
|
||||||
|
Mario Kart 7 has some problems in yuzu. Graphically, the game suffers from minor issues,
|
||||||
|
but requires decent hardware to obtain near full speed. It suffers from minor audio issues at times,
|
||||||
|
but this does not hinder gameplay in any way. You may experience crashes on some tracks, slow down,
|
||||||
|
and may need to transfer save files from yuzu to your 3DS to complete certain tracks.
|
||||||
|
```
|
|
@ -0,0 +1,6 @@
|
||||||
|
# yuzu Games Wiki
|
||||||
|
This is a database of how games will work in the yuzu Nintendo Switch Emulator using TOML. The site generated from this info can be found [here](https://yuzu-emu.org/game/).
|
||||||
|
|
||||||
|
If you interested in contributing, take a look at the [Contributing Guide](https://github.com/yuzu-emu/yuzu-games-wiki/blob/master/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
For more info about yuzu, go [here](https://yuzu-emu.org/). The repository for the yuzu emulator can be found [here](https://github.com/yuzu-emu/yuzu), and the yuzu website [here](https://github.com/yuzu-emu/yuzu-emu.github.io/tree/hugo).
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,415 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const config = require('./config.json');
|
||||||
|
|
||||||
|
const groupBy = require('group-by');
|
||||||
|
const sizeOf = require('image-size');
|
||||||
|
const readChunk = require('read-chunk');
|
||||||
|
const imageType = require('image-type');
|
||||||
|
|
||||||
|
const toml = require('toml');
|
||||||
|
|
||||||
|
let currentGame = null;
|
||||||
|
let errors = [];
|
||||||
|
|
||||||
|
// Catch non-formatting errors
|
||||||
|
let miscError = false;
|
||||||
|
|
||||||
|
function getDirectories(srcpath) {
|
||||||
|
return fs.readdirSync(srcpath)
|
||||||
|
.filter(file => fs.lstatSync(path.join(srcpath, file)).isDirectory())
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFiles(srcpath) {
|
||||||
|
return fs.readdirSync(srcpath)
|
||||||
|
.filter(file => fs.lstatSync(path.join(srcpath, file)).isFile())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that a filename matches the following:
|
||||||
|
/// [any of a-z, A-Z, 0-9, a '-' or a '_'](one or more) . [a-z](two or three)
|
||||||
|
function isValidFilename(name) {
|
||||||
|
return name.match(/^([a-zA-Z0-9_\-])+\.([a-z]){2,3}$/);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that a image is correctly sized and of the right format.
|
||||||
|
function validateImage(path, config) {
|
||||||
|
if (fs.existsSync(path) === false) {
|
||||||
|
validationError(`Image \"${path}\"' was not found at ${path}.`);
|
||||||
|
} else {
|
||||||
|
// Read first 12 bytes (enough for '%PNG', etc)
|
||||||
|
const buffer = readChunk.sync(path, 0, 12);
|
||||||
|
const type = imageType(buffer).mime;
|
||||||
|
if (type !== config.type) {
|
||||||
|
validationError(`Incorrect format of image (${type} != ${config.type})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dimensions = sizeOf(path);
|
||||||
|
|
||||||
|
for (const sizeIndex in config.sizes) {
|
||||||
|
const size = config.sizes[sizeIndex];
|
||||||
|
|
||||||
|
if (dimensions.width === size.width && dimensions.height === size.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build our error message
|
||||||
|
let possibleSizes = config.sizes.reduce((acc, curVal) => {
|
||||||
|
if (acc.length !== 0) {
|
||||||
|
acc += ", ";
|
||||||
|
}
|
||||||
|
acc += `${curVal.width} x ${curVal.height}`;
|
||||||
|
return acc;
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
validationError(`Image \"${path}\"'s dimensions are ${dimensions.width} x ${dimensions.height} ` +
|
||||||
|
`instead of the any of the following: ${possibleSizes}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that a folder (if it exists) of images contains images that are
|
||||||
|
// correctly sized and of the right format.
|
||||||
|
function validateDirImages(path, config) {
|
||||||
|
// TODO: Do we want to enforce having screenshots?
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
const files = getFiles(path);
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
if (!isValidFilename(file)) {
|
||||||
|
validationError(`File \"${file}\" contains bad characters!`);
|
||||||
|
} else {
|
||||||
|
validateImage(`${path}/${file}`, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Could these errors be prefixed with the section/line in which they come from?
|
||||||
|
|
||||||
|
/// Validates the existance of a particular entry in a structure
|
||||||
|
function validateExists(struct, name) {
|
||||||
|
if (struct[name] === undefined) {
|
||||||
|
validationError("Field \"" + name + "\" missing");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the existence of a particular entry in a structure, and
|
||||||
|
/// ensures that it meets a particular set of criteria.
|
||||||
|
function validateContents(struct, name, test) {
|
||||||
|
if (validateExists(struct, name)) {
|
||||||
|
test(struct[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the existence of a particular entry in a structure, and
|
||||||
|
/// ensures that it is not a empty string.
|
||||||
|
function validateNotEmpty(struct, name) {
|
||||||
|
validateContents(struct, name, field => {
|
||||||
|
if (typeof field !== "string") {
|
||||||
|
validationError("Field \"" + name + "\" is not a string");
|
||||||
|
} else if (field === "") {
|
||||||
|
validationError("Field \"" + name + "\" is empty");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the existence of a particular entry in a structure, and
|
||||||
|
/// ensures that it is not a empty string.
|
||||||
|
function validateIsBoolean(struct, name) {
|
||||||
|
if (struct[name] !== false && struct[name] !== true) {
|
||||||
|
validationError("Field \"" + name + "\" is not a boolean");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates pattern of YYYY-MM-DD in a field of a structure.
|
||||||
|
function validateIsDate(struct, name) {
|
||||||
|
validateContents(struct, name, field => {
|
||||||
|
if (!field.match(/^[0-9]{4}-((0[1-9])|(1[0-2]))-((0[1-9])|([1-2][0-9])|(3[0-1]))$/)) {
|
||||||
|
validationError(`\"${name}\" is not a valid date (\"${field}\").`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFileExists(dir) {
|
||||||
|
if (fs.existsSync(dir) === false) {
|
||||||
|
validationError(`\"${dir}\" does not exist!`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a TOML document
|
||||||
|
function validateTOML(path) {
|
||||||
|
if (fs.existsSync(path) === false) {
|
||||||
|
validationError(`TOML was not found at ${path}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawContents = fs.readFileSync(path);
|
||||||
|
let tomlDoc;
|
||||||
|
try {
|
||||||
|
tomlDoc = toml.parse(rawContents);
|
||||||
|
} catch (e) {
|
||||||
|
validationError("TOML parse error (" + e.line + "): " + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the global header section
|
||||||
|
validateNotEmpty(tomlDoc, "title");
|
||||||
|
validateNotEmpty(tomlDoc, "description");
|
||||||
|
if (tomlDoc["github_issues"] !== undefined) {
|
||||||
|
validateContents(tomlDoc, "github_issues", field => {
|
||||||
|
if (Array.isArray(field) === false) {
|
||||||
|
validationError("Github issues field is not an array!")
|
||||||
|
} else {
|
||||||
|
// Validate each individual entry
|
||||||
|
field.forEach(elem => {
|
||||||
|
if (typeof elem !== "number") {
|
||||||
|
validationError("Github issues entry is not a number!")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tomlDoc["gametypes"] !== undefined) {
|
||||||
|
validateContents(tomlDoc, "gametypes", field => {
|
||||||
|
if (config.gametypes.indexOf(field) === -1) {
|
||||||
|
validationError(`Could not find gametype \"${field}\"!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === "vc") {
|
||||||
|
validateContents(tomlDoc, "vc_system", field => {
|
||||||
|
if (config.vc_systems.indexOf(field) === -1) {
|
||||||
|
validationError(`Could not find VC console \"${field}\"!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let section;
|
||||||
|
|
||||||
|
// Check each release individually
|
||||||
|
if (tomlDoc["releases"] !== undefined) {
|
||||||
|
section = tomlDoc["releases"];
|
||||||
|
section.forEach(release => {
|
||||||
|
validateContents(release, "title", field => {
|
||||||
|
if (field.length !== 16) {
|
||||||
|
validationError(`Release: Game title ID has an invalid length`);
|
||||||
|
} else if (!field.match(/^([A-Z0-9]){16}$/)) {
|
||||||
|
validationError(`Release: Game title ID is not a hexadecimal ID`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
validateContents(release, "region", field => {
|
||||||
|
if (config.regions.indexOf(field) === -1) {
|
||||||
|
validationError(`Release: Invalid region ${field}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
validateIsDate(release, "release_date");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
validationError("No releases.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxCompatibility = 999;
|
||||||
|
|
||||||
|
// Check each testcase individually
|
||||||
|
if (tomlDoc["testcases"] !== undefined) {
|
||||||
|
section = tomlDoc["testcases"];
|
||||||
|
section.forEach(testcase => {
|
||||||
|
validateContents(testcase, "title", field => {
|
||||||
|
if (field.length !== 16) {
|
||||||
|
validationError(`Testcase: Game title ID has an invalid length`);
|
||||||
|
} else if (!field.match(/^([A-Z0-9]){16}$/)) {
|
||||||
|
validationError(`Testcase: Game title ID is not a hexadecimal ID`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
validateNotEmpty(testcase, "compatibility");
|
||||||
|
if (testcase["compatibility"] !== undefined) {
|
||||||
|
let compat = parseInt(testcase["compatibility"]);
|
||||||
|
if (compat < maxCompatibility) {
|
||||||
|
maxCompatibility = compat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateIsDate(testcase, "date");
|
||||||
|
validateContents(testcase, "version", test => {
|
||||||
|
if (test.length !== 12) {
|
||||||
|
validationError(`Testcase: Version is of incorrect length`);
|
||||||
|
} else if (!test.startsWith("HEAD-")) {
|
||||||
|
validationError(`Testcase: Unknown version commit source`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
validateNotEmpty(testcase, "author");
|
||||||
|
|
||||||
|
validateNotEmpty(testcase, "cpu");
|
||||||
|
validateNotEmpty(testcase, "gpu");
|
||||||
|
validateNotEmpty(testcase, "os");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate dates are properly ordered
|
||||||
|
section.reduce(function(previousValue, currentValue) {
|
||||||
|
if (typeof previousValue === "undefined" || previousValue.date <= currentValue.date) {
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
validationError("Test case dates are not properly sorted in ascending order.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only check these if we have a known test result (we cannot know if a game needs
|
||||||
|
// resources if it doesn't even run!)
|
||||||
|
if (maxCompatibility < 5) {
|
||||||
|
validateIsBoolean(tomlDoc, "needs_system_files");
|
||||||
|
validateIsBoolean(tomlDoc, "needs_shared_font");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the basic structure of a save game's TOML. Assumes it exists.
|
||||||
|
function validateSaveTOML(path) {
|
||||||
|
let rawContents = fs.readFileSync(path);
|
||||||
|
let tomlDoc;
|
||||||
|
try {
|
||||||
|
tomlDoc = toml.parse(rawContents);
|
||||||
|
} catch (e) {
|
||||||
|
validationError("TOML parse error (" + e.line + "): " + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the global header section
|
||||||
|
validateNotEmpty(tomlDoc, "title");
|
||||||
|
validateNotEmpty(tomlDoc, "description");
|
||||||
|
validateNotEmpty(tomlDoc, "author");
|
||||||
|
validateContents(tomlDoc, "title_id", field => {
|
||||||
|
if (field.length !== 16) {
|
||||||
|
validationError(`Game save data: Game title ID has an invalid length`);
|
||||||
|
} else if (!field.match(/^([A-Z0-9]){16}$/)) {
|
||||||
|
validationError(`Game save data: Game title ID is not a hexadecimal ID`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that a save is actually a .zip.
|
||||||
|
function validateSaveZip(path) {
|
||||||
|
// TODO: Would a node library MIME check be better?
|
||||||
|
const zipHeader = Buffer.from([0x50, 0x4B, 0x03, 0x04]);
|
||||||
|
|
||||||
|
const data = readChunk.sync(path, 0, 4);
|
||||||
|
|
||||||
|
if (zipHeader.compare(data) !== 0) {
|
||||||
|
validationError(`File ${path} is not a .zip!`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a folder of game saves.
|
||||||
|
function validateSaves(dir) {
|
||||||
|
if (fs.existsSync(dir) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = getFiles(dir);
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
if (!isValidFilename(file)) {
|
||||||
|
validationError(`File \"${file}\" contains bad characters!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Strip extensions, so we know what save 'groups' we are dealing with
|
||||||
|
const strippedFiles = files.map(file => {
|
||||||
|
return file.substr(0, file.lastIndexOf("."))
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups = strippedFiles.filter((element, i) => {
|
||||||
|
return strippedFiles.indexOf(element) === i
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check each group
|
||||||
|
groups.forEach(group => {
|
||||||
|
if (validateFileExists(`${dir}/${group}.dat`)) {
|
||||||
|
validateSaveTOML(`${dir}/${group}.dat`);
|
||||||
|
}
|
||||||
|
if (validateFileExists(`${dir}/${group}.zip`)) {
|
||||||
|
validateSaveZip(`${dir}/${group}.zip`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validationError(err) {
|
||||||
|
errors.push({game: currentGame, error: err});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through each game folder, validating each game.
|
||||||
|
getDirectories(config.directory).forEach(function (game) {
|
||||||
|
try {
|
||||||
|
if (game === '.git' || game === '_validation') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputDirectoryGame = `${config.directory}/${game}`;
|
||||||
|
currentGame = game;
|
||||||
|
|
||||||
|
// Check that everything is lowercase and is a known file.
|
||||||
|
getFiles(inputDirectoryGame).forEach(file => {
|
||||||
|
if (config.permitted_files.indexOf(file) === -1) {
|
||||||
|
validationError(`Unknown file \"${file}\"!`);
|
||||||
|
} else if (!isValidFilename(file)) {
|
||||||
|
validationError(`File \"${file}\" contains bad characters!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that all directories are known.
|
||||||
|
getDirectories(inputDirectoryGame).forEach(file => {
|
||||||
|
if (config.permitted_dirs.indexOf(file) === -1) {
|
||||||
|
validationError(`Unknown directory \"${file}\"!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the game's boxart.
|
||||||
|
validateImage(`${inputDirectoryGame}/${config.boxart.filename}`, config.boxart);
|
||||||
|
|
||||||
|
// Verify the game's image.
|
||||||
|
validateImage(`${inputDirectoryGame}/${config.icon.filename}`, config.icon);
|
||||||
|
|
||||||
|
// Verify the game's metadata.
|
||||||
|
validateTOML(`${inputDirectoryGame}/${config.data.filename}`);
|
||||||
|
|
||||||
|
// Verify the game's screenshots.
|
||||||
|
validateDirImages(`${inputDirectoryGame}/${config.screenshots.dirname}`,
|
||||||
|
config.screenshots);
|
||||||
|
|
||||||
|
// Verify the game's save files.
|
||||||
|
validateSaves(`${inputDirectoryGame}/${config.saves.dirname}`);
|
||||||
|
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn(`${game} has encountered an unexpected error.`);
|
||||||
|
console.error(ex);
|
||||||
|
miscError = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length > 0 || miscError) {
|
||||||
|
console.warn('Validation completed with errors.');
|
||||||
|
|
||||||
|
const groups = groupBy(errors, "game");
|
||||||
|
|
||||||
|
for (let key in groups) {
|
||||||
|
let group = groups[key];
|
||||||
|
|
||||||
|
console.info(` ${key}:`);
|
||||||
|
|
||||||
|
group.forEach(issue => {
|
||||||
|
console.info(` - ${issue.error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.info('Validation completed without errors.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"directory": "../games",
|
||||||
|
"regions": ["jpn", "usa", "eur", "aus", "chn", "kor", "twn", "all"],
|
||||||
|
"gametypes": ["3ds", "vc", "dsi", "eshop"],
|
||||||
|
"vc_systems": ["nes", "snes", "gb", "gbc", "gba", "gg"],
|
||||||
|
|
||||||
|
"permitted_files": ["boxart.png", "icon.png", "game.dat"],
|
||||||
|
"permitted_dirs": ["screenshots", "savefiles"],
|
||||||
|
|
||||||
|
"boxart": { "filename": "boxart.png", "sizes": [{"width": 328, "height": 300}, {"width": 500, "height": 300}], "type": "image/png"},
|
||||||
|
"icon": { "filename": "icon.png", "sizes": [{"width": 48, "height": 48}], "type": "image/png"},
|
||||||
|
"screenshots": { "dirname": "screenshots", "sizes": [{"width": 400, "height": 480}], "type": "image/png"},
|
||||||
|
"saves": { "dirname": "savefiles"},
|
||||||
|
"data": { "filename": "game.dat" }
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "yuzu-games-wiki-validation",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Used to validate yuzu-games-wiki code is valid.",
|
||||||
|
"homepage": "https://yuzu-emu.org/",
|
||||||
|
"author": "Flame Sage <chris062689@gmail.com>",
|
||||||
|
"main": "app.js",
|
||||||
|
"dependencies": {
|
||||||
|
"group-by": "0.0.1",
|
||||||
|
"image-size": "^0.5.4",
|
||||||
|
"image-type": "^3.0.0",
|
||||||
|
"read-chunk": "latest",
|
||||||
|
"toml": "^2.3.2"
|
||||||
|
},
|
||||||
|
"preferGlobal": false,
|
||||||
|
"private": true,
|
||||||
|
"license": "AGPL-3.0"
|
||||||
|
}
|
Reference in New Issue