diff --git a/xtrn/gttrivia/gttrivia.ini b/xtrn/gttrivia/gttrivia.ini index 9adeff4e5d99842039bcd1ecb07c0334f50a700e..f4f514718e5826444f8721f9754a49053594e330 100644 --- a/xtrn/gttrivia/gttrivia.ini +++ b/xtrn/gttrivia/gttrivia.ini @@ -2,6 +2,10 @@ numQuestionsPerPlay=30 numTriesPerQuestion=4 maxNumPlayerScoresToDisplay=10 +; scoresMsgSubBoardsForPosting specifies a comma-separated list of internal sub-board +; codes for sub-boards to post user scores in, as a backup in case the server +; specified in the REMOTE_SERVER section can't be reached +scoresMsgSubBoardsForPosting= [COLORS] error=YH @@ -31,5 +35,11 @@ dirty_minds=AGE 18 server=digitaldistortionbbs.com port=10088 +; The SERVER section is for hosting game scores only [SERVER] deleteScoresOlderThanDays=182 +; scoresMsgSubBoardsForReading specifies a comma-separated list of internal sub-board +; codes for sub-boards to read user scores from. To use this, set up an event in +; SCFG > External Programs > Timed Events to run gttrivia.js with the command-line +; parameter -read_scores_from_subboard +scoresMsgSubBoardsForReading= diff --git a/xtrn/gttrivia/gttrivia.js b/xtrn/gttrivia/gttrivia.js index 1921b51d0acf0fb2e4364f76b36d625aec0f8ba4..a206a25405d1b307ba397a878eeb5ce5759999e1 100644 --- a/xtrn/gttrivia/gttrivia.js +++ b/xtrn/gttrivia/gttrivia.js @@ -11,6 +11,10 @@ Date Author Description also sysop functions to remove players and users from the hosted inter-BBS scores. Also, answer clues now don't mask spaces in the answer. +2022-12-08 Eric Oulashin Version 1.02 + The game can now post scores in (networked) message sub-boards as + a backup to using a JSON DB server in case the server can't be + contacted. */ @@ -37,11 +41,8 @@ if (system.version_num < 31500) } // Version information -var GAME_VERSION = "1.01"; -var GAME_VER_DATE = "2022-11-25"; -// Version of data written to the server, if applicable. This might not necessarily be the same as -// the version of the game. -var SERVER_DATA_VERSION = "1.01"; +var GAME_VERSION = "1.02"; +var GAME_VER_DATE = "2022-12-08"; // Determine the location of this script (its startup directory). // The code for figuring this out is a trick that was created by Deuce, @@ -141,18 +142,91 @@ var JSON_DB_LOCK_UNLOCK = -1; -// Enable debugging if the first command-line parameter is -debug -var gDebug = false; -if (argv.length > 0) - gDebug = (argv[0].toUpperCase() == "-DEBUG"); +// Load the settings from the .ini file +var gSettings = loadSettings(gStartupPath); +// Parse command-line arguments +var gCmdLineArgs = parseCmdLineArgs(argv); -// Display the program logo -displayProgramLogo(true, false); +// If the command-line argument was specified to post or read scores in the configured message +// sub-board, then do so and exit. +if (gCmdLineArgs.postScoresToSubBoard) +{ + if (gSettings.behavior.scoresMsgSubBoardsForPosting.length == 0) + { + log(LOG_ERR, format("%s - Post scores to sub-boards specified, but scoresMsgSubBoardsForPosting is not set", GAME_NAME)); + exit(2); + } -// Load the settings from the .ini file -var gSettings = loadSettings(gStartupPath); + var exitCode = 0; + for (var i = 0; i < gSettings.behavior.scoresMsgSubBoardsForPosting.length; ++i) + { + var subCode = gSettings.behavior.scoresMsgSubBoardsForPosting[i]; + if (!msg_area.sub.hasOwnProperty(subCode)) + { + log(LOG_ERR, format("%s - Sub-board-code %s does not exist (specified in %s)", GAME_NAME, subCode, "scoresMsgSubBoardsForPosting")); + exitCode = 3; + continue; + } + var postSuccessful = postGTTriviaScoresToSubBoard(subCode); + // For logging + var subBoardInfoStr = msg_area.sub[subCode].name + " - " + msg_area.sub[subCode].description; + // Write the status to the log + var logMsg = ""; + var logLevel = LOG_INFO; + if (postSuccessful) + logMsg = format("%s - Successfully posted local scores to sub-board %s (%s)", GAME_NAME, subCode, subBoardInfoStr); + else + { + logLevel = LOG_ERR; + logMsg = format("%s - Posting scores to sub-board %s (%s) failed!", GAME_NAME, subCode, subBoardInfoStr); + exitCode = 3; + } + log(logLevel, logMsg); + } + exit(exitCode); +} +else if (gCmdLineArgs.readScoresFromSubBoard) +{ + if (gSettings.server.scoresMsgSubBoardsForReading.length == 0) + { + log(LOG_ERR, format("%s - Read scores from sub-boards specified, but scoresMsgSubBoardsForReading is not set", GAME_NAME)); + exit(2); + } + + var exitCode = 0; + for (var i = 0; i < gSettings.server.scoresMsgSubBoardsForReading.length; ++i) + { + var subCode = gSettings.server.scoresMsgSubBoardsForReading[i]; + if (subCode.length == 0 || !msg_area.sub.hasOwnProperty(subCode)) + { + log(LOG_ERR, format("%s - Invalid sub-board code specified in scoresMsgSubBoardsForReading: %s", GAME_NAME, subCode)); + continue; + } + + var readSuccessful = readGTTriviaScoresFromSubBoard(subCode); + // For logging + var subBoardInfoStr = msg_area.sub[subCode].name + " - " + msg_area.sub[subCode].description; + // Write the status to the log + var logMsg = ""; + var logLevel = LOG_INFO; + if (readSuccessful) + logMsg = format("%s - Successfully read scores from sub-board %s (%s)", GAME_NAME, subCode, subBoardInfoStr); + else + { + logLevel = LOG_ERR; + logMsg = format("%s - Reading scores from sub-board %s (%s) failed!", GAME_NAME, subCode, subBoardInfoStr); + exitCode = 4; + } + log(logLevel, logMsg); + } + exit(exitCode); +} + + +// Display the program logo +displayProgramLogo(true, false); //console.clear("\x01n"); @@ -296,7 +370,6 @@ function playTrivia() console.attributes = "N" + gSettings.colors.clue; console.print(partiallyHiddenStr(QAArray[i].answer, tryI-1) + "\x01n"); console.crlf(); - } // Prompt for an answer console.attributes = "N" + gSettings.colors.answerPrompt; @@ -360,8 +433,50 @@ function playTrivia() console.crlf(); console.print("\x01b\x01hUpdating the scores file..."); console.crlf(); - updateScoresFile(userPoints, qaFilenameInfo[chosenSectionIdx].sectionName); - console.print("Done.\x01n"); + // Update the local scores file + var updateLocalScoresRetObj = updateScoresFile(userPoints, qaFilenameInfo[chosenSectionIdx].sectionName); + if (updateLocalScoresRetObj.succeeded) + { + // If there is a server configured, then send the user's score to the server too. + // If there are no server settings configured, or posting scores to the server fails, + // then if there's a sub-board configured, then write the user scores to the sub-board + var writeUserScoresToSubBoard = false; + if (gSettings.hasValidServerSettings()) + { + writeUserScoresToSubBoard = !updateScoresOnServer(user.alias, updateLocalScoresRetObj.userScoresObj); + if (writeUserScoresToSubBoard) + { + var errorMsg = "\x01n" + attrCodeStr(gSettings.colors.error) + "Failed to update scores on the remote server."; + if (gSettings.behavior.scoresMsgSubBoardsForPosting.length > 0) + errorMsg += " Will post server scores in message area(s); server scores will be delayed.\x01n"; + console.putmsg(errorMsg, P_WORDWRAP|P_NOATCODES); + console.attributes = "BH"; // As before + } + } + else + writeUserScoresToSubBoard = true; + if (writeUserScoresToSubBoard) + { + // If there are any message sub-boards configured, post the scores in there + for (var i = 0; i < gSettings.behavior.scoresMsgSubBoardsForPosting.length; ++i) + { + var subCode = gSettings.behavior.scoresMsgSubBoardsForPosting[i]; + if (msg_area.sub.hasOwnProperty(subCode)) + { + if (postGTTriviaScoresToSubBoard(subCode)) + log(LOG_INFO, format("%s - Successfully posted scores in the sub-board", GAME_NAME)); + else + log(LOG_INFO, format("%s - Posting scores in the sub-board failed!", GAME_NAME)); + } + } + } + console.print("Done.\x01n"); + } + else + { + console.attributes = "N" + gSettings.colors.error; + console.print("Failed to save the scores!\x01n"); + } console.crlf(); return 0; } @@ -384,6 +499,7 @@ function loadSettings(pStartupPath) settings.colors = iniFile.iniGetObject("COLORS"); settings.category_ars = iniFile.iniGetObject("CATEGORY_ARS"); settings.remoteServer = iniFile.iniGetObject("REMOTE_SERVER"); + settings.server = iniFile.iniGetObject("SERVER"); // Ensure the actual expected setting name & color names exist in the settings if (typeof(settings.behavior) !== "object") @@ -394,6 +510,8 @@ function loadSettings(pStartupPath) settings.category_ars = {}; if (typeof(settings.remoteServer) !== "object") settings.remoteServer = {}; + if (typeof(settings.server) !== "object") + settings.server = {}; if (typeof(settings.behavior.numQuestionsPerPlay) !== "number") settings.behavior.numQuestionsPerPlay = 10; @@ -440,7 +558,10 @@ function loadSettings(pStartupPath) settings.colors.clue = "GH"; if (typeof(settings.colors.answerAfterIncorrect) !== "string") settings.colors.answerAfterIncorrect = "G"; - + + settings.behavior.scoresMsgSubBoardsForPosting = splitAndVerifyMsgSubCodes(settings.behavior.scoresMsgSubBoardsForPosting, "scoresMsgSubBoardsForPosting"); + settings.server.scoresMsgSubBoardsForReading = splitAndVerifyMsgSubCodes(settings.server.scoresMsgSubBoardsForReading, "scoresMsgSubBoardsForReading"); + // Sanity checking if (settings.behavior.numQuestionsPerPlay <= 0) settings.behavior.numQuestionsPerPlay = 10; @@ -465,11 +586,7 @@ function loadSettings(pStartupPath) settings.remoteServer.gtTriviaScope = "GTTRIVIA"; // JSON location: For the BBS name, use the QWK ID if available, but if not, use the system name and replace spaces // with underscores (since spaces may cause issues in JSON property names) - var BBS_ID = ""; - if (system.qwk_id.length > 0) - BBS_ID = system.qwk_id; - else - BBS_ID = system.name.replace(/ /g, "_"); + var BBS_ID = getBBSIDForJSON(); settings.remoteServer.scoresJSONLocation = "SCORES"; settings.remoteServer.BBSJSONLocation = settings.remoteServer.scoresJSONLocation + ".systems." + BBS_ID; settings.remoteServer.userScoresJSONLocationWithoutUsername = settings.remoteServer.BBSJSONLocation + ".user_scores"; @@ -505,6 +622,39 @@ function genFullPathCfgFilename(pFilename, pDefaultPath) } return fullyPathedFilename; } +// Takes a comma-separated list of internal sub-board codes and splits them into an array, +// and also verifies they exist; the returned array will contain only ones that exist. +// Also ensures there are no duplicates in the array. +// +// Parameters: +// pSubCodeList: A comma-separated list of message sub-board codes (string) +// pSettingName: Optional string representing the configuration setting name, for logging +// invalid sub-board codes. If this is missing/null or empty, no logging will be done. +function splitAndVerifyMsgSubCodes(pSubCodeList, pSettingName) +{ + if (typeof(pSubCodeList) !== "string") + return []; + + var settingName = (typeof(pSettingName) === "string" ? pSettingName : ""); + + var subCodes = []; + var subCodesFromList = pSubCodeList.split(","); + for (var i = 0; i < subCodesFromList.length; ++i) + { + if (msg_area.sub.hasOwnProperty(subCodesFromList[i])) + { + if (!subCodes.indexOf(subCodesFromList[i]) > -1) + subCodes.push(subCodesFromList[i]); + } + else if (settingName.length > 0) + { + var errMsg = format("%s - For configuration setting %s, %s is an invalid sub-board code", GAME_NAME, settingName, subCodesFromList[i]); + log(LOG_ERR, errMsg); + } + } + //!msg_area.sub.hasOwnProperty( + return subCodes; +} // Displays the program logo // @@ -814,10 +964,19 @@ function levenshteinDistance(pStr1, pStr2) // Parameters: // pUserCurrentGameScore: The user's score for their current game // pLastSectionName: The name of the last trivia section the user played +// +// Return value: An object with the following properties: +// succeeded: Boolean: Whether or not saving the scores to the file succeeded +// userScoresObj: An object containing information on the user's scores function updateScoresFile(pUserCurrentGameScore, pLastSectionName) { + var retObj = { + succeeded: false, + userScoresObj: {} + }; + if (typeof(pUserCurrentGameScore) !== "number") - return false; + return retObj; var lastSectionName = (typeof(pLastSectionName) === "string" ? pLastSectionName : ""); @@ -858,8 +1017,7 @@ function updateScoresFile(pUserCurrentGameScore, pLastSectionName) if (typeof(scoresObj) !== "object") scoresObj = {}; - var scoresForUser = {}; // Will store just the current user's score information - + retObj.succeeded = true; // Add/update the user's score, and save the scores file try { @@ -939,10 +1097,11 @@ function updateScoresFile(pUserCurrentGameScore, pLastSectionName) scoresObj[user.alias].last_score = pUserCurrentGameScore; scoresObj[user.alias].last_trivia_category = lastSectionName; scoresObj[user.alias].last_time = currentTime; - scoresForUser = scoresObj[user.alias]; + retObj.userScoresObj = scoresObj[user.alias]; } catch (error) { + retObj.succeeded = false; console.print("* Line " + error.lineNumber + ": " + error); console.crlf(); log(LOG_ERR, GAME_NAME + " - Updating trivia score object: Line " + error.lineNumber + ": " + error); @@ -954,13 +1113,13 @@ function updateScoresFile(pUserCurrentGameScore, pLastSectionName) scoresFile.write(JSON.stringify(scoresObj)); scoresFile.close(); } + else + retObj.succeeded = false; // Delete the semaphore file file_remove(SCORES_SEMAPHORE_FILENAME); - // If there is a server configured, then send the user's score to the server too - if (gSettings.hasValidServerSettings()) - updateScoresOnServer(user.alias, scoresForUser); + return retObj; } // Updates user scores on the server (if there is one configured) @@ -968,35 +1127,54 @@ function updateScoresFile(pUserCurrentGameScore, pLastSectionName) // Parameters: // pUserNameForScores: The user's name as used for the scores // pUserScoreInfo: An object containing user scores, as created by updateScoresFile() +// +// Return value: Boolean: Whether or not the update was successful function updateScoresOnServer(pUserNameForScores, pUserScoreInfo) { // Make sure the settings have valid server settings and the user score info object is valid if (!gSettings.hasValidServerSettings()) - return; + return false; if (typeof(pUserNameForScores) !== "string" || pUserNameForScores.length == 0 || typeof(pUserScoreInfo) !== "object") - return; + return false; + var updateSuccessful = true; try { + // You could lock for each individual write like this: + // + // var JSONLocation = gSettings.remoteServer.BBSJSONLocation + ".bbs_name"; + // jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation, system.name, JSON_DB_LOCK_WRITE); + // + // You can also call lock() to lock the JSON location you want to use, do your reads & writes, and then + // unlock at the end. The code here locks on the BBS ID JSON location and does its writes, so that + // readGTTriviaScoresFromSubBoard() can also lock on the same location to do its writes when importing + // scores from the messagebase. + var jsonClient = new JSONClient(gSettings.remoteServer.server, gSettings.remoteServer.port); + jsonClient.lock(gSettings.remoteServer.gtTriviaScope, gSettings.remoteServer.BBSJSONLocation, JSON_DB_LOCK_WRITE); // Ensure the BBS name on the server has been set var JSONLocation = gSettings.remoteServer.BBSJSONLocation + ".bbs_name"; - jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation, system.name, JSON_DB_LOCK_WRITE); + jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation, system.name); // Write the scores on the server JSONLocation = gSettings.remoteServer.userScoresJSONLocationWithoutUsername + "." + pUserNameForScores; - jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation, pUserScoreInfo, JSON_DB_LOCK_WRITE); + jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation, pUserScoreInfo); // Write the client & version information in the user scores too var gameInfo = format("%s version %s (%s)", GAME_NAME, GAME_VERSION, GAME_VER_DATE); - jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation + ".game_client", gameInfo, JSON_DB_LOCK_WRITE); + JSONLocation += ".game_client"; + jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation, gameInfo); + // Now that we're done, unlock and disconnect + jsonClient.unlock(gSettings.remoteServer.gtTriviaScope, gSettings.remoteServer.BBSJSONLocation); jsonClient.disconnect(); } catch (error) { + updateSuccessful = false; console.print("* Line " + error.lineNumber + ": " + error); console.crlf(); log(LOG_ERR, GAME_NAME + " - Updating scores on server: Line " + error.lineNumber + ": " + error); bbs.log_str(GAME_NAME + " - Updating scores on server: Line " + error.lineNumber + ": " + error); } + return updateSuccessful; } // Shows the saved scores - First the locally saved scores, and then if there is a @@ -1125,7 +1303,7 @@ function showServerScores() showUserScoresArray(sortedScores, data.systems[BBS_ID].bbs_name); // If debugging is enabled, then also show the game_client property (game_client stores the name // & version of the game that wrote the user score data for this player) - if (gDebug) + if (gCmdLineArgs.debug) { if (data.systems[BBS_ID].user_scores[playerName].hasOwnProperty("game_client")) { @@ -1165,7 +1343,7 @@ function showUserScoresArray(pUserScoresArray, pBBSName) // Make the format string for printf() var scoreWidth = 6; var dateWidth = 10; - var categoryWidth = 15; + var categoryWidth = 25; //15; var nameWidth = 0; var formatStr = ""; if (console.screen_columns >= 80) @@ -1558,4 +1736,295 @@ function doSysopMenu() } } } -} \ No newline at end of file +} + +// Returns a BBS ID to use for JSON (the QWK ID if existing; otherwise, the BBS name with +// spaces converted to underscores) +function getBBSIDForJSON() +{ + var BBS_ID = ""; + if (system.qwk_id.length > 0) + BBS_ID = system.qwk_id; + else + BBS_ID = system.name.replace(/ /g, "_"); + return BBS_ID; +} + +// Posts all users' scores from the local scores file to a message sub-board +// +// Parameters: +// pSubCode: The internal code of the sub-board to post the scores to +function postGTTriviaScoresToSubBoard(pSubCode) +{ + if (typeof(pSubCode) !== "string" || !msg_area.sub.hasOwnProperty(pSubCode)) + return false; + + // Prepare the user scores for posting in the message sub-board + // JSON location: For the BBS name, use the QWK ID if available, but if not, use the system name and replace spaces + // with underscores (since spaces may cause issues in JSON property names) + var BBS_ID = getBBSIDForJSON(); + var scoresForThisBBS = {}; + scoresForThisBBS[BBS_ID] = {}; + scoresForThisBBS[BBS_ID].bbs_name = system.name; + scoresForThisBBS[BBS_ID].user_scores = {}; + + // Read the scores file to see if the user has an existing score in there already + var scoresFile = new File(SCORES_FILENAME); + if (file_exists(SCORES_FILENAME)) + { + if (scoresFile.open("r")) + { + var scoreFileArray = scoresFile.readAll(); + scoresFile.close(); + var scoreFileContents = ""; + for (var i = 0; i < scoreFileArray.length; ++i) + scoreFileContents += (scoreFileArray[i] + "\n"); + try + { + scoresForThisBBS[BBS_ID].user_scores = JSON.parse(scoreFileContents); + } + catch (error) + { + scoresForThisBBS[BBS_ID].user_scores = {}; + log(LOG_ERR, GAME_NAME + " - Loading scores: Line " + error.lineNumber + ": " + error); + bbs.log_str(GAME_NAME + " - Loading scores: Line " + error.lineNumber + ": " + error); + } + } + } + if (typeof(scoresForThisBBS[BBS_ID]) !== "object") + scoresForThisBBS[BBS_ID].user_scores = {}; + + if (Object.keys(scoresForThisBBS[BBS_ID].user_scores).length === 0) + return false; + + var postSuccessful = false; + var dataMsgbase = new MsgBase(pSubCode); + if (dataMsgbase.open()) + { + // Create the message header, and send the message. + var header = { + to: GAME_NAME, // "Good Time Trivia" + from: system.username(1), + from_ext: 1, + subject: system.name + //from_net_type: NET_NONE, + //to_net_type: NET_NONE + }; + /* + if ((dataMsgbase.settings & SUB_QNET) == SUB_QNET) + { + header.from_net_type = NET_QWK; + header.to_net_type = NET_QWK; + } + else if ((dataMsgbase.settings & SUB_PNET) == SUB_PNET) + { + header.from_net_type = NET_POSTLINK; + header.to_net_type = NET_POSTLINK; + } + else if ((dataMsgbase.settings & SUB_FIDO) == SUB_FIDO) + { + header.from_net_type = NET_FIDO; + header.to_net_type = NET_FIDO; + } + else if ((dataMsgbase.settings & SUB_INET) == SUB_INET) + { + header.from_net_type = NET_INTERNET; + header.to_net_type = NET_INTERNET; + } + */ + + //postSuccessful = dataMsgbase.save_msg(header, JSON.stringify(scoresForThisBBS)); + var message = lfexpand(JSON.stringify(scoresForThisBBS, null, 1)); + message += " --- " + GAME_NAME + " " + GAME_VERSION + " (" + GAME_VER_DATE + ")"; + postSuccessful = dataMsgbase.save_msg(header, message); + + dataMsgbase.close(); + } + return postSuccessful; +} + +// Reads trivia scores from a sub-board and posts on the host system (if configured) +// +// Parameters: +// pSubCode: An internal code of a sub-board to read the game scores from +// +// Return value: Boolean - Whether or not the score update succeeded +function readGTTriviaScoresFromSubBoard(pSubCode) +{ + if (typeof(pSubCode) !== "string" || !msg_area.sub.hasOwnProperty(pSubCode)) + return false; + + // For logging + var subBoardInfoStr = msg_area.sub[subCode].name + " - " + msg_area.sub[subCode].description; + log(LOG_INFO, format("%s - Reading score posts from sub-board %s (%s)", GAME_NAME, pSubCode, subBoardInfoStr)); + + // For posting to the local JSON server, get the configured JSON service port number + var localJSONServicePort = getJSONSvcPortFromServicesIni(); + if (localJSONServicePort <= 0) + { + log(LOG_ERR, format("%s - Local JSON service port is invalid (%d)", GAME_NAME, localJSONServicePort)); + return false; + } + + var scoreUpdateSucceeded = true; + var dataMsgbase = new MsgBase(pSubCode); + if (dataMsgbase.open()) + { + try + { + // Create the JSON Client object for updating the scores on the local JSON DB server. + // For each user score in the JSON object, if their last time is after the current last + // time in the server's JSON, then post the user's score. + var jsonClient = new JSONClient("127.0.0.1", localJSONServicePort); + + var to_crc = crc16_calc(GAME_NAME.toLowerCase()); + var index = dataMsgbase.get_index(); + for (var i = 0; index && i < index.length; i++) + { + var idx = index[i]; + if ((idx.attr & MSG_DELETE) == MSG_DELETE || idx.to != to_crc) + continue; + + var msgHdr = dataMsgbase.get_msg_header(true, idx.offset); + if (!msgHdr) + continue; + if (/*!msgHdr.from_net_type ||*/ msgHdr.to != GAME_NAME) + continue; + + var msgBody = dataMsgbase.get_msg_body(msgHdr, false, false, false); + if (msgBody == null || msgBody.length == 0) //if (!msgBody) + continue; + + log(LOG_INFO, "Scores message imported at " + strftime("%Y-%m-%d %H:%M:%S", msgHdr.when_imported_time)); + // Clean up the message body so that it only has JSON + var txtIdx = msgBody.indexOf(" --- " + GAME_NAME); + if (txtIdx > 0) + msgBody = msgBody.substr(0, txtIdx); + // Parse the JSON from the message, and then go through the BBSes and users + // in it. For any user scores that are more recent than what's on the server, + // post those to the server. + try + { + var scoresObjFromMsg = JSON.parse(msgBody); + // For each user score, if their last time is after the current last time in the + // server's JSON, then post the user's score. + for (var BBS_ID in scoresObjFromMsg) + { + // Lock on the BBS name location in the JSON, do the writes, and unlock when we're done + jsonClient.lock(gSettings.remoteServer.gtTriviaScope, gSettings.remoteServer.BBSJSONLocation, JSON_DB_LOCK_WRITE); + // Ensure the BBS name on the server has been set + var JSONLocation = gSettings.remoteServer.scoresJSONLocation + ".systems." + BBS_ID + ".bbs_name"; + jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation, system.name); + for (var userID in scoresObjFromMsg[BBS_ID].user_scores) + { + // For logging + var msgLastTimeFormatted = strftime("%Y-%m-%d %H:%M:%S", scoresObjFromMsg[BBS_ID].user_scores[userID].last_time); + var serverLastTimeFormatted = ""; + + // Read the current user's scores from the server and compare the user's last_time + // from the message in the sub-board with the one from the server, and only update + // if newer. + JSONLocation = gSettings.remoteServer.scoresJSONLocation + ".systems." + BBS_ID + ".user_scores." + userID; + var serverUserScoreData = jsonClient.read(gSettings.remoteServer.gtTriviaScope, JSONLocation); + var postUserScoresToServer = false; + if (typeof(serverUserScoreData) === "object") + { + postUserScoresToServer = (scoresObjFromMsg[BBS_ID].user_scores[userID].last_time > serverUserScoreData.last_time); + serverLastTimeFormatted = strftime("%Y-%m-%d %H:%M:%S", serverUserScoreData.last_time) + } + else + { + postUserScoresToServer = true; + serverLastTimeFormatted = "N/A"; + } + + // Log the user, BBS, and date of the scores seen + var logMsg = format("%s - Saw scores for %s on %s; in message: %s, on server: %s; will update server scores: %s", + GAME_NAME, userID, scoresObjFromMsg[BBS_ID].bbs_name, msgLastTimeFormatted, serverLastTimeFormatted, + postUserScoresToServer); + log(LOG_INFO, logMsg); + + // If the scores from the message are newer, write the scores on the server + if (postUserScoresToServer) + { + JSONLocation = gSettings.remoteServer.scoresJSONLocation + ".systems." + BBS_ID + ".user_scores." + userID; + jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation, scoresObjFromMsg[BBS_ID].user_scores[userID]); + } + // Now that we've written the user scores, unlock this BBS in the JSON + jsonClient.unlock(gSettings.remoteServer.gtTriviaScope, gSettings.remoteServer.BBSJSONLocation); + } + } + } + catch (error) + { + scoreUpdateSucceeded = false; + console.print("* Line " + error.lineNumber + ": " + error); + console.crlf(); + log(LOG_ERR, GAME_NAME + " - Updating scores on server: Line " + error.lineNumber + ": " + error); + bbs.log_str(GAME_NAME + " - Updating scores on server: Line " + error.lineNumber + ": " + error); + } + } + + jsonClient.disconnect(); + } + catch (error) + { + scoreUpdateSucceeded = false; + console.print("* Line " + error.lineNumber + ": " + error); + console.crlf(); + log(LOG_ERR, GAME_NAME + " - Connecting to JSON DB server (for scores update): Line " + error.lineNumber + ": " + error); + bbs.log_str(GAME_NAME + " - Connecting to JSON DB server (for scores update): Line " + error.lineNumber + ": " + error); + } + + dataMsgbase.close(); + } + else + { + var errMsg = format("%s - Unable to open sub-board %s (%s)", GAME_NAME, pSubCode, subBoardInfoStr); + log(LOG_ERR, errMsg); + } + + log(LOG_INFO, format("%s - End of reading score posts from sub-board %s (%s). All succeeded: %s", GAME_NAME, pSubCode, + subBoardInfoStr, scoreUpdateSucceeded)); + + return scoreUpdateSucceeded; +} + +// Parses command-line arguments. Returns an object with settings/actions specified. +// +// Parameters: +// argv: The array of command-line arguments +// +// Return value: An object with the following properties: +// debug: Boolean: Whether or not to enable debugging +// postScoresToSubBoard: Boolean: Whether or not to post scores in the configured sub-board. If this is +// enabled, scores are to be posted and then the script should exit. +// readScoresFromSubBoard: Boolean: Whether or not to read scores from the configured sub-board. +// If this is enabled, scores are to be posted and then the script should exit. +function parseCmdLineArgs(argv) +{ + var retObj = { + debug: false, + postScoresToSubBoard: false, + readScoresFromSubBoard: false + }; + + if (!Array.isArray(argv)) + return retObj; + + var postScoresToSubOpt = "POST_SCORES_TO_SUBBOARD"; + var readScoresFromSubOpt = "READ_SCORES_FROM_SUBBOARD"; + for (var i = 0; i < argv.length; ++i) + { + var argUpper = argv[i].toUpperCase(); + if (argUpper == "DEBUG" || argUpper == "-DEBUG" || argUpper == "--DEBUG") + retObj.debug = true; + else if (argUpper == postScoresToSubOpt || argUpper == "-" + postScoresToSubOpt || argUpper == "--" + postScoresToSubOpt) + retObj.postScoresToSubBoard = true; + else if (argUpper == readScoresFromSubOpt || argUpper == "-" + readScoresFromSubOpt || argUpper == "--" + readScoresFromSubOpt) + retObj.readScoresFromSubBoard = true; + } + + return retObj; +} + diff --git a/xtrn/gttrivia/qa/converter.js b/xtrn/gttrivia/qa/converter.js index 8193ffa004d3b581a375bec2f42a0a359e8255d4..f418f9d74a02996b89070c0c7e2332727239a87c 100644 --- a/xtrn/gttrivia/qa/converter.js +++ b/xtrn/gttrivia/qa/converter.js @@ -1,15 +1,36 @@ require("sbbsdefs.js", "K_NOCRLF"); -var inputFilename = "/mnt/data/SharedMedia/triviaQuestions/music_and_movies.txt"; -var outputFilename = inputFilename + "-converted.txt"; +var opts = parseCmdLine(argv); print(""); -print("Converting " + inputFilename); + +if (opts.inputFilename.length == 0) +{ + print("No input filename was specified."); + print(""); + exit(1); +} +if (!file_exists(opts.inputFilename)) +{ + print("Specified file does not exist:"); + print(opts.inputFilename); + print(""); + exit(2); +} + +print("Input filename:" + opts.inputFilename + ":"); +print(""); + + +var outputFilename = opts.inputFilename + "-converted.txt"; + +print(""); +print("Converting " + opts.inputFilename); print("Output: " + outputFilename); print(""); -var inFile = new File(inputFilename); +var inFile = new File(opts.inputFilename); var outFile = new File(outputFilename); if (inFile.open("r")) { @@ -112,7 +133,10 @@ if (inFile.open("r")) inFile.close(); } else - print("* Failed to open " + inputFilename + " for reading!"); +{ + print("* Failed to open " + opts.inputFilename + " for reading!"); + exit(3); +} @@ -127,4 +151,35 @@ function QA(pQuestion, pAnswer, pNumPoints) this.question = pQuestion; this.answer = pAnswer; this.numPoints = pNumPoints; +} + +// Parses command line options +function parseCmdLine(argv) +{ + var retObj = { + inputFilename: "" + }; + + if (!Array.isArray(argv)) + return retObj; + + for (var i = 0; i < argv.length; ++i) + { + if (argv[i].length == 0) continue; + if (argv[i].charAt(0) == "-") + { + if (i >= argv.length - 1) continue; + var paramNameUpper = argv[i].substr(1).toUpperCase(); + if (paramNameUpper == "INPUTFILENAME" || paramNameUpper == "INPUT_FILENAME") + retObj.inputFilename = argv[i+1]; + ++i; // To avoid analyzing the next parameter, since the next one is the value for this one + } + else + { + if (i == 0) + retObj.inputFilename = argv[i]; + } + } + + return retObj; } \ No newline at end of file diff --git a/xtrn/gttrivia/qa/general.qa b/xtrn/gttrivia/qa/general.qa index ad94809b98ee7db459681d2136f8a7fc3b992d01..db0eb50f0b4a682d43fe8fd9266a80be810774ce 100644 --- a/xtrn/gttrivia/qa/general.qa +++ b/xtrn/gttrivia/qa/general.qa @@ -174,7 +174,7 @@ What is often seen as the smallest unit of memory? Kilobyte 10 -Which planet is the hottest in the solar system? +Which planet is the hottest in Earth's solar system? Venus 10 @@ -614,10 +614,6 @@ What was the first Disney animated film based on the life of a real person? Pocahontas 10 -What character did Michael J. Fox play in 'Back to the Future'? -Marty McFly -10 - What was the predecessor to the United Nations? League of Nations 10 @@ -842,10 +838,6 @@ What grows from an acorn? Oak Tree 10 -What prison film starring Tim Robbins was based on a story by Stephen King? -The Shawshank Redemption -10 - Which U.S. state has "Garden State" as its nickname? New Jersey 10 @@ -966,10 +958,6 @@ Rihanna banned fans from bringing what items to her U.K. concerts in 2008? Umbrellas 10 -Who created the alien rock superstar Ziggy Stardust? -David Bowie -10 - Which young girl helped drive the English from French soil in the 15th century? Joan of Arc 10 @@ -979,7 +967,7 @@ Theodore Roosevelt 10 What is the biggest supermarket chain in the U.S.? -Kroger Co. +Kroger 10 On every continent there is a city named what? @@ -1511,7 +1499,7 @@ Sense and Sensibility 10 Which two countries have the longest shared international border? -Canada and the U.S. +Canada and the US 10 What city hosted the 2014 Winter Olympics? @@ -1535,7 +1523,7 @@ Three 10 How many bones do sharks have? -Zero! +0 10 What is the deadliest mammal? @@ -1612,7 +1600,7 @@ Des Moines What is the most commonly spoken language in Brazil? Portuguese -10 +5 In what country do more than half of people believe in elves? Iceland @@ -1706,8 +1694,8 @@ Where is Harvard University located? Cambridge, Massachusetts 10 -Which marine animals hold hands in their sleep to prevent drifting apart? -Sea otters, and my system is unable to process that level of cuteness. +Which marine animal species hold hands in their sleep to prevent drifting apart? +Sea otter 10 What famous document begins: "When in the course of human events..."? @@ -1853,3 +1841,183 @@ Washington Monument Where is Mount Rushmore (City, State)? Keystone, South Dakota 5 + +What country has the highest life expectancy? +Hong Kong +10 + +Where would you be if you were standing on the Spanish Steps? +Rome +10 + +Which language has the more native speakers: English or Spanish? +Spanish +10 + +What is the most common surname in the United States? +Smith +10 + +What disease commonly spread on pirate ships? +Scurvy +10 + +Who was the Ancient Greek God of the Sun? +Apollo +10 + +What was the name of the crime boss who was head of the feared Chicago Outfit? +Al Capone +10 + +What year was the United Nations established? +1945 +10 + +Who has won the most total Academy Awards? +Walt Disney +10 + +What artist has the most streams on Spotify? +Drake +10 + +How many minutes are in a full week? +10080 +10 + +What car manufacturer had the highest revenue in 2020? +Volkswagen +10 + +How many elements are in the periodic table? +118 +10 + +What company was originally called "Cadabra"? +Amazon +10 + +How many faces does a Dodecahedron have? +12 +10 + +Queen guitarist Brian May is also an expert in what scientific field? +Astrophysics +10 + +Aureolin is a shade of what color? +Yellow +10 + +How many ghosts chase Pac-Man at the start of each game? +4 +10 + +What Renaissance artist is buried in Rome's Pantheon? +Raphael +10 + +What shoe brand makes the "Mexico 66"? +Onitsuka Tiger +15 + +What game studio makes the Red Dead Redemption series? +Rockstar Games +10 + +Who was the last Tsar of Russia? +Nicholas II +10 + +What country drinks the most coffee per capita? +Finland +10 + +What is the 4th letter of the Greek alphabet? +Delta +5 + +What sports car company manufactures the 911? +Porsche +2 + +What city is known as "The Eternal City"? +Rome +10 + +The first person to reach the South Pole was Roald Amundsen. Where was he from? +Norway +10 + +Who discovered that the earth revolves around the sun (last name)? +Copernicus +10 + +What company was initially known as "Blue Ribbon Sports"? +Nike +10 + +What art form is described as "decorative handwriting or handwritten lettering"? +Calligraphy +10 + +Which planet in Earth's solar system has the most moons? +Saturn +10 + +What country has won the most World Cups? +Brazil +10 + +Kratos is the main character of what video game series? +God of War +10 + +In what country would you find Mount Kilimanjaro? +Tanzania +10 + +A group of pandas is known as a what? +Embarrassment +10 + +What European country experienced the highest rate of population decline from 2015 - 2020? +Lithuania +10 + +How many bones do we have in an ear? +3 +10 + +Who famously crossed the Alps with elephants on the way to war with the Romans? +Hannibal +10 + +True or False: Halloween originated as an ancient Irish festival. +True +5 + +What Netflix show had the most streaming views in 2021? +Squid Game +10 + +What software company is headquartered in Redmond, Washington? +Microsoft +2 + +What is the largest Spanish-speaking city in the world? +Mexico City +10 + +What is the world's fastest bird? +Peregrine Falcon +10 + +In what country is the Chernobyl nuclear plant located? +Ukraine +10 + +The Parthenon Marbles are controversially located in what museum? +British Museum +10 diff --git a/xtrn/gttrivia/qa/music_movies_and_entertainment.qa b/xtrn/gttrivia/qa/music_movies_and_entertainment.qa index 5755239d00b198c9934915fbea53bc41cdaade4c..d4662c6ce6a2e9ae3921d34c1cf5cb9b3dc9b5a4 100644 --- a/xtrn/gttrivia/qa/music_movies_and_entertainment.qa +++ b/xtrn/gttrivia/qa/music_movies_and_entertainment.qa @@ -510,7 +510,7 @@ What prominent American director won an Oscar for helming Forrest Gump? Robert Zemeckis 10 -Three of Jim Carrey's blockbusters�The Mask, Dumb and Dumber and Ace Ventura: Pet Detective�were all released in what year? +Three of Jim Carrey's blockbusters The Mask, Dumb and Dumber and Ace Ventura: Pet Detective were all released in what year? 1994 10 @@ -630,7 +630,7 @@ What was the first pandemic era movie to gross over $1 billion at the box office Spider-Man: No Way Home 10 -Who is the only actor to appear in Robert Wise�s 1961 West Side Story movie and the 2021 remake? +Who is the only actor to appear in Robert Wise's 1961 West Side Story movie and the 2021 remake? Rita Moreno 10 @@ -642,7 +642,7 @@ What internationally esteemed Malaysian actress has starred in a Bond film, Crou Michelle Yeoh 10 -Who won his second Best Actor Oscar in 2021, in the ceremony�s biggest upset? +Who won his second Best Actor Oscar in 2021, in the ceremony's biggest upset? Anthony Hopkins 10 @@ -650,11 +650,11 @@ What indie horror movie boogeyman became an unexpected LGBTQ+ icon of the 21st c The Babadook 10 -What kind of bug is on the back of Ryan Gosling�s silk jacket in Drive? +What kind of bug is on the back of Ryan Gosling's silk jacket in Drive? Scorpion 10 -What actress was the queen of 1970s �Blaxploitation� cinema? +What actress was the queen of 1970s "Blaxploitation" cinema? Pam Grier 10 @@ -666,15 +666,15 @@ What is the second (and last) fantasy movie to win Best Picture at the Oscars? The Shape of Water 10 -What famous heartthrob is unrecognizable under layers of makeup as The Penguin in 2021�s The Batman? +What famous heartthrob is unrecognizable under layers of makeup as The Penguin in 2021s The Batman? Colin Farrell 10 -In Clueless, what character said, �You�re a virgin who can�t drive?� +In Clueless, what character said, "You're a virgin who can't drive"? Tai 10 -What is the name of the love interest whose �hair looks sexy pushed back� in Mean Girls? +What is the name of the love interest whose hair looks sexy pushed back in Mean Girls? Aaron Samuels 10 @@ -682,7 +682,7 @@ What highly acclaimed Richard Linklater drama was filmed over and produced over Boyhood 10 -What is the name of Humperdinck�s kingdom in The Princess Bride? +What is the name of Humperdinck's kingdom in The Princess Bride? Florin 10 @@ -704,7 +704,7 @@ What critically maligned 2004 superhero film co-stars Sharon Stone as an evil co Catwoman 10 -Which actress replaced Rachel Weisz as Evelyn O�Connor in The Mummy: Tomb of the Dragon Emperor? +Which actress replaced Rachel Weisz as Evelyn O'Connor in The Mummy: Tomb of the Dragon Emperor? Maria Bello 10 @@ -716,11 +716,11 @@ Who plays Duncan Idaho in Dune (2021)? Jason Momoa 10 -Dakota Johnson dropped out of Olivia Wilde�s sophomore feature Don�t Worry Darling to appear in what critically acclaimed 2021 drama? +Dakota Johnson dropped out of Olivia Wilde's sophomore feature Don't Worry Darling to appear in what critically acclaimed 2021 drama? The Lost Daughter 10 -What legendary pop star judges a fashion �walk-off� between Ben Stiller and Owen Wilson in Zoolander? +What legendary pop star judges a fashion "walk-off" between Ben Stiller and Owen Wilson in Zoolander? David Bowie 10 @@ -1182,3 +1182,27 @@ Lady Gaga What type of music has been shown to help plants grow better and faster? Classical 10 + +What character has both Robert Downey Jr. and Benedict Cumberbatch played? +Sherlock Holmes +10 + +What prison film starring Tim Robbins was based on a story by Stephen King? +The Shawshank Redemption +10 + +What is the highest-rated film on IMDb as of January 1st, 2022? +The Shawshank Redemption +10 + +Which grammy-nominated New York rapper died in April of 2021? +DMX +10 + +Who created the alien rock superstar Ziggy Stardust? +David Bowie +10 + +What character did Michael J. Fox play in 'Back to the Future'? +Marty McFly +10 diff --git a/xtrn/gttrivia/readme.txt b/xtrn/gttrivia/readme.txt index ee5b49f53f8d612c0c456ac96640eb52d7e79a79..26451b122f518306540d8cac07cf22ade0c68e77 100644 --- a/xtrn/gttrivia/readme.txt +++ b/xtrn/gttrivia/readme.txt @@ -1,11 +1,11 @@ Good Time Trivia - Version 1.01 - Release date: 2022-11-25 + Version 1.02 + Release date: 2022-12-08 by Eric Oulashin - AKA Nightfox + AKA Nightfox Sysop of Digital Distortion BBS internet address: digitaldistortionbbs.com Alternate address: digdist.bbsindex.com @@ -57,10 +57,6 @@ scores. This will delete the scores.json file. This is currently a single-player game, but multiple users on different nodes can play it simultaneously. -Currently, this trivia game is local to the current BBS only. In the future, -I think it would be good to add a feature for networked/inter-BBS games. - - Answer matching: When a user answers a question, the game can allow non-exact answer matching in some circumstances, to account for typos and spelling mistakes. If the answer is a single word up to 12 characters, the game will @@ -74,6 +70,48 @@ For more information on Levenshtein distances: https://www.cuelogic.com/blog/the-levenshtein-algorithm +Shared game scores on a server BBS +---------------------------------- +The game can be configured to share its local game scores, to be stored on a +remote BBS. The scores can be shared either by directly contacting the remote +BBS (via the JSON DB service) and/or by posting the local game scores in one or +more (networked) message sub-boards (both configurable in gttrivia.ini). The +option to post scores in a message sub-board is a backup in case the remote BBS +is down and cannot be contacted directly. You may also opt to just have Good +Time Trivia post scores in a message sub-board and not configure a remote BBS +server. + +Digital Distortion (BBS) is set up to host scores for this game, and the +default settings in the REMOTE_SERVER section of gttrivia.ini point to +Digital Distortion, to use the direct-connect JSON DB method to update remote +scores. + +Digital Distortion is also set up to look for scores for this game in the +Dove-Net Synchronet Data message area, as well as FSXNet Inter-BBS Data. + +If your BBS has the Dove-Net message sub-boards, you could configure Good Time +Trivia to post scores in the Dove-Net Synchronet Data sub-board (using the +internal code configured on your BBS). If there are other BBSes besides Digital +Distortion hosting scores, the host BBS would also need to have Dove-Net and +have an event configured to periodically run Good Time Trivia to poll Dove-Net +Synchronet Data and read the game scores. + +By default, the game is set up to post scores to Digital Distortion, so you may +choose to view the scores there or host scores yourself. See section 4 +(Configuration file) for more information on the options to send scores to a +remote BBS. + +When configured to send user scores to a remote BBS and to write scores to a +message sub-board, that happens whenever a user stops playing a game. The logic +for sending the scores is as follows: +- Try to post the scores to the remote BBS +- If that fails, then post the scores in the configured message sub-board, if + there is one configured. +That logic should ensure that the scores get posted. The remote BBS should then +be configured to have their JSON-DB service running and/or have Good Time Trivia +periodically scan the same (networked) message sub-board to read the scores. + + 3. Installation & Setup ======================= Aside from readme.txt and revision_history.txt, Good Time Trivia is comprised @@ -178,6 +216,20 @@ scripts): ╚══════════════════════════════════════════════════════════╝ +That is all you need to do to get Good Time Trivia running. + + +Optional +-------- +As mentioned in the introduction, server scores can be sent to a remote BBS so +that scores from players on multiple BBSes can be viewed. Normally, if Good Time +Trivia is unable to connect to the remote BBS directly, it will fall back to +posting scores in a networked message sub-board (if configured) as a backup +option. That happens automatically after a user finishes playing a game, but +Good Time Trivia can also (optionally) be configured to post game scores in a +sub-board as a timed event by running gttrivia.js with the +-post_scores_to_subboard command-line argument. + 4. Configuration File ===================== @@ -200,6 +252,27 @@ numTriesPerQuestion The maximum number of times a user is maxNumPlayerScoresToDisplay The maximum number of player scores to display in the list of high scores +scoresMsgSubBoardsForPosting This can be used to specify a comma-separated + list of internal sub-board codes for the game + to post user scores in, if you want your user + scores to be shared. In case the remote BBS + in the REMOTE_SERVER setting can't be reached + or there is no remote BBS configured, the game + will post scores in the sub-board(s) specified + here. You can specify more than one sub-board + code in case there are multiple BBSes that + host scores for this game. + Note that this setting is empty by default, + because internal sub-board codes are probably + different on each BBS. Digital Distortion is + set up to host scores for this game, and if + your BBS is connected to Dove-Net, it is + recommended to use your BBS's internal code + code for the Dove-Net Synchronet Data + sub-board here. FSXNet also has an InterBBS + Data area that might be used for conveying + game scores to a host BBS. + [COLORS] section ---------------- In this section, the color codes are simply specified by a string of color @@ -270,6 +343,19 @@ deleteScoresOlderThanDays The number of days to keep old player scores. The background service will remove player scores older than this number of days. +scoresMsgSubBoardsForReading This can be used to specify a comma-separated + list of internal sub-board codes for the game + to read user scores from, for client BBSes + that post their game scores there. See section + 5 (Optional: Configuring your BBS to host + player scores) for information on adding an + event in SCFG to periodically read scores + from the sub-board(s) if you want to host game + scores on your BBS. By default, the game is + set up to post scores to Digital Distortion, + so you may choose to view the scores there or + host scores yourself. + 5. Optional: Configuring your BBS to host player scores ======================================================= @@ -297,3 +383,51 @@ dir=../xtrn/gttrivia/server/ It would then probably be a good idea to stop and re-start your Synchronet BBS in order for it to recognize that you have a new JSON database configured. + + +Periodic reading of scores from a message sub-board +--------------------------------------------------- +BBSes with the game installed could configure their game to post scores in a +(networked) message sub-board. If you decide to host game scores on your BBS, +it's also a good idea to configure your BBS to read scores from a networked +message sub-board (which would need to be the same one that BBSes post in). For +instance, you could set it up to read scores from Dove-Net Synchronet Data, +FSXNet InterBBS Data, or perhaps another networked sub-board that is meant to +carry BBS data. + +To specify which sub-board(s) to read scores from, you can specify those as a +comma-separated list of internal sub-board codes using the +scoresMsgSubBoardsForReading setting under the [SERVER] section of +gttrivia.ini. + +Then, in SCFG, you will need to configure an event to run periodically to run +gttrivia.js to read those message sub-boards for game scores. You can do that +in SCFG > External Programs > Timed Events. Set it up to run gttrivia.js with +the command-line parameter -read_scores_from_subboard +Add an event as follows (internal code can be what you want; GTRIVSIM is short +for Good Time Trivia scores import): + +╔═══════════════════════════════════════════════════════════════[< >]╗ +║ GTRIVSIM Timed Event ║ +╠════════════════════════════════════════════════════════════════════╣ +║ │Internal Code GTRIVSIM ║ +║ │Start-up Directory ../xtrn/gttrivia ║ +║ │Command Line ?gttrivia.js -read_scores_from_subboard +║ │Enabled Yes ║ +║ │Execution Node 12 ║ +║ │Execution Months Any ║ +║ │Execution Days of Month Any ║ +║ │Execution Days of Week All ║ +║ │Execution Frequency 96 times a day ║ +║ │Requires Exclusive Execution No ║ +║ │Force Users Off-line For Event No ║ +║ │Native Executable/Script Yes ║ +║ │Use Shell or New Context No ║ +║ │Background Execution No ║ +║ │Always Run After Init/Re-init No ║ +║ │Error Log Level Error ║ +╚════════════════════════════════════════════════════════════════════╝ + +The number of times per day is up to you, but it would probably be beneficial +for this to happen frequently so that scores are kept up to date. In the above +example, 96 times per day would mean it would run every 15 minutes. \ No newline at end of file