Initial commit.

This commit is contained in:
Chris 2018-05-12 16:00:50 -04:00
parent 4b837bb771
commit f93153595c
9 changed files with 648 additions and 0 deletions

7
.directory Normal file
View file

@ -0,0 +1,7 @@
[Dolphin]
SortRole=type
Timestamp=2018,5,12,15,59,59
Version=4
[Settings]
HiddenFilesShown=true

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
validation/node_modules

10
.travis.yml Normal file
View file

@ -0,0 +1,10 @@
language: node_js
node_js:
- "node"
install:
- cd validation
- npm install
script:
- node app.js

175
CONTRIBUTING.md Normal file
View file

@ -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.
```

6
README.md Normal file
View file

@ -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).

1
games/.gitkeep Normal file
View file

@ -0,0 +1 @@

415
validation/app.js Normal file
View file

@ -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);
}

15
validation/config.json Normal file
View file

@ -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" }
}

18
validation/package.json Normal file
View file

@ -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"
}