diff --git a/WHATSNEW.md b/WHATSNEW.md
index 01fc8edc..39dfca49 100644
--- a/WHATSNEW.md
+++ b/WHATSNEW.md
@@ -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).
* 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.
+* Total minutes online is now tracked for users. Of course, it only starts after you get the update :)
## 0.0.8-alpha
diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson
index 1dfeac55..99164a2b 100644
--- a/art/themes/luciano_blocktronics/theme.hjson
+++ b/art/themes/luciano_blocktronics/theme.hjson
@@ -989,9 +989,9 @@
pointsSGR: "|12"
textSGR: "|00|03"
globalTextSGR: "|03"
- boardName: "|10"
- userName: "|11"
- achievedValue: "|15"
+ boardNameSGR: "|10"
+ userNameSGR: "|11"
+ achievedValueSGR: "|15"
}
overrides: {
diff --git a/config/achievements.hjson b/config/achievements.hjson
index ba050673..63ea5ee1 100644
--- a/config/achievements.hjson
+++ b/config/achievements.hjson
@@ -307,29 +307,60 @@
match: {
1: {
title: "Nevermind!"
- globalText: "{userName} ran a door for {achievedValue!durationSeconds}. Guess it's not their thing!"
- text: "You ran a door for only {achievedValue!durationSeconds}. Not your thing?"
+ globalText: "{userName} ran a door for {achievedValue!durationMinutes}. Guess it's not their thing!"
+ text: "You ran a door for only {achievedValue!durationMinutes}. Not your thing?"
points: 5
}
10: {
title: "It's OK I Guess"
- globalText: "{userName} ran a door for {achievedValue!durationSeconds}!"
- text: "You ran a door for {achievedValue!durationSeconds}!"
+ globalText: "{userName} ran a door for {achievedValue!durationMinutes}!"
+ text: "You ran a door for {achievedValue!durationMinutes}!"
points: 10
}
30: {
title: "Good Game"
- globalText: "{userName} ran a door for {achievedValue!durationSeconds}!"
- text: "You ran a door for {achievedValue!durationSeconds}!"
+ globalText: "{userName} ran a door for {achievedValue!durationMinutes}!"
+ text: "You ran a door for {achievedValue!durationMinutes}!"
points: 20
}
60: {
title: "Textmode Dragon Slayer"
- globalText: "{userName} has spent {achievedValue!durationSeconds} in a door!"
- text: "You've spent {achievedValue!durationSeconds} in a door!"
+ globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!"
+ text: "You've spent {achievedValue!durationMinutes} in a door!"
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
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/core/achievement.js b/core/achievement.js
index ccd43a54..ad7644ba 100644
--- a/core/achievement.js
+++ b/core/achievement.js
@@ -406,19 +406,23 @@ class Achievements {
getFormattedTextFor(info, textType, defaultSgr = '|07') {
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {});
- const defSgr = themeDefaults[`${textType}SGR`] || defaultSgr;
+ const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr;
- const wrap = (fieldName, value) => {
- return `${themeDefaults[fieldName] || defSgr}${value}${defSgr}`;
+ const formatObj = this.getFormatObject(info);
+
+ 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);
- formatObj = _.reduce(formatObj, (out, v, k) => {
- out[k] = wrap(k, v);
- return out;
- }, {});
-
- return stringFormat(`${defSgr}${info.details[textType]}`, formatObj);
+ return stringFormat(`${textTypeSgr}${wrap(info.details[textType])}`, formatObj);
}
createAchievementInterruptItems(info, cb) {
diff --git a/core/acs_parser.js b/core/acs_parser.js
index d6983b17..d4084b95 100644
--- a/core/acs_parser.js
+++ b/core/acs_parser.js
@@ -1004,7 +1004,7 @@ function peg$parse(input, options) {
TW : function termWidth() {
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
},
- ID : function isUserId(value) {
+ ID : function isUserId() {
if(!user) {
return false;
}
@@ -1024,6 +1024,20 @@ function peg$parse(input, options) {
const midnight = now.clone().startOf('day')
const minutesPastMidnight = now.diff(midnight, 'minutes');
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);
} catch (e) {
diff --git a/core/ansi_term.js b/core/ansi_term.js
index f00fd011..353c46c8 100644
--- a/core/ansi_term.js
+++ b/core/ansi_term.js
@@ -23,6 +23,8 @@
// General
// * http://en.wikipedia.org/wiki/ANSI_escape_code
// * 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
// * https://github.com/chjj/term.js/blob/master/src/term.js
diff --git a/core/client.js b/core/client.js
index 300285a3..894119bf 100644
--- a/core/client.js
+++ b/core/client.js
@@ -40,6 +40,7 @@ const MenuStack = require('./menu_stack.js');
const ACS = require('./acs.js');
const Events = require('./events.js');
const UserInterruptQueue = require('./user_interrupt_queue.js');
+const UserProps = require('./user_property.js');
// deps
const stream = require('stream');
@@ -442,13 +443,36 @@ Client.prototype.startIdleMonitor = function() {
//
// Every 1m, check for idle.
+ // We also update minutes spent online the system here,
+ // if we have a authenticated user.
//
this.idleCheck = setInterval( () => {
const nowMs = Date.now();
- const idleLogoutSeconds = this.user.isAuthenticated() ?
- Config().users.idleLogoutSeconds :
- Config().users.preAuthIdleLogoutSeconds;
+ let idleLogoutSeconds;
+ if(this.user.isAuthenticated()) {
+ 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)) {
this.emit('idle timeout');
@@ -473,6 +497,14 @@ Client.prototype.end = function () {
currentModule.leave();
}
+ // persist time online for authenticated users
+ if(this.user.isAuthenticated()) {
+ this.user.persistProperty(
+ UserProps.MinutesOnlineTotalCount,
+ this.user.getProperty(UserProps.MinutesOnlineTotalCount)
+ );
+ }
+
this.stopIdleMonitor();
try {
diff --git a/core/predefined_mci.js b/core/predefined_mci.js
index 39e339cc..2e7ed5ff 100644
--- a/core/predefined_mci.js
+++ b/core/predefined_mci.js
@@ -160,6 +160,10 @@ const PREDEFINED_MCI_GENERATORS = {
const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0;
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
diff --git a/core/user.js b/core/user.js
index ba89b387..3b261dc6 100644
--- a/core/user.js
+++ b/core/user.js
@@ -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) {
return this.properties[propName];
}
diff --git a/core/user_property.js b/core/user_property.js
index a1489e82..56e47e66 100644
--- a/core/user_property.js
+++ b/core/user_property.js
@@ -55,5 +55,7 @@ module.exports = {
AchievementTotalCount : 'achievement_total_count',
AchievementTotalPoints : 'achievement_total_points',
+
+ MinutesOnlineTotalCount : 'minutes_online_total_count',
};
diff --git a/docs/configuration/acs.md b/docs/configuration/acs.md
index 1ed83bb5..d0a45d06 100644
--- a/docs/configuration/acs.md
+++ b/docs/configuration/acs.md
@@ -34,7 +34,9 @@ The following are ACS codes available as of this writing:
| NRratio | User has upload/download count ratio >= _ratio_ |
| KRratio | User has a upload/download byte ratio >= _ratio_ |
| PCratio | User has a post/call ratio >= _ratio_ |
-| MMminutes | It is currently >= _minutes_ past midnight (system time)
+| MMminutes | It is currently >= _minutes_ past midnight (system time) |
+| ACachievementCount | User has >= _achievementCount_ achievements |
+| APachievementPoints | User has >= _achievementPoints_ achievement points |
\* Many more ACS codes are planned for the near future.
diff --git a/misc/acs_parser.pegjs b/misc/acs_parser.pegjs
index bd6a8d96..8a39deea 100644
--- a/misc/acs_parser.pegjs
+++ b/misc/acs_parser.pegjs
@@ -160,7 +160,7 @@
TW : function termWidth() {
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
},
- ID : function isUserId(value) {
+ ID : function isUserId() {
if(!user) {
return false;
}
@@ -180,6 +180,20 @@
const midnight = now.clone().startOf('day')
const minutesPastMidnight = now.diff(midnight, 'minutes');
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);
} catch (e) {