+ ACS: AC for achievement count check

+ ACS: AP for achievement point check
+ User minutes used on the system are now tracked
+ MCI: TO for total time spent online system (friendly format)
* Fix up a couple ACS bugs with |value|
* Fix formatting of achievement text
+ Add more achievements
* Fix achievement duration formatting
This commit is contained in:
Bryan Ashby 2019-01-10 20:34:52 -07:00
parent 091a9ae2c7
commit 2788c37492
12 changed files with 149 additions and 27 deletions

View File

@ -26,6 +26,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
* `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md). * `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md).
* Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats. * Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats.
* Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt. * Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt.
* Total minutes online is now tracked for users. Of course, it only starts after you get the update :)
## 0.0.8-alpha ## 0.0.8-alpha

View File

@ -989,9 +989,9 @@
pointsSGR: "|12" pointsSGR: "|12"
textSGR: "|00|03" textSGR: "|00|03"
globalTextSGR: "|03" globalTextSGR: "|03"
boardName: "|10" boardNameSGR: "|10"
userName: "|11" userNameSGR: "|11"
achievedValue: "|15" achievedValueSGR: "|15"
} }
overrides: { overrides: {

View File

@ -307,29 +307,60 @@
match: { match: {
1: { 1: {
title: "Nevermind!" title: "Nevermind!"
globalText: "{userName} ran a door for {achievedValue!durationSeconds}. Guess it's not their thing!" globalText: "{userName} ran a door for {achievedValue!durationMinutes}. Guess it's not their thing!"
text: "You ran a door for only {achievedValue!durationSeconds}. Not your thing?" text: "You ran a door for only {achievedValue!durationMinutes}. Not your thing?"
points: 5 points: 5
} }
10: { 10: {
title: "It's OK I Guess" title: "It's OK I Guess"
globalText: "{userName} ran a door for {achievedValue!durationSeconds}!" globalText: "{userName} ran a door for {achievedValue!durationMinutes}!"
text: "You ran a door for {achievedValue!durationSeconds}!" text: "You ran a door for {achievedValue!durationMinutes}!"
points: 10 points: 10
} }
30: { 30: {
title: "Good Game" title: "Good Game"
globalText: "{userName} ran a door for {achievedValue!durationSeconds}!" globalText: "{userName} ran a door for {achievedValue!durationMinutes}!"
text: "You ran a door for {achievedValue!durationSeconds}!" text: "You ran a door for {achievedValue!durationMinutes}!"
points: 20 points: 20
} }
60: { 60: {
title: "Textmode Dragon Slayer" title: "Textmode Dragon Slayer"
globalText: "{userName} has spent {achievedValue!durationSeconds} in a door!" globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!"
text: "You've spent {achievedValue!durationSeconds} in a door!" text: "You've spent {achievedValue!durationMinutes} in a door!"
points: 25 points: 25
} }
} }
} }
user_total_system_online_minutes: {
type: userStatSet
statName: minutes_online_total_count
match: {
30: {
title: "Just Poking Around"
globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!"
text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
points: 5
}
60: {
title: "Mildly Interesting"
globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!"
text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
points: 15
}
120: {
title: "Nothing Better to Do"
globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!"
text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
points: 25
}
1440: {
title: "Idle Bot"
globalText: "{userName} is probably a bot. They've spent {achievedValue!durationMinutes} on {boardName}!"
text: "You're a bot, aren't you? You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
points: 50
}
}
}
} }
} }

View File

@ -406,19 +406,23 @@ class Achievements {
getFormattedTextFor(info, textType, defaultSgr = '|07') { getFormattedTextFor(info, textType, defaultSgr = '|07') {
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {});
const defSgr = themeDefaults[`${textType}SGR`] || defaultSgr; const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr;
const wrap = (fieldName, value) => { const formatObj = this.getFormatObject(info);
return `${themeDefaults[fieldName] || defSgr}${value}${defSgr}`;
const wrap = (input) => {
const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g');
return input.replace(re, (m, formatVar, formatOpts) => {
const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr;
let r = `${varSgr}{${formatVar}`;
if(formatOpts) {
r += formatOpts;
}
return `${r}}${textTypeSgr}`;
});
}; };
let formatObj = this.getFormatObject(info); return stringFormat(`${textTypeSgr}${wrap(info.details[textType])}`, formatObj);
formatObj = _.reduce(formatObj, (out, v, k) => {
out[k] = wrap(k, v);
return out;
}, {});
return stringFormat(`${defSgr}${info.details[textType]}`, formatObj);
} }
createAchievementInterruptItems(info, cb) { createAchievementInterruptItems(info, cb) {

View File

@ -1004,7 +1004,7 @@ function peg$parse(input, options) {
TW : function termWidth() { TW : function termWidth() {
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
}, },
ID : function isUserId(value) { ID : function isUserId() {
if(!user) { if(!user) {
return false; return false;
} }
@ -1024,6 +1024,20 @@ function peg$parse(input, options) {
const midnight = now.clone().startOf('day') const midnight = now.clone().startOf('day')
const minutesPastMidnight = now.diff(midnight, 'minutes'); const minutesPastMidnight = now.diff(midnight, 'minutes');
return !isNaN(value) && minutesPastMidnight >= value; return !isNaN(value) && minutesPastMidnight >= value;
},
AC : function achievementCount() {
if(!user) {
return false;
}
const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0;
return !isNan(value) && points >= value;
},
AP : function achievementPoints() {
if(!user) {
return false;
}
const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0;
return !isNan(value) && points >= value;
} }
}[acsCode](value); }[acsCode](value);
} catch (e) { } catch (e) {

View File

@ -23,6 +23,8 @@
// General // General
// * http://en.wikipedia.org/wiki/ANSI_escape_code // * http://en.wikipedia.org/wiki/ANSI_escape_code
// * http://www.inwap.com/pdp10/ansicode.txt // * http://www.inwap.com/pdp10/ansicode.txt
// * Excellent information with many standards covered (for hterm):
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/doc/ControlSequences.md
// //
// Other Implementations // Other Implementations
// * https://github.com/chjj/term.js/blob/master/src/term.js // * https://github.com/chjj/term.js/blob/master/src/term.js

View File

@ -40,6 +40,7 @@ const MenuStack = require('./menu_stack.js');
const ACS = require('./acs.js'); const ACS = require('./acs.js');
const Events = require('./events.js'); const Events = require('./events.js');
const UserInterruptQueue = require('./user_interrupt_queue.js'); const UserInterruptQueue = require('./user_interrupt_queue.js');
const UserProps = require('./user_property.js');
// deps // deps
const stream = require('stream'); const stream = require('stream');
@ -442,13 +443,36 @@ Client.prototype.startIdleMonitor = function() {
// //
// Every 1m, check for idle. // Every 1m, check for idle.
// We also update minutes spent online the system here,
// if we have a authenticated user.
// //
this.idleCheck = setInterval( () => { this.idleCheck = setInterval( () => {
const nowMs = Date.now(); const nowMs = Date.now();
const idleLogoutSeconds = this.user.isAuthenticated() ? let idleLogoutSeconds;
Config().users.idleLogoutSeconds : if(this.user.isAuthenticated()) {
Config().users.preAuthIdleLogoutSeconds; idleLogoutSeconds = Config().users.idleLogoutSeconds;
//
// We don't really want to be firing off an event every 1m for
// every user, but want at least some updates for various things
// such as achievements. Send off every 5m.
//
const minOnline = this.user.incrementProperty(UserProps.MinutesOnlineTotalCount, 1);
if(0 === (minOnline % 5)) {
Events.emit(
Events.getSystemEvents().UserStatIncrement,
{
user : this.user,
statName : UserProps.MinutesOnlineTotalCount,
statIncrementBy : 1,
statValue : minOnline
}
);
}
} else {
idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds;
}
if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) {
this.emit('idle timeout'); this.emit('idle timeout');
@ -473,6 +497,14 @@ Client.prototype.end = function () {
currentModule.leave(); currentModule.leave();
} }
// persist time online for authenticated users
if(this.user.isAuthenticated()) {
this.user.persistProperty(
UserProps.MinutesOnlineTotalCount,
this.user.getProperty(UserProps.MinutesOnlineTotalCount)
);
}
this.stopIdleMonitor(); this.stopIdleMonitor();
try { try {

View File

@ -160,6 +160,10 @@ const PREDEFINED_MCI_GENERATORS = {
const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0; const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0;
return moment.duration(minutes, 'minutes').humanize(); return moment.duration(minutes, 'minutes').humanize();
}, },
TO : function friendlyTotalTimeOnSystem(client) {
const minutes = client.user.properties[UserProps.MinutesOnlineTotalCount] || 0;
return moment.duration(minutes, 'minutes').humanize();
},
// //
// Date/Time // Date/Time

View File

@ -443,6 +443,22 @@ module.exports = class User {
); );
} }
setProperty(propName, propValue) {
this.properties[propName] = propValue;
}
incrementProperty(propName, incrementBy) {
incrementBy = incrementBy || 1;
let newValue = parseInt(this.getProperty(propName));
if(newValue) {
newValue += incrementBy;
} else {
newValue = incrementBy;
}
this.setProperty(propName, newValue);
return newValue;
}
getProperty(propName) { getProperty(propName) {
return this.properties[propName]; return this.properties[propName];
} }

View File

@ -55,5 +55,7 @@ module.exports = {
AchievementTotalCount : 'achievement_total_count', AchievementTotalCount : 'achievement_total_count',
AchievementTotalPoints : 'achievement_total_points', AchievementTotalPoints : 'achievement_total_points',
MinutesOnlineTotalCount : 'minutes_online_total_count',
}; };

View File

@ -34,7 +34,9 @@ The following are ACS codes available as of this writing:
| NR<i>ratio</i> | User has upload/download count ratio >= _ratio_ | | NR<i>ratio</i> | User has upload/download count ratio >= _ratio_ |
| KR<i>ratio</i> | User has a upload/download byte ratio >= _ratio_ | | KR<i>ratio</i> | User has a upload/download byte ratio >= _ratio_ |
| PC<i>ratio</i> | User has a post/call ratio >= _ratio_ | | PC<i>ratio</i> | User has a post/call ratio >= _ratio_ |
| MM<i>minutes</i> | It is currently >= _minutes_ past midnight (system time) | MM<i>minutes</i> | It is currently >= _minutes_ past midnight (system time) |
| AC<i>achievementCount</i> | User has >= _achievementCount_ achievements |
| AP<i>achievementPoints</i> | User has >= _achievementPoints_ achievement points |
\* Many more ACS codes are planned for the near future. \* Many more ACS codes are planned for the near future.

View File

@ -160,7 +160,7 @@
TW : function termWidth() { TW : function termWidth() {
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
}, },
ID : function isUserId(value) { ID : function isUserId() {
if(!user) { if(!user) {
return false; return false;
} }
@ -180,6 +180,20 @@
const midnight = now.clone().startOf('day') const midnight = now.clone().startOf('day')
const minutesPastMidnight = now.diff(midnight, 'minutes'); const minutesPastMidnight = now.diff(midnight, 'minutes');
return !isNaN(value) && minutesPastMidnight >= value; return !isNaN(value) && minutesPastMidnight >= value;
},
AC : function achievementCount() {
if(!user) {
return false;
}
const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0;
return !isNan(value) && points >= value;
},
AP : function achievementPoints() {
if(!user) {
return false;
}
const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0;
return !isNan(value) && points >= value;
} }
}[acsCode](value); }[acsCode](value);
} catch (e) { } catch (e) {