diff --git a/xtrn/gttrivia/gttrivia.ini b/xtrn/gttrivia/gttrivia.ini index fc2da28edea9d3f6ebd17a11c9a836678c025fd9..9adeff4e5d99842039bcd1ecb07c0334f50a700e 100644 --- a/xtrn/gttrivia/gttrivia.ini +++ b/xtrn/gttrivia/gttrivia.ini @@ -25,4 +25,11 @@ clue=GH answerAfterIncorrect=G [CATEGORY_ARS] -dirty_minds=AGE 18 \ No newline at end of file +dirty_minds=AGE 18 + +[REMOTE_SERVER] +server=digitaldistortionbbs.com +port=10088 + +[SERVER] +deleteScoresOlderThanDays=182 diff --git a/xtrn/gttrivia/gttrivia.js b/xtrn/gttrivia/gttrivia.js index 235b990790a71910b0fe30066e80c2a388f3c8ac..1921b51d0acf0fb2e4364f76b36d625aec0f8ba4 100644 --- a/xtrn/gttrivia/gttrivia.js +++ b/xtrn/gttrivia/gttrivia.js @@ -5,8 +5,15 @@ are saved to a file. Date Author Description 2022-11-18 Eric Oulashin Version 1.00 +2022-11-25 Eric Oulashin Version 1.01 + Added the ability to store & retrieve scores to/from a server, + so that scores from multiple BBSes can be displayed. There are + 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. */ + "use strict"; @@ -30,16 +37,11 @@ if (system.version_num < 31500) } // Version information -var GAME_VERSION = "1.00"; -var GAME_VER_DATE = "2022-11-18"; - -// Load required .js libraries -var requireFnExists = (typeof(require) === "function"); -if (requireFnExists) - require("sbbsdefs.js", "P_NONE"); -else - load("sbbsdefs.js"); - +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"; // Determine the location of this script (its startup directory). // The code for figuring this out is a trick that was created by Deuce, @@ -51,6 +53,22 @@ try { throw dig.dist(dist); } catch(e) { gThisScriptFilename = file_getname(e.fileName); } +// Load required .js libraries +var requireFnExists = (typeof(require) === "function"); +if (requireFnExists) +{ + require("sbbsdefs.js", "P_NONE"); + require("json-client.js", "JSONClient"); + require(gStartupPath + "lib.js", "getJSONSvcPortFromServicesIni"); +} +else +{ + load("sbbsdefs.js"); + load("json-client.js"); + load(gStartupPath + "lib.js"); +} + + // Characters for display // Box-drawing/border characters: Single-line var UPPER_LEFT_SINGLE = "\xDA"; @@ -109,13 +127,26 @@ var ACTION_PLAY = 0; var ACTION_SHOW_HELP_SCREEN = 1; var ACTION_SHOW_HIGH_SCORES = 2; var ACTION_QUIT = 3; -var ACTION_SYSOP_CLEAR_SCORES = 4; +var ACTION_SYSOP_MENU = 4; + + +// Values for JSON DB reading and writing +var JSON_DB_LOCK_READ = 1; +var JSON_DB_LOCK_WRITE = 2; +var JSON_DB_LOCK_UNLOCK = -1; // Upon exit for any reason, make sure the scores semaphore filename doesn't exist so future instances don't get frozen //js.on_exit("if (file_exists(\"" + SCORES_SEMAPHORE_FILENAME + "\")) file_remove(\"" + SCORES_SEMAPHORE_FILENAME + "\");"); + +// Enable debugging if the first command-line parameter is -debug +var gDebug = false; +if (argv.length > 0) + gDebug = (argv[0].toUpperCase() == "-DEBUG"); + + // Display the program logo displayProgramLogo(true, false); @@ -139,26 +170,11 @@ while (continueOn) showHelpScreen(); break; case ACTION_SHOW_HIGH_SCORES: - showScores(false); + showScores(); break; - case ACTION_SYSOP_CLEAR_SCORES: + case ACTION_SYSOP_MENU: if (user.is_sysop) - { - console.print("\x01n"); - console.crlf(); - if (file_exists(SCORES_FILENAME)) - { - if (!console.noyes("\x01y\x01hAre you SURE you want to clear the scores\x01b")) - { - file_remove(SCORES_FILENAME); - console.print("\x01n\x01c\x01hThe score file has been deleted."); - } - } - else - console.print("\x01n\x01c\x01hThere is no score file yet."); - console.print("\x01n"); - console.crlf(); - } + doSysopMenu(); break; case ACTION_QUIT: default: @@ -367,6 +383,7 @@ function loadSettings(pStartupPath) settings.behavior = iniFile.iniGetObject("BEHAVIOR"); settings.colors = iniFile.iniGetObject("COLORS"); settings.category_ars = iniFile.iniGetObject("CATEGORY_ARS"); + settings.remoteServer = iniFile.iniGetObject("REMOTE_SERVER"); // Ensure the actual expected setting name & color names exist in the settings if (typeof(settings.behavior) !== "object") @@ -375,6 +392,8 @@ function loadSettings(pStartupPath) settings.colors = {}; if (typeof(settings.category_ars) !== "object") settings.category_ars = {}; + if (typeof(settings.remoteServer) !== "object") + settings.remoteServer = {}; if (typeof(settings.behavior.numQuestionsPerPlay) !== "number") settings.behavior.numQuestionsPerPlay = 10; @@ -429,6 +448,8 @@ function loadSettings(pStartupPath) settings.behavior.numTriesPerQuestion = 3; if (settings.behavior.maxNumPlayerScoresToDisplay <= 0) settings.behavior.maxNumPlayerScoresToDisplay = 10; + if (!/^[0-9]+$/.test(settings.remoteServer.port)) + settings.remoteServer.port = 0; // No need to do this: // For each color, replace any instances of specifying the control character in substWord with the actual control character @@ -437,6 +458,25 @@ function loadSettings(pStartupPath) iniFile.close(); } + + // Other settings - Not read from the configuration file, but things we want to use in multiple places + // in this script + // JSON scope and JSON location for scores on the server (if a server is to be used) + 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, "_"); + settings.remoteServer.scoresJSONLocation = "SCORES"; + settings.remoteServer.BBSJSONLocation = settings.remoteServer.scoresJSONLocation + ".systems." + BBS_ID; + settings.remoteServer.userScoresJSONLocationWithoutUsername = settings.remoteServer.BBSJSONLocation + ".user_scores"; + settings.hasValidServerSettings = function() { + return (this.remoteServer.hasOwnProperty("server") && this.remoteServer.hasOwnProperty("port") && this.remoteServer.server.length > 0); + }; + return settings; } // For configuration files, this function returns a fully-pathed filename. @@ -487,6 +527,7 @@ function displayProgramLogo(pClearScreenFirst, pPauseAfter) // ACTION_PLAY // ACTION_SHOW_HIGH_SCORES // ACTION_SHOW_HELP_SCREEN +// ACTION_SYSOP_MENU // ACTION_QUIT function doMainMenu() { @@ -500,7 +541,7 @@ function doMainMenu() console.print("\x01c2\x01y\x01h) \x01bHelp \x01n"); console.print("\x01c3\x01y\x01h) \x01bShow high scores \x01n"); if (user.is_sysop) - console.print("\x01c9\x01y\x01h) \x01bClear high scores \x01n"); // Option 9 + console.print("\x01c9\x01y\x01h) \x01bSysop menu \x01n"); // Option 9 console.print("\x01cQ\x01y\x01h)\x01buit"); console.crlf(); console.print("\x01n"); @@ -515,7 +556,7 @@ function doMainMenu() if (user.is_sysop) validKeys += "9"; // Clear scores var userChoice = console.getkeys(validKeys, -1, K_UPPER).toString(); - console.print("\x01n"); + console.attributes = "N"; if (userChoice.length == 0 || userChoice == "Q") menuAction = ACTION_QUIT; else if (userChoice == "1") @@ -525,7 +566,7 @@ function doMainMenu() else if (userChoice == "3") menuAction = ACTION_SHOW_HIGH_SCORES; else if (userChoice == "9" && user.is_sysop) - menuAction = ACTION_SYSOP_CLEAR_SCORES; + menuAction = ACTION_SYSOP_MENU; return menuAction; } @@ -809,26 +850,30 @@ function updateScoresFile(pUserCurrentGameScore, pLastSectionName) } catch (error) { - log(LOG_ERR, GAME_NAME + " - Loading scores: " + error); - bbs.log_str(GAME_NAME + " - Loading scores: " + error); + log(LOG_ERR, GAME_NAME + " - Loading scores: Line " + error.lineNumber + ": " + error); + bbs.log_str(GAME_NAME + " - Loading scores: Line " + error.lineNumber + ": " + error); } } } if (typeof(scoresObj) !== "object") scoresObj = {}; + var scoresForUser = {}; // Will store just the current user's score information + // Add/update the user's score, and save the scores file try { // Score object example (note: last_time is a UNIX time): // Username: // category_stats: - // category_1: + // 0: + // category_name: General // last_time: 166000 - // total_score: 20 - // category_2: + // last_score: 20 + // 1: + // category_name: Misc // last_time: 146000 - // total_score: 80 + // last_score: 80 // total_score: 100 // last_score: 20 // last_trivia_category: category_1 @@ -838,17 +883,54 @@ function updateScoresFile(pUserCurrentGameScore, pLastSectionName) // Ensure the score object has an object for the current user if (!scoresObj.hasOwnProperty(user.alias)) scoresObj[user.alias] = {}; - // Ensure the user object in the scores object has a category_total_scores object + // Ensure the user object in the scores object has a category_stats array if (!scoresObj[user.alias].hasOwnProperty(userCategoryStatsPropName)) - scoresObj[user.alias][userCategoryStatsPropName] = {}; - // Add/update the user's score for the category in their category_total_scores object - if (!scoresObj[user.alias][userCategoryStatsPropName].hasOwnProperty(pLastSectionName)) - scoresObj[user.alias][userCategoryStatsPropName][pLastSectionName] = {}; - scoresObj[user.alias][userCategoryStatsPropName][pLastSectionName].last_time = currentTime; - if (!scoresObj[user.alias][userCategoryStatsPropName][pLastSectionName].hasOwnProperty("total_score")) - scoresObj[user.alias][userCategoryStatsPropName][pLastSectionName].total_score = pUserCurrentGameScore; + scoresObj[user.alias][userCategoryStatsPropName] = []; else - scoresObj[user.alias][userCategoryStatsPropName][pLastSectionName].total_score += pUserCurrentGameScore; + { + // In case version 1.00 of this door has been run before, the category stats would be an object + // with category names as the properties.. Convert that to an array + if (!Array.isArray(scoresObj[user.alias][userCategoryStatsPropName])) + { + var userCatStats = []; + for (var categoryName in scoresObj[user.alias][userCategoryStatsPropName]) + { + var statsObj = { + category_name: categoryName, + last_score: 0, + last_time: scoresObj[user.alias][userCategoryStatsPropName].last_time + }; + // Version 1.00 has a total_score in the category sections + if (scoresObj[user.alias][userCategoryStatsPropName].hasOwnProperty("total_score")) + statsObj.last_score = scoresObj[user.alias][userCategoryStatsPropName].total_score; + else if (scoresObj[user.alias][userCategoryStatsPropName].hasOwnProperty("last_score")) + statsObj.last_score = scoresObj[user.alias][userCategoryStatsPropName].last_score; + userCatStats.push(statsObj); + } + scoresObj[user.alias][userCategoryStatsPropName] = userCatStats; + } + } + // See if the category stats already has an element with the last section name + var catStatsElementIdx = -1; + for (var i = 0; i < scoresObj[user.alias][userCategoryStatsPropName].length && catStatsElementIdx == -1; ++i) + { + if (scoresObj[user.alias][userCategoryStatsPropName][i].category_name == pLastSectionName) + catStatsElementIdx = i; + } + if (catStatsElementIdx == -1) + { + // The last section info doesn't exist in the array + scoresObj[user.alias][userCategoryStatsPropName].push({ + category_name: pLastSectionName, + last_score: pUserCurrentGameScore, + last_time: currentTime + }); + } + else // The last section info already exists in the array + { + scoresObj[user.alias][userCategoryStatsPropName][catStatsElementIdx].last_score = pUserCurrentGameScore; + scoresObj[user.alias][userCategoryStatsPropName][catStatsElementIdx].last_time = currentTime; + } // Update the user's grand total score value if (scoresObj[user.alias].hasOwnProperty("total_score")) scoresObj[user.alias].total_score += pUserCurrentGameScore; @@ -857,13 +939,14 @@ 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]; } catch (error) { - console.print("* " + error); + console.print("* Line " + error.lineNumber + ": " + error); console.crlf(); - log(LOG_ERR, GAME_NAME + " - Updating trivia score object: " + error); - bbs.log_str(GAME_NAME + " - Updating trivia score object: " + error); + log(LOG_ERR, GAME_NAME + " - Updating trivia score object: Line " + error.lineNumber + ": " + error); + bbs.log_str(GAME_NAME + " - Updating trivia score object: Line " + error.lineNumber + ": " + error); } scoresFile = new File(SCORES_FILENAME); if (scoresFile.open("w")) @@ -874,13 +957,68 @@ function updateScoresFile(pUserCurrentGameScore, pLastSectionName) // 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); } -// Shows the saved scores +// Updates user scores on the server (if there is one configured) // // Parameters: -// pPauseAtEnd: Boolean - Whether or not to pause at the end. The default is false. -function showScores(pPauseAtEnd) +// pUserNameForScores: The user's name as used for the scores +// pUserScoreInfo: An object containing user scores, as created by updateScoresFile() +function updateScoresOnServer(pUserNameForScores, pUserScoreInfo) +{ + // Make sure the settings have valid server settings and the user score info object is valid + if (!gSettings.hasValidServerSettings()) + return; + if (typeof(pUserNameForScores) !== "string" || pUserNameForScores.length == 0 || typeof(pUserScoreInfo) !== "object") + return; + + try + { + var jsonClient = new JSONClient(gSettings.remoteServer.server, gSettings.remoteServer.port); + // 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); + // Write the scores on the server + JSONLocation = gSettings.remoteServer.userScoresJSONLocationWithoutUsername + "." + pUserNameForScores; + jsonClient.write(gSettings.remoteServer.gtTriviaScope, JSONLocation, pUserScoreInfo, JSON_DB_LOCK_WRITE); + // 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); + jsonClient.disconnect(); + } + catch (error) + { + 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); + } +} + +// Shows the saved scores - First the locally saved scores, and then if there is a +// server configured, shows the remote saved scores (after prompting the user whether +// to show those) +function showScores() +{ + // Show local scores. Then, if there is a server configured, prompt the user if they + // want to see server scores, and if so, show those. + showLocalScores(); + + if (gSettings.hasValidServerSettings()) + { + var showServerScoresConfirm = console.yesno("\x01n\x01b\x01hShow multi-BBS scores"); + console.attributes = "N"; + if (showServerScoresConfirm) + showServerScores(); + } +} + +// Shows the locally saved scores (on the current BBS) +function showLocalScores() { console.print("\x01n"); console.crlf(); @@ -899,90 +1037,218 @@ function showScores(pPauseAtEnd) var scoresObj = JSON.parse(scoreFileContents); for (var prop in scoresObj) { - sortedScores.push({ - name: prop, - total_score: scoresObj[prop].total_score, - last_score: scoresObj[prop].last_score, - last_trivia_category: scoresObj[prop].last_trivia_category, - last_time: scoresObj[prop].last_time - }); + sortedScores.push(new UserScoreObj(prop, scoresObj[prop].total_score, scoresObj[prop].last_score, + scoresObj[prop].last_trivia_category, scoresObj[prop].last_time)); } } // Sort the array: High total score first - sortedScores.sort(function(objA, objB) { - if (objA.total_score > objB.total_score) - return -1; - else if (objA.total_score < objB.total_score) - return 1; - else - return 0; - }); + sortedScores.sort(userScoresSortTotalScoreHighest); } // Print the scores if there are any if (sortedScores.length > 0) + showUserScoresArray(sortedScores); + else + console.print("\x01gThere are no saved scores yet.\x01n"); + console.crlf(); +} + +// Shows the scores from the server (multi-BBS scores) +function showServerScores() +{ + if (!gSettings.hasValidServerSettings()) + return; + + // Use a JSONClient to get the scores JSON from the server + try { - // Make the format string for printf() - var scoreWidth = 6; - var dateWidth = 10; - var categoryWidth = 15; - var nameWidth = 0; - var formatStr = ""; - if (console.screen_columns >= 80) + var jsonClient = new JSONClient(gSettings.remoteServer.server, gSettings.remoteServer.port); + var data = jsonClient.read(gSettings.remoteServer.gtTriviaScope, "SCORES", JSON_DB_LOCK_READ); + jsonClient.disconnect(); + // Example of scores from the server (as of data version 1.01): + /* + SCORES: + systems: + DIGDIST: + bbs_name: Digital Distortion + user_scores: + Nightfox: + category_stats: + 0: + category_name: General + last_score: 20 + last_time: 2022-11-24 + total_score: 60 + last_score: 20 + last_trivia_category: General + last_time: 2022-11-24 + game_client: Good Time Trivia version 1.01 Beta (2022-11-24) + */ + /* + if (typeof(data) === "string") + data = JSON.parse(data); + */ + // Sanity checking: Make sure the data is an object and has a "systems" property + if (typeof(data) !== "object") { - nameWidth = console.screen_columns - dateWidth - categoryWidth - (scoreWidth * 2) - 5; - formatStr = "%-" + nameWidth + "s %-" + dateWidth + "s %-" + categoryWidth + "s %" + scoreWidth + "d %" + scoreWidth + "d"; - } - else - { - nameWidth = console.screen_columns - (scoreWidth * 2) - 3; - formatStr = "%-" + nameWidth + "s %" + scoreWidth + "d %" + scoreWidth + "d"; + console.attributes = "N" + gSettings.colors.error; + console.print("Invalid scores data was received from the server\x01n"); + console.crlf(); + return; } - // Print the scores - console.center("\x01g\x01hHigh Scores\x01n"); - console.crlf(); - if (console.screen_columns >= 80) + if (!data.hasOwnProperty("systems")) { - printf("\x01w\x01h%-" + nameWidth + "s %-" + dateWidth + "s %-" + categoryWidth + "s %" + scoreWidth + "s %" + scoreWidth + "s", - "Player", "Last date", "Last category", "Total", "Last"); + console.attributes = "N" + gSettings.colors.error; + console.print("Invalid scores data was received from the server\x01n"); + console.crlf(); + return; } - else - printf("\x01w\x01h%-" + nameWidth + "s %" + scoreWidth + "s %" + scoreWidth + "s\x01n", "Player", "Total", "Last"); - console.crlf(); - console.print("\x01n\x01b"); - for (var i = 0; i < console.screen_columns-1; ++i) - console.print(HORIZONTAL_DOUBLE); - console.crlf(); - // Print the list of high scores - console.print("\x01g"); - if (console.screen_columns >= 80) + + // For each BBS in the scores, sort the player scores and then show the scores for + // the BBS + for (var BBS_ID in data.systems) { - for (var i = 0; i < sortedScores.length && i < gSettings.behavior.maxNumPlayerScoresToDisplay; ++i) + if (!data.systems[BBS_ID].hasOwnProperty("user_scores")) + continue; + var sortedScores = []; + for (var playerName in data.systems[BBS_ID].user_scores) { - var playerName = sortedScores[i].name.substr(0, nameWidth); - var lastDate = strftime("%Y-%m-%d", sortedScores[i].last_time); - var sectionName = sortedScores[i].last_trivia_category.substr(0, categoryWidth); - printf(formatStr, playerName, lastDate, sectionName, sortedScores[i].total_score, sortedScores[i].last_score); - console.crlf(); + // Player category stats are also available (as an array): + //data.systems[BBS_ID].user_scores[playerName].category_stats + var scoreObj = new UserScoreObj(playerName, data.systems[BBS_ID].user_scores[playerName].total_score, data.systems[BBS_ID].user_scores[playerName].last_score, + data.systems[BBS_ID].user_scores[playerName].last_trivia_category, data.systems[BBS_ID].user_scores[playerName].last_time); + sortedScores.push(scoreObj); } - } - else - { - for (var i = 0; i < sortedScores.length && i < gSettings.behavior.maxNumPlayerScoresToDisplay; ++i) + // Sort the array: High total score first. Then display them. + sortedScores.sort(userScoresSortTotalScoreHighest); + if (sortedScores.length > 0) { - printf(formatStr, sortedScores[i].name.substr(0, nameWidth), sortedScores[i].total_score, sortedScores[i].last_score); + 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 (data.systems[BBS_ID].user_scores[playerName].hasOwnProperty("game_client")) + { + console.print("\x01n\x01cGame client: \x01h" + data.systems[BBS_ID].user_scores[playerName].game_client + "\x01n"); + console.crlf(); + } + } console.crlf(); } } - console.print("\x01n\x01b"); - for (var i = 0; i < console.screen_columns-1; ++i) - console.print(HORIZONTAL_DOUBLE); + } + catch (error) + { + log(LOG_ERR, GAME_NAME + " - Getting server scores: Line " + error.lineNumber + ": " + error); + bbs.log_str(GAME_NAME + " - Getting server scores: Line " + error.lineNumber + ": " + error); + console.attributes = "N" + gSettings.colors.error; + console.print("* Line: " + error.lineNumber + ": " + error); console.crlf(); } +} + +// Shows an array of user scores +// +// Parameters: +// pUserScoresArray: An array of objects with the following properties: +// name: Player's name +// total_score: Player's total score from all games they've played +// last_score: The player's score from the last game they played +// last_trivia_category: The name of the last trivia category the player played +// last_time: The UNIX timestamp when the player's scores were saved +// pBBSName: Optional - The name of a BBS where the scores are coming from (if applicable) +function showUserScoresArray(pUserScoresArray, pBBSName) +{ + if (typeof(pUserScoresArray) !== "object" || pUserScoresArray.length == 0) + return; + + // Make the format string for printf() + var scoreWidth = 6; + var dateWidth = 10; + var categoryWidth = 15; + var nameWidth = 0; + var formatStr = ""; + if (console.screen_columns >= 80) + { + nameWidth = console.screen_columns - dateWidth - categoryWidth - (scoreWidth * 2) - 5; + formatStr = "%-" + nameWidth + "s %-" + dateWidth + "s %-" + categoryWidth + "s %" + scoreWidth + "d %" + scoreWidth + "d"; + } else - console.print("\x01gThere are no saved scores yet.\x01n"); + { + nameWidth = console.screen_columns - (scoreWidth * 2) - 3; + formatStr = "%-" + nameWidth + "s %" + scoreWidth + "d %" + scoreWidth + "d"; + } + // Show the "High scores" header + /* + if (typeof(pBBSName) === "string" && pBBSName.length > 0) + console.center("\x01g\x01hHigh Scores: \x01c" + pBBSName + "\x01n"); + else + console.center("\x01g\x01hHigh Scores\x01n"); + */ + console.center("\x01n\x01g\x01hHigh Scores\x01n"); + if (typeof(pBBSName) === "string" && pBBSName.length > 0) + console.center("\x01n\x01gBBS\x01h: \x01c" + pBBSName); console.crlf(); - if (typeof(pPauseAtEnd) === "boolean" && pPauseAtEnd) - console.pause(); + // Print the scores + if (console.screen_columns >= 80) + { + printf("\x01w\x01h%-" + nameWidth + "s %-" + dateWidth + "s %-" + categoryWidth + "s %" + scoreWidth + "s %" + scoreWidth + "s", + "Player", "Last date", "Last category", "Total", "Last"); + } + else + printf("\x01w\x01h%-" + nameWidth + "s %" + scoreWidth + "s %" + scoreWidth + "s\x01n", "Player", "Total", "Last"); + console.crlf(); + console.print("\x01n\x01b"); + for (var i = 0; i < console.screen_columns-1; ++i) + console.print(HORIZONTAL_DOUBLE); + console.crlf(); + // Print the list of high scores + console.print("\x01g"); + if (console.screen_columns >= 80) + { + for (var i = 0; i < pUserScoresArray.length && i < gSettings.behavior.maxNumPlayerScoresToDisplay; ++i) + { + var playerName = pUserScoresArray[i].name.substr(0, nameWidth); + var lastDate = strftime("%Y-%m-%d", pUserScoresArray[i].last_time); + var sectionName = pUserScoresArray[i].last_trivia_category.substr(0, categoryWidth); + printf(formatStr, playerName, lastDate, sectionName, pUserScoresArray[i].total_score, pUserScoresArray[i].last_score); + console.crlf(); + } + } + else + { + for (var i = 0; i < pUserScoresArray.length && i < gSettings.behavior.maxNumPlayerScoresToDisplay; ++i) + { + printf(formatStr, pUserScoresArray[i].name.substr(0, nameWidth), pUserScoresArray[i].total_score, pUserScoresArray[i].last_score); + console.crlf(); + } + } + console.print("\x01n\x01b"); + for (var i = 0; i < console.screen_columns-1; ++i) + console.print(HORIZONTAL_DOUBLE); + console.crlf(); +} + +// Creates a user score object for display in the high scores +function UserScoreObj(pPlayerName, pTotalScore, pLastScore, pLastCategory, pLastTime) +{ + this.name = pPlayerName; + this.total_score = pTotalScore; + this.last_score = pLastScore; + this.last_trivia_category = pLastCategory; + this.last_time = pLastTime; +} + +// An array sorting function for UserScoreObj objects to sort the array by +// highest total_score first +function userScoresSortTotalScoreHighest(pPlayerA, pPlayerB) +{ + if (pPlayerA.total_score > pPlayerB.total_score) + return -1; + else if (pPlayerA.total_score < pPlayerB.total_score) + return 1; + else + return 0; } // Displays the game help to the user @@ -1060,7 +1326,8 @@ function showHelpScreen() console.pause(); } -// Returns a version of a string that is masked, possibly with some of its characters unmasked +// Returns a version of a string that is masked, possibly with some of its characters unmasked. +// Spaces will not be included. // // Parameters: // pStr: A string to mask @@ -1068,23 +1335,38 @@ function showHelpScreen() // pMaskChar: Optional - The mask character. Defaults to "*". function partiallyHiddenStr(pStr, pNumLettersUncovered, pMaskChar) { - if (typeof(pStr) !== "string") + if (typeof(pStr) !== "string" || pStr.length == 0) return ""; + + // Count the number of spaces in the string + var numSpaces = 0; + for (var i = 0; i < pStr.length; ++i) + { + if (pStr.charAt(i) == " ") + ++numSpaces; + } + var maskChar = (typeof(pMaskChar) === "string" && pMaskChar.length > 0 ? pMaskChar.substr(0, 1) : "*"); var numLetersUncovered = (typeof(pNumLettersUncovered) === "number" && pNumLettersUncovered > 0 ? pNumLettersUncovered : 0); var str = ""; - if (numLetersUncovered >= pStr.length) + if (numLetersUncovered >= pStr.length - numSpaces) //if (numLetersUncovered >= pStr.length) str = pStr; else { var i = 0; + var charCount = 0; for (i = 0; i < pStr.length; ++i) { - if (i < numLetersUncovered) - str += pStr.charAt(i); + var currentChar = pStr.charAt(i); + if (currentChar == " ") + { + str += " "; + continue; + } + if (charCount++ < numLetersUncovered) + str += currentChar; else str += maskChar; - } } return str; @@ -1131,4 +1413,149 @@ function attrCodeStr(pAttrCodeCharStr) for (var i = 0; i < pAttrCodeCharStr.length; ++i) str += "\x01" + pAttrCodeCharStr[i]; return str; +} + +// Sysop menu: Provides the sysop with some maintenance options +function doSysopMenu() +{ + if (!user.is_sysop) + return; + + var continueOn = true; + while (continueOn) + { + console.attributes = "N"; + console.print("\x01c\x01hSysop menu"); + console.crlf(); + console.attributes = "NB"; + for (var i = 0; i < console.screen_columns-1; ++i) + console.print(HORIZONTAL_DOUBLE); + console.crlf(); + var validKeys = "1Q"; // Clear high scores, Quit + console.print("\x01c1\x01y\x01h) \x01bClear high scores\x01n"); + console.print(" \x01cQ\x01y\x01h)\x01buit\x01n"); + // If there is an inter-BBS scores JSON file, then add some options to manage that + if (file_exists(backslash(gStartupPath + "server") + "gttrivia.json")) + { + validKeys += "23"; + console.crlf(); + console.print("\x01gInter-BBS scores\x01n"); + console.crlf(); + console.attributes = "KH"; + for (var i = 0; i < 16; ++i) + console.print(HORIZONTAL_SINGLE); + console.attributes = "N"; + console.crlf(); + console.print("\x01c2\x01y\x01h) \x01bDelete user (from all systems)\x01n"); + console.crlf(); + console.print("\x01c3\x01y\x01h) \x01bDelete BBS scores\x01n"); + console.crlf(); + } + console.attributes = "NB"; + for (var i = 0; i < console.screen_columns-1; ++i) + console.print(HORIZONTAL_DOUBLE); + console.attributes = "N"; + console.crlf(); + console.print("\x01cYour choice\x01g\x01h: \x01c"); + var userChoice = console.getkeys(validKeys, -1, K_UPPER).toString(); + console.attributes = "N"; + if (userChoice.length == 0 || userChoice == "Q") + continueOn = false; + else if (userChoice == "1") + { + console.crlf(); + if (file_exists(SCORES_FILENAME)) + { + if (!console.noyes("\x01y\x01hAre you SURE you want to clear the scores\x01b")) + { + file_remove(SCORES_FILENAME); + console.print("\x01n\x01c\x01hThe score file has been deleted."); + } + } + else + console.print("\x01n\x01c\x01hThere is no score file yet."); + console.print("\x01n"); + console.crlf(); + } + else if (userChoice == "2") + { + // Delete user from all systems from server scores + console.print("\x01cPlayer name\x01g\x01h: \x01c"); + var playerName = console.getstr("", -1, K_UPRLWR); + if (playerName.length > 0) + { + if (!console.noyes("\x01y\x01hAre you SURE you want to remove \x01g" + playerName + "\x01b")) + { + var localJSONServicePort = getJSONSvcPortFromServicesIni(); + var jsonClient = new JSONClient("127.0.0.1", localJSONServicePort); + try + { + var data = jsonClient.read(gSettings.remoteServer.gtTriviaScope, "SCORES", JSON_DB_LOCK_READ); + if (typeof(data) === "object" && data.hasOwnProperty("systems")) + { + for (var BBS_ID in data.systems) + { + if (data.systems[BBS_ID].hasOwnProperty("user_scores")) + { + var playerNameUpper = playerName.toUpperCase(); + for (var playerName in data.systems[BBS_ID].user_scores) + { + if (playerName.toUpperCase() == playerNameUpper) + { + var JSONLocation = format("SCORES.systems.%s.user_scores.%s", BBS_ID, playerName); + jsonClient.remove(gSettings.remoteServer.gtTriviaScope, JSONLocation, JSON_DB_LOCK_WRITE); + } + } + } + } + } + } + catch (error) + { + console.print("* " + error + "\r\n"); + log(LOG_ERR, GAME_NAME + " - Deleting user from server scores: Line " + error.lineNumber + ": " + error); + bbs.log_str(GAME_NAME + " - Deleting user from server scores: Line " + error.lineNumber + ": " + error); + } + jsonClient.disconnect(); + } + } + } + else if (userChoice == "3") + { + // Delete BBS from server scores + console.print("\x01cBBS name\x01g\x01h: \x01c"); + var BBSName = console.getstr("", -1, K_UPRLWR); + if (BBSName.length > 0) + { + if (!console.noyes("\x01y\x01hAre you SURE you want to remove \x01g" + BBSName + "\x01b")) + { + var localJSONServicePort = getJSONSvcPortFromServicesIni(); + var jsonClient = new JSONClient("127.0.0.1", localJSONServicePort); + try + { + var data = jsonClient.read(gSettings.remoteServer.gtTriviaScope, "SCORES", JSON_DB_LOCK_READ); + if (typeof(data) === "object" && data.hasOwnProperty("systems")) + { + var BBSNameUpper = BBSName.toUpperCase(); + for (var BBS_ID in data.systems) + { + if (data.systems[BBS_ID].hasOwnProperty("bbs_name") && data.systems[BBS_ID].bbs_name.toUpperCase() == BBSNameUpper) + { + var JSONLocation = format("SCORES.systems.%s", BBS_ID); + jsonClient.remove(gSettings.remoteServer.gtTriviaScope, JSONLocation, JSON_DB_LOCK_WRITE); + } + } + } + } + catch (error) + { + console.print("* " + error + "\r\n"); + log(LOG_ERR, GAME_NAME + " - Deleting user from server scores: Line " + error.lineNumber + ": " + error); + bbs.log_str(GAME_NAME + " - Deleting user from server scores: Line " + error.lineNumber + ": " + error); + } + jsonClient.disconnect(); + } + } + } + } } \ No newline at end of file diff --git a/xtrn/gttrivia/lib.js b/xtrn/gttrivia/lib.js new file mode 100644 index 0000000000000000000000000000000000000000..a9da76b645b2db03efe776b05c76af419d00a114 --- /dev/null +++ b/xtrn/gttrivia/lib.js @@ -0,0 +1,21 @@ +// Gets the JSON service port number from services.ini (if possible) +function getJSONSvcPortFromServicesIni() +{ + var portNum = 0; + var servicesIniFile = new File(system.ctrl_dir + "services.ini"); + if (servicesIniFile.open("r")) + { + var sectionNamesToTry = [ "JSON_DB", "JSON-DB", "JSON" ]; + for (var i = 0; i < sectionNamesToTry.length; ++i) + { + var jsonServerCfg = servicesIniFile.iniGetObject(sectionNamesToTry[i]); + if (jsonServerCfg != null && typeof(jsonServerCfg) === "object") + { + portNum = jsonServerCfg.Port; + break; + } + } + servicesIniFile.close(); + } + return portNum; +} \ No newline at end of file diff --git a/xtrn/gttrivia/qa/dirty_minds.qa b/xtrn/gttrivia/qa/dirty_minds.qa index d812a1f8d8221fb8dde094c9c1c9270eabc2e315..52a5481f3db5f6589f1f511b38a839c0e894b6e1 100644 --- a/xtrn/gttrivia/qa/dirty_minds.qa +++ b/xtrn/gttrivia/qa/dirty_minds.qa @@ -1426,7 +1426,7 @@ I'm a 4-letter word ending in UNT. You do me when you lose your balls. When you' Hunt 10 -I hang between your chest and knes. The older I am, the bigger I get. There's hair growing on my hole. +I hang between your chest and knees. The older I am, the bigger I get. There's hair growing on my hole. Belly 10 @@ -2122,7 +2122,7 @@ I can whip a big pussy. I can stick my head in a pussy. I let people watch me in Lion tamer 10 -Trum phas a long one. Most people are dressed when they come inside of me. After you're in me, you can watch TV. +Trump has a long one. Most people are dressed when they come inside of me. After you're in me, you can watch TV. Limousine 10 diff --git a/xtrn/gttrivia/qa/general.qa b/xtrn/gttrivia/qa/general.qa index db0ae85e199b04dff77a8495041bba59b7e9feea..ad94809b98ee7db459681d2136f8a7fc3b992d01 100644 --- a/xtrn/gttrivia/qa/general.qa +++ b/xtrn/gttrivia/qa/general.qa @@ -1347,7 +1347,7 @@ Zloty 10 How tall is the Empire State Building (without the spire and antenna)? -1,250 feet +1250 feet 10 Which country produces the most tea? diff --git a/xtrn/gttrivia/readme.txt b/xtrn/gttrivia/readme.txt index 2abb73e8b85d951bf51304e2adcafcf7ff51aa4e..ee5b49f53f8d612c0c456ac96640eb52d7e79a79 100644 --- a/xtrn/gttrivia/readme.txt +++ b/xtrn/gttrivia/readme.txt @@ -1,6 +1,6 @@ Good Time Trivia - Version 1.00 - Release date: 2022-11-18 + Version 1.01 + Release date: 2022-11-25 by @@ -22,6 +22,9 @@ Contents 3. Installation & Setup - Installation in SCFG 4. Configuration file +5. Optional: Configuring your BBS to host player scores + - Only do this if you would prefer to host scores on your BBS rather than + using the inter-BBS scores hosted on Digital Distortion 1. Disclaimer @@ -84,7 +87,11 @@ of the following files and directories: 3. gttrivia.asc The logo/startup screen to be shown to the user. This is in Synchronet attribute code format. -4. qa This is a subdirectory that contains the trivia +4. install-xtrn.ini A configuration file for automated installation into + Synchronet's external programs section; for use with + install-xtrn.js. This is optional. + +5. qa This is a subdirectory that contains the trivia question & answer files. Each file contains a collection of questions, answers, and number of points for each question. Each filename must have @@ -93,6 +100,17 @@ of the following files and directories: questions (underscores are required between each word). The filename extension must be .qa . +6. server This directory contains a couple scripts that are + used if you enable the gttrivia JSON database (for + hosting game scores on a Synchronet BBS so that scores + from players on multiple BBSes can be hosted and + displayed). As of this writing, I host scores for + Good Time Trivia on my BBS (Digital Distortion), so + unless you really want to do your own score hosting, + you can have your installation of the game read + inter-BBS scores from Digital Distortion. + + The trivia category files (in the qa directory, with filenames ending in .qa) are plain text files and contain questions, their answers, and their number of points. For eqch question in a category file, there are 3 lines: @@ -135,7 +153,8 @@ gttrivia.js. Installation in SCFG -------------------- This is an example of adding the game in one of your external programs sections -in SCFG: +in SCFG (note that the 'Native Executable/Script value doesn't matter for JS +scripts): ╔═════════════════════════════════════════════════════[< >]╗ ║ Good Time Trivia ║ ╠══════════════════════════════════════════════════════════╣ @@ -149,7 +168,7 @@ in SCFG: ║ │Execution Requirements ║ ║ │Multiple Concurrent Users Yes ║ ║ │I/O Method FOSSIL or UART ║ -║ │Native Executable/Script No ║ +║ │Native Executable/Script Yes ║ ║ │Use Shell or New Context No ║ ║ │Modify User Data No ║ ║ │Execute on Event No ║ @@ -166,6 +185,7 @@ gttrivia.ini is the configuration file for the door game. There are 3 sections: [BEHAVIOR], [COLORS], and [CATEGORY_ARS]. The settings are described below: [BEHAVIOR] section +------------------ Setting Description ------- ----------- numQuestionsPerPlay The maximum number of trivia questions @@ -181,10 +201,12 @@ maxNumPlayerScoresToDisplay The maximum number of player scores to display in the list of high scores [COLORS] section +---------------- In this section, the color codes are simply specified by a string of color (attribute) code characters (i.e., YH for yellow and high). See this page for Synchronet attribute codes: http://wiki.synchro.net/custom:ctrl-a_codes + Setting Element applied to ------- ------------------- error Errors @@ -211,6 +233,7 @@ clue Clue text answerAfterIncorrect The answer printed after incorrect response [CATEGORY_ARS] section +---------------------- In this section, the format is section_name=ARS string section_name must match the part of a filename in the qa directory without the filename extension. The ARS string is a string that Synchronet uses to describe @@ -220,3 +243,57 @@ categories that you may want age-restricted, for instance). See the following web page for documentation on Synchronet's ARS strings: http://wiki.synchro.net/access:requirements +[REMOTE_SERVER] section +----------------------- +This section is used for specifying a remote server to connect to. Currently, +this is used for writing & reading player scores on the remote system. Inter- +BBS scores (retrieved from the remote system) can be optionally viewed when a +user is viewing scores. + +Setting Description +------- ----------- +server The server hostname/IP address. The default + value can be used if you want to use Digital + Distortion BBS. + +port The port number to use to connect to the + remote host. The default value is set for + Digital Distortion. + +[SERVER] section +---------------- +This section is only used if you decide to host scores for Good Time Trivia. + +Setting Description +------- ----------- +deleteScoresOlderThanDays The number of days to keep old player scores. + The background service will remove player + scores older than this number of days. + + +5. Optional: Configuring your BBS to host player scores +======================================================= +You should only do this if you would prefer to host scores on your BBS rather +than using the inter-BBS scores hosted on Digital Distortion. + +If you want to host player scores for Good Time Trivia, first ensure you have a +section in your ctrl/services.ini configured to run json-service.js (usually +with a name of JSON, or JSON in the name): + +[JSON] +Port=10088 +Options=STATIC | LOOP +Command=json-service.js + +Note that those settings configure it to run on port 10088. Also, you might +already have a similar section in your services.ini if you currently host any +JSON databases. + +Then, open your ctrl/json-service.ini and add these lines to enable the gttrivia +JSON dtaabase: + +[gttrivia] +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. diff --git a/xtrn/gttrivia/revision_history.txt b/xtrn/gttrivia/revision_history.txt index 6fd3dc68def4f10af33c3355bd8736dffb0e5a5f..08927161eaeda725c252dc8f1083599f5f2e3fad 100644 --- a/xtrn/gttrivia/revision_history.txt +++ b/xtrn/gttrivia/revision_history.txt @@ -4,4 +4,11 @@ Revision History (change log) ============================= Version Date Description ------- ---- ----------- +1.01 2022-11-25 Added the ability to store & retrieve scores to/from a + server, so that scores from multiple BBSes can be + displayed. By default, it's configured to use Digital + Distortion as the host. There are 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. 1.00 2022-11-18 Initial version/release \ No newline at end of file diff --git a/xtrn/gttrivia/server/commands.js b/xtrn/gttrivia/server/commands.js new file mode 100644 index 0000000000000000000000000000000000000000..f576c87bd571cf6d85a886f7957b869bc61472e8 --- /dev/null +++ b/xtrn/gttrivia/server/commands.js @@ -0,0 +1,13 @@ +this.QUERY = function(client, packet) +{ + // Operations that are allowed by clients + var openOpers = [ "READ", "WRITE", "SUBSCRIBE", "UNSUBSCRIBE" ]; + // The openOpers operations are okay to run; also, allow anything from the current machine + if (openOpers.indexOf(packet.oper) >= 0 || client.remote_ip_address === '127.0.0.1') + return false; + else + return true; + + // Disallowed operations + //var invalidOps = [ "PUSH", "POP", "SHIFT", "UNSHIFT", "DELETE", "SLICE" ]; +} diff --git a/xtrn/gttrivia/server/service.js b/xtrn/gttrivia/server/service.js new file mode 100644 index 0000000000000000000000000000000000000000..12d5bfb73c6dd21e67e107de7fe2cf0a9495a377 --- /dev/null +++ b/xtrn/gttrivia/server/service.js @@ -0,0 +1,167 @@ +// For Good Time Trivia JSON database. +// This is a long-running background script. + + +// Values for JSON DB reading and writing +var JSON_DB_LOCK_READ = 1; +var JSON_DB_LOCK_WRITE = 2; +var JSON_DB_LOCK_UNLOCK = -1; + +var NUM_SECONDS_PER_DAY = 86400; + +// gRootTriviaScriptDir is the root directory where service.js is located +var gRootTriviaScriptDir = argv[0]; + + +var requireFnExists = (typeof(require) === "function"); +if (requireFnExists) +{ + require("json-client.js", "JSONClient"); + require(gRootTriviaScriptDir + "../lib.js", "getJSONSvcPortFromServicesIni"); +} +else +{ + load("json-client.js"); + load(gRootTriviaScriptDir + "../lib.js"); +} + + +var gSettings, gJSONClient; + +function processUpdate(update) +{ + log(LOG_INFO, "Good Time Trivia: Update"); + + if (gSettings.server.deleteScoresOlderThanDays > 0) + { + // Look through the server data for old scores that we might want to delete + try + { + var data = gJSONClient.read(gSettings.remoteServer.gtTriviaScope, "SCORES", JSON_DB_LOCK_READ); + // Example of scores from the server (as of data version 1.01): + /* + SCORES: + systems: + DIGDIST: + bbs_name: Digital Distortion + user_scores: + Nightfox: + category_stats: + 0: + category_name: General + last_score: 20 + last_time: 2022-11-24 + total_score: 60 + last_score: 20 + last_trivia_category: General + last_time: 2022-11-24 + */ + /* + if (typeof(data) === "string") + data = JSON.parse(data); + */ + // Sanity checking: Make sure the data is an object and has a "systems" property + if (typeof(data) !== "object") + return; + if (!data.hasOwnProperty("systems")) + return; + + for (var BBS_ID in data.systems) + { + var now = time(); + if (!data.systems[BBS_ID].hasOwnProperty("user_scores")) + continue; + for (var playerName in data.systems[BBS_ID].user_scores) + { + if (data.systems[BBS_ID].user_scores[playerName].last_time < now - (NUM_SECONDS_PER_DAY * gSettings.server.deleteScoresOlderThanDays)) + { + // Delete this user's entry + var JSONLocation = format("SCORES.systems.%s.user_scores.%s", BBS_ID, playerName); + gJSONClient.remove(gtTriviaScope, JSONLocation, LOCK_WRITE); + } + } + } + } + catch (err) + { + log(LOG_ERR, "Good Time Trivia: Line " + err.lineNumber + ": " + err); + } + } +} + +function init() +{ + // Assuming this script is in a 'server' subdirectory, gttrivia.ini should be one directory up + gSettings = readSettings(gRootTriviaScriptDir + "../"); + try + { + gJSONClient = new JSONClient("127.0.0.1", getJSONSvcPortFromServicesIni()); + gJSONClient.subscribe(gSettings.remoteServer.gtTriviaScope, "SCORES"); + gJSONClient.callback = processUpdate; + processUpdate(); + log(LOG_INFO, "Good Time Trivia JSON DB service task initialized"); + } + catch (error) + { + log(LOG_ERROR, error); + } +} +var readSettings = function(path) +{ + var settings = { + readSuccessful: false + }; + var iniFile = new File(path + "gttrivia.ini"); + if (iniFile.open("r")) + { + settings.remoteServer = iniFile.iniGetObject("REMOTE_SERVER"); + settings.server = iniFile.iniGetObject("SERVER"); + iniFile.close(); + settings.readSuccessful = true; + + if (typeof(settings.remoteServer) !== "object") + settings.remoteServer = {}; + if (!/^[0-9]+$/.test(settings.remoteServer.port)) + settings.remoteServer.port = 0; + if (typeof(settings.server.deleteScoresOlderThanDays) !== "number") + settings.server.deleteScoresOlderThanDays = 0; + else if (settings.server.deleteScoresOlderThanDays < 0) + settings.server.deleteScoresOlderThanDays = 0; + + // Other settings - Not read from the configuration file, but things we want to use in multiple places + // in this script + // JSON scope + settings.remoteServer.gtTriviaScope = "GTTRIVIA"; + + if (settings.server.deleteScoresOlderThanDays > 0) + log(LOG_INFO, "Good Time Trivia service: Will delete user scores older than " + settings.server.deleteScoresOlderThanDays + " days"); + } + return settings; +} + +function main() +{ + while (!js.terminated) + { + mswait(5); + gJSONClient.cycle(); + } +} + + +function cleanUp() +{ + gJSONClient.disconnect(); +} + + +try +{ + init(); + main(); + cleanUp(); +} catch (err) { } + +exit(); + +