Image Source: https://www.sportingnews.com/us/nfl/news/nfl-mvp-odds-stats-comparison-allen-jackson-burrow-barkley/ff4fb064efb79163d63b2f02
This is a project designed to predict the NFL MVP using machine
learning techniques. I will be repeating the process described by Ryan
Brill and Ryan Weisman in their 2021 paper
“Predicting
the Quarterback-MVP”, which uses logistic regression to predict
which QB is statistically most likely to receive the MVP title each
year.
Brill and Weisman use the following parameters in their model:
-
tot_td_rank: a rank of the QB’s total touchdowns
-
tot_yard_rank: a rank of the QB’s total yards (rushing + passing)
-
win_rank: a rank of the QB’s team’s number of wins
-
epa_rank: a rank of the QB’s expected points added per play
-
tot_ints: the number of interceptions thrown by the QB in a given year
Ranks are used over the number of touchdowns, yards, wins, and epa
due to stat inflation over time. To quote Brill and Weisman, “in 2003
Peyton Manning threw for a leaguehigh 4267 yards, whereas in 2016 Drew
Brees threw for a leaguehigh 5208 yards. Over the last 20 years, the
average number of passing yards has experienced inflation, but we don’t
want our model to think less of Manning in 2003.”
I will begin by loading the nflverse library, which will allow me to
load the statistics for teams, schedules and players in the years
between 2003 and 2024. I will also load dplyr and tidyr for their data
manipulation and cleaning functions.
library(nflverse)
library(dplyr)
library(tidyr)
Next, I’ll write a function to load the regular season statistics of
interest for all QBs. It’s highly unlikely for a backup quarterback to
be voted MVP, so I’ll limit my results to only quarterbacks who threw at
least 150 passes. This dataset differentiates between rushing, passing,
and receiving statistics, so I will sum rushing and passing to get total
yards, touchdowns, and EPA for each QB. While Bo Nix, Jared Goff, and
Josh Allen all had a receiving touchdown this year, I believe this
information has little impact on MVP voting, so I will not include
receiving stats in my model.
Brill and Weisman used win_rank as a predictor variable in their
model. I also calculated strength of victory (SOV), which I believe
tells a more complete story of a quarterback’s season. Strength of
victory is the sum of the win percentages of all teams a QB wins against
divided by the number of wins the QB’s team has. To calculate this, I
will use the schedule data to calculate each team’s wins, losses, and
ties. I’ll also calculate average completion percentage, an important
statistic for comparing quarterbacks. I calculate their completion
percentage per game as the number of completed passes divided by the
number of attempted passes, then average those values across the
season.
#This function inputs a year and returns the QB stats of interest for that season
gen_player_stats <- function(year){
#Loads all player data from the given year
players <- load_player_stats(year)
#Selects only the statistics of interest from all quarterbacks who attempted at least 150 passes
qb_data <- players[players$position == 'QB' & sum(players$attempts) >= 150 & players$season_type == 'REG', c('recent_team', 'player_display_name', 'week', 'completions', 'attempts', 'passing_tds', 'rushing_tds', 'passing_yards', 'rushing_yards', 'passing_epa', 'rushing_epa', 'interceptions', 'carries')]
#Summarizes the statistics to get season totals for all QBs
qb_data <- qb_data %>%
#Replaces NA numeric values with 0 (NA denotes the player did not play that week, which is unimportant for season totals)
replace_na(list(passing_tds = 0, rushing_tds = 0, passing_yards = 0, rushing_yards = 0, passing_epa = 0, rushing_epa = 0, interceptions = 0, carries = 0)) %>%
#Groups data by player name
group_by(recent_team, player_display_name) %>%
#Sums the numeric values for each week to get season totals
summarize(tot_td = sum(passing_tds + rushing_tds), tot_yard = sum(passing_yards + rushing_yards), epa = sum(passing_epa + rushing_epa), ints = sum(interceptions), avg_comp_pct = round(mean(completions/attempts), 4), avg_ypc = round(sum(rushing_yards)/sum(carries), 4)) %>%
#If name groups are present, rank will assign each quarterback rank 1 because they are the best performing QB with their name
#Ungroup removes the groupings in order to rank tds, yards, and epa among all QBs
ungroup() %>%
#Ranks total touchdowns, yards, and epa from highest to lowest
mutate(tot_td_rank = rank(-tot_td, ties.method = 'min'), tot_yard_rank = rank(-tot_yard, ties.method = 'min'), epa_rank = rank(-epa, ties.method = 'min'))
#Removes the total touchdowns, yards, and epa columns in favor of the rank columns
qb_data <- subset(qb_data, select = -c(tot_td, tot_yard, epa))
#Loads 2024 schedule
schedule <- load_schedules()
#Selects the season, week, teams, and scores for all regular season games
schedule <- schedule[schedule$game_type == 'REG' & schedule$season == year,c('season', 'week', 'away_team', 'away_score', 'home_team', 'home_score')]
#Loads team abbreviated name, team name, and division for all current teams
teams <- load_teams(current = FALSE)[,c('team_abbr', 'team_name', 'team_division')]
#Creates a column denoting which teams won each game (or NA if tie)
schedule$win <- if_else(schedule$home_score == schedule$away_score, 'Tie', if_else(schedule$home_score > schedule$away_score, schedule$home_team, schedule$away_team))
#Creates a column denoting which teams lost each each game (or NA if tie)
schedule$loss <- if_else(schedule$home_score == schedule$away_score, 'Tie', if_else(schedule$home_score < schedule$away_score, schedule$home_team, schedule$away_team))
#Creates a boolean column which is true if the game ends in a tie
schedule$tie <- schedule$home_score == schedule$away_score
#Adds columns counting each team's wins and losses to the teams dataframe
teams <- merge(teams, as.data.frame(table(schedule$win)), by.x = 'team_abbr', by.y = 'Var1', all = TRUE)
teams <- merge(teams, as.data.frame(table(schedule$loss)), by.x = 'team_abbr', by.y = 'Var1', all = TRUE)
#Counts the amount of times each team ties in the current season and adds it as a column to the teams dataframe
#Or sets ties to 0 if there are no tie games in the current season
ties <- table(c(schedule[schedule$tie,][['home_team']], schedule[schedule$tie,][['away_team']]))
if(nrow(ties) > 0){
teams <- merge(teams, as.data.frame(ties), by.x = 'team_abbr', by.y = 'Var1', all = TRUE)
} else {
teams$Freq <- 0
}
#Sets the column names
colnames(teams) <- c('abbr', 'name', 'division', 'wins', 'losses', 'ties')
#Replaces NA (team did not win/lose/tie) values with 0
teams <- teams %>%
mutate(wins = replace_na(wins, 0), losses = replace_na(losses, 0), ties = replace_na(ties, 0), win_rank = rank(-wins, ties.method = 'min'))
#Calculates record as total wins + 0.5 for all ties, divided by total games played
teams$record <- (teams$wins + (0.5 * teams$ties)) / (teams$wins + teams$losses + teams$ties)
#Merges the winners and losers of each game with the records for the losing team (for calculating SOV)
losses <- merge(data.frame(schedule[,c('win', 'loss')]), teams, by.x = 'loss', by.y = 'abbr', all.x = TRUE)
#Creates a column for strength of victory
teams$sov <- 0
#Calculates the strength of victory for each team as the mean record of all teams they beat
for(sov_team in teams$abbr){
if(teams[teams$abbr == sov_team, 'wins'] > 0){
teams[teams$abbr == sov_team, 'sov'] <- sum(losses[losses$win == sov_team,][['record']]) / teams[teams$abbr == sov_team, 'wins']
}
}
#Cleans names/abbreviations for teams that moved (LAR, OAK, STL, SD)
#LAR/STL becomes LA, SD becomes LAC, OAK becomes LV
if(year < 2016){
teams[teams$abbr == 'LAR' | teams$abbr == 'STL', 'abbr'] <- 'LA'
teams$abbr[teams$abbr == 'LAR' | teams$abbr == 'STL'] <- 'LA'
}
if(year < 2017){
teams$abbr[teams$abbr == 'SD'] <- 'LAC'
}
if(year < 2020){
teams$abbr[teams$abbr == 'OAK'] <- 'LV'
}
#Removes teams that didn't exist in a given year
teams <- teams[(teams$wins > 0 | teams$losses > 0) & teams$abbr != 'Tie',]
#Merges QB data with team wins to measure how many games each QB's team won
df <- merge(qb_data, teams[,c('abbr', 'wins', 'sov', 'win_rank')], by.x = 'recent_team', by.y = 'abbr')
#Creates a column to track the season
df$season <- year
#Returns the QB stat dataframe
return(df)
}
The gen_player_stats function is used to load all stats for each of
the years between 2003 and 2024 that a QB won MVP. The award was given
to a running back in 2005, 2006, and 2012, so I will exclude that data.
After the data is loaded, I will mark which QB won the MVP award each
year by creating a new column, MVP, and assigning each a value of 1.
df <- gen_player_stats(2003)
for (year in 2004:get_latest_season()){
#Excludes all years where a running back won MVP
if (year != 2005 && year != 2006 && year != 2012){
df <- rbind(df, gen_player_stats(year))
}
}
#Hard codes all of the AP MVP winners from 2003 - 2023
df$mvp <- 0
df[df$player_display_name == 'Steve McNair' & df$season == 2003, 'mvp'] <- 1
df[df$player_display_name == 'Peyton Manning' & df$season %in% c(2003, 2004, 2008, 2009, 2013), 'mvp'] <- 1
df[df$player_display_name == 'Tom Brady' & df$season %in% c(2007, 2010, 2017), 'mvp'] <- 1
df[df$player_display_name == 'Aaron Rodgers' & df$season %in% c(2011, 2014, 2020, 2021), 'mvp'] <- 1
df[df$player_display_name == 'Cam Newton' & df$season == 2015, 'mvp'] <- 1
df[df$player_display_name == 'Matt Ryan' & df$season == 2016, 'mvp'] <- 1
df[df$player_display_name == 'Patrick Mahomes' & df$season %in% c(2018, 2022), 'mvp'] <- 1
df[df$player_display_name == 'Lamar Jackson' & df$season %in% c(2019, 2023), 'mvp'] <- 1
df[df$mvp == 1,]
Next, I train a logistic regression model (glm with binomial family)
using the statistics calculated above. I trained the data on all of the
data before 2019, then tested on all of the data before 2024. I tested
with all of the data to measure accuracy over 18 years rather than 5
years.
#Sets the random seed to 26 so the results are repeatable
#26 was chosen as it is Saquon Barkeley's jersey number (Go Birds)
set.seed(26)
train <- df[df['season'] < 2020,]
test <- df[df['season'] < 2024,]
model <- suppressWarnings(glm(mvp ~ tot_td_rank + tot_yard_rank + epa_rank + win_rank + sov + avg_comp_pct - ints, family = 'binomial', data = train))
summary(model)
Call:
glm(formula = mvp ~ tot_td_rank + tot_yard_rank + epa_rank +
win_rank + sov + avg_comp_pct - ints, family = "binomial",
data = train)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -1.3344 11.8484 -0.113 0.9103
tot_td_rank -0.5470 0.3645 -1.501 0.1334
tot_yard_rank -0.0941 0.1195 -0.787 0.4311
epa_rank -0.9685 0.4519 -2.143 0.0321 *
win_rank -0.4748 0.2482 -1.913 0.0558 .
sov 23.3712 20.0665 1.165 0.2441
avg_comp_pct -6.3956 15.3808 -0.416 0.6775
---
Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 152.002 on 882 degrees of freedom
Residual deviance: 30.974 on 876 degrees of freedom
(184 observations deleted due to missingness)
AIC: 44.974
Number of Fisher Scoring iterations: 14
The model summary displays the p-value of each variable, with a
smaller p-value indicating a higher weight on that variable. We can see
that epa_rank and win_rank are the most important variables when
determining who the QB MVP should be.
predicted <- predict(model, test, type="response")
df_pred <- cbind(test, predicted)
df_pred[,c('player_display_name', 'season', 'mvp', 'predicted', 'tot_td_rank', 'tot_yard_rank', 'epa_rank', 'win_rank', 'sov', 'avg_comp_pct', 'ints')] %>%
group_by(season) %>%
filter(predicted == max(predicted, na.rm=TRUE)) %>%
rbind(test[test$mvp == 1,]) %>%
filter(mvp == 0, na.rm=TRUE) %>%
arrange(season)
The model predicts the wrong MVP 5/18 years, meaning it is only
72.22% accurate. The majority of the incorrect predictions are players
who received MVP votes that year. For example, Drew Brees received 15%
of the votes for MVP in 2009. The exception is Carson Palmer, who lead
the league in EPA in 2015 and was likely selected for that reason.
All that remains is to apply the model to the 2024 data and see who
is predicted as this year’s MVP.
predicted <- round(predict(model, df[df$season == 2024,], type="response"), 4)
mvp24 <- cbind(df[df$season == 2024,], predicted)
mvp24[, c('player_display_name', 'recent_team', 'predicted', 'tot_td_rank', 'tot_yard_rank', 'epa_rank', 'win_rank', 'sov', 'avg_comp_pct', 'ints')] %>%
filter(predicted >= 0.2, na.rm=TRUE) %>%
arrange(-predicted)
With 73.5% certainty, the model predicts Lamar Jackson of the
Baltimore Ravens to be the 2024 QB MVP. While he did not lead in the
model’s two most important stats - epa_rank and win_rank - he led in
total touchdowns, had fewer interceptions, and led his team to greater
SOV than Jared Goff and Josh Allen. It is important to note that the MVP
vote and my analysis consider only a quarterback’s regular season
performance.
Sources
-
Predicting the Quarterback-MVP
https://wsb.wharton.upenn.edu/wp-content/uploads/2022/09/2022_Football_Brill_PredictingMVP.pdf
-
nflreadr
https://nflreadr.nflverse.com/reference/index.html
-
What is strength of victory in the NFL?
https://www.sportskeeda.com/nfl/what-strength-victory-nfl-exploring-method-used-determine-playoff-teams-case-tiebreak
LS0tDQp0aXRsZTogIjIwMjQgTkZMIE1WUCBQcmVkaWN0b3IiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6DQogICAgZGZfcHJpbnQ6IHBhZ2VkDQogICAgY3NzOiBib290c3RyYXAuY3NzDQogICAgdGhlbWU6IG51bGwNCiAgcGRmX2RvY3VtZW50OiBkZWZhdWx0DQogIGh0bWxfbm90ZWJvb2s6IGRlZmF1bHQNCi0tLQ0KDQo8aW1nIHNyYz0naHR0cHM6Ly9uZy1zcG9ydGluZ25ld3MuY29tL3MzL2ZpbGVzL3N0eWxlcy9jcm9wX3N0eWxlXzE2XzlfZGVza3RvcC9zMy8yMDI1LTAxL01WUC1CcmVha2Rvd24tRlRSLmpwZz9oPTkyMDkyOWM0Jml0b2s9bHRNbmVXVUknPjwvaW1nPg0KPGJyIC8+DQo8aT5JbWFnZSBTb3VyY2U6IGh0dHBzOi8vd3d3LnNwb3J0aW5nbmV3cy5jb20vdXMvbmZsL25ld3MvbmZsLW12cC1vZGRzLXN0YXRzLWNvbXBhcmlzb24tYWxsZW4tamFja3Nvbi1idXJyb3ctYmFya2xleS9mZjRmYjA2NGVmYjc5MTYzZDYzYjJmMDI8L2k+DQoNClRoaXMgaXMgYSBwcm9qZWN0IGRlc2lnbmVkIHRvIHByZWRpY3QgdGhlIE5GTCBNVlAgdXNpbmcgbWFjaGluZSBsZWFybmluZyB0ZWNobmlxdWVzLiBJIHdpbGwgYmUgcmVwZWF0aW5nIHRoZSBwcm9jZXNzIGRlc2NyaWJlZCBieSBSeWFuIEJyaWxsIGFuZCBSeWFuIFdlaXNtYW4gaW4gdGhlaXIgMjAyMSBwYXBlciA8YSBocmVmPSdodHRwczovL3dzYi53aGFydG9uLnVwZW5uLmVkdS93cC1jb250ZW50L3VwbG9hZHMvMjAyMi8wOS8yMDIyX0Zvb3RiYWxsX0JyaWxsX1ByZWRpY3RpbmdNVlAucGRmJz4iUHJlZGljdGluZyB0aGUgUXVhcnRlcmJhY2stTVZQIjwvYT4sIHdoaWNoIHVzZXMgbG9naXN0aWMgcmVncmVzc2lvbiB0byBwcmVkaWN0IHdoaWNoIFFCIGlzIHN0YXRpc3RpY2FsbHkgbW9zdCBsaWtlbHkgdG8gcmVjZWl2ZSB0aGUgTVZQIHRpdGxlIGVhY2ggeWVhci4gDQoNCkJyaWxsIGFuZCBXZWlzbWFuIHVzZSB0aGUgZm9sbG93aW5nIHBhcmFtZXRlcnMgaW4gdGhlaXIgbW9kZWw6IA0KPHVsPg0KICA8bGk+dG90X3RkX3Jhbms6IGEgcmFuayBvZiB0aGUgUUIncyB0b3RhbCB0b3VjaGRvd25zPC9saT4NCiAgPGxpPnRvdF95YXJkX3Jhbms6IGEgcmFuayBvZiB0aGUgUUIncyB0b3RhbCB5YXJkcyAocnVzaGluZyArIHBhc3NpbmcpPC9saT4NCiAgPGxpPndpbl9yYW5rOiBhIHJhbmsgb2YgdGhlIFFCJ3MgdGVhbSdzIG51bWJlciBvZiB3aW5zPC9saT4NCiAgPGxpPmVwYV9yYW5rOiBhIHJhbmsgb2YgdGhlIFFCJ3MgZXhwZWN0ZWQgcG9pbnRzIGFkZGVkIHBlciBwbGF5PC9saT4NCiAgPGxpPnRvdF9pbnRzOiB0aGUgbnVtYmVyIG9mIGludGVyY2VwdGlvbnMgdGhyb3duIGJ5IHRoZSBRQiBpbiBhIGdpdmVuIHllYXI8L2xpPg0KPC91bD4NCg0KUmFua3MgYXJlIHVzZWQgb3ZlciB0aGUgbnVtYmVyIG9mIHRvdWNoZG93bnMsIHlhcmRzLCB3aW5zLCBhbmQgZXBhIGR1ZSB0byBzdGF0IGluZmxhdGlvbiBvdmVyIHRpbWUuIFRvIHF1b3RlIEJyaWxsIGFuZCBXZWlzbWFuLCAiaW4gMjAwMyBQZXl0b24gTWFubmluZyB0aHJldyBmb3IgYSBsZWFndWXCrWhpZ2gNCjQyNjcgeWFyZHMsIHdoZXJlYXMgaW4gMjAxNiBEcmV3IEJyZWVzIHRocmV3IGZvciBhIGxlYWd1ZcKtaGlnaCA1MjA4IHlhcmRzLiBPdmVyIHRoZSBsYXN0IDIwIHllYXJzLCB0aGUgYXZlcmFnZSBudW1iZXIgb2YgcGFzc2luZyB5YXJkcyBoYXMgZXhwZXJpZW5jZWQgaW5mbGF0aW9uLCBidXQgd2UgZG9u4oCZdCB3YW50IG91ciBtb2RlbCB0byB0aGluayBsZXNzIG9mIE1hbm5pbmcgaW4gMjAwMy4iIA0KDQpJIHdpbGwgYmVnaW4gYnkgbG9hZGluZyB0aGUgbmZsdmVyc2UgbGlicmFyeSwgd2hpY2ggd2lsbCBhbGxvdyBtZSB0byBsb2FkIHRoZSBzdGF0aXN0aWNzIGZvciB0ZWFtcywgc2NoZWR1bGVzIGFuZCBwbGF5ZXJzIGluIHRoZSB5ZWFycyBiZXR3ZWVuIDIwMDMgYW5kIDIwMjQuIEkgd2lsbCBhbHNvIGxvYWQgZHBseXIgYW5kIHRpZHlyIGZvciB0aGVpciBkYXRhIG1hbmlwdWxhdGlvbiBhbmQgY2xlYW5pbmcgZnVuY3Rpb25zLiANCmBgYHtyLHdhcm5pbmc9RkFMU0UsbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkobmZsdmVyc2UpDQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeSh0aWR5cikNCmBgYA0KDQpOZXh0LCBJJ2xsIHdyaXRlIGEgZnVuY3Rpb24gdG8gbG9hZCB0aGUgcmVndWxhciBzZWFzb24gc3RhdGlzdGljcyBvZiBpbnRlcmVzdCBmb3IgYWxsIFFCcy4gSXQncyBoaWdobHkgdW5saWtlbHkgZm9yIGEgYmFja3VwIHF1YXJ0ZXJiYWNrIHRvIGJlIHZvdGVkIE1WUCwgc28gSSdsbCBsaW1pdCBteSByZXN1bHRzIHRvIG9ubHkgcXVhcnRlcmJhY2tzIHdobyB0aHJldyBhdCBsZWFzdCAxNTAgcGFzc2VzLiBUaGlzIGRhdGFzZXQgZGlmZmVyZW50aWF0ZXMgYmV0d2VlbiBydXNoaW5nLCBwYXNzaW5nLCBhbmQgcmVjZWl2aW5nIHN0YXRpc3RpY3MsIHNvIEkgd2lsbCBzdW0gcnVzaGluZyBhbmQgcGFzc2luZyB0byBnZXQgdG90YWwgeWFyZHMsIHRvdWNoZG93bnMsIGFuZCBFUEEgZm9yIGVhY2ggUUIuIFdoaWxlIEJvIE5peCwgSmFyZWQgR29mZiwgYW5kIEpvc2ggQWxsZW4gYWxsIGhhZCBhIHJlY2VpdmluZyB0b3VjaGRvd24gdGhpcyB5ZWFyLCBJIGJlbGlldmUgdGhpcyBpbmZvcm1hdGlvbiBoYXMgbGl0dGxlIGltcGFjdCBvbiBNVlAgdm90aW5nLCBzbyBJIHdpbGwgbm90IGluY2x1ZGUgcmVjZWl2aW5nIHN0YXRzIGluIG15IG1vZGVsLiANCg0KQnJpbGwgYW5kIFdlaXNtYW4gdXNlZCB3aW5fcmFuayBhcyBhIHByZWRpY3RvciB2YXJpYWJsZSBpbiB0aGVpciBtb2RlbC4gSSBhbHNvIGNhbGN1bGF0ZWQgc3RyZW5ndGggb2YgdmljdG9yeSAoU09WKSwgd2hpY2ggSSBiZWxpZXZlIHRlbGxzIGEgbW9yZSBjb21wbGV0ZSBzdG9yeSBvZiBhIHF1YXJ0ZXJiYWNrJ3Mgc2Vhc29uLiBTdHJlbmd0aCBvZiB2aWN0b3J5IGlzIHRoZSBzdW0gb2YgdGhlIHdpbiBwZXJjZW50YWdlcyBvZiBhbGwgdGVhbXMgYSBRQiB3aW5zIGFnYWluc3QgZGl2aWRlZCBieSB0aGUgbnVtYmVyIG9mIHdpbnMgdGhlIFFCJ3MgdGVhbSBoYXMuIFRvIGNhbGN1bGF0ZSB0aGlzLCBJIHdpbGwgdXNlIHRoZSBzY2hlZHVsZSBkYXRhIHRvIGNhbGN1bGF0ZSBlYWNoIHRlYW0ncyB3aW5zLCBsb3NzZXMsIGFuZCB0aWVzLiBJJ2xsIGFsc28gY2FsY3VsYXRlIGF2ZXJhZ2UgY29tcGxldGlvbiBwZXJjZW50YWdlLCBhbiBpbXBvcnRhbnQgc3RhdGlzdGljIGZvciBjb21wYXJpbmcgcXVhcnRlcmJhY2tzLiBJIGNhbGN1bGF0ZSB0aGVpciBjb21wbGV0aW9uIHBlcmNlbnRhZ2UgcGVyIGdhbWUgYXMgdGhlIG51bWJlciBvZiBjb21wbGV0ZWQgcGFzc2VzIGRpdmlkZWQgYnkgdGhlIG51bWJlciBvZiBhdHRlbXB0ZWQgcGFzc2VzLCB0aGVuIGF2ZXJhZ2UgdGhvc2UgdmFsdWVzIGFjcm9zcyB0aGUgc2Vhc29uLiANCmBgYHtyfQ0KI1RoaXMgZnVuY3Rpb24gaW5wdXRzIGEgeWVhciBhbmQgcmV0dXJucyB0aGUgUUIgc3RhdHMgb2YgaW50ZXJlc3QgZm9yIHRoYXQgc2Vhc29uDQpnZW5fcGxheWVyX3N0YXRzIDwtIGZ1bmN0aW9uKHllYXIpew0KICAjTG9hZHMgYWxsIHBsYXllciBkYXRhIGZyb20gdGhlIGdpdmVuIHllYXINCiAgcGxheWVycyA8LSBsb2FkX3BsYXllcl9zdGF0cyh5ZWFyKQ0KICANCiAgI1NlbGVjdHMgb25seSB0aGUgc3RhdGlzdGljcyBvZiBpbnRlcmVzdCBmcm9tIGFsbCBxdWFydGVyYmFja3Mgd2hvIGF0dGVtcHRlZCBhdCBsZWFzdCAxNTAgcGFzc2VzDQogIHFiX2RhdGEgPC0gcGxheWVyc1twbGF5ZXJzJHBvc2l0aW9uID09ICdRQicgJiBzdW0ocGxheWVycyRhdHRlbXB0cykgPj0gMTUwICYgcGxheWVycyRzZWFzb25fdHlwZSA9PSAnUkVHJywgYygncmVjZW50X3RlYW0nLCAncGxheWVyX2Rpc3BsYXlfbmFtZScsICd3ZWVrJywgJ2NvbXBsZXRpb25zJywgJ2F0dGVtcHRzJywgJ3Bhc3NpbmdfdGRzJywgJ3J1c2hpbmdfdGRzJywgJ3Bhc3NpbmdfeWFyZHMnLCAncnVzaGluZ195YXJkcycsICdwYXNzaW5nX2VwYScsICdydXNoaW5nX2VwYScsICdpbnRlcmNlcHRpb25zJywgJ2NhcnJpZXMnKV0NCiAgDQogICNTdW1tYXJpemVzIHRoZSBzdGF0aXN0aWNzIHRvIGdldCBzZWFzb24gdG90YWxzIGZvciBhbGwgUUJzDQogIHFiX2RhdGEgPC0gcWJfZGF0YSAlPiUNCiAgICAjUmVwbGFjZXMgTkEgbnVtZXJpYyB2YWx1ZXMgd2l0aCAwIChOQSBkZW5vdGVzIHRoZSBwbGF5ZXIgZGlkIG5vdCBwbGF5IHRoYXQgd2Vlaywgd2hpY2ggaXMgdW5pbXBvcnRhbnQgZm9yIHNlYXNvbiB0b3RhbHMpDQogICAgcmVwbGFjZV9uYShsaXN0KHBhc3NpbmdfdGRzID0gMCwgcnVzaGluZ190ZHMgPSAwLCBwYXNzaW5nX3lhcmRzID0gMCwgcnVzaGluZ195YXJkcyA9IDAsIHBhc3NpbmdfZXBhID0gMCwgcnVzaGluZ19lcGEgPSAwLCBpbnRlcmNlcHRpb25zID0gMCwgY2FycmllcyA9IDApKSAlPiUNCiAgICAjR3JvdXBzIGRhdGEgYnkgcGxheWVyIG5hbWUNCiAgICBncm91cF9ieShyZWNlbnRfdGVhbSwgcGxheWVyX2Rpc3BsYXlfbmFtZSkgJT4lDQogICAgI1N1bXMgdGhlIG51bWVyaWMgdmFsdWVzIGZvciBlYWNoIHdlZWsgdG8gZ2V0IHNlYXNvbiB0b3RhbHMNCiAgICBzdW1tYXJpemUodG90X3RkID0gc3VtKHBhc3NpbmdfdGRzICsgcnVzaGluZ190ZHMpLCB0b3RfeWFyZCA9IHN1bShwYXNzaW5nX3lhcmRzICsgcnVzaGluZ195YXJkcyksIGVwYSA9IHN1bShwYXNzaW5nX2VwYSArIHJ1c2hpbmdfZXBhKSwgaW50cyA9IHN1bShpbnRlcmNlcHRpb25zKSwgYXZnX2NvbXBfcGN0ID0gcm91bmQobWVhbihjb21wbGV0aW9ucy9hdHRlbXB0cyksIDQpLCBhdmdfeXBjID0gcm91bmQoc3VtKHJ1c2hpbmdfeWFyZHMpL3N1bShjYXJyaWVzKSwgNCkpICU+JQ0KICAgICNJZiBuYW1lIGdyb3VwcyBhcmUgcHJlc2VudCwgcmFuayB3aWxsIGFzc2lnbiBlYWNoIHF1YXJ0ZXJiYWNrIHJhbmsgMSBiZWNhdXNlIHRoZXkgYXJlIHRoZSBiZXN0IHBlcmZvcm1pbmcgUUIgd2l0aCB0aGVpciBuYW1lDQogICAgI1VuZ3JvdXAgcmVtb3ZlcyB0aGUgZ3JvdXBpbmdzIGluIG9yZGVyIHRvIHJhbmsgdGRzLCB5YXJkcywgYW5kIGVwYSBhbW9uZyBhbGwgUUJzDQogICAgdW5ncm91cCgpICU+JQ0KICAgICNSYW5rcyB0b3RhbCB0b3VjaGRvd25zLCB5YXJkcywgYW5kIGVwYSBmcm9tIGhpZ2hlc3QgdG8gbG93ZXN0DQogICAgbXV0YXRlKHRvdF90ZF9yYW5rID0gcmFuaygtdG90X3RkLCB0aWVzLm1ldGhvZCA9ICdtaW4nKSwgdG90X3lhcmRfcmFuayA9IHJhbmsoLXRvdF95YXJkLCB0aWVzLm1ldGhvZCA9ICdtaW4nKSwgZXBhX3JhbmsgPSByYW5rKC1lcGEsIHRpZXMubWV0aG9kID0gJ21pbicpKQ0KICANCiAgI1JlbW92ZXMgdGhlIHRvdGFsIHRvdWNoZG93bnMsIHlhcmRzLCBhbmQgZXBhIGNvbHVtbnMgaW4gZmF2b3Igb2YgdGhlIHJhbmsgY29sdW1ucw0KICBxYl9kYXRhIDwtIHN1YnNldChxYl9kYXRhLCBzZWxlY3QgPSAtYyh0b3RfdGQsIHRvdF95YXJkLCBlcGEpKQ0KICANCiAgI0xvYWRzIDIwMjQgc2NoZWR1bGUNCiAgc2NoZWR1bGUgPC0gbG9hZF9zY2hlZHVsZXMoKQ0KICAjU2VsZWN0cyB0aGUgc2Vhc29uLCB3ZWVrLCB0ZWFtcywgYW5kIHNjb3JlcyBmb3IgYWxsIHJlZ3VsYXIgc2Vhc29uIGdhbWVzDQogIHNjaGVkdWxlIDwtIHNjaGVkdWxlW3NjaGVkdWxlJGdhbWVfdHlwZSA9PSAnUkVHJyAmIHNjaGVkdWxlJHNlYXNvbiA9PSB5ZWFyLGMoJ3NlYXNvbicsICd3ZWVrJywgJ2F3YXlfdGVhbScsICdhd2F5X3Njb3JlJywgJ2hvbWVfdGVhbScsICdob21lX3Njb3JlJyldDQogIA0KICAjTG9hZHMgdGVhbSBhYmJyZXZpYXRlZCBuYW1lLCB0ZWFtIG5hbWUsIGFuZCBkaXZpc2lvbiBmb3IgYWxsIGN1cnJlbnQgdGVhbXMNCiAgdGVhbXMgPC0gbG9hZF90ZWFtcyhjdXJyZW50ID0gRkFMU0UpWyxjKCd0ZWFtX2FiYnInLCAndGVhbV9uYW1lJywgJ3RlYW1fZGl2aXNpb24nKV0NCiAgDQogICNDcmVhdGVzIGEgY29sdW1uIGRlbm90aW5nIHdoaWNoIHRlYW1zIHdvbiBlYWNoIGdhbWUgKG9yIE5BIGlmIHRpZSkNCiAgc2NoZWR1bGUkd2luIDwtIGlmX2Vsc2Uoc2NoZWR1bGUkaG9tZV9zY29yZSA9PSBzY2hlZHVsZSRhd2F5X3Njb3JlLCAnVGllJywgaWZfZWxzZShzY2hlZHVsZSRob21lX3Njb3JlID4gc2NoZWR1bGUkYXdheV9zY29yZSwgc2NoZWR1bGUkaG9tZV90ZWFtLCBzY2hlZHVsZSRhd2F5X3RlYW0pKQ0KICAjQ3JlYXRlcyBhIGNvbHVtbiBkZW5vdGluZyB3aGljaCB0ZWFtcyBsb3N0IGVhY2ggZWFjaCBnYW1lIChvciBOQSBpZiB0aWUpDQogIHNjaGVkdWxlJGxvc3MgPC0gaWZfZWxzZShzY2hlZHVsZSRob21lX3Njb3JlID09IHNjaGVkdWxlJGF3YXlfc2NvcmUsICdUaWUnLCBpZl9lbHNlKHNjaGVkdWxlJGhvbWVfc2NvcmUgPCBzY2hlZHVsZSRhd2F5X3Njb3JlLCBzY2hlZHVsZSRob21lX3RlYW0sIHNjaGVkdWxlJGF3YXlfdGVhbSkpDQogICNDcmVhdGVzIGEgYm9vbGVhbiBjb2x1bW4gd2hpY2ggaXMgdHJ1ZSBpZiB0aGUgZ2FtZSBlbmRzIGluIGEgdGllDQogIHNjaGVkdWxlJHRpZSA8LSBzY2hlZHVsZSRob21lX3Njb3JlID09IHNjaGVkdWxlJGF3YXlfc2NvcmUNCiAgDQogICNBZGRzIGNvbHVtbnMgY291bnRpbmcgZWFjaCB0ZWFtJ3Mgd2lucyBhbmQgbG9zc2VzIHRvIHRoZSB0ZWFtcyBkYXRhZnJhbWUNCiAgdGVhbXMgPC0gbWVyZ2UodGVhbXMsIGFzLmRhdGEuZnJhbWUodGFibGUoc2NoZWR1bGUkd2luKSksIGJ5LnggPSAndGVhbV9hYmJyJywgYnkueSA9ICdWYXIxJywgYWxsID0gVFJVRSkNCiAgdGVhbXMgPC0gbWVyZ2UodGVhbXMsIGFzLmRhdGEuZnJhbWUodGFibGUoc2NoZWR1bGUkbG9zcykpLCBieS54ID0gJ3RlYW1fYWJicicsIGJ5LnkgPSAnVmFyMScsIGFsbCA9IFRSVUUpDQogIA0KICAjQ291bnRzIHRoZSBhbW91bnQgb2YgdGltZXMgZWFjaCB0ZWFtIHRpZXMgaW4gdGhlIGN1cnJlbnQgc2Vhc29uIGFuZCBhZGRzIGl0IGFzIGEgY29sdW1uIHRvIHRoZSB0ZWFtcyBkYXRhZnJhbWUNCiAgI09yIHNldHMgdGllcyB0byAwIGlmIHRoZXJlIGFyZSBubyB0aWUgZ2FtZXMgaW4gdGhlIGN1cnJlbnQgc2Vhc29uDQogIHRpZXMgPC0gdGFibGUoYyhzY2hlZHVsZVtzY2hlZHVsZSR0aWUsXVtbJ2hvbWVfdGVhbSddXSwgc2NoZWR1bGVbc2NoZWR1bGUkdGllLF1bWydhd2F5X3RlYW0nXV0pKQ0KICBpZihucm93KHRpZXMpID4gMCl7DQogICAgdGVhbXMgPC0gbWVyZ2UodGVhbXMsIGFzLmRhdGEuZnJhbWUodGllcyksIGJ5LnggPSAndGVhbV9hYmJyJywgYnkueSA9ICdWYXIxJywgYWxsID0gVFJVRSkNCiAgfSBlbHNlIHsNCiAgICB0ZWFtcyRGcmVxIDwtIDANCiAgfQ0KICANCiAgI1NldHMgdGhlIGNvbHVtbiBuYW1lcw0KICBjb2xuYW1lcyh0ZWFtcykgPC0gYygnYWJicicsICduYW1lJywgJ2RpdmlzaW9uJywgJ3dpbnMnLCAnbG9zc2VzJywgJ3RpZXMnKQ0KICANCiAgI1JlcGxhY2VzIE5BICh0ZWFtIGRpZCBub3Qgd2luL2xvc2UvdGllKSB2YWx1ZXMgd2l0aCAwDQogIHRlYW1zIDwtIHRlYW1zICU+JQ0KICAgIG11dGF0ZSh3aW5zID0gcmVwbGFjZV9uYSh3aW5zLCAwKSwgbG9zc2VzID0gcmVwbGFjZV9uYShsb3NzZXMsIDApLCB0aWVzID0gcmVwbGFjZV9uYSh0aWVzLCAwKSwgd2luX3JhbmsgPSByYW5rKC13aW5zLCB0aWVzLm1ldGhvZCA9ICdtaW4nKSkNCiAgDQogICNDYWxjdWxhdGVzIHJlY29yZCBhcyB0b3RhbCB3aW5zICsgMC41IGZvciBhbGwgdGllcywgZGl2aWRlZCBieSB0b3RhbCBnYW1lcyBwbGF5ZWQNCiAgdGVhbXMkcmVjb3JkIDwtICh0ZWFtcyR3aW5zICsgKDAuNSAqIHRlYW1zJHRpZXMpKSAvICh0ZWFtcyR3aW5zICsgdGVhbXMkbG9zc2VzICsgdGVhbXMkdGllcykNCiAgDQogICNNZXJnZXMgdGhlIHdpbm5lcnMgYW5kIGxvc2VycyBvZiBlYWNoIGdhbWUgd2l0aCB0aGUgcmVjb3JkcyBmb3IgdGhlIGxvc2luZyB0ZWFtIChmb3IgY2FsY3VsYXRpbmcgU09WKQ0KICBsb3NzZXMgPC0gbWVyZ2UoZGF0YS5mcmFtZShzY2hlZHVsZVssYygnd2luJywgJ2xvc3MnKV0pLCB0ZWFtcywgYnkueCA9ICdsb3NzJywgYnkueSA9ICdhYmJyJywgYWxsLnggPSBUUlVFKQ0KICAjQ3JlYXRlcyBhIGNvbHVtbiBmb3Igc3RyZW5ndGggb2YgdmljdG9yeQ0KICB0ZWFtcyRzb3YgPC0gMA0KICANCiAgI0NhbGN1bGF0ZXMgdGhlIHN0cmVuZ3RoIG9mIHZpY3RvcnkgZm9yIGVhY2ggdGVhbSBhcyB0aGUgbWVhbiByZWNvcmQgb2YgYWxsIHRlYW1zIHRoZXkgYmVhdA0KICBmb3Ioc292X3RlYW0gaW4gdGVhbXMkYWJicil7DQogICAgaWYodGVhbXNbdGVhbXMkYWJiciA9PSBzb3ZfdGVhbSwgJ3dpbnMnXSA+IDApew0KICAgICAgdGVhbXNbdGVhbXMkYWJiciA9PSBzb3ZfdGVhbSwgJ3NvdiddIDwtIHN1bShsb3NzZXNbbG9zc2VzJHdpbiA9PSBzb3ZfdGVhbSxdW1sncmVjb3JkJ11dKSAvIHRlYW1zW3RlYW1zJGFiYnIgPT0gc292X3RlYW0sICd3aW5zJ10NCiAgICB9DQogIH0NCiAgDQogICNDbGVhbnMgbmFtZXMvYWJicmV2aWF0aW9ucyBmb3IgdGVhbXMgdGhhdCBtb3ZlZCAoTEFSLCBPQUssIFNUTCwgU0QpDQogICNMQVIvU1RMIGJlY29tZXMgTEEsIFNEIGJlY29tZXMgTEFDLCBPQUsgYmVjb21lcyBMVg0KICBpZih5ZWFyIDwgMjAxNil7DQogICAgdGVhbXNbdGVhbXMkYWJiciA9PSAnTEFSJyB8IHRlYW1zJGFiYnIgPT0gJ1NUTCcsICdhYmJyJ10gPC0gJ0xBJw0KICAgIHRlYW1zJGFiYnJbdGVhbXMkYWJiciA9PSAnTEFSJyB8IHRlYW1zJGFiYnIgPT0gJ1NUTCddIDwtICdMQScNCiAgfQ0KICBpZih5ZWFyIDwgMjAxNyl7DQogICAgdGVhbXMkYWJiclt0ZWFtcyRhYmJyID09ICdTRCddIDwtICdMQUMnDQogIH0NCiAgaWYoeWVhciA8IDIwMjApew0KICAgIHRlYW1zJGFiYnJbdGVhbXMkYWJiciA9PSAnT0FLJ10gPC0gJ0xWJw0KICB9DQogIA0KICAjUmVtb3ZlcyB0ZWFtcyB0aGF0IGRpZG4ndCBleGlzdCBpbiBhIGdpdmVuIHllYXINCiAgdGVhbXMgPC0gdGVhbXNbKHRlYW1zJHdpbnMgPiAwIHwgdGVhbXMkbG9zc2VzID4gMCkgJiB0ZWFtcyRhYmJyICE9ICdUaWUnLF0NCiAgDQogICNNZXJnZXMgUUIgZGF0YSB3aXRoIHRlYW0gd2lucyB0byBtZWFzdXJlIGhvdyBtYW55IGdhbWVzIGVhY2ggUUIncyB0ZWFtIHdvbg0KICBkZiA8LSBtZXJnZShxYl9kYXRhLCB0ZWFtc1ssYygnYWJicicsICd3aW5zJywgJ3NvdicsICd3aW5fcmFuaycpXSwgYnkueCA9ICdyZWNlbnRfdGVhbScsIGJ5LnkgPSAnYWJicicpDQogIA0KICAjQ3JlYXRlcyBhIGNvbHVtbiB0byB0cmFjayB0aGUgc2Vhc29uDQogIGRmJHNlYXNvbiA8LSB5ZWFyDQogIA0KICAjUmV0dXJucyB0aGUgUUIgc3RhdCBkYXRhZnJhbWUNCiAgcmV0dXJuKGRmKQ0KfQ0KYGBgDQoNClRoZSBnZW5fcGxheWVyX3N0YXRzIGZ1bmN0aW9uIGlzIHVzZWQgdG8gbG9hZCBhbGwgc3RhdHMgZm9yIGVhY2ggb2YgdGhlIHllYXJzIGJldHdlZW4gMjAwMyBhbmQgMjAyNCB0aGF0IGEgUUIgd29uIE1WUC4gVGhlIGF3YXJkIHdhcyBnaXZlbiB0byBhIHJ1bm5pbmcgYmFjayBpbiAyMDA1LCAyMDA2LCBhbmQgMjAxMiwgc28gSSB3aWxsIGV4Y2x1ZGUgdGhhdCBkYXRhLiBBZnRlciB0aGUgZGF0YSBpcyBsb2FkZWQsIEkgd2lsbCBtYXJrIHdoaWNoIFFCIHdvbiB0aGUgTVZQIGF3YXJkIGVhY2ggeWVhciBieSBjcmVhdGluZyBhIG5ldyBjb2x1bW4sIE1WUCwgYW5kIGFzc2lnbmluZyBlYWNoIGEgdmFsdWUgb2YgMS4gDQpgYGB7cix3YXJuaW5nPUZBTFNFLG1lc3NhZ2U9RkFMU0V9DQpkZiA8LSBnZW5fcGxheWVyX3N0YXRzKDIwMDMpDQoNCmZvciAoeWVhciBpbiAyMDA0OmdldF9sYXRlc3Rfc2Vhc29uKCkpew0KICAjRXhjbHVkZXMgYWxsIHllYXJzIHdoZXJlIGEgcnVubmluZyBiYWNrIHdvbiBNVlANCiAgaWYgKHllYXIgIT0gMjAwNSAmJiB5ZWFyICE9IDIwMDYgJiYgeWVhciAhPSAyMDEyKXsNCiAgICBkZiA8LSByYmluZChkZiwgZ2VuX3BsYXllcl9zdGF0cyh5ZWFyKSkNCiAgfQ0KfQ0KDQojSGFyZCBjb2RlcyBhbGwgb2YgdGhlIEFQIE1WUCB3aW5uZXJzIGZyb20gMjAwMyAtIDIwMjMNCmRmJG12cCA8LSAwDQoNCmRmW2RmJHBsYXllcl9kaXNwbGF5X25hbWUgPT0gJ1N0ZXZlIE1jTmFpcicgJiBkZiRzZWFzb24gPT0gMjAwMywgJ212cCddIDwtIDENCmRmW2RmJHBsYXllcl9kaXNwbGF5X25hbWUgPT0gJ1BleXRvbiBNYW5uaW5nJyAmIGRmJHNlYXNvbiAlaW4lIGMoMjAwMywgMjAwNCwgMjAwOCwgMjAwOSwgMjAxMyksICdtdnAnXSA8LSAxDQpkZltkZiRwbGF5ZXJfZGlzcGxheV9uYW1lID09ICdUb20gQnJhZHknICYgZGYkc2Vhc29uICVpbiUgYygyMDA3LCAyMDEwLCAyMDE3KSwgJ212cCddIDwtIDENCmRmW2RmJHBsYXllcl9kaXNwbGF5X25hbWUgPT0gJ0Fhcm9uIFJvZGdlcnMnICYgZGYkc2Vhc29uICVpbiUgYygyMDExLCAyMDE0LCAyMDIwLCAyMDIxKSwgJ212cCddIDwtIDENCmRmW2RmJHBsYXllcl9kaXNwbGF5X25hbWUgPT0gJ0NhbSBOZXd0b24nICYgZGYkc2Vhc29uID09IDIwMTUsICdtdnAnXSA8LSAxDQpkZltkZiRwbGF5ZXJfZGlzcGxheV9uYW1lID09ICdNYXR0IFJ5YW4nICYgZGYkc2Vhc29uID09IDIwMTYsICdtdnAnXSA8LSAxDQpkZltkZiRwbGF5ZXJfZGlzcGxheV9uYW1lID09ICdQYXRyaWNrIE1haG9tZXMnICYgZGYkc2Vhc29uICVpbiUgYygyMDE4LCAyMDIyKSwgJ212cCddIDwtIDENCmRmW2RmJHBsYXllcl9kaXNwbGF5X25hbWUgPT0gJ0xhbWFyIEphY2tzb24nICYgZGYkc2Vhc29uICVpbiUgYygyMDE5LCAyMDIzKSwgJ212cCddIDwtIDENCg0KZGZbZGYkbXZwID09IDEsXQ0KYGBgDQoNCk5leHQsIEkgdHJhaW4gYSBsb2dpc3RpYyByZWdyZXNzaW9uIG1vZGVsIChnbG0gd2l0aCBiaW5vbWlhbCBmYW1pbHkpIHVzaW5nIHRoZSBzdGF0aXN0aWNzIGNhbGN1bGF0ZWQgYWJvdmUuIEkgdHJhaW5lZCB0aGUgZGF0YSBvbiBhbGwgb2YgdGhlIGRhdGEgYmVmb3JlIDIwMTksIHRoZW4gdGVzdGVkIG9uIGFsbCBvZiB0aGUgZGF0YSBiZWZvcmUgMjAyNC4gSSB0ZXN0ZWQgd2l0aCBhbGwgb2YgdGhlIGRhdGEgdG8gbWVhc3VyZSBhY2N1cmFjeSBvdmVyIDE4IHllYXJzIHJhdGhlciB0aGFuIDUgeWVhcnMuIA0KYGBge3Isd2FybmluZz1GQUxTRSxtZXNzYWdlPUZBTFNFfQ0KI1NldHMgdGhlIHJhbmRvbSBzZWVkIHRvIDI2IHNvIHRoZSByZXN1bHRzIGFyZSByZXBlYXRhYmxlDQojMjYgd2FzIGNob3NlbiBhcyBpdCBpcyBTYXF1b24gQmFya2VsZXkncyBqZXJzZXkgbnVtYmVyIChHbyBCaXJkcykNCnNldC5zZWVkKDI2KQ0KDQp0cmFpbiA8LSBkZltkZlsnc2Vhc29uJ10gPCAyMDIwLF0NCnRlc3QgPC0gZGZbZGZbJ3NlYXNvbiddIDwgMjAyNCxdDQoNCm1vZGVsIDwtIGdsbShtdnAgfiB0b3RfdGRfcmFuayArIHRvdF95YXJkX3JhbmsgKyBlcGFfcmFuayArIHdpbl9yYW5rICsgc292ICsgYXZnX2NvbXBfcGN0IC0gaW50cywgZmFtaWx5ID0gJ2Jpbm9taWFsJywgZGF0YSA9IHRyYWluKQ0KDQpzdW1tYXJ5KG1vZGVsKQ0KYGBgDQpUaGUgbW9kZWwgc3VtbWFyeSBkaXNwbGF5cyB0aGUgcC12YWx1ZSBvZiBlYWNoIHZhcmlhYmxlLCB3aXRoIGEgc21hbGxlciBwLXZhbHVlIGluZGljYXRpbmcgYSBoaWdoZXIgd2VpZ2h0IG9uIHRoYXQgdmFyaWFibGUuIFdlIGNhbiBzZWUgdGhhdCBlcGFfcmFuayBhbmQgd2luX3JhbmsgYXJlIHRoZSBtb3N0IGltcG9ydGFudCB2YXJpYWJsZXMgd2hlbiBkZXRlcm1pbmluZyB3aG8gdGhlIFFCIE1WUCBzaG91bGQgYmUuIA0KYGBge3J9DQpwcmVkaWN0ZWQgPC0gcHJlZGljdChtb2RlbCwgdGVzdCwgdHlwZT0icmVzcG9uc2UiKQ0KZGZfcHJlZCA8LSBjYmluZCh0ZXN0LCBwcmVkaWN0ZWQpDQoNCmRmX3ByZWRbLGMoJ3BsYXllcl9kaXNwbGF5X25hbWUnLCAnc2Vhc29uJywgJ212cCcsICdwcmVkaWN0ZWQnLCAndG90X3RkX3JhbmsnLCAndG90X3lhcmRfcmFuaycsICdlcGFfcmFuaycsICd3aW5fcmFuaycsICdzb3YnLCAnYXZnX2NvbXBfcGN0JywgJ2ludHMnKV0gJT4lDQogIGdyb3VwX2J5KHNlYXNvbikgJT4lDQogIGZpbHRlcihwcmVkaWN0ZWQgPT0gbWF4KHByZWRpY3RlZCwgbmEucm09VFJVRSkpICU+JQ0KICByYmluZCh0ZXN0W3Rlc3QkbXZwID09IDEsXSkgJT4lDQogIGZpbHRlcihtdnAgPT0gMCwgbmEucm09VFJVRSkgJT4lDQogIGFycmFuZ2Uoc2Vhc29uKQ0KYGBgDQpUaGUgbW9kZWwgcHJlZGljdHMgdGhlIHdyb25nIE1WUCA1LzE4IHllYXJzLCBtZWFuaW5nIGl0IGlzIG9ubHkgNzIuMjIlIGFjY3VyYXRlLiBUaGUgbWFqb3JpdHkgb2YgdGhlIGluY29ycmVjdCBwcmVkaWN0aW9ucyBhcmUgcGxheWVycyB3aG8gcmVjZWl2ZWQgTVZQIHZvdGVzIHRoYXQgeWVhci4gRm9yIGV4YW1wbGUsIERyZXcgQnJlZXMgcmVjZWl2ZWQgMTUlIG9mIHRoZSB2b3RlcyBmb3IgTVZQIGluIDIwMDkuIFRoZSBleGNlcHRpb24gaXMgQ2Fyc29uIFBhbG1lciwgd2hvIGxlYWQgdGhlIGxlYWd1ZSBpbiBFUEEgaW4gMjAxNSBhbmQgd2FzIGxpa2VseSBzZWxlY3RlZCBmb3IgdGhhdCByZWFzb24uDQoNCkFsbCB0aGF0IHJlbWFpbnMgaXMgdG8gYXBwbHkgdGhlIG1vZGVsIHRvIHRoZSAyMDI0IGRhdGEgYW5kIHNlZSB3aG8gaXMgcHJlZGljdGVkIGFzIHRoaXMgeWVhcidzIE1WUC4gIA0KYGBge3J9DQpwcmVkaWN0ZWQgPC0gcm91bmQocHJlZGljdChtb2RlbCwgZGZbZGYkc2Vhc29uID09IDIwMjQsXSwgdHlwZT0icmVzcG9uc2UiKSwgNCkNCm12cDI0IDwtIGNiaW5kKGRmW2RmJHNlYXNvbiA9PSAyMDI0LF0sIHByZWRpY3RlZCkNCg0KbXZwMjRbLCBjKCdwbGF5ZXJfZGlzcGxheV9uYW1lJywgJ3JlY2VudF90ZWFtJywgJ3ByZWRpY3RlZCcsICd0b3RfdGRfcmFuaycsICd0b3RfeWFyZF9yYW5rJywgJ2VwYV9yYW5rJywgJ3dpbl9yYW5rJywgJ3NvdicsICdhdmdfY29tcF9wY3QnLCAnaW50cycpXSAlPiUNCiAgZmlsdGVyKHByZWRpY3RlZCA+PSAwLjIsIG5hLnJtPVRSVUUpICU+JQ0KICBhcnJhbmdlKC1wcmVkaWN0ZWQpDQpgYGANCldpdGggNzMuNSUgY2VydGFpbnR5LCB0aGUgbW9kZWwgcHJlZGljdHMgTGFtYXIgSmFja3NvbiBvZiB0aGUgQmFsdGltb3JlIFJhdmVucyB0byBiZSB0aGUgMjAyNCBRQiBNVlAuIFdoaWxlIGhlIGRpZCBub3QgbGVhZCBpbiB0aGUgbW9kZWwncyB0d28gbW9zdCBpbXBvcnRhbnQgc3RhdHMgLSBlcGFfcmFuayBhbmQgd2luX3JhbmsgLSBoZSBsZWQgaW4gdG90YWwgdG91Y2hkb3ducywgaGFkIGZld2VyIGludGVyY2VwdGlvbnMsIGFuZCBsZWQgaGlzIHRlYW0gdG8gZ3JlYXRlciBTT1YgdGhhbiBKYXJlZCBHb2ZmIGFuZCBKb3NoIEFsbGVuLiBJdCBpcyBpbXBvcnRhbnQgdG8gbm90ZSB0aGF0IHRoZSBNVlAgdm90ZSBhbmQgbXkgYW5hbHlzaXMgY29uc2lkZXIgb25seSBhIHF1YXJ0ZXJiYWNrJ3MgcmVndWxhciBzZWFzb24gcGVyZm9ybWFuY2UuDQoNCjxociAvPg0KDQpTb3VyY2VzDQo8b2w+DQogIDxsaT5QcmVkaWN0aW5nIHRoZSBRdWFydGVyYmFjay1NVlAgPGEgaHJlZj0naHR0cHM6Ly93c2Iud2hhcnRvbi51cGVubi5lZHUvd3AtY29udGVudC91cGxvYWRzLzIwMjIvMDkvMjAyMl9Gb290YmFsbF9CcmlsbF9QcmVkaWN0aW5nTVZQLnBkZic+aHR0cHM6Ly93c2Iud2hhcnRvbi51cGVubi5lZHUvd3AtY29udGVudC91cGxvYWRzLzIwMjIvMDkvMjAyMl9Gb290YmFsbF9CcmlsbF9QcmVkaWN0aW5nTVZQLnBkZjwvbGk+DQogIDxsaT5uZmxyZWFkciA8YSBocmVmPSdodHRwczovL25mbHJlYWRyLm5mbHZlcnNlLmNvbS9yZWZlcmVuY2UvaW5kZXguaHRtbCc+aHR0cHM6Ly9uZmxyZWFkci5uZmx2ZXJzZS5jb20vcmVmZXJlbmNlL2luZGV4Lmh0bWw8L2E+PC9saT4NCiAgPGxpPldoYXQgaXMgc3RyZW5ndGggb2YgdmljdG9yeSBpbiB0aGUgTkZMPyA8YSBocmVmPSdodHRwczovL3d3dy5zcG9ydHNrZWVkYS5jb20vbmZsL3doYXQtc3RyZW5ndGgtdmljdG9yeS1uZmwtZXhwbG9yaW5nLW1ldGhvZC11c2VkLWRldGVybWluZS1wbGF5b2ZmLXRlYW1zLWNhc2UtdGllYnJlYWsnPmh0dHBzOi8vd3d3LnNwb3J0c2tlZWRhLmNvbS9uZmwvd2hhdC1zdHJlbmd0aC12aWN0b3J5LW5mbC1leHBsb3JpbmctbWV0aG9kLXVzZWQtZGV0ZXJtaW5lLXBsYXlvZmYtdGVhbXMtY2FzZS10aWVicmVhazwvYT4NCjwvb2w+