WIP on user achievements
+ Add MCI codes for points/count + Add docs for MCI codes + Record in stats, stat log, etc. * Do not trigger more than once * Code cleanup & organization, add classes, etc. * Tweaks to DB table
This commit is contained in:
parent
6410637359
commit
c332b0f3ec
|
@ -5,58 +5,192 @@
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const UserDb = require('./database.js').dbs.user;
|
const UserDb = require('./database.js').dbs.user;
|
||||||
|
const {
|
||||||
|
getISOTimestampString
|
||||||
|
} = require('./database.js');
|
||||||
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||||
const {
|
const {
|
||||||
getConnectionByUserId
|
getConnectionByUserId
|
||||||
} = require('./client_connections.js');
|
} = require('./client_connections.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const {
|
||||||
|
Errors,
|
||||||
|
ErrorReasons
|
||||||
|
} = require('./enig_error.js');
|
||||||
const { getThemeArt } = require('./theme.js');
|
const { getThemeArt } = require('./theme.js');
|
||||||
const { pipeToAnsi } = require('./color_codes.js');
|
const { pipeToAnsi } = require('./color_codes.js');
|
||||||
const stringFormat = require('./string_format.js');
|
const stringFormat = require('./string_format.js');
|
||||||
|
const StatLog = require('./stat_log.js');
|
||||||
|
const Log = require('./logger.js').log;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
|
class Achievement {
|
||||||
|
constructor(data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static factory(data) {
|
||||||
|
let achievement;
|
||||||
|
switch(data.type) {
|
||||||
|
case Achievement.Types.UserStat : achievement = new UserStatAchievement(data); break;
|
||||||
|
default : return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(achievement.isValid()) {
|
||||||
|
return achievement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get Types() {
|
||||||
|
return {
|
||||||
|
UserStat : 'userStat',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
switch(this.data.type) {
|
||||||
|
case Achievement.Types.UserStat :
|
||||||
|
if(!_.isString(this.data.statName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(!_.isObject(this.data.match)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default : return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMatchDetails(/*matchAgainst*/) {
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidMatchDetails(details) {
|
||||||
|
if(!_.isString(details.title) || !_.isString(details.text) || !_.isNumber(details.points)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (_.isString(details.globalText) || !details.globalText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserStatAchievement extends Achievement {
|
||||||
|
constructor(data) {
|
||||||
|
super(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
if(!super.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !Object.keys(this.data.match).some(k => !parseInt(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMatchDetails(matchValue) {
|
||||||
|
let matchField = Object.keys(this.data.match || {}).sort( (a, b) => b - a).find(v => matchValue >= v);
|
||||||
|
if(matchField) {
|
||||||
|
const match = this.data.match[matchField];
|
||||||
|
if(this.isValidMatchDetails(match)) {
|
||||||
|
return [ match, parseInt(matchField), matchValue ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Achievements {
|
class Achievements {
|
||||||
constructor(events) {
|
constructor(events) {
|
||||||
this.events = events;
|
this.events = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
init(cb) {
|
init(cb) {
|
||||||
|
// :TODO: if enabled/etc., load achievements.hjson -> if theme achievements.hjson{}, merge @ display time?
|
||||||
|
// merge for local vs global (per theme) clients
|
||||||
|
// ...only merge/override text
|
||||||
this.monitorUserStatUpdateEvents();
|
this.monitorUserStatUpdateEvents();
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAchievementHitCount(user, achievementTag, field, value, cb) {
|
loadAchievementHitCount(user, achievementTag, field, cb) {
|
||||||
UserDb.get(
|
UserDb.get(
|
||||||
`SELECT COUNT() AS count
|
`SELECT COUNT() AS count
|
||||||
FROM user_achievement
|
FROM user_achievement
|
||||||
WHERE user_id = ? AND achievement_tag = ? AND match_field = ? AND match_value >= ?;`,
|
WHERE user_id = ? AND achievement_tag = ? AND match_field = ?;`,
|
||||||
[ user.userId, achievementTag, field, value ],
|
[ user.userId, achievementTag, field],
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
return cb(err, row && row.count || 0);
|
return cb(err, row && row.count || 0);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record(info, cb) {
|
||||||
|
StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1);
|
||||||
|
StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points);
|
||||||
|
|
||||||
|
UserDb.run(
|
||||||
|
`INSERT INTO user_achievement (user_id, achievement_tag, timestamp, match_field, match_value)
|
||||||
|
VALUES (?, ?, ?, ?, ?);`,
|
||||||
|
[ info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField, info.matchValue ],
|
||||||
|
err => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Events.emit(
|
||||||
|
Events.getSystemEvents().UserAchievementEarned,
|
||||||
|
{
|
||||||
|
user : info.client.user,
|
||||||
|
achievementTag : info.achievementTag,
|
||||||
|
points : info.details.points,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
display(info, cb) {
|
||||||
|
this.createAchievementInterruptItems(info, (err, interruptItems) => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(interruptItems.local) {
|
||||||
|
UserInterruptQueue.queue(interruptItems.local, { clients : info.client } );
|
||||||
|
}
|
||||||
|
|
||||||
|
if(interruptItems.global) {
|
||||||
|
UserInterruptQueue.queue(interruptItems.global, { omit : info.client } );
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
monitorUserStatUpdateEvents() {
|
monitorUserStatUpdateEvents() {
|
||||||
this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => {
|
this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => {
|
||||||
|
if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const statValue = parseInt(userStatEvent.statValue, 10);
|
const statValue = parseInt(userStatEvent.statValue, 10);
|
||||||
if(isNaN(statValue)) {
|
if(isNaN(statValue)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = Config();
|
const config = Config();
|
||||||
|
// :TODO: Make this code generic - find + return factory created object
|
||||||
const achievementTag = _.findKey(
|
const achievementTag = _.findKey(
|
||||||
_.get(config, 'userAchievements.achievements', {}),
|
_.get(config, 'userAchievements.achievements', {}),
|
||||||
achievement => {
|
achievement => {
|
||||||
if(false === achievement.enabled) {
|
if(false === achievement.enabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return 'userStat' === achievement.type &&
|
return Achievement.Types.UserStat === achievement.type &&
|
||||||
achievement.statName === userStatEvent.statName;
|
achievement.statName === userStatEvent.statName;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -65,20 +199,24 @@ class Achievements {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const achievement = config.userAchievements.achievements[achievementTag];
|
const achievement = Achievement.factory(config.userAchievements.achievements[achievementTag]);
|
||||||
let matchValue = Object.keys(achievement.match || {}).sort( (a, b) => b - a).find(v => statValue >= v);
|
if(!achievement) {
|
||||||
if(matchValue) {
|
return;
|
||||||
const details = achievement.match[matchValue];
|
}
|
||||||
matchValue = parseInt(matchValue);
|
|
||||||
|
|
||||||
async.series(
|
const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue);
|
||||||
|
if(!details || _.isUndefined(matchField) || _.isUndefined(matchValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
[
|
[
|
||||||
(callback) => {
|
(callback) => {
|
||||||
this.loadAchievementHitCount(userStatEvent.user, achievementTag, null, matchValue, (err, count) => {
|
this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => {
|
||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
return callback(count > 0 ? Errors.General('Achievement already acquired') : null);
|
return callback(count > 0 ? Errors.General('Achievement already acquired', ErrorReasons.TooMany) : null);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(callback) => {
|
(callback) => {
|
||||||
|
@ -88,31 +226,33 @@ class Achievements {
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
|
achievementTag,
|
||||||
achievement,
|
achievement,
|
||||||
details,
|
details,
|
||||||
client,
|
client,
|
||||||
value : matchValue,
|
matchField,
|
||||||
|
matchValue,
|
||||||
user : userStatEvent.user,
|
user : userStatEvent.user,
|
||||||
timestamp : moment(),
|
timestamp : moment(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.createAchievementInterruptItems(info, (err, interruptItems) => {
|
return callback(null, info);
|
||||||
if(err) {
|
},
|
||||||
return callback(err);
|
(info, callback) => {
|
||||||
}
|
this.record(info, err => {
|
||||||
|
return callback(err, info);
|
||||||
if(interruptItems.local) {
|
|
||||||
UserInterruptQueue.queue(interruptItems.local, { clients : client } );
|
|
||||||
}
|
|
||||||
|
|
||||||
if(interruptItems.global) {
|
|
||||||
UserInterruptQueue.queue(interruptItems.global, { omit : client } );
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
(info, callback) => {
|
||||||
|
return this.display(info, callback);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
if(err && ErrorReasons.TooMany !== err.reasonCode) {
|
||||||
|
Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +273,8 @@ class Achievements {
|
||||||
title : info.details.title,
|
title : info.details.title,
|
||||||
text : info.global ? info.details.globalText : info.details.text,
|
text : info.global ? info.details.globalText : info.details.text,
|
||||||
points : info.details.points,
|
points : info.details.points,
|
||||||
value : info.value,
|
matchField : info.matchField,
|
||||||
|
matchValue : info.matchValue,
|
||||||
timestamp : moment(info.timestamp).format(dateTimeFormat),
|
timestamp : moment(info.timestamp).format(dateTimeFormat),
|
||||||
boardName : config.general.boardName,
|
boardName : config.general.boardName,
|
||||||
};
|
};
|
||||||
|
@ -175,12 +316,12 @@ class Achievements {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
(callback) => {
|
(callback) => {
|
||||||
getArt('header', headerArt => {
|
getArt(`${itemType}Header`, headerArt => {
|
||||||
return callback(null, headerArt);
|
return callback(null, headerArt);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(headerArt, callback) => {
|
(headerArt, callback) => {
|
||||||
getArt('footer', footerArt => {
|
getArt(`${itemType}Footer`, footerArt => {
|
||||||
return callback(null, headerArt, footerArt);
|
return callback(null, headerArt, footerArt);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -191,7 +332,7 @@ class Achievements {
|
||||||
pause : true,
|
pause : true,
|
||||||
};
|
};
|
||||||
if(headerArt || footerArt) {
|
if(headerArt || footerArt) {
|
||||||
interruptItems[itemType].contents = `${headerArt || ''}\r\n${pipeToAnsi(itemText)}\r\n${footerArt || ''}`;
|
interruptItems[itemType].contents = `${headerArt || ''}\r\n${pipeToAnsi(title)}\r\n${pipeToAnsi(itemText)}\r\n${footerArt || ''}`;
|
||||||
}
|
}
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1009,8 +1009,10 @@ function getDefaultConfig() {
|
||||||
enabled : true,
|
enabled : true,
|
||||||
|
|
||||||
art : {
|
art : {
|
||||||
header : 'achievement_header',
|
localHeader : 'achievement_local_header',
|
||||||
footer : 'achievement_footer',
|
localFooter : 'achievement_local_footer',
|
||||||
|
globalHeader : 'achievement_global_header',
|
||||||
|
globalFooter : 'achievement_global_footer',
|
||||||
},
|
},
|
||||||
|
|
||||||
// :TODO: achievements should be a path/filename -> achievements.hjson & allow override/theming
|
// :TODO: achievements should be a path/filename -> achievements.hjson & allow override/theming
|
||||||
|
@ -1023,20 +1025,20 @@ function getDefaultConfig() {
|
||||||
match : {
|
match : {
|
||||||
10 : {
|
10 : {
|
||||||
title : 'Return Caller',
|
title : 'Return Caller',
|
||||||
globalText : '{userName} has logged in {value} times!',
|
globalText : '{userName} has logged in {matchValue} times!',
|
||||||
text : 'You\'ve logged in {value} times!',
|
text : 'You\'ve logged in {matchValue} times!',
|
||||||
points : 5,
|
points : 5,
|
||||||
},
|
},
|
||||||
25 : {
|
25 : {
|
||||||
title : 'Seems To Like It!',
|
title : 'Seems To Like It!',
|
||||||
globalText : '{userName} has logged in {value} times!',
|
globalText : '{userName} has logged in {matchValue} times!',
|
||||||
text : 'You\'ve logged in {value} times!',
|
text : 'You\'ve logged in {matchValue} times!',
|
||||||
points : 10,
|
points : 10,
|
||||||
},
|
},
|
||||||
100 : {
|
100 : {
|
||||||
title : '{boardName} Addict',
|
title : '{boardName} Addict',
|
||||||
globalText : '{userName} the BBS {boardName} addict has logged in {value} times!',
|
globalText : '{userName} the BBS {boardName} addict has logged in {matchValue} times!',
|
||||||
text : 'You\'re a {boardName} addict! You\'ve logged in {value} times!',
|
text : 'You\'re a {boardName} addict! You\'ve logged in {matchValue} times!',
|
||||||
points : 10,
|
points : 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,7 +196,7 @@ const DB_INIT_TABLE = {
|
||||||
timestamp DATETIME NOT NULL,
|
timestamp DATETIME NOT NULL,
|
||||||
match_field VARCHAR NOT NULL,
|
match_field VARCHAR NOT NULL,
|
||||||
match_value VARCHAR NOT NULL,
|
match_value VARCHAR NOT NULL,
|
||||||
UNIQUE(user_id, achievement_tag, match_field, match_value),
|
UNIQUE(user_id, achievement_tag, match_field),
|
||||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
|
|
|
@ -90,7 +90,7 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
return moment(client.user.properties[UserProps.Birthdate]).format(client.currentTheme.helpers.getDateFormat());
|
return moment(client.user.properties[UserProps.Birthdate]).format(client.currentTheme.helpers.getDateFormat());
|
||||||
},
|
},
|
||||||
US : function sex(client) { return userStatAsString(client, UserProps.Sex, ''); },
|
US : function sex(client) { return userStatAsString(client, UserProps.Sex, ''); },
|
||||||
UE : function emailAddres(client) { return userStatAsString(client, UserProps.EmailAddress, ''); },
|
UE : function emailAddress(client) { return userStatAsString(client, UserProps.EmailAddress, ''); },
|
||||||
UW : function webAddress(client) { return userStatAsString(client, UserProps.WebAddress, ''); },
|
UW : function webAddress(client) { return userStatAsString(client, UserProps.WebAddress, ''); },
|
||||||
UF : function affils(client) { return userStatAsString(client, UserProps.Affiliations, ''); },
|
UF : function affils(client) { return userStatAsString(client, UserProps.Affiliations, ''); },
|
||||||
UT : function themeName(client) {
|
UT : function themeName(client) {
|
||||||
|
@ -122,7 +122,7 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
return getUserRatio(client, UserProps.FileUlTotalBytes, UserProps.FileDlTotalBytes);
|
return getUserRatio(client, UserProps.FileUlTotalBytes, UserProps.FileDlTotalBytes);
|
||||||
},
|
},
|
||||||
|
|
||||||
MS : function accountCreatedclient(client) {
|
MS : function accountCreated(client) {
|
||||||
return moment(client.user.properties[UserProps.AccountCreated]).format(client.currentTheme.helpers.getDateFormat());
|
return moment(client.user.properties[UserProps.AccountCreated]).format(client.currentTheme.helpers.getDateFormat());
|
||||||
},
|
},
|
||||||
PS : function userPostCount(client) { return userStatAsString(client, UserProps.MessagePostCount, 0); },
|
PS : function userPostCount(client) { return userStatAsString(client, UserProps.MessagePostCount, 0); },
|
||||||
|
@ -152,6 +152,9 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
SH : function termHeight(client) { return client.term.termHeight.toString(); },
|
SH : function termHeight(client) { return client.term.termHeight.toString(); },
|
||||||
SW : function termWidth(client) { return client.term.termWidth.toString(); },
|
SW : function termWidth(client) { return client.term.termWidth.toString(); },
|
||||||
|
|
||||||
|
AC : function achievementCount(client) { return userStatAsString(client, UserProps.AchievementTotalCount, 0); },
|
||||||
|
AP : function achievementPoints(client) { return userStatAsString(client, UserProps.AchievementTotalPoints, 0); },
|
||||||
|
|
||||||
//
|
//
|
||||||
// Date/Time
|
// Date/Time
|
||||||
//
|
//
|
||||||
|
@ -166,7 +169,7 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
OS : function operatingSystem() {
|
OS : function operatingSystem() {
|
||||||
return {
|
return {
|
||||||
linux : 'Linux',
|
linux : 'Linux',
|
||||||
darwin : 'Mac OS X',
|
darwin : 'OS X',
|
||||||
win32 : 'Windows',
|
win32 : 'Windows',
|
||||||
sunos : 'SunOS',
|
sunos : 'SunOS',
|
||||||
freebsd : 'FreeBSD',
|
freebsd : 'FreeBSD',
|
||||||
|
|
|
@ -359,6 +359,7 @@ class StatLog {
|
||||||
systemEvents.UserUpload, systemEvents.UserDownload,
|
systemEvents.UserUpload, systemEvents.UserDownload,
|
||||||
systemEvents.UserPostMessage, systemEvents.UserSendMail,
|
systemEvents.UserPostMessage, systemEvents.UserSendMail,
|
||||||
systemEvents.UserRunDoor, systemEvents.UserSendNodeMsg,
|
systemEvents.UserRunDoor, systemEvents.UserSendNodeMsg,
|
||||||
|
systemEvents.UserAchievementEarned,
|
||||||
];
|
];
|
||||||
|
|
||||||
Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => {
|
Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => {
|
||||||
|
|
|
@ -22,4 +22,5 @@ module.exports = {
|
||||||
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 }
|
UserStatUpdate : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue }
|
||||||
|
UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // {..., achievementTag, points }
|
||||||
};
|
};
|
||||||
|
|
|
@ -49,5 +49,8 @@ module.exports = {
|
||||||
MessageConfTag : 'message_conf_tag',
|
MessageConfTag : 'message_conf_tag',
|
||||||
MessageAreaTag : 'message_area_tag',
|
MessageAreaTag : 'message_area_tag',
|
||||||
MessagePostCount : 'post_count',
|
MessagePostCount : 'post_count',
|
||||||
|
|
||||||
|
AchievementTotalCount : 'achievement_total_count',
|
||||||
|
AchievementTotalPoints : 'achievement_total_points',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
|------|--------------|
|
|------|--------------|
|
||||||
| `BN` | Board Name |
|
| `BN` | Board Name |
|
||||||
| `VL` | Version *label*, e.g. "ENiGMA½ v0.0.3-alpha" |
|
| `VL` | Version *label*, e.g. "ENiGMA½ v0.0.9-alpha" |
|
||||||
| `VN` | Version *number*, eg.. "0.0.3-alpha" |
|
| `VN` | Version *number*, eg.. "0.0.9-alpha" |
|
||||||
| `SN` | SysOp username |
|
| `SN` | SysOp username |
|
||||||
| `SR` | SysOp real name |
|
| `SR` | SysOp real name |
|
||||||
| `SL` | SysOp location |
|
| `SL` | SysOp location |
|
||||||
|
@ -30,7 +30,7 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et
|
||||||
| `UR` | Current user's real name |
|
| `UR` | Current user's real name |
|
||||||
| `LO` | Current user's location |
|
| `LO` | Current user's location |
|
||||||
| `UA` | Current user's age |
|
| `UA` | Current user's age |
|
||||||
| `BD` | Current user's birthdate (using theme date format) |
|
| `BD` | Current user's birthday (using theme date format) |
|
||||||
| `US` | Current user's sex |
|
| `US` | Current user's sex |
|
||||||
| `UE` | Current user's email address |
|
| `UE` | Current user's email address |
|
||||||
| `UW` | Current user's web address |
|
| `UW` | Current user's web address |
|
||||||
|
@ -58,6 +58,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et
|
||||||
| `CM` | Current user's active message conference description |
|
| `CM` | Current user's active message conference description |
|
||||||
| `SH` | Current user's term height |
|
| `SH` | Current user's term height |
|
||||||
| `SW` | Current user's term width |
|
| `SW` | Current user's term width |
|
||||||
|
| `AC` | Current user's total achievements |
|
||||||
|
| `AP` | Current user's total achievement points |
|
||||||
| `DT` | Current date (using theme date format) |
|
| `DT` | Current date (using theme date format) |
|
||||||
| `CT` | Current time (using theme time format) |
|
| `CT` | Current time (using theme time format) |
|
||||||
| `OS` | System OS (Linux, Windows, etc.) |
|
| `OS` | System OS (Linux, Windows, etc.) |
|
||||||
|
@ -149,7 +151,7 @@ Standard style types available for `textStyle` and `focusTextStyle`:
|
||||||
| `mixed` | EnIGma BUlLEtIn BoaRd SOfTWarE (randomly assigned) |
|
| `mixed` | EnIGma BUlLEtIn BoaRd SOfTWarE (randomly assigned) |
|
||||||
| `l33t` | 3n1gm4 bull371n b04rd 50f7w4r3 |
|
| `l33t` | 3n1gm4 bull371n b04rd 50f7w4r3 |
|
||||||
|
|
||||||
### Entry Fromatting
|
### Entry Formatting
|
||||||
Various strings can be formatted using a syntax that allows width & precision specifiers, text styling, etc. Depending on the context, various elements can be referenced by `{name}`. Additional text styles can be supplied as well. The syntax is largely modeled after Python's [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language).
|
Various strings can be formatted using a syntax that allows width & precision specifiers, text styling, etc. Depending on the context, various elements can be referenced by `{name}`. Additional text styles can be supplied as well. The syntax is largely modeled after Python's [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language).
|
||||||
|
|
||||||
### Additional Text Styles
|
### Additional Text Styles
|
||||||
|
|
Loading…
Reference in New Issue