Filtering and plotting
Then we define a function that can be used to filter the dataset to a particular encounter and difficulty. It also performs outlier filtering - for each 5-ilvl bucket, for each spec, we compute the 95th percentile of DPS, then remove any parses from the dataset for that ilvl/spec above that cutoff.
get_encounter_data <- function(encounter_name, difficulty_name, dataset, cutoff=c(0.05, 0.95), bucket_size=1, min_ilvl=855, max_ilvl=875) {
specs <- unique(dataset$spec)
d <- filter(dataset, ilvl >= min_ilvl, ilvl <= max_ilvl, encounter == encounter_name, difficulty == difficulty_name, date >= cutoff_date)
for(bucket in seq(800, 900, by=bucket_size)) {
for(f_spec in specs) {
q <- quantile(d[d$spec == f_spec & d$ilvl >= bucket & d$ilvl < bucket + bucket_size,]$dps, cutoff)
if(!is.na(q[1])) {
d <- filter(d, !(spec == f_spec & ilvl >= bucket & ilvl < bucket + bucket_size & (dps > q[2] | dps < q[1])))
}
}
}
return(d)
}
Plotting the data is straightforward. Here we plot both boxplots for each spec/ilvl bucket, as well as a smoothed moving average and a linear trendline.
plot_encounter <- function(encounter_name, difficulty_name, dataset=data, bucket_size=2, cutoff=c(0.05, 0.95)) {
encounter_data <- get_encounter_data(encounter_name, difficulty_name, dataset=dataset, bucket_size=bucket_size, cutoff=cutoff)
long_data <- melt(encounter_data, id.vars=c("spec", "ilvl"), measure.vars=c("dps"))
p <- ggplot(data=long_data, aes(x=ilvl, y=value, group=spec, colour=spec)) +
geom_boxplot(aes(group=interaction(spec, floor(ilvl/bucket_size) * bucket_size))) +
geom_smooth() + # loess fit
geom_smooth(linetype = "dashed", method = "lm", se = FALSE) + # linear fit
labs(y="DPS", x="iLevel", title=paste("Performance:", encounter_name, difficulty_name))
print(p)
cat('\r\n\r\n')
p <- ggplot(data=long_data, aes(x=ilvl, group=spec, colour=spec, fill=spec)) + geom_bar(position="dodge") +
labs(y="Parses", x="iLevel", title=paste("Parses:", encounter_name, difficulty_name))
print(p)
cat('\r\n\r\n')
}
Results
plot_encounter("Nythendra", "Normal")
plot_encounter("Nythendra", "Heroic")
plot_encounter("Nythendra", "Mythic")
plot_encounter("Ursoc", "Normal")
plot_encounter("Ursoc", "Heroic")
plot_encounter("Ursoc", "Mythic")
plot_encounter("Elerethe_Renferal", "Normal")
plot_encounter("Elerethe_Renferal", "Heroic")
plot_encounter("Elerethe_Renferal", "Mythic")
plot_encounter("Il_gynoth__Heart_of_Corruption", "Normal")
plot_encounter("Il_gynoth__Heart_of_Corruption", "Heroic")
plot_encounter("Il_gynoth__Heart_of_Corruption", "Mythic")
plot_encounter("Dragons_of_Nightmare", "Normal")
plot_encounter("Dragons_of_Nightmare", "Heroic")
plot_encounter("Dragons_of_Nightmare", "Mythic")
plot_encounter("Cenarius", "Normal")
plot_encounter("Cenarius", "Heroic")
plot_encounter("Cenarius", "Mythic")
plot_encounter("Xavius", "Normal")
plot_encounter("Xavius", "Heroic")
plot_encounter("Xavius", "Mythic")
Scaling
So we have the raw (smoothed) damage progressions, but how about scaling?
plot_dps_gain <- function(encounter, difficulty, dataset=data, bucket_size=1, cutoff=c(0.05, 0.95)) {
d <- dataset[dataset$encounter == encounter & dataset$difficulty == difficulty & dataset$ilvl <= 870 & dataset$date >= cutoff_date, ]
d <- aggregate(d$dps, by=list(ilvl=d$ilvl, spec=d$spec), FUN=mean)
d$dps <- d$x
d <- d[order(d$ilvl),]
d$diff <- ave(d$dps, factor(d$spec), FUN=function(x) c(NA, diff(x)))
d <- filter(d, !is.na(diff))
ggplot(data=d, aes(x=ilvl, y=diff, group=interaction(spec, encounter), colour=spec)) + geom_smooth(se=FALSE, fullrange=TRUE) +
labs(y="DPS gain per tier", x="iLevel", title=paste(encounter, difficulty, sep = " ")) + coord_cartesian(ylim = c(0, 15000))
}
# Commented fights have too little data to produce anything useful
plot_dps_gain("Nythendra", "Normal")
plot_dps_gain("Nythendra", "Heroic")
plot_dps_gain("Nythendra", "Mythic")
plot_dps_gain("Ursoc", "Normal")
plot_dps_gain("Ursoc", "Heroic")
plot_dps_gain("Ursoc", "Mythic")
plot_dps_gain("Elerethe_Renferal", "Normal")
plot_dps_gain("Elerethe_Renferal", "Heroic")
plot_dps_gain("Elerethe_Renferal", "Mythic")
plot_dps_gain("Il_gynoth__Heart_of_Corruption", "Normal")
plot_dps_gain("Il_gynoth__Heart_of_Corruption", "Heroic")
# plot_dps_gain("il_gynoth__heart_of_corruption", "Mythic")
plot_dps_gain("Dragons_of_Nightmare", "Normal")
plot_dps_gain("Dragons_of_Nightmare", "Heroic")
# plot_dps_gain("Dragons_of_Nightmare", "Mythic")
plot_dps_gain("Cenarius", "Normal")
plot_dps_gain("Cenarius", "Heroic")
# plot_dps_gain("cenarius", "Mythic")
plot_dps_gain("Xavius", "Normal")
plot_dps_gain("Xavius", "Heroic")
# plot_dps_gain("xavius", "Mythic")
Finally, here’s a really blunt linear regression of each encounter, with the scaling factor (slope of the regression line) plotted:
d <- data
m <- d %>% group_by(spec, encounter) %>% do(tidy(lm(dps ~ ilvl, data=.)))
slopes <- m[m$term == "ilvl",]
slopes$spec <- factor(slopes$spec, levels = slopes$spec[order(slopes$estimate)])
duplicated levels in factors are deprecated
ggplot(data=slopes, aes(x=encounter, y=estimate, group=interaction(spec, encounter), reorder=estimate, fill=spec)) + geom_bar(position="dodge", stat="identity") +
labs(y="DPS gain/ilvl", x="Encounter", title="DPS scaling per ilvl: Overall")
LS0tDQp0aXRsZTogIldhcnJpb3Igc3BlYyBzY2FsaW5nIg0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOiANCiAgICBmaWdfaGVpZ2h0OiA2DQogICAgZmlnX3dpZHRoOiAxOA0KICBwZGZfZG9jdW1lbnQ6IGRlZmF1bHQNCi0tLQ0KDQojIyBGZXRjaGluZyB0aGUgZGF0YQ0KDQpEYXRhIGlzIGdyYWJiZWQgZnJvbSBXYXJjcmFmdExvZ3MgdmlhIHRoZWlyIEFQSS4gV2UgZ2V0IHRoZSB0b3AgMWsgcGFyc2VzIHBlciBzcGVjL2VuY291bnRlci9kaWZmaWN1bHR5Lg0KDQpTb3VyY2UgaXMgYXQgaHR0cHM6Ly9naXRodWIuY29tL2NoZWFsZC93YXJjcmFmdGxvZ3MtcGFyc2VzDQoNCmBgYA0KJCBBUElfS0VZPXlvdXJfa2V5X2hlcmUgcnVieSBncmFiYmVyLnJiIHdhcnJpb3INCmBgYA0KDQpPbmNlIHdlIGhhdmUgdGhlIEpTT04gZmlsZXMsIHdlIHVzZSBhIHNtYWxsIHNjcmlwdCB0byBib2lsIHRoZW0gZG93biBpbnRvIGEgQ1NWOg0KDQpgYGANCiQgQVBJX0tFWT15b3VyX2tleV9oZXJlIHJ1YnkgZGlzdGlsbC5yYiB3YXJyaW9yDQpgYGANCg0KVGhpcyByZXN1bHRzIGluIHRoZSBgY3N2L3dhcnJpb3IuY3N2YCB0aGF0IHdlIHRoZW4gY29uc3VtZSBpbiBSLg0KDQpPbmNlIGluIFIsIHdlIGxvYWQgaXQgdXA6DQoNCmBgYHtyfQ0KbGlicmFyeShyZXNoYXBlMikNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkoZHBseXIpDQpsaWJyYXJ5KGJyb29tKQ0KZGF0YSA8LSByZWFkLmNzdigiY3N2L3dhcnJpb3IuY3N2IikNCmRhdGEkZGF0ZSA8LSBhcy5EYXRlKGRhdGEkZGF0ZSkNCmN1dG9mZl9kYXRlID0gIjIwMTYtMTAtMTUiDQpgYGANCg0KIyMgRmlsdGVyaW5nIGFuZCBwbG90dGluZw0KDQpUaGVuIHdlIGRlZmluZSBhIGZ1bmN0aW9uIHRoYXQgY2FuIGJlIHVzZWQgdG8gZmlsdGVyIHRoZSBkYXRhc2V0IHRvIGEgcGFydGljdWxhciBlbmNvdW50ZXIgYW5kIGRpZmZpY3VsdHkuIEl0IGFsc28gcGVyZm9ybXMgb3V0bGllciBmaWx0ZXJpbmcgLSBmb3IgZWFjaCA1LWlsdmwgYnVja2V0LCBmb3IgZWFjaCBzcGVjLCB3ZSBjb21wdXRlIHRoZSA5NXRoIHBlcmNlbnRpbGUgb2YgRFBTLCB0aGVuIHJlbW92ZSBhbnkgcGFyc2VzIGZyb20gdGhlIGRhdGFzZXQgZm9yIHRoYXQgaWx2bC9zcGVjIGFib3ZlIHRoYXQgY3V0b2ZmLg0KDQpgYGB7cn0NCg0KZ2V0X2VuY291bnRlcl9kYXRhIDwtIGZ1bmN0aW9uKGVuY291bnRlcl9uYW1lLCBkaWZmaWN1bHR5X25hbWUsIGRhdGFzZXQsIGN1dG9mZj1jKDAuMDUsIDAuOTUpLCBidWNrZXRfc2l6ZT0xLCBtaW5faWx2bD04NTUsIG1heF9pbHZsPTg3NSkgew0KICBzcGVjcyA8LSB1bmlxdWUoZGF0YXNldCRzcGVjKQ0KICBkIDwtIGZpbHRlcihkYXRhc2V0LCBpbHZsID49IG1pbl9pbHZsLCBpbHZsIDw9IG1heF9pbHZsLCBlbmNvdW50ZXIgPT0gZW5jb3VudGVyX25hbWUsIGRpZmZpY3VsdHkgPT0gZGlmZmljdWx0eV9uYW1lLCBkYXRlID49IGN1dG9mZl9kYXRlKQ0KICBmb3IoYnVja2V0IGluIHNlcSg4MDAsIDkwMCwgYnk9YnVja2V0X3NpemUpKSB7DQogICAgZm9yKGZfc3BlYyBpbiBzcGVjcykgew0KICAgICAgcSA8LSBxdWFudGlsZShkW2Qkc3BlYyA9PSBmX3NwZWMgJiBkJGlsdmwgPj0gYnVja2V0ICYgZCRpbHZsIDwgYnVja2V0ICsgYnVja2V0X3NpemUsXSRkcHMsIGN1dG9mZikNCiAgICAgIGlmKCFpcy5uYShxWzFdKSkgew0KICAgICAgICBkIDwtIGZpbHRlcihkLCAhKHNwZWMgPT0gZl9zcGVjICYgaWx2bCA+PSBidWNrZXQgJiBpbHZsIDwgYnVja2V0ICsgYnVja2V0X3NpemUgJiAoZHBzID4gcVsyXSB8IGRwcyA8IHFbMV0pKSkNCiAgICAgIH0NCiAgICB9DQogIH0NCiAgcmV0dXJuKGQpDQp9DQpgYGANCg0KUGxvdHRpbmcgdGhlIGRhdGEgaXMgc3RyYWlnaHRmb3J3YXJkLiBIZXJlIHdlIHBsb3QgYm90aCBib3hwbG90cyBmb3IgZWFjaCBzcGVjL2lsdmwgYnVja2V0LCBhcyB3ZWxsIGFzIGEgc21vb3RoZWQgbW92aW5nIGF2ZXJhZ2UgYW5kIGEgbGluZWFyIHRyZW5kbGluZS4NCg0KYGBge3J9DQpwbG90X2VuY291bnRlciA8LSBmdW5jdGlvbihlbmNvdW50ZXJfbmFtZSwgZGlmZmljdWx0eV9uYW1lLCBkYXRhc2V0PWRhdGEsIGJ1Y2tldF9zaXplPTIsIGN1dG9mZj1jKDAuMDUsIDAuOTUpKSB7DQogIGVuY291bnRlcl9kYXRhIDwtIGdldF9lbmNvdW50ZXJfZGF0YShlbmNvdW50ZXJfbmFtZSwgZGlmZmljdWx0eV9uYW1lLCBkYXRhc2V0PWRhdGFzZXQsIGJ1Y2tldF9zaXplPWJ1Y2tldF9zaXplLCBjdXRvZmY9Y3V0b2ZmKQ0KDQogIGxvbmdfZGF0YSA8LSBtZWx0KGVuY291bnRlcl9kYXRhLCBpZC52YXJzPWMoInNwZWMiLCAiaWx2bCIpLCBtZWFzdXJlLnZhcnM9YygiZHBzIikpDQoNCiAgcCA8LSBnZ3Bsb3QoZGF0YT1sb25nX2RhdGEsIGFlcyh4PWlsdmwsIHk9dmFsdWUsIGdyb3VwPXNwZWMsIGNvbG91cj1zcGVjKSkgKw0KICAgIGdlb21fYm94cGxvdChhZXMoZ3JvdXA9aW50ZXJhY3Rpb24oc3BlYywgZmxvb3IoaWx2bC9idWNrZXRfc2l6ZSkgKiBidWNrZXRfc2l6ZSkpKSArDQogICAgZ2VvbV9zbW9vdGgoKSArICMgbG9lc3MgZml0DQogICAgZ2VvbV9zbW9vdGgobGluZXR5cGUgPSAiZGFzaGVkIiwgbWV0aG9kID0gImxtIiwgc2UgPSBGQUxTRSkgKyAjIGxpbmVhciBmaXQNCiAgICBsYWJzKHk9IkRQUyIsIHg9ImlMZXZlbCIsIHRpdGxlPXBhc3RlKCJQZXJmb3JtYW5jZToiLCBlbmNvdW50ZXJfbmFtZSwgZGlmZmljdWx0eV9uYW1lKSkNCiAgcHJpbnQocCkNCiAgY2F0KCdcclxuXHJcbicpDQogIA0KICBwIDwtIGdncGxvdChkYXRhPWxvbmdfZGF0YSwgYWVzKHg9aWx2bCwgZ3JvdXA9c3BlYywgY29sb3VyPXNwZWMsIGZpbGw9c3BlYykpICsgZ2VvbV9iYXIocG9zaXRpb249ImRvZGdlIikgKw0KICAgIGxhYnMoeT0iUGFyc2VzIiwgeD0iaUxldmVsIiwgdGl0bGU9cGFzdGUoIlBhcnNlczoiLCBlbmNvdW50ZXJfbmFtZSwgZGlmZmljdWx0eV9uYW1lKSkNCiAgcHJpbnQocCkNCiAgY2F0KCdcclxuXHJcbicpDQp9DQpgYGANCg0KIyMgUmVzdWx0cw0KDQoNCmBgYHtyLCBmaWcud2lkdGg9MTIsIGZpZy5oZWlnaHQ9Nn0NCg0KcGxvdF9lbmNvdW50ZXIoIk55dGhlbmRyYSIsICJOb3JtYWwiKQ0KcGxvdF9lbmNvdW50ZXIoIk55dGhlbmRyYSIsICJIZXJvaWMiKQ0KcGxvdF9lbmNvdW50ZXIoIk55dGhlbmRyYSIsICJNeXRoaWMiKQ0KDQpwbG90X2VuY291bnRlcigiVXJzb2MiLCAiTm9ybWFsIikNCnBsb3RfZW5jb3VudGVyKCJVcnNvYyIsICJIZXJvaWMiKQ0KcGxvdF9lbmNvdW50ZXIoIlVyc29jIiwgIk15dGhpYyIpDQoNCnBsb3RfZW5jb3VudGVyKCJFbGVyZXRoZV9SZW5mZXJhbCIsICJOb3JtYWwiKQ0KcGxvdF9lbmNvdW50ZXIoIkVsZXJldGhlX1JlbmZlcmFsIiwgIkhlcm9pYyIpDQpwbG90X2VuY291bnRlcigiRWxlcmV0aGVfUmVuZmVyYWwiLCAiTXl0aGljIikNCg0KcGxvdF9lbmNvdW50ZXIoIklsX2d5bm90aF9fSGVhcnRfb2ZfQ29ycnVwdGlvbiIsICJOb3JtYWwiKQ0KcGxvdF9lbmNvdW50ZXIoIklsX2d5bm90aF9fSGVhcnRfb2ZfQ29ycnVwdGlvbiIsICJIZXJvaWMiKQ0KcGxvdF9lbmNvdW50ZXIoIklsX2d5bm90aF9fSGVhcnRfb2ZfQ29ycnVwdGlvbiIsICJNeXRoaWMiKQ0KDQpwbG90X2VuY291bnRlcigiRHJhZ29uc19vZl9OaWdodG1hcmUiLCAiTm9ybWFsIikNCnBsb3RfZW5jb3VudGVyKCJEcmFnb25zX29mX05pZ2h0bWFyZSIsICJIZXJvaWMiKQ0KcGxvdF9lbmNvdW50ZXIoIkRyYWdvbnNfb2ZfTmlnaHRtYXJlIiwgIk15dGhpYyIpDQoNCnBsb3RfZW5jb3VudGVyKCJDZW5hcml1cyIsICJOb3JtYWwiKQ0KcGxvdF9lbmNvdW50ZXIoIkNlbmFyaXVzIiwgIkhlcm9pYyIpDQpwbG90X2VuY291bnRlcigiQ2VuYXJpdXMiLCAiTXl0aGljIikNCg0KcGxvdF9lbmNvdW50ZXIoIlhhdml1cyIsICJOb3JtYWwiKQ0KcGxvdF9lbmNvdW50ZXIoIlhhdml1cyIsICJIZXJvaWMiKQ0KcGxvdF9lbmNvdW50ZXIoIlhhdml1cyIsICJNeXRoaWMiKQ0KYGBgDQoNCiMjIyBTY2FsaW5nDQoNClNvIHdlIGhhdmUgdGhlIHJhdyAoc21vb3RoZWQpIGRhbWFnZSBwcm9ncmVzc2lvbnMsIGJ1dCBob3cgYWJvdXQgc2NhbGluZz8gDQoNCmBgYHtyLCBmaWcud2lkdGg9MTIsIGZpZy5oZWlnaHQ9Nn0NCnBsb3RfZHBzX2dhaW4gPC0gZnVuY3Rpb24oZW5jb3VudGVyLCBkaWZmaWN1bHR5LCBkYXRhc2V0PWRhdGEsIGJ1Y2tldF9zaXplPTEsIGN1dG9mZj1jKDAuMDUsIDAuOTUpKSB7DQogIGQgPC0gZGF0YXNldFtkYXRhc2V0JGVuY291bnRlciA9PSBlbmNvdW50ZXIgJiBkYXRhc2V0JGRpZmZpY3VsdHkgPT0gZGlmZmljdWx0eSAmIGRhdGFzZXQkaWx2bCA8PSA4NzAgJiBkYXRhc2V0JGRhdGUgPj0gY3V0b2ZmX2RhdGUsIF0NCiAgZCA8LSBhZ2dyZWdhdGUoZCRkcHMsIGJ5PWxpc3QoaWx2bD1kJGlsdmwsIHNwZWM9ZCRzcGVjKSwgRlVOPW1lYW4pDQogIGQkZHBzIDwtIGQkeA0KICBkIDwtIGRbb3JkZXIoZCRpbHZsKSxdDQogIGQkZGlmZiA8LSBhdmUoZCRkcHMsIGZhY3RvcihkJHNwZWMpLCBGVU49ZnVuY3Rpb24oeCkgYyhOQSwgZGlmZih4KSkpDQogIGQgPC0gZmlsdGVyKGQsICFpcy5uYShkaWZmKSkNCiAgZ2dwbG90KGRhdGE9ZCwgYWVzKHg9aWx2bCwgeT1kaWZmLCBncm91cD1pbnRlcmFjdGlvbihzcGVjLCBlbmNvdW50ZXIpLCBjb2xvdXI9c3BlYykpICsgZ2VvbV9zbW9vdGgoc2U9RkFMU0UsIGZ1bGxyYW5nZT1UUlVFKSArDQogICAgbGFicyh5PSJEUFMgZ2FpbiBwZXIgdGllciIsIHg9ImlMZXZlbCIsIHRpdGxlPXBhc3RlKGVuY291bnRlciwgZGlmZmljdWx0eSwgc2VwID0gIiAiKSkgKyBjb29yZF9jYXJ0ZXNpYW4oeWxpbSA9IGMoMCwgMTUwMDApKQ0KfQ0KDQojIENvbW1lbnRlZCBmaWdodHMgaGF2ZSB0b28gbGl0dGxlIGRhdGEgdG8gcHJvZHVjZSBhbnl0aGluZyB1c2VmdWwNCg0KcGxvdF9kcHNfZ2FpbigiTnl0aGVuZHJhIiwgIk5vcm1hbCIpDQpwbG90X2Rwc19nYWluKCJOeXRoZW5kcmEiLCAiSGVyb2ljIikNCnBsb3RfZHBzX2dhaW4oIk55dGhlbmRyYSIsICJNeXRoaWMiKQ0KDQpwbG90X2Rwc19nYWluKCJVcnNvYyIsICJOb3JtYWwiKQ0KcGxvdF9kcHNfZ2FpbigiVXJzb2MiLCAiSGVyb2ljIikNCnBsb3RfZHBzX2dhaW4oIlVyc29jIiwgIk15dGhpYyIpDQoNCnBsb3RfZHBzX2dhaW4oIkVsZXJldGhlX1JlbmZlcmFsIiwgIk5vcm1hbCIpDQpwbG90X2Rwc19nYWluKCJFbGVyZXRoZV9SZW5mZXJhbCIsICJIZXJvaWMiKQ0KcGxvdF9kcHNfZ2FpbigiRWxlcmV0aGVfUmVuZmVyYWwiLCAiTXl0aGljIikNCg0KcGxvdF9kcHNfZ2FpbigiSWxfZ3lub3RoX19IZWFydF9vZl9Db3JydXB0aW9uIiwgIk5vcm1hbCIpDQpwbG90X2Rwc19nYWluKCJJbF9neW5vdGhfX0hlYXJ0X29mX0NvcnJ1cHRpb24iLCAiSGVyb2ljIikNCiMgcGxvdF9kcHNfZ2FpbigiaWxfZ3lub3RoX19oZWFydF9vZl9jb3JydXB0aW9uIiwgIk15dGhpYyIpDQoNCnBsb3RfZHBzX2dhaW4oIkRyYWdvbnNfb2ZfTmlnaHRtYXJlIiwgIk5vcm1hbCIpDQpwbG90X2Rwc19nYWluKCJEcmFnb25zX29mX05pZ2h0bWFyZSIsICJIZXJvaWMiKQ0KIyBwbG90X2Rwc19nYWluKCJEcmFnb25zX29mX05pZ2h0bWFyZSIsICJNeXRoaWMiKQ0KDQpwbG90X2Rwc19nYWluKCJDZW5hcml1cyIsICJOb3JtYWwiKQ0KcGxvdF9kcHNfZ2FpbigiQ2VuYXJpdXMiLCAiSGVyb2ljIikNCiMgcGxvdF9kcHNfZ2FpbigiY2VuYXJpdXMiLCAiTXl0aGljIikNCg0KcGxvdF9kcHNfZ2FpbigiWGF2aXVzIiwgIk5vcm1hbCIpDQpwbG90X2Rwc19nYWluKCJYYXZpdXMiLCAiSGVyb2ljIikNCiMgcGxvdF9kcHNfZ2FpbigieGF2aXVzIiwgIk15dGhpYyIpDQoNCmBgYA0KDQpGaW5hbGx5LCBoZXJlJ3MgYSByZWFsbHkgYmx1bnQgbGluZWFyIHJlZ3Jlc3Npb24gb2YgZWFjaCBlbmNvdW50ZXIsIHdpdGggdGhlIHNjYWxpbmcgZmFjdG9yIChzbG9wZSBvZiB0aGUgcmVncmVzc2lvbiBsaW5lKSBwbG90dGVkOg0KDQpgYGB7ciwgZmlnLndpZHRoPTE2fQ0KZCA8LSBkYXRhDQptIDwtIGQgJT4lIGdyb3VwX2J5KHNwZWMsIGVuY291bnRlcikgJT4lIGRvKHRpZHkobG0oZHBzIH4gaWx2bCwgZGF0YT0uKSkpDQpzbG9wZXMgPC0gbVttJHRlcm0gPT0gImlsdmwiLF0NCnNsb3BlcyRzcGVjIDwtIGZhY3RvcihzbG9wZXMkc3BlYywgbGV2ZWxzID0gc2xvcGVzJHNwZWNbb3JkZXIoc2xvcGVzJGVzdGltYXRlKV0pDQpnZ3Bsb3QoZGF0YT1zbG9wZXMsIGFlcyh4PWVuY291bnRlciwgeT1lc3RpbWF0ZSwgZ3JvdXA9aW50ZXJhY3Rpb24oc3BlYywgZW5jb3VudGVyKSwgcmVvcmRlcj1lc3RpbWF0ZSwgZmlsbD1zcGVjKSkgKyBnZW9tX2Jhcihwb3NpdGlvbj0iZG9kZ2UiLCBzdGF0PSJpZGVudGl0eSIpICsNCiAgbGFicyh5PSJEUFMgZ2Fpbi9pbHZsIiwgeD0iRW5jb3VudGVyIiwgdGl0bGU9IkRQUyBzY2FsaW5nIHBlciBpbHZsOiBPdmVyYWxsIikNCmBgYA0K