"use strict";
var mv = function(mv) {
mv.parser = {
records: [],
fullyDefinedCutoff: function() { return fullyDefinedCutoff; },
parseRecords: parseRecords,
parseScrubbed: parseScrubbed,
postProcessing: postProcessing,
createBlobLink: createBlobLink
};
/* The most recent information of a pc's stat */
var pcstat = {};
/*
* The first recorded state of a pc's stat.
* This is saved for a second pass, in which instances unknown at the time can have the pc's stat applied.
*/
var firstpcstat = {};
/*
* The time stamp of the last unknown instance.
*/
var fullyDefinedCutoff = 0;
/*
* 0: No mob just died
* Positive: A mob just died, this is its ID.
*/
var killedMobID = 0;
/*
* mob ID -> { mobClass, numAttackers, player IDs -> { total, weapon names -> { sum damage } } }
*/
var combat = {};
function combatPerformed(mobClass, target, pc, wpn, damage) {
/* Update combat state */
var mobData = combat[target] || (combat[target] = { mobClass: mobClass });
var pcData;
if (pc in mobData) {
pcData = mobData[pc];
} else {
(++mobData.numAttackers) || (mobData.numAttackers = 1);
pcData = mobData[pc] = {};
}
(pcData[wpn] += damage) || (pcData[wpn] = damage);
(pcData.total += damage) || (pcData.total = damage);
}
function freeMob() {
/* We no longer need detailed information on the mob's combat. */
if (!killedMobID) {
return;
}
delete combat[killedMobID];
killedMobID = 0;
}
function parseRecords(data) {
var spl = data.split(/\r?\n/);
spl.forEach(function(e) {
/* Check for each of the record types we're looking for. */
if (checkDmg(e) || checkMobMobDmg(e) || checkStat(e)) {
/* We have a record that has nothing to do with killed mobs, so no mob just died. */
freeMob();
} else {
checkXP(e) || checkMobDeath(e);
/* These functions deal directly with killed mobs and will handle setting or clearing of the ID internally. */
}
});
};
function checkXP(e) {
/* Try to parse an XP record. */
var d = e.match(/^(\d+\.\d+|\d+-\d+-\d+ \d+:\d+:\d+\.\d+):? PC(\d+) ([^,]+):(\d+),(\d+) GAINXP (\d+) (\d+) (\w+)/);;
if (!d) {
return false;
}
/* We have an XP record. */
/* Map's server ID. */
var mapSID = parseInt(d[3]);
/* Record timestamp. */
var ts = new Date(0);
ts.setUTCSeconds(d[1]) || (ts = new Date(d[1])); /* Backwards compatability - older logs used unix timestamps. */
var rec = {
date: ts,
pc: parseInt(d[2]),
map: map.nameByServerID(parseInt(d[3]), ts),
x: parseInt(d[4]),
y: parseInt(d[5]),
e: parseInt(d[6]),
j: parseInt(d[7]),
type: d[8],
pcstat: pcstat[d[2]],
target: "UNKNOWN",
dmg: 0,
wpn: "UNKNOWN",
numAttackers: 0
};
if (pcstat[d[2]] == undefined && (!fullyDefinedCutoff || ts > fullyDefinedCutoff)) {
/* Undefined, and newer than any existing definedness cutoff */
fullyDefinedCutoff = ts;
}
if (rec.type == "KILLXP") {
if (killedMobID && killedMobID in combat && rec.pc in combat[killedMobID]) {
var mob = combat[killedMobID];
rec.target = mob.mobClass;
rec.numAttackers = mob.numAttackers || 0;
var weapons = mob[rec.pc];
/* We have the needed information. */
rec.dmg = weapons.total;
var maxDamage = 0;
for (var key in weapons) {
if (key == "total") {
continue;
}
if (weapons[key] > maxDamage) {
maxDamage = weapons[key];
rec.wpn = key;
}
}
}
} else {
freeMob();
}
mv.parser.records.push(rec);
return true;
}
function checkStat(e) {
var d = e.match(/^(?:\d+\.\d+) PC(\d+) (?:[^,]+):(?:\d+),(?:\d+) STAT (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) /);
if (!d) {
return false;
}
var s = {
str: parseInt(d[2]),
agi: parseInt(d[3]),
vit: parseInt(d[4]),
int: parseInt(d[5]),
dex: parseInt(d[6]),
luk: parseInt(d[7])
};
/* The base level is not logged. Derive the minimum needed base level for one to have the stats that they have. */
s.blvl = stat.minLevelForStats(s.str, s.agi, s.vit, s.int, s.dex, s.luk);
/* Round the stats down to groups of 10. Accurate enough to identify trends and hot spots, fuzzy enough to make unique identification hard. */
s.str = Math.floor(s.str / 10);
s.agi = Math.floor(s.agi / 10);
s.vit = Math.floor(s.vit / 10);
s.int = Math.floor(s.int / 10);
s.dex = Math.floor(s.dex / 10);
s.luk = Math.floor(s.luk / 10);
/* Record these. */
if (!(d[1] in firstpcstat)) {
firstpcstat[d[1]] = s;
}
pcstat[d[1]] = s;
return true;
}
function checkDmg(e) {
var d = e.match(/^(\d+\.\d+|\d+-\d+-\d+ \d+:\d+:\d+\.\d+):? PC(\d+) ([^,]+):(\d+),(\d+) ([A-Z]+)DMG MOB(\d+) (\d+) FOR (\d+) (?:WPN|BY) ([^ ]+)/);
if (!d) {
return false;
}
/* Parse out values */
var mobClass = mob.nameByServerID(d[8]);
var target = parseInt(d[7]);
var pc = parseInt(d[2]);
var wpn = d[6] == "SPELL" ? d[10] : item.nameByServerID(d[10]);
var damage = parseInt(d[9]);
combatPerformed(mobClass, target, pc, wpn, damage);
return true;
}
function checkMobMobDmg(e) {
var d = e.match(/^(\d+\.\d+|\d+-\d+-\d+ \d+:\d+:\d+\.\d+):? PC(\d+) ([^,]+):(\d+),(\d+) MOB-TO-MOB-DMG FROM MOB(\d+) (\d+) TO MOB(\d+) (\d+) FOR (\d+)/);
if (!d) {
return false;
}
/* Parse out values */
var mobClass = mob.nameByServerID(d[9]);
var target = parseInt(d[8]);
var pc = parseInt(d[2]);
var wpn = mob.nameByServerID(d[7]);
var damage = parseInt(d[10]);
/* Update combat state */
combatPerformed(mobClass, target, pc, wpn, damage);
return true;
}
function checkMobDeath(e) {
var d = e.match(/^(\d+\.\d+|\d+-\d+-\d+ \d+:\d+:\d+\.\d+):? MOB(\d+) DEAD/);
if (!d) {
return false;
}
killedMobID = parseInt(d[2]);
return true;
}
function postProcessing() {
/* Scrub reference to pc id, and scan up until the fully defined cutoff line, assigning the pcstat from those that logged off */
var i = 0;
/* This name has way too many warts; suggestions for a replacement welcome! */
var postProcessedfullyDefinedCutoff = 0;
for (; i != mv.parser.records.length && mv.parser.records[i].date <= fullyDefinedCutoff; ++i) {
/* See if we've found out what the stats were from information logged after the record. */
if (mv.parser.records[i].pc in firstpcstat) {
mv.parser.records[i].pcstat = firstpcstat[mv.parser.records[i].pc];
} else {
/* If not, adjust the fully defined cutoff. */
postProcessedfullyDefinedCutoff = mv.parser.records[i].date;
}
/* Remove references to pc from these records. */
delete mv.parser.records[i].pc;
}
/* Remove references to pc from the remaining records. */
for (; i != mv.parser.records.length; ++i) {
delete mv.parser.records[i].pc;
}
fullyDefinedCutoff = postProcessedfullyDefinedCutoff;
}
function createBlobLink() {
/* Make the scrubbed data available for download as a blob. */
var blob = new Blob([JSON.stringify(mv.parser.records)]);
var a = d3.select('body').append('a');
a
.text("Scrubbed records")
.attr("download", "map.scrubbed")
.attr("href", window.URL.createObjectURL(blob))
;
}
function parseScrubbed(scrubbedRecords) {
scrubbedRecords = JSON.parse(scrubbedRecords);
/*
* The work is mostly all done for us. Just scan through to see if there
* are any undefined records, and update the pointer if so.
*/
/*
* Note that because we do not have the IDs, we cannot do a second pass
* to see if there is any information outside of the file that would
* tell us what the stats are, because we do not have that information.
* We can only get as good as what we were given!
*/
for (var i = 0; i != scrubbedRecords.length; ++i) {
scrubbedRecords[i].date = new Date(scrubbedRecords[i].date);
if (scrubbedRecords[i].pcstat == undefined && (!fullyDefinedCutoff || scrubbedRecords[i].date > fullyDefinedCutoff)) {
fullyDefinedCutoff = scrubbedRecords[i].date;
}
}
/* It's simple when everything's already been done. */
mv.parser.records = mv.parser.records.concat(scrubbedRecords);
}
return mv;
}(mv || {});