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:

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 league­high 4267 yards, whereas in 2016 Drew Brees threw for a league­high 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
  1. Predicting the Quarterback-MVP https://wsb.wharton.upenn.edu/wp-content/uploads/2022/09/2022_Football_Brill_PredictingMVP.pdf
  2. nflreadr https://nflreadr.nflverse.com/reference/index.html
  3. 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+