diff --git a/.directory b/.directory new file mode 100644 index 0000000..9824996 --- /dev/null +++ b/.directory @@ -0,0 +1,7 @@ +[Dolphin] +SortRole=type +Timestamp=2018,5,12,15,59,59 +Version=4 + +[Settings] +HiddenFilesShown=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1433ca --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +validation/node_modules diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..12a9694 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js + +node_js: + - "node" + +install: + - cd validation + - npm install +script: + - node app.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..07517f9 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 `` 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 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 (` = `). 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.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 `//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 `//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 `//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 `//savefiles/.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 `//savefiles/.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. +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..41c4ed6 --- /dev/null +++ b/README.md @@ -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). diff --git a/games/.gitkeep b/games/.gitkeep new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/games/.gitkeep @@ -0,0 +1 @@ + diff --git a/validation/app.js b/validation/app.js new file mode 100644 index 0000000..07f3b14 --- /dev/null +++ b/validation/app.js @@ -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); +} + diff --git a/validation/config.json b/validation/config.json new file mode 100644 index 0000000..1673acd --- /dev/null +++ b/validation/config.json @@ -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" } +} diff --git a/validation/package.json b/validation/package.json new file mode 100644 index 0000000..dd83697 --- /dev/null +++ b/validation/package.json @@ -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 ", + "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" +}