Achievement & Event improvements

* User stat set vs user stat increment system events
* Proper addMultipleEventListener() and removeMultipleEventListener() Events APIs
* userStatSet vs userStatInc user stat achievement types. userStatInc for example can be used for door minutes used
This commit is contained in:
Bryan Ashby 2019-01-06 21:56:12 -07:00
parent 925ca134c6
commit 34c9178099
6 changed files with 107 additions and 35 deletions

View File

@ -31,7 +31,7 @@
- Applying customizations via the achievements section in theme.hjson - Applying customizations via the achievements section in theme.hjson
Some tips: 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 forget to RTFM ...er uh... see the documentation for more information and
don"t be shy to ask for help: don"t be shy to ask for help:
@ -53,7 +53,7 @@
achievements: { achievements: {
user_login_count: { user_login_count: {
type: userStat type: userStatSet
statName: login_count statName: login_count
match: { match: {
2: { 2: {
@ -90,7 +90,7 @@
} }
user_post_count: { user_post_count: {
type: userStat type: userStatSet
statName: post_count statName: post_count
match: { match: {
5: { 5: {
@ -121,7 +121,7 @@
} }
user_upload_count: { user_upload_count: {
type: userStat type: userStatSet
statName: ul_total_count statName: ul_total_count
match: { match: {
1: { 1: {
@ -159,7 +159,7 @@
} }
user_download_count: { user_download_count: {
type: userStat type: userStatSet
statName: dl_total_count statName: dl_total_count
match: { match: {
1: { 1: {
@ -196,7 +196,7 @@
} }
user_door_runs: { user_door_runs: {
type: userStat type: userStatSet
statName: door_run_total_count statName: door_run_total_count
match: { match: {
1: { 1: {
@ -233,7 +233,7 @@
} }
user_door_total_minutes: { user_door_total_minutes: {
type: userStat type: userStatInc
statName: door_run_total_minutes statName: door_run_total_minutes
match: { match: {
1: { 1: {

View File

@ -45,7 +45,11 @@ class Achievement {
static factory(data) { static factory(data) {
let achievement; let achievement;
switch(data.type) { 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; default : return;
} }
@ -56,13 +60,15 @@ class Achievement {
static get Types() { static get Types() {
return { return {
UserStat : 'userStat', UserStatSet : 'userStatSet',
UserStatInc : 'userStatInc',
}; };
} }
isValid() { isValid() {
switch(this.data.type) { switch(this.data.type) {
case Achievement.Types.UserStat : case Achievement.Types.UserStatSet :
case Achievement.Types.UserStatInc :
if(!_.isString(this.data.statName)) { if(!_.isString(this.data.statName)) {
return false; return false;
} }
@ -129,12 +135,12 @@ class Achievements {
const configLoaded = (achievementConfig) => { const configLoaded = (achievementConfig) => {
if(true !== achievementConfig.enabled) { if(true !== achievementConfig.enabled) {
Log.info('Achievements are not enabled'); Log.info('Achievements are not enabled');
this.stopMonitoringUserStatUpdateEvents(); this.stopMonitoringUserStatEvents();
delete this.achievementConfig; delete this.achievementConfig;
} else { } else {
Log.info('Achievements are enabled'); Log.info('Achievements are enabled');
this.achievementConfig = achievementConfig; this.achievementConfig = achievementConfig;
this.monitorUserStatUpdateEvents(); this.monitorUserStatEvents();
} }
}; };
@ -240,18 +246,22 @@ class Achievements {
); );
} }
monitorUserStatUpdateEvents() { monitorUserStatEvents() {
if(this.userStatEventListener) { if(this.userStatEventListeners) {
return; // already listening 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)) { if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) {
return; return;
} }
const statValue = parseInt(userStatEvent.statValue, 10); if(!_.isNumber(userStatEvent.statValue) && !_.isNumber(userStatEvent.statIncrementBy)) {
if(isNaN(statValue)) {
return; return;
} }
@ -262,7 +272,7 @@ class Achievements {
if(false === achievement.enabled) { if(false === achievement.enabled) {
return false; return false;
} }
return Achievement.Types.UserStat === achievement.type && return [ Achievement.Types.UserStatSet, Achievement.Types.UserStatInc ].includes(achievement.type) &&
achievement.statName === userStatEvent.statName; achievement.statName === userStatEvent.statName;
} }
); );
@ -276,6 +286,14 @@ class Achievements {
return; 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); const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue);
if(!details || _.isUndefined(matchField) || _.isUndefined(matchValue)) { if(!details || _.isUndefined(matchField) || _.isUndefined(matchValue)) {
return; return;
@ -345,10 +363,10 @@ class Achievements {
}); });
} }
stopMonitoringUserStatUpdateEvents() { stopMonitoringUserStatEvents() {
if(this.userStatEventListener) { if(this.userStatEventListeners) {
this.events.removeListener(Events.getSystemEvents().UserStatUpdate, this.userStatEventListener); this.events.removeMultipleEventListener(this.userStatEventListeners);
delete this.userStatEventListener; delete this.userStatEventListeners;
} }
} }

View File

@ -5,10 +5,14 @@
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const { resetScreen } = require('./ansi_term.js'); const { resetScreen } = require('./ansi_term.js');
const { Errors } = require('./enig_error.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 // deps
const async = require('async'); const async = require('async');
const SSHClient = require('ssh2').Client; const SSHClient = require('ssh2').Client;
const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'DoorParty', name : 'DoorParty',
@ -54,10 +58,18 @@ exports.getModule = class DoorPartyModule extends MenuModule {
let pipeRestored = false; let pipeRestored = false;
let pipedStream; let pipedStream;
const startTime = moment();
const restorePipe = function() { const restorePipe = function() {
if(pipedStream && !pipeRestored && !clientTerminated) { if(pipedStream && !pipeRestored && !clientTerminated) {
self.client.term.output.unpipe(pipedStream); self.client.term.output.unpipe(pipedStream);
self.client.term.output.resume(); 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`; const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin); 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... pipedStream = stream; // :TODO: this is hacky...
self.client.term.output.pipe(stream); self.client.term.output.pipe(stream);

View File

@ -5,6 +5,9 @@ const events = require('events');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const SystemEvents = require('./system_events.js'); const SystemEvents = require('./system_events.js');
// deps
const _ = require('lodash');
module.exports = new class Events extends events.EventEmitter { module.exports = new class Events extends events.EventEmitter {
constructor() { constructor() {
super(); super();
@ -35,12 +38,30 @@ module.exports = new class Events extends events.EventEmitter {
return super.once(event, listener); 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 => { events.forEach(eventName => {
this.on(eventName, event => { const listenWrapper = _.partial(listener, _, eventName);
listener(eventName, event); 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);
}); });
} }

View File

@ -122,12 +122,18 @@ class StatLog {
// User specific stats // User specific stats
// These are simply convenience methods to the user's properties // 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 // note: cb is optional in PersistUserProperty
user.persistProperty(statName, statValue, cb); user.persistProperty(statName, statValue, cb);
if(!options.noEvent) {
const Events = require('./events.js'); // we need to late load currently const Events = require('./events.js'); // we need to late load currently
return Events.emit(Events.getSystemEvents().UserStatUpdate, { user, statName, statValue } ); Events.emit(Events.getSystemEvents().UserStatSet, { user, statName, statValue } );
}
}
setUserStat(user, statName, statValue, cb) {
return this.setUserStatWithOptions(user, statName, statValue, {}, cb);
} }
getUserStat(user, statName) { getUserStat(user, statName) {
@ -143,16 +149,27 @@ class StatLog {
let newValue = parseInt(user.properties[statName]); let newValue = parseInt(user.properties[statName]);
if(newValue) { if(newValue) {
if(!_.isNumber(newValue)) { if(!_.isNumber(newValue) && cb) {
return cb(new Error(`Value for ${statName} is not a number!`)); return cb(new Error(`Value for ${statName} is not a number!`));
} }
newValue += incrementBy; newValue += incrementBy;
} else { } else {
newValue = incrementBy; 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 :) // the time "now" in the ISO format we use and love :)
@ -362,7 +379,7 @@ class StatLog {
systemEvents.UserAchievementEarned, systemEvents.UserAchievementEarned,
]; ];
Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => { Events.addMultipleEventListener(interestedEvents, (event, eventName) => {
this.appendUserLogEntry( this.appendUserLogEntry(
event.user, event.user,
'system_event', 'system_event',

View File

@ -21,6 +21,7 @@ module.exports = {
UserSendMail : 'codes.l33t.enigma.system.user_send_mail', UserSendMail : 'codes.l33t.enigma.system.user_send_mail',
UserRunDoor : 'codes.l33t.enigma.system.user_run_door', UserRunDoor : 'codes.l33t.enigma.system.user_run_door',
UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', 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 } UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // {..., achievementTag, points }
}; };