#!/bin/sh # tmww plugin: activity # whatis: player online activity statistics # conflicts: - # depends: alts # recommends: log # this file is part of tmww - the mana world watcher # willee, 2012-2014 # GPL v3 # check if not run as plugin if [ "$TMWW_PLUGINS" != "yes" ] ; then echo >&2 "This script is tmww plugin and rely heavily on it's facilities." exit 1 fi help_activity() { cat << EOF activity -- player online activity statistics - examine registered logon/logoff timeline - compare online presence of given characters - display daily (in hours) and monthly (in days) relative online presence - display average online presence per day of week and per hour Command line options: subcommand: lastseen -- timeline of logon/logoff events options: a:c:C:p:x:n:d:f:t: subcommand: daily -- daily (in hours) online presence options: rsa:c:C:p:x:d:f:t: subcommand: monthly -- monthly (in days) online presence options: rsa:c:C:p:x:d:m:f:t: subcommand: average -- average online presence per day of week and per hour options: rsa:c:C:p:x:d:m:f:t: Common options: [ -n N ] -- limit output by N lines; default to 1 for all commands [ -r ] -- show ruler [ -s ] -- split stats and ruler with space after each 10 chars - time options: [ { -d | -m } N ] -- during N last days/month [ -f yyyy-mm[-dd] ] -- start interval [ -t yyyy-mm[-dd] ] -- end interval. defaults to current day if omitted - target options: [ -a ACCID ] -- account ID [ -c CHARNAME ] -- character [ -C CHARNAME ] -- all chars on account (account by char) [ -p PLAYER ] -- all chars on player [ -x CHARNAME ] -- exclude CHARNAME from result chars list EOF } [ "$TMWW_PLUGINHELP" = "yes" ] && help_activity && return 0 requireplugin activity.lib.sh || return 1 # # aux # # func_activity_chars() { chnames=$( aux_client_chars ) || return 1 [ -z "${chnames}" ] && return 1 check_dir "${TMWW_PRIVTMP}" set_pattern_lock printf "%s" "${activity_excluded_chars}" | sort > "${activity_patt_file}" chnames=$( printf "%s" "${chnames}" | sort | \ comm_23 - "${activity_patt_file}" ) [ -z "${chnames}" ] && return 1 printf "%s\n" "${chnames}" > "${activity_patt_file}" # debug # printf "%s\n" "${chnames}" } aux_activity_parse() { case "$1" in monthly) intern_activity_init_hourly="-1" intern_activity_process_days="if (prev_month!=current_month) dump_monthly(); dump_dayofmonth()" intern_activity_rotate_days="monthly_aux()" intern_activity_output_functions="dump_dayofmonth(); dump_monthly()" ;; daily) intern_activity_init_hourly="-1" intern_activity_process_days="dump_daily()" intern_activity_rotate_days="target_day = get_target_day()" intern_activity_output_functions="dump_daily()" ;; average) intern_activity_init_hourly="0" intern_activity_process_days="dump_weekly()" intern_activity_rotate_days="target_day = get_target_day()" intern_activity_output_functions="dump_weekly(); show_weekly()" ;; *) return 1; ;; esac cd "${TMWW_VERSIONREPORT}/${servername}" && ${AWK} ${AWKPARAMS} " BEGIN { fname=\"\" for (i=1; i<8; i++) weekly[i]=0 for (i=0; i<24; i++) hourly[i]=${intern_activity_init_hourly}; split(\"\",recordtime) for (i=0; i<24; i++) average_hourly[i]=0 for (i=1; i<32; i++) monthly[i]=-1; prev_month=\"\" dow[1]=\"Mon\"; dow[2]=\"Tue\"; dow[3]=\"Wed\"; dow[4]=\"Thu\"; dow[5]=\"Fri\"; dow[6]=\"Sat\"; dow[7]=\"Sun\"; chartimestamp=-1 # record time rtime=0; rhour=-1 # uninitialized state prevhour=-1; prevepoch=0 # rtime, r* mean record*; prevtime - same from previous file # (one day = one file) } function debug_dump_hourly(m) { printf \"%s %d %d\n\", m, prevhour, rhour printf \"hourly dump \" for (j=0;j<24;j++) printf \"%s \",hourly[j] printf \"\\n\" } function get_target_day() { cmd=\"date -d \\\"\" FILENAME \"\\\" -u +%u\"; sub(\"/\",\"-\",cmd); sub(\"\\\\.yml\",\"\",cmd) cmd | getline day return day } function get_name_of_day() { return dow[target_day] } function get_epoch() { cmd=\"date -d \\\"\" FILENAME \"\\\" -u +%s\"; sub(\"/\",\"-\",cmd); sub(\"\\\\.yml\",\"\",cmd) cmd | getline ep return ep } # checking if date is valid (e.g. not 31 Feb) # could be ignored with date_with_day interval function check_epoch() { epoch=get_epoch() if (epoch==\"\") nextfile # 60*60*24 = 86400 if (epoch - prevepoch > 86500) { for (i in charonoff) charonoff[i]=0 } prevepoch = epoch } # assuming max of 150 players per logon function logon() { split(\$0,chars,/\"/); for ( i = 2 ; i <= 300 ; i += 2 ) { if ( chars[i] == \"\" ) return 0 thischar=chars[i] if ( thischar in charonoff ) { for (i in charonoff) if (charonoff[i]==1) return if (chartimestamp=-1) chartimestamp=rtime charonoff[thischar]=1 # debug # debug_dump_hourly(\"match logon\") } } } # assuming max of 150 players per logoff function logoff() { split(\$0,chars,/\"/); counttime=0 for ( i = 2 ; i <= 300 ; i += 2 ) { if ( chars[i] == \"\" ) break thischar=chars[i] if (( thischar in charonoff) && (charonoff[thischar]==1)) { charonoff[thischar]=0; counttime=1 } } if (counttime) { for (i in charonoff) if (charonoff[i]==1) return hourly[rhour] += rtime - chartimestamp # debug # debug_dump_hourly(\"match logoff\") } } # increase hourly index, add hourly time online for chars function dump_hourly() { if (hourly[prevhour]==-1) hourly[prevhour]=0 counttime=0 for (i in charonoff) if (charonoff[i]==1) { counttime=1; break; } if (counttime) { if (rhour == 0) hourly[prevhour] += 86400 - chartimestamp else hourly[prevhour] += rhour*3600 - chartimestamp # cut hourly result if exceeds 3600 # (e.g. when idle limit is more than hour # and combines results from few prev hours) if ( hourly[prevhour] > 3600 ) hourly[prevhour] = 3600 chartimestamp=rhour*3600 } # debug # debug_dump_hourly(\"dump_hourly\") } function dump_weekly() { for (i=0; i<24; i++) weekly[target_day] += hourly[i] for (i=0; i<24; i++) average_hourly[i] += hourly[i] for (i=0; i<24; i++) hourly[i]=0 } function show_weekly() { for (i=1; i<8; i++) printf \"%s \", weekly[i] printf \"\\n\" for (i=0; i<24; i++) printf \"%s \", average_hourly[i] } function dump_daily() { fn=fname; sub(\"\\\\.yml\",\"\",fn); print fn, get_name_of_day() for (i=0; i<24; i++) printf \"%s \", hourly[i] printf \"\\n\"; for (i=0; i<24; i++) hourly[i]=-1 } function dump_dayofmonth() { td=sprintf(\"%i\",day_of_month); monthly[td]=0 for (i=0; i<24; i++) if (hourly[i]>0) monthly[td] += hourly[i] for (i=0; i<24; i++) hourly[i]=-1 } function dump_monthly() { printf \"%s\\n\", prev_month for (i=1; i<32; i++) printf \"%s \", monthly[i] printf \"\\n\" prev_month=current_month for (i=1; i<32; i++) monthly[i]=-1 } function monthly_aux() { day_of_month = FILENAME; sub(\"^.*/\",\"\",day_of_month); sub(\"\\\\.yml\",\"\",day_of_month) current_month=FILENAME; sub(\"/.*\$\",\"\",current_month) if (prev_month==\"\") prev_month=current_month } FNR==NR { charonoff[\$0]=0; next } { if (fname != FILENAME) { check_epoch() if (fname != \"\") { # rhour = prevhour + 1 ${intern_activity_process_days} # if (rhour != -1) rhour=0 rtime=0; prevhour=0; prevtime=0 } chartimestamp=0; fname = FILENAME ${intern_activity_rotate_days} } } # debug # { printf \"record: \"; print } /^- time/ { sub(\"^[^\\\"]*\\\"\",\"\"); sub(\"\\\".*\$\",\"\") split(\$0,recordtime,\":\") rtime=recordtime[1]*3600+recordtime[2]*60+recordtime[3] rhour=int(recordtime[1]) if ((rtime-prevtime) > 23*60*60+50*60) { rtime = 0; rhour = 0 } # idle limit - max difference between time records: should reset online marks if ((rtime - prevtime) > ${internal_activity_idle_limit}*60 ) for (i in charonoff) charonoff[i]=0 if (prevhour!=rhour) dump_hourly() prevhour=rhour; prevtime=rtime next } /^ *logon/ { logon() } /^ *logoff/ { logoff() } END { rhour = prevhour + 1 dump_hourly() ${intern_activity_output_functions} } " "${activity_patt_file}" ${interval} 2>/dev/null | aux_activity_output "$1" } aux_activity_output() { case "$1" in monthly) minspark=0; maxspark=86400 while read line; do printf "%s " "${line}"; read line spark $line | if [ "${TMWW_STATSPLIT}" = "yes" ]; then sed ${ESED} 's/(.{10})/\1 /g'; else cat; fi; echo done minspark=''; maxspark='' ;; daily) minspark=0; maxspark=3600 while read line; do printf "%s " "${line}"; read line spark $line | if [ "${TMWW_STATSPLIT}" = "yes" ]; then sed ${ESED} 's/(.{10})/\1 /g'; else cat; fi; echo #printf "~ %s\n" "${line}" done minspark=''; maxspark='' ;; average) read line; spark $line; printf " "; read line spark $line | if [ "${TMWW_STATSPLIT}" = "yes" ]; then sed ${ESED} 's/(.{10})/\1 /g'; else cat; fi; echo ;; esac } # # options parser # # activity_subcommand='' case "$1" in lastseen) activity_subcommand="lastseen" opts=a:c:C:p:x:n:d:f:t: ;; daily) activity_subcommand="daily" opts=rsa:c:C:p:x:d:f:t: ;; monthly) activity_subcommand="monthly" opts=rsa:c:C:p:x:d:m:f:t: ;; average) activity_subcommand="average" opts=rsa:c:C:p:x:d:m:f:t: ;; '') error_missing; return 1 ;; *) error_incorrect; return 1 ;; esac shift [ -z "$1" ] && { error_params "Not enough options. Aborting."; return 1; } client_method='' interval_method='' suppress_ruler='' activity_excluded_chars='' # next variable should be clean # in case same plugin was called before in same script interval_day='' interval_month='' interval_from='' interval_to='' OPTIND=1 while ${GETOPTS} ${opts} opt ; do case "${opt}" in a) client_value="$OPTARG" client_method="accid" ;; c) client_value="$OPTARG" client_method="charname" ;; C) client_value="$OPTARG" client_method="accbychar" ;; p) client_value="$OPTARG" client_method="player" ;; x) activity_excluded_chars=$( printf "%s\n%s\n" "$OPTARG" "${activity_excluded_chars}" ) ;; n) activity_lines_limit="$OPTARG" check_string_chars "${activity_lines_limit}" "*[!0-9]*" \ "Incorrect output lines limit. Aborting." || return 1 ;; f) interval_from="$OPTARG" [ -z "${interval_day}" -a -z "${interval_month}" ] || \ { error_params "Options conflict. Aborting"; return 1; } ;; t) interval_to="$OPTARG" [ -z "${interval_from}" ] && \ { error_params "No -f argument before -t. Aborting."; return 1; } ;; d) interval_day="$OPTARG" [ -z "${interval_from}" ] || \ { error_params "Options conflict. Aborting"; return 1; } check_string_chars "${interval_day}" "*[!0-9]*" \ "Incorrect days number. Aborting." || return 1 ;; m) interval_month="$OPTARG" [ -z "${interval_from}" ] || \ { error_params "Options conflict. Aborting"; return 1; } check_string_chars "${interval_month}" "*[!0-9]*" \ "Incorrect months number. Aborting." || return 1 ;; r) show_ruler="yes" ;; s) TMWW_STATSPLIT="yes" ;; *) error_incorrect; return 1 ;; esac done shift $(expr $OPTIND - 1) [ -z "$1" ] || { error_params "Too much arguments. Aborting."; return 1; } [ -z "${activity_lines_limit}" ] && activity_lines_limit=1 if [ -z "${interval_day}" -a -z "${interval_month}" -a -z "${interval_from}" ]; then if [ "${activity_subcommand}" != "lastseen" ]; then if [ "${activity_subcommand}" = "daily" ]; then interval_day=5 else interval_month=2 fi fi fi # # main # # interval='' case "${activity_subcommand}" in lastseen) [ -z "${client_method}" ] && { error_params "Target missing. Aborting."; return 1; } func_activity_chars || return 1 # still slow because of forming interval with checking every file from interval # but much better then with aux functions if [ -z "${interval_day}" -a -z "${interval_month}" -a -z "${interval_from}" -a \ "${activity_lines_limit}" -eq 1 ]; then cd "${TMWW_VERSIONREPORT}/${servername}" && find -maxdepth 1 -type d -name '*' | \ sed ${ESED} -n '/^\.\/[0-9]{4}-[0-9]{2}$/p' | sort -rn | while read line; do intern_lastseen() { find "$1" -maxdepth 1 -type f -name '*.yml' | \ sort -rn | while read i; do result=$( tac $i | ${AWK} ${AWKPARAMS} ' BEGIN { recordtime = 0; matched = 0 } function parser(message) { split($0,chars,/"/); for ( i = 2 ; i <= 200 ; i += 2 ) { if ( chars[i] == "" ) break if ( chars[i] in a ) { result = message " " chars[i] matched = 1; return } } } FNR==NR { a[$0]=1; next } /^ *logon/ { if (! matched) parser("on: ") } /^ *logoff/ { if (! matched) parser("off:") } /^- time/ { if (matched) { sub("^[^\"]*\"",""); sub("\".*$","") print $0, result; exit } } END { if (matched == 1) exit 0 ; else exit 1 } ' "${activity_patt_file}" - ) if [ $? -eq 0 ]; then i="${i#*/}"; i="${i%%.*}" printf "%s %s\n" "$i" "$result" unset_pattern_lock return 55 fi done } intern_lastseen "$line" [ $? -eq 55 ] && break done return unset_pattern_lock return fi if [ -z "${interval_day}" -a -z "${interval_month}" -a -z "${interval_from}" ]; then interval_day=${interval_day_limit} fi aux_form_interval || return 1 [ "${date_with_day}" = "yes" -o ! -z "$interval_day" ] || { error_params "Interval should be in days. Aborting."; return 1; } interval=$( printf "%s" "${interval}" | ${AWK} ${AWKPARAMS} -- '{for (i=NF;i>0;i--) printf "%s ",$i}') cd "${TMWW_VERSIONREPORT}/${servername}" && ${AWK} ${AWKPARAMS} ' BEGIN { recordtime = 0 } function parser(message) { split($0,chars,/"/); for ( i = 2 ; i <= 200 ; i += 2 ) { if ( chars[i] == "" ) break if ( chars[i] in a ) { sub("\\.yml","",FILENAME) print FILENAME, recordtime, message, chars[i] } } } FNR==NR { a[$0]=1; next } /^- time/ { sub("^[^\"]*\"",""); sub("\".*$",""); recordtime=$0; next } /^ *logon/ { parser("on: ") } /^ *logoff/ { parser("off:") } ' "${activity_patt_file}" ${interval} 2>/dev/null | tail -n "${activity_lines_limit}" unset_pattern_lock ;; average) [ -z "${client_method}" ] && { error_params "Target missing. Aborting."; return 1; } aux_form_interval || return 1 interval=$( printf "%s" "${interval}" | ${AWK} ${AWKPARAMS} -- '{for (i=NF;i>0;i--) printf "%s ",$i}') func_activity_chars || return 1 [ "${show_ruler}" = "yes" ] && { printf 'MTWTFSS ' echo '012345678901234567890123' | if [ "${TMWW_STATSPLIT}" = "yes" ]; then sed ${ESED} 's/(.{10})/\1 /g'; else cat; fi } aux_activity_parse "average" unset_pattern_lock ;; daily) [ -z "${client_method}" ] && { error_params "Target missing. Aborting."; return 1; } aux_form_interval || return 1 [ "${date_with_day}" = "yes" -o ! -z "$interval_day" ] || { error_params "Interval should be in days. Aborting."; return 1; } interval=$( printf "%s" "${interval}" | ${AWK} ${AWKPARAMS} -- '{for (i=NF;i>0;i--) printf "%s ",$i}') func_activity_chars || return 1 [ "${show_ruler}" = "yes" ] && { printf 'yyyy-mm/dd dow ' echo '012345678901234567890123' | if [ "${TMWW_STATSPLIT}" = "yes" ]; then sed ${ESED} 's/(.{10})/\1 /g'; else cat; fi } aux_activity_parse "daily" unset_pattern_lock ;; monthly) [ -z "${client_method}" ] && { error_params "Target missing. Aborting."; return 1; } aux_form_interval || return 1 interval=$( printf "%s" "${interval}" | ${AWK} ${AWKPARAMS} -- '{for (i=NF;i>0;i--) printf "%s ",$i}') func_activity_chars || return 1 [ "${show_ruler}" = "yes" ] && { printf 'yyyy-mm ' echo '1234567890123456789012345678901' | if [ "${TMWW_STATSPLIT}" = "yes" ]; then sed ${ESED} 's/(.{10})/\1 /g'; else cat; fi } aux_activity_parse "monthly" unset_pattern_lock ;; esac return 0