diff --git a/config/achievements.hjson b/config/achievements.hjson index a711bff4..8dbcb637 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -31,7 +31,7 @@ - Applying customizations via the achievements section in theme.hjson Some tips: - - For 'userStat' types, see user_property.js + - For 'userStatSet' types, see user_property.js Don"t forget to RTFM ...er uh... see the documentation for more information and don"t be shy to ask for help: @@ -53,7 +53,7 @@ achievements: { user_login_count: { - type: userStat + type: userStatSet statName: login_count match: { 2: { @@ -90,7 +90,7 @@ } user_post_count: { - type: userStat + type: userStatSet statName: post_count match: { 5: { @@ -121,7 +121,7 @@ } user_upload_count: { - type: userStat + type: userStatSet statName: ul_total_count match: { 1: { @@ -159,7 +159,7 @@ } user_download_count: { - type: userStat + type: userStatSet statName: dl_total_count match: { 1: { @@ -196,7 +196,7 @@ } user_door_runs: { - type: userStat + type: userStatSet statName: door_run_total_count match: { 1: { @@ -233,7 +233,7 @@ } user_door_total_minutes: { - type: userStat + type: userStatInc statName: door_run_total_minutes match: { 1: { diff --git a/core/achievement.js b/core/achievement.js index f3a04f1f..96975628 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -45,7 +45,11 @@ class Achievement { static factory(data) { let achievement; switch(data.type) { - case Achievement.Types.UserStat : achievement = new UserStatAchievement(data); break; + case Achievement.Types.UserStatSet : + case Achievement.Types.UserStatInc : + achievement = new UserStatAchievement(data); + break; + default : return; } @@ -56,13 +60,15 @@ class Achievement { static get Types() { return { - UserStat : 'userStat', + UserStatSet : 'userStatSet', + UserStatInc : 'userStatInc', }; } isValid() { switch(this.data.type) { - case Achievement.Types.UserStat : + case Achievement.Types.UserStatSet : + case Achievement.Types.UserStatInc : if(!_.isString(this.data.statName)) { return false; } @@ -129,12 +135,12 @@ class Achievements { const configLoaded = (achievementConfig) => { if(true !== achievementConfig.enabled) { Log.info('Achievements are not enabled'); - this.stopMonitoringUserStatUpdateEvents(); + this.stopMonitoringUserStatEvents(); delete this.achievementConfig; } else { Log.info('Achievements are enabled'); this.achievementConfig = achievementConfig; - this.monitorUserStatUpdateEvents(); + this.monitorUserStatEvents(); } }; @@ -240,18 +246,22 @@ class Achievements { ); } - monitorUserStatUpdateEvents() { - if(this.userStatEventListener) { + monitorUserStatEvents() { + if(this.userStatEventListeners) { return; // already listening } - this.userStatEventListener = this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => { + const listenEvents = [ + Events.getSystemEvents().UserStatSet, + Events.getSystemEvents().UserStatIncrement + ]; + + this.userStatEventListeners = this.events.addMultipleEventListener(listenEvents, userStatEvent => { if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) { return; } - const statValue = parseInt(userStatEvent.statValue, 10); - if(isNaN(statValue)) { + if(!_.isNumber(userStatEvent.statValue) && !_.isNumber(userStatEvent.statIncrementBy)) { return; } @@ -262,7 +272,7 @@ class Achievements { if(false === achievement.enabled) { return false; } - return Achievement.Types.UserStat === achievement.type && + return [ Achievement.Types.UserStatSet, Achievement.Types.UserStatInc ].includes(achievement.type) && achievement.statName === userStatEvent.statName; } ); @@ -276,6 +286,14 @@ class Achievements { return; } + const statValue = parseInt( + Achievement.Types.UserStatSet === achievement.data.type ? userStatEvent.statValue : userStatEvent.statIncrementBy, + 10 + ); + if(isNaN(statValue)) { + return; + } + const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue); if(!details || _.isUndefined(matchField) || _.isUndefined(matchValue)) { return; @@ -345,10 +363,10 @@ class Achievements { }); } - stopMonitoringUserStatUpdateEvents() { - if(this.userStatEventListener) { - this.events.removeListener(Events.getSystemEvents().UserStatUpdate, this.userStatEventListener); - delete this.userStatEventListener; + stopMonitoringUserStatEvents() { + if(this.userStatEventListeners) { + this.events.removeMultipleEventListener(this.userStatEventListeners); + delete this.userStatEventListeners; } } diff --git a/core/door_party.js b/core/door_party.js index f6bc7be9..dcd6037e 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -5,10 +5,14 @@ const { MenuModule } = require('./menu_module.js'); const { resetScreen } = require('./ansi_term.js'); const { Errors } = require('./enig_error.js'); +const Events = require('./events.js'); +const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); const SSHClient = require('ssh2').Client; +const moment = require('moment'); exports.moduleInfo = { name : 'DoorParty', @@ -54,10 +58,18 @@ exports.getModule = class DoorPartyModule extends MenuModule { let pipeRestored = false; let pipedStream; + const startTime = moment(); + const restorePipe = function() { if(pipedStream && !pipeRestored && !clientTerminated) { self.client.term.output.unpipe(pipedStream); self.client.term.output.resume(); + + const endTime = moment(); + const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); + if(runTimeMinutes > 0) { + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + } } }; @@ -83,6 +95,9 @@ exports.getModule = class DoorPartyModule extends MenuModule { const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; stream.write(rlogin); + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); + Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); + pipedStream = stream; // :TODO: this is hacky... self.client.term.output.pipe(stream); diff --git a/core/events.js b/core/events.js index 73253fe3..541a5cae 100644 --- a/core/events.js +++ b/core/events.js @@ -5,6 +5,9 @@ const events = require('events'); const Log = require('./logger.js').log; const SystemEvents = require('./system_events.js'); +// deps +const _ = require('lodash'); + module.exports = new class Events extends events.EventEmitter { constructor() { super(); @@ -35,12 +38,30 @@ module.exports = new class Events extends events.EventEmitter { return super.once(event, listener); } - addListenerMultipleEvents(events, listener) { - Log.trace( { events }, 'Registring event listeners'); + // + // Listen to multiple events for a single listener. + // Called with: listener(event, eventName) + // + // The returned object must be used with removeMultipleEventListener() + // + addMultipleEventListener(events, listener) { + Log.trace( { events }, 'Registering event listeners'); + + const listeners = []; + events.forEach(eventName => { - this.on(eventName, event => { - listener(eventName, event); - }); + const listenWrapper = _.partial(listener, _, eventName); + this.on(eventName, listenWrapper); + listeners.push( { eventName, listenWrapper } ); + }); + + return listeners; + } + + removeMultipleEventListener(listeners) { + Log.trace( { events }, 'Removing listeners'); + listeners.forEach(listener => { + this.removeListener(listener.eventName, listener.listenWrapper); }); } diff --git a/core/stat_log.js b/core/stat_log.js index 8627b6f2..0ff6aff6 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -122,12 +122,18 @@ class StatLog { // User specific stats // These are simply convenience methods to the user's properties // - setUserStat(user, statName, statValue, cb) { + setUserStatWithOptions(user, statName, statValue, options, cb) { // note: cb is optional in PersistUserProperty user.persistProperty(statName, statValue, cb); - const Events = require('./events.js'); // we need to late load currently - return Events.emit(Events.getSystemEvents().UserStatUpdate, { user, statName, statValue } ); + if(!options.noEvent) { + const Events = require('./events.js'); // we need to late load currently + Events.emit(Events.getSystemEvents().UserStatSet, { user, statName, statValue } ); + } + } + + setUserStat(user, statName, statValue, cb) { + return this.setUserStatWithOptions(user, statName, statValue, {}, cb); } getUserStat(user, statName) { @@ -143,16 +149,27 @@ class StatLog { let newValue = parseInt(user.properties[statName]); if(newValue) { - if(!_.isNumber(newValue)) { + if(!_.isNumber(newValue) && cb) { return cb(new Error(`Value for ${statName} is not a number!`)); } - newValue += incrementBy; } else { newValue = incrementBy; } - return this.setUserStat(user, statName, newValue, cb); + this.setUserStatWithOptions(user, statName, newValue, { noEvent : true }, err => { + if(!err) { + const Events = require('./events.js'); // we need to late load currently + Events.emit( + Events.getSystemEvents().UserStatIncrement, + { user, statName, statIncrementBy: incrementBy, statValue : newValue } + ); + } + + if(cb) { + return cb(err); + } + }); } // the time "now" in the ISO format we use and love :) @@ -362,7 +379,7 @@ class StatLog { systemEvents.UserAchievementEarned, ]; - Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => { + Events.addMultipleEventListener(interestedEvents, (event, eventName) => { this.appendUserLogEntry( event.user, 'system_event', diff --git a/core/system_events.js b/core/system_events.js index 50a0c464..c0c09f35 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -21,6 +21,7 @@ module.exports = { UserSendMail : 'codes.l33t.enigma.system.user_send_mail', UserRunDoor : 'codes.l33t.enigma.system.user_run_door', UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', - UserStatUpdate : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } + UserStatSet : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } + UserStatIncrement : 'codes.l33t.enigma.system.user_stat_increment', // {..., statName, statIncrementBy, statValue } UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // {..., achievementTag, points } };