From b914780a65a8ea12c1fa5322fc31599b18bf817e Mon Sep 17 00:00:00 2001 From: James Date: Thu, 7 Jun 2018 12:54:28 +1000 Subject: [PATCH] Add game DB lookup support (#32) * Add game DB lookup support * Fix compatibility typo * Fix typo 2 * Use async function in game DB listing, avoid redundant HTTP requests --- env.json | 13 ++++++ package.json | 2 + src/commands/game.js | 94 ++++++++++++++++++++++++++++++++++++++++++++ src/state.js | 3 ++ 4 files changed, 112 insertions(+) create mode 100644 src/commands/game.js diff --git a/env.json b/env.json index 12bc539..b7afb11 100644 --- a/env.json +++ b/env.json @@ -41,5 +41,18 @@ "GITHUB_OLD_THRESHOLD": { "required": false, "description": "Issues below this treshold should be ignored" + }, + "COMPAT_DB_SOURCE": { + "description": "URL to pull game compatibility information from" + }, + "COMPAT_DB_REFRESH": { + "required": false, + "description": "Minimum time in milliseconds between compatibility DB refreshes (default 20 minutes)" + }, + "COMPAT_ICON_BASE": { + "description": "URL (with trailing slash) to directory with .png files" + }, + "COMPAT_URL_BASE": { + "description": "URL (with trailing slash) to directory with game information pages" } } diff --git a/package.json b/package.json index a7b8ec8..7339537 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "logdna": "^1.2.3", "node-schedule": "^1.2.3", "request": "^2.79.0", + "request-promise-native": "^1.0.5", + "string-similarity": "^1.2.0", "winston": "^2.3.0" }, "devDependencies": { diff --git a/src/commands/game.js b/src/commands/game.js new file mode 100644 index 0000000..2172a1b --- /dev/null +++ b/src/commands/game.js @@ -0,0 +1,94 @@ +const request = require('request-promise-native'); +const discord = require('discord.js'); +const stringSimilarity = require('string-similarity'); + +const logger = require('../logging.js'); +const state = require('../state.js'); + +const targetServer = process.env.COMPAT_DB_SOURCE; +const refreshTime = process.env.COMPAT_REFRESH_TIME ? parseInt(process.env.COMPAT_REFRESH_TIME) : 1000 * 60 * 20; +const iconBase = process.env.COMPAT_ICON_BASE; +const urlBase = process.env.COMPAT_URL_BASE; + +const compatStrings = { + 0: { "key": "0", "name": "Perfect", "color": "#5c93ed", "description": "Game functions flawless with no audio or graphical glitches, all tested functionality works as intended without any workarounds needed." }, + 1: { "key": "1", "name": "Great", "color": "#47d35c", "description": "Game functions with minor graphical or audio glitches and is playable from start to finish. May require some workarounds." }, + 2: { "key": "2", "name": "Okay", "color": "#94b242", "description": "Game functions with major graphical or audio glitches, but game is playable from start to finish with workarounds." }, + 3: { "key": "3", "name": "Bad", "color": "#f2d624", "description": "Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches even with workarounds." }, + 4: { "key": "4", "name": "Intro/Menu", "color": "red", "description": "Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start Screen." }, + 5: { "key": "5", "name": "Won't Boot", "color": "#828282", "description": "The game crashes when attempting to startup." }, + 99: { "key": "99", "name": "Not Tested", "color": "black", "description": "The game has not yet been tested." } +}; + +async function updateDatabase() { + let body; + try { + body = await request(targetServer); + } catch (e) { + logger.error("Unable to download latest games list!"); + throw e; + } + + state.gameDB = JSON.parse(body); + state.lastGameDBUpdate = Date.now(); + logger.info(`Updated games list (${state.gameDB.length} games)`); + + state.gameDBPromise = null; +} + +exports.command = async function (message) { + if (Date.now() - state.lastGameDBUpdate > refreshTime) { + // Update remote list of games locally. + const waitMessage = message.channel.send("This will take a second..."); + + if (state.gameDBPromise == null) { + state.gameDBPromise = updateDatabase(message); + } + + try { + await state.gameDBPromise; + } catch (e) { + message.channel.send("Game compatibility feed temporarily unavailable - sorry!"); + throw e; + } finally { + // We don't need this message anymore + waitMessage.then(waitMessageResult => waitMessageResult.delete()); + } + } + + const game = message.content.substr(message.content.indexOf(' ') + 1); + + // Search all games. This is only linear time, so /shrug? + let bestGame = undefined; + let bestScore = 0.5; // Game names must have at least a 50% similarity to be matched + + state.gameDB.forEach(testGame => { + const newDistance = stringSimilarity.compareTwoStrings(game.toLowerCase(), testGame.title.toLowerCase()); + if (newDistance > bestScore) { + bestGame = testGame; + bestScore = newDistance; + } + }); + + if (bestGame === undefined) { + message.channel.send("Game could not be found."); + return; + } + + const screenshot = `${iconBase}${bestGame.id}.png`; + const url = `${urlBase}${bestGame.id}/`; + + const compat = compatStrings[bestGame.compatibility]; + + const embed = new discord.RichEmbed() + .addField("Status", compat.name, true) + .addField("Needs System Files", bestGame.needs_system_files ? "Yes" : "No", true) + .addField("Needs Shared Font", bestGame.needs_shared_font ? "Yes" : "No", true) + .setTitle(bestGame.title) + .setColor(compat.color) + .setDescription(bestGame.description) + .setURL(url) + .setThumbnail(screenshot); + + message.channel.send(embed); +}; diff --git a/src/state.js b/src/state.js index 87ca670..fb057f6 100644 --- a/src/state.js +++ b/src/state.js @@ -11,6 +11,9 @@ var State = function () { leaves: 0, warnings: 0 }; + this.lastGameDBUpdate = 0; + this.gameDB = []; + this.gameDBPromise = null; }; module.exports = new State();