library(twitteR)
library(tm)
library(topicmodels)
library(sentiment)
library(ggplot2)
library(wordcloud)
library(data.table)

Collecting Tweets

First, we need to collect some tweets from @BlizzHeroes for analysis. We’ve requested up to 3200 (the maximum amount). By default, this includes replies but excludes retweets. To prevent downloading different tweets every time this code is run, I saved the inital batch of tweets as an RDS.

#tweets<-userTimeline("BlizzHeroes", n=3200)
tweets<-readRDS("tweets.rds")
length(tweets)
[1] 366

366!? But we requested 3200!

This is because the Twitter API limits us in how far back we can go. It appears we’ve only received tweets since October 2016.

Cleaning

First, we want to convert our tweets to a data frame.

tweets_df<-twListToDF(tweets)

Next, we transform tweets into a corpus of tweets. We also remove numbers and URLs from the tweets to make the data more digestible.

tweets_corpus <- Corpus(VectorSource(tweets_df$text))
removeURL <- function(x) gsub("http[^[:space:]]*", "", x)
tweets_corpus<-tm_map(tweets_corpus, content_transformer(removeURL))
tweets_corpus<-tm_map(tweets_corpus, removeNumbers)
corpusCopy<-tweets_corpus #keep this for later
corpusCopy<-tm_map(corpusCopy, PlainTextDocument)

Now we are in a bind. If we use the removePunctuation function in the tm package, R will remove apostrophes. However, our stopwords dictionary contains words with apostrophes.

I created a RegEx function to remove the apostrophes, and then converted the text to lowercase.

allButApost <- function(x) gsub("[^[:alnum:][:space:]']", "", x)
tweets_corpus<-tm_map(tweets_corpus, content_transformer(allButApost))
tweets_corpus <- tm_map(tweets_corpus, tolower) 

Then, I removed stopwords found in the “SMART” dictionary. Stopwords are common words that won’t be useful in our analysis (think: a, the, an, very, etc). Then I removed any residual punctuation.

tweets_corpus<-tm_map(tweets_corpus, removeWords, stopwords("SMART"))
tweets_corpus<-tm_map(tweets_corpus, removePunctuation)

Stemming

In some cases in text processing, the same words can change tense. For example, pwn could also exist as pwning or pwned, but the meaning of the word is the same. Without stemming, the algorithm would count pwn, pwning, and pwned as three seperate words, instead of just “pwn.”

We fix this via stemming–removing extra letters at the end of certain words.

tweets_corpus<-tm_map(tweets_corpus, stemDocument)
tweets_corpus<-tm_map(tweets_corpus, PlainTextDocument)

Term Document Matrix

Remember how we have a corpus of each tweet? We are going to expand upon that by tallying every word in every tweet. Imagine a table with individual tweet numbers as columns and every word from every tweet as rows.

Each cell represents the number of times a word appears in a tweet. As there is a large amount of words (every word from every tweet), most tweets only contain a small fraction of the total corpus of words. As such, most cells are 0 (indicating the word does not appear in this specific tweet).

The Wikipedia page provides a nice example.

Note that Wikipedia demonstrates a Document Term Matrix, whereas here we are using a Term Document Matrix. The TDM will be the transpose of what is shown on Wikipedia.

tdm<-TermDocumentMatrix(tweets_corpus, control = list(wordLengths = c(1, Inf)))
term_freq<-rowSums(as.matrix(tdm))
term_freq<-subset(term_freq, term_freq>=20)
tdm_df<-data.frame(term=names(term_freq), freq=term_freq)

We’ll plot the most frequent words. Frequent words are defined above as those that appear over 20 times.

ggplot(data=tdm_df)+geom_bar(aes(x=term, y=freq), stat="identity")+xlab("Terms")+ylab("Count")+coord_flip()+theme(axis.text=element_text(size=10))

It looks like ‘Heroes’ is the most popular word of choice. No suprise there. Other words, such as ‘live’ and ‘tune’ might be linked to promoting e-sports and streamers.

WordClouds

Everyone’s favorite.

Here, I show words appearing at least 6 times.

tdm_matrix<-as.matrix(tdm)
word_freq<-sort(rowSums(tdm_matrix), decreasing=T)
pal<-brewer.pal(8, "Dark2")
wordcloud(words=names(word_freq), freq=word_freq, min.freq=6, random.order = F, colors=pal)

@BlizzHeroes loves calling “Heroes” to the “Nexus” and mentioning Twitch streamers. We also encounter numerous Hero names, likely from when they were released or received a rework.

Associations

We can find words associated with common words. Let’s look at the hero Lucio and see what we can find!

findAssocs(tdm, "lucio", 0.2)
$lucio
        arrives heroespowerhour      highlights     impressions    intothenexus           talks            test 
           0.50            0.50            0.50            0.50            0.50            0.50            0.47 
         public           realm            dive         itncast             amp           murky        reworked 
           0.40            0.40            0.35            0.35            0.29            0.28            0.28 
            feb 
           0.24 

Since this was scraped a week after his release, most words associated with Lucio indicate his release buzz (arrives, highlights, impressions). Other notable words include Murky and reworked since everyone’s (least?) favorite Murloc made a comeback to the Nexus after some changes.

Topic modeling

What does @BlizzHeroes tweet about on a higher level? We can use topic modeling to discover commonly associated words and group them into predetermined buckets.

After playing around with the number of topics, I settled on 5 which gave the least amount of overlap.

dtm<-as.DocumentTermMatrix(tdm)
rowTotals <- apply(dtm , 1, sum) 
dtm_corrected   <- dtm[rowTotals> 0, ] 
lda<-LDA(dtm_corrected, control=list(seed=123) ,k=5) 
term<-terms(lda, 5)
(term<-apply(term, MARGIN=2, paste, collapse=", "))
                                 Topic 1                                  Topic 2 
    "heroes, love, lunar, region, enjoy"      "hero, tune, league, matches, live" 
                                 Topic 3                                  Topic 4 
"play, kendricswissh, gt, team, valeera"   "patch, notes, stream, great, balance" 
                                 Topic 5 
 "nexus, rewards, hgc, hencyheccu, week" 

The algorithm isn’t going to assign our topics names. Here’s my attempt.

Topic 1 appears to be promoting events (Lunar New Year, Valentine’s Day).

Topic 2 is e-sports promotion.

Topic 3 looks to be related to plays, possibly pro teams?

Topic 4 is related to talking about updates to the game.

Topic 5 is possibly promoting streamers and rewards.

Note there is some overlap between topics.

So @BlizzHeroes mainly concerns: promotional events, promoting e-sports, sharing wicked plays, informing about updates, and promoting streamers.

Sentiment Analysis

What is the overall attitude of @BlizzHeroes? Here sentiment analysis saves the day. It classifies every tweet as either “negative”, “neutral”, or “positive” based on the amount of positive/negative words.

new_twitter_df<-data.frame(text = sapply(corpusCopy, paste, collapse = " "), stringsAsFactors = FALSE)
sentiments<-sentiment(new_twitter_df$text)
table(sentiments$polarity)

negative  neutral positive 
       4      171      191 
sentiments$score<-0
sentiments$score[sentiments$polarity=="positive"]<- 1
sentiments$score[sentiments$polarity=="negative"]<- -1
sentiments$date <- as.IDate(tweets_df$created)
result <- aggregate(score ~ date, data = sentiments, sum)

Wow! Most of @BlizzHeroes tweets are quite positive. Only 4 were flagged as negative. Let’s investigate what those negative tweets were.

sent[sent$polarity=="negative",]$text
[1]  @AronDark Sorry to hear that! We re putting out a fix for the issue tomorrow!                                               
[2]  @iakona We re investigating this issue, sorry for the poor experience!                                                      
[3]  @JKesselring Hi! Sorry for the poor experience. We re looking into it! Did you happen to just recently install the game?    
[4]  @daniellovejr Sorry you had a bad experience! We re talking about ways to make it less punishing when teammates leave games.
366 Levels:   ...Changes are a few months off, but we are actively looking at Murky. From our Balance Q&amp;A:<e2><U+0080><U+00A6>  ...

Most of the negative sentiment comes from apoloigizing due to game issues/bugs.

Visualizing Sentiment

Let’s examine the sentiment over time.

ggplot(data=result) + geom_smooth(aes(x=date, y=score))+xlab("Month")+ylab("Score")+ggtitle("@BlizzHeroes sentiment over time")+theme(plot.title = element_text(hjust = 0.5))

Did Blizzard get a case of the holiday blues? Their sentiment is decreasing around December. Most likely this is do to employees (social media managers) taking a break (and sending fewer positively flagged tweets).

Finally, we visualize the sentiments.

ggplot(sentiments, aes(x=polarity)) +
  geom_bar(aes(y=..count.., fill=polarity)) +
  scale_fill_brewer(palette="RdGy") +
  labs(x="Polarity", y="Number of Tweets") +
  ggtitle("Sentiment Analysis of @BlizzHeroes")+
  theme(plot.title = element_text(hjust = 0.5))

Overall, @BlizzHeroes is a very positive Twitter user. Great job, @BlizzHeroes!

LS0tDQp0aXRsZTogJ0BCbGl6ekhlcm9lcyBBbmFseXNpcycNCmF1dGhvcjogImhvdHNfZGF0YV9ndXkiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0d2l0dGVSKQ0KbGlicmFyeSh0bSkNCmxpYnJhcnkodG9waWNtb2RlbHMpDQpsaWJyYXJ5KHNlbnRpbWVudCkNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkod29yZGNsb3VkKQ0KbGlicmFyeShkYXRhLnRhYmxlKQ0KYGBgDQoNCg0KIyNDb2xsZWN0aW5nIFR3ZWV0cw0KDQpGaXJzdCwgd2UgbmVlZCB0byBjb2xsZWN0IHNvbWUgdHdlZXRzIGZyb20gXEBCbGl6ekhlcm9lcyBmb3IgYW5hbHlzaXMuIFdlJ3ZlIHJlcXVlc3RlZCB1cCB0byAzMjAwICh0aGUgbWF4aW11bSBhbW91bnQpLiBCeSBkZWZhdWx0LCB0aGlzIGluY2x1ZGVzIHJlcGxpZXMgYnV0IGV4Y2x1ZGVzIHJldHdlZXRzLiBUbyBwcmV2ZW50IGRvd25sb2FkaW5nIGRpZmZlcmVudCB0d2VldHMgZXZlcnkgdGltZSB0aGlzIGNvZGUgaXMgcnVuLCBJIHNhdmVkIHRoZSBpbml0YWwgYmF0Y2ggb2YgdHdlZXRzIGFzIGFuIFJEUy4NCmBgYHtyfQ0KI3R3ZWV0czwtdXNlclRpbWVsaW5lKCJCbGl6ekhlcm9lcyIsIG49MzIwMCkNCnR3ZWV0czwtcmVhZFJEUygidHdlZXRzLnJkcyIpDQpsZW5ndGgodHdlZXRzKQ0KYGBgDQoNCjM2NiE/IEJ1dCB3ZSByZXF1ZXN0ZWQgMzIwMCEgDQoNClRoaXMgaXMgYmVjYXVzZSB0aGUgVHdpdHRlciBBUEkgbGltaXRzIHVzIGluIGhvdyBmYXIgYmFjayB3ZSBjYW4gZ28uIEl0IGFwcGVhcnMgd2UndmUgb25seSByZWNlaXZlZCB0d2VldHMgc2luY2UgT2N0b2JlciAyMDE2LiAgDQoNCiMjQ2xlYW5pbmcgDQoNCkZpcnN0LCB3ZSB3YW50IHRvIGNvbnZlcnQgb3VyIHR3ZWV0cyB0byBhIGRhdGEgZnJhbWUuDQoNCmBgYHtyLH0NCnR3ZWV0c19kZjwtdHdMaXN0VG9ERih0d2VldHMpDQpgYGANCg0KDQpOZXh0LCB3ZSB0cmFuc2Zvcm0gdHdlZXRzIGludG8gYSBjb3JwdXMgb2YgdHdlZXRzLiBXZSBhbHNvIHJlbW92ZSBudW1iZXJzIGFuZCBVUkxzIGZyb20gdGhlIHR3ZWV0cyB0byBtYWtlIHRoZSBkYXRhIG1vcmUgZGlnZXN0aWJsZS4NCg0KYGBge3J9DQp0d2VldHNfY29ycHVzIDwtIENvcnB1cyhWZWN0b3JTb3VyY2UodHdlZXRzX2RmJHRleHQpKQ0KcmVtb3ZlVVJMIDwtIGZ1bmN0aW9uKHgpIGdzdWIoImh0dHBbXls6c3BhY2U6XV0qIiwgIiIsIHgpDQp0d2VldHNfY29ycHVzPC10bV9tYXAodHdlZXRzX2NvcnB1cywgY29udGVudF90cmFuc2Zvcm1lcihyZW1vdmVVUkwpKQ0KdHdlZXRzX2NvcnB1czwtdG1fbWFwKHR3ZWV0c19jb3JwdXMsIHJlbW92ZU51bWJlcnMpDQoNCmNvcnB1c0NvcHk8LXR3ZWV0c19jb3JwdXMgI2tlZXAgdGhpcyBmb3IgbGF0ZXINCmNvcnB1c0NvcHk8LXRtX21hcChjb3JwdXNDb3B5LCBQbGFpblRleHREb2N1bWVudCkNCmBgYA0KDQoNCk5vdyB3ZSBhcmUgaW4gYSBiaW5kLiBJZiB3ZSB1c2UgdGhlIGByZW1vdmVQdW5jdHVhdGlvbmAgZnVuY3Rpb24gaW4gdGhlIHRtIHBhY2thZ2UsIFIgd2lsbCByZW1vdmUgYXBvc3Ryb3BoZXMuIEhvd2V2ZXIsIG91ciBzdG9wd29yZHMgZGljdGlvbmFyeSBjb250YWlucyB3b3JkcyB3aXRoIGFwb3N0cm9waGVzLiANCg0KSSBjcmVhdGVkIGEgUmVnRXggZnVuY3Rpb24gdG8gcmVtb3ZlIHRoZSBhcG9zdHJvcGhlcywgYW5kIHRoZW4gY29udmVydGVkIHRoZSB0ZXh0IHRvIGxvd2VyY2FzZS4NCmBgYHtyfQ0KYWxsQnV0QXBvc3QgPC0gZnVuY3Rpb24oeCkgZ3N1YigiW15bOmFsbnVtOl1bOnNwYWNlOl0nXSIsICIiLCB4KQ0KdHdlZXRzX2NvcnB1czwtdG1fbWFwKHR3ZWV0c19jb3JwdXMsIGNvbnRlbnRfdHJhbnNmb3JtZXIoYWxsQnV0QXBvc3QpKQ0KdHdlZXRzX2NvcnB1cyA8LSB0bV9tYXAodHdlZXRzX2NvcnB1cywgdG9sb3dlcikgDQpgYGANCg0KDQpUaGVuLCBJIHJlbW92ZWQgc3RvcHdvcmRzIGZvdW5kIGluIHRoZSAiU01BUlQiIGRpY3Rpb25hcnkuIFN0b3B3b3JkcyBhcmUgY29tbW9uIHdvcmRzIHRoYXQgd29uJ3QgYmUgdXNlZnVsIGluIG91ciBhbmFseXNpcyAodGhpbms6IGEsIHRoZSwgYW4sIHZlcnksIGV0YykuIFRoZW4gSSByZW1vdmVkIGFueSByZXNpZHVhbCBwdW5jdHVhdGlvbi4NCmBgYHtyfQ0KdHdlZXRzX2NvcnB1czwtdG1fbWFwKHR3ZWV0c19jb3JwdXMsIHJlbW92ZVdvcmRzLCBzdG9wd29yZHMoIlNNQVJUIikpDQp0d2VldHNfY29ycHVzPC10bV9tYXAodHdlZXRzX2NvcnB1cywgcmVtb3ZlUHVuY3R1YXRpb24pDQpgYGANCg0KDQojI1N0ZW1taW5nDQoNCkluIHNvbWUgY2FzZXMgaW4gdGV4dCBwcm9jZXNzaW5nLCB0aGUgc2FtZSB3b3JkcyBjYW4gY2hhbmdlIHRlbnNlLiBGb3IgZXhhbXBsZSwgcHduIGNvdWxkIGFsc28gZXhpc3QgYXMgcHduaW5nIG9yIHB3bmVkLCBidXQgdGhlIG1lYW5pbmcgb2YgdGhlIHdvcmQgaXMgdGhlIHNhbWUuIFdpdGhvdXQgc3RlbW1pbmcsIHRoZSBhbGdvcml0aG0gd291bGQgY291bnQgcHduLCBwd25pbmcsIGFuZCBwd25lZCBhcyB0aHJlZSBzZXBlcmF0ZSB3b3JkcywgaW5zdGVhZCBvZiBqdXN0ICJwd24uIiANCg0KV2UgZml4IHRoaXMgdmlhIHN0ZW1taW5nLS1yZW1vdmluZyBleHRyYSBsZXR0ZXJzIGF0IHRoZSBlbmQgb2YgY2VydGFpbiB3b3Jkcy4gDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KdHdlZXRzX2NvcnB1czwtdG1fbWFwKHR3ZWV0c19jb3JwdXMsIHN0ZW1Eb2N1bWVudCkNCnR3ZWV0c19jb3JwdXM8LXRtX21hcCh0d2VldHNfY29ycHVzLCBQbGFpblRleHREb2N1bWVudCkNCmBgYA0KDQojI1Rlcm0gRG9jdW1lbnQgTWF0cml4DQoNClJlbWVtYmVyIGhvdyB3ZSBoYXZlIGEgY29ycHVzIG9mIGVhY2ggdHdlZXQ/IFdlIGFyZSBnb2luZyB0byBleHBhbmQgdXBvbiB0aGF0IGJ5IHRhbGx5aW5nIGV2ZXJ5IHdvcmQgaW4gZXZlcnkgdHdlZXQuIEltYWdpbmUgYSB0YWJsZSB3aXRoIGluZGl2aWR1YWwgdHdlZXQgbnVtYmVycyBhcyBjb2x1bW5zIGFuZCBldmVyeSB3b3JkIGZyb20gZXZlcnkgdHdlZXQgYXMgcm93cy4NCg0KRWFjaCBjZWxsIHJlcHJlc2VudHMgdGhlIG51bWJlciBvZiB0aW1lcyBhIHdvcmQgYXBwZWFycyBpbiBhIHR3ZWV0LiBBcyB0aGVyZSBpcyBhIGxhcmdlIGFtb3VudCBvZiB3b3JkcyAoKipldmVyeSoqIHdvcmQgZnJvbSAqKmV2ZXJ5KiogdHdlZXQpLCBtb3N0IHR3ZWV0cyBvbmx5IGNvbnRhaW4gYSBzbWFsbCBmcmFjdGlvbiBvZiB0aGUgdG90YWwgY29ycHVzIG9mIHdvcmRzLiBBcyBzdWNoLCBtb3N0IGNlbGxzIGFyZSAwIChpbmRpY2F0aW5nIHRoZSB3b3JkIGRvZXMgbm90IGFwcGVhciBpbiB0aGlzIHNwZWNpZmljIHR3ZWV0KS4NCg0KVGhlIFdpa2lwZWRpYSBwYWdlIHByb3ZpZGVzIGEgbmljZSBbZXhhbXBsZV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvRG9jdW1lbnQtdGVybV9tYXRyaXgpLiANCg0KTm90ZSB0aGF0IFdpa2lwZWRpYSBkZW1vbnN0cmF0ZXMgYSBEb2N1bWVudCBUZXJtIE1hdHJpeCwgd2hlcmVhcyBoZXJlIHdlIGFyZSB1c2luZyBhIFRlcm0gRG9jdW1lbnQgTWF0cml4LiBUaGUgVERNIHdpbGwgYmUgdGhlIHRyYW5zcG9zZSBvZiB3aGF0IGlzIHNob3duIG9uIFdpa2lwZWRpYS4NCg0KYGBge3J9DQp0ZG08LVRlcm1Eb2N1bWVudE1hdHJpeCh0d2VldHNfY29ycHVzLCBjb250cm9sID0gbGlzdCh3b3JkTGVuZ3RocyA9IGMoMSwgSW5mKSkpDQoNCnRlcm1fZnJlcTwtcm93U3Vtcyhhcy5tYXRyaXgodGRtKSkNCnRlcm1fZnJlcTwtc3Vic2V0KHRlcm1fZnJlcSwgdGVybV9mcmVxPj0yMCkNCnRkbV9kZjwtZGF0YS5mcmFtZSh0ZXJtPW5hbWVzKHRlcm1fZnJlcSksIGZyZXE9dGVybV9mcmVxKQ0KYGBgDQoNCldlJ2xsIHBsb3QgdGhlIG1vc3QgZnJlcXVlbnQgd29yZHMuIEZyZXF1ZW50IHdvcmRzIGFyZSBkZWZpbmVkIGFib3ZlIGFzIHRob3NlIHRoYXQgYXBwZWFyIG92ZXIgMjAgdGltZXMuDQpgYGB7cn0NCmdncGxvdChkYXRhPXRkbV9kZikrZ2VvbV9iYXIoYWVzKHg9dGVybSwgeT1mcmVxKSwgc3RhdD0iaWRlbnRpdHkiKSt4bGFiKCJUZXJtcyIpK3lsYWIoIkNvdW50IikrY29vcmRfZmxpcCgpK3RoZW1lKGF4aXMudGV4dD1lbGVtZW50X3RleHQoc2l6ZT0xMCkpDQpgYGANCg0KSXQgbG9va3MgbGlrZSAnSGVyb2VzJyBpcyB0aGUgbW9zdCBwb3B1bGFyIHdvcmQgb2YgY2hvaWNlLiBObyBzdXByaXNlIHRoZXJlLiBPdGhlciB3b3Jkcywgc3VjaCBhcyAnbGl2ZScgYW5kICd0dW5lJyBtaWdodCBiZSBsaW5rZWQgdG8gcHJvbW90aW5nIGUtc3BvcnRzIGFuZCBzdHJlYW1lcnMuDQoNCiMjV29yZENsb3Vkcw0KDQpFdmVyeW9uZSdzICpmYXZvcml0ZSouIA0KDQpIZXJlLCBJIHNob3cgd29yZHMgYXBwZWFyaW5nIGF0IGxlYXN0IDYgdGltZXMuDQpgYGB7cn0NCnRkbV9tYXRyaXg8LWFzLm1hdHJpeCh0ZG0pDQoNCndvcmRfZnJlcTwtc29ydChyb3dTdW1zKHRkbV9tYXRyaXgpLCBkZWNyZWFzaW5nPVQpDQpwYWw8LWJyZXdlci5wYWwoOCwgIkRhcmsyIikNCg0Kd29yZGNsb3VkKHdvcmRzPW5hbWVzKHdvcmRfZnJlcSksIGZyZXE9d29yZF9mcmVxLCBtaW4uZnJlcT02LCByYW5kb20ub3JkZXIgPSBGLCBjb2xvcnM9cGFsKQ0KYGBgDQpcQEJsaXp6SGVyb2VzIGxvdmVzIGNhbGxpbmcgIkhlcm9lcyIgdG8gdGhlICJOZXh1cyIgYW5kIG1lbnRpb25pbmcgVHdpdGNoIHN0cmVhbWVycy4gV2UgYWxzbyBlbmNvdW50ZXIgbnVtZXJvdXMgSGVybyBuYW1lcywgbGlrZWx5IGZyb20gd2hlbiB0aGV5IHdlcmUgcmVsZWFzZWQgb3IgcmVjZWl2ZWQgYSByZXdvcmsuICANCg0KDQojI0Fzc29jaWF0aW9ucw0KDQpXZSBjYW4gZmluZCB3b3JkcyBhc3NvY2lhdGVkIHdpdGggY29tbW9uIHdvcmRzLiBMZXQncyBsb29rIGF0IHRoZSBoZXJvIGBMdWNpb2AgYW5kIHNlZSB3aGF0IHdlIGNhbiBmaW5kIQ0KDQpgYGB7cn0NCmZpbmRBc3NvY3ModGRtLCAibHVjaW8iLCAwLjIpDQpgYGANCg0KU2luY2UgdGhpcyB3YXMgc2NyYXBlZCBhIHdlZWsgYWZ0ZXIgaGlzIHJlbGVhc2UsIG1vc3Qgd29yZHMgYXNzb2NpYXRlZCB3aXRoIEx1Y2lvIGluZGljYXRlIGhpcyByZWxlYXNlIGJ1enogKGBhcnJpdmVzYCwgYGhpZ2hsaWdodHNgLCBgaW1wcmVzc2lvbnNgKS4gT3RoZXIgbm90YWJsZSB3b3JkcyBpbmNsdWRlIGBNdXJreWAgYW5kIGByZXdvcmtlZGAgc2luY2UgZXZlcnlvbmUncyAobGVhc3Q/KSBmYXZvcml0ZSBNdXJsb2MgbWFkZSBhIGNvbWViYWNrIHRvIHRoZSBOZXh1cyBhZnRlciBzb21lIGNoYW5nZXMuDQoNCiMjVG9waWMgbW9kZWxpbmcgDQoNCldoYXQgZG9lcyBcQEJsaXp6SGVyb2VzIHR3ZWV0IGFib3V0IG9uIGEgaGlnaGVyIGxldmVsPyBXZSBjYW4gdXNlIHRvcGljIG1vZGVsaW5nIHRvIGRpc2NvdmVyIGNvbW1vbmx5IGFzc29jaWF0ZWQgd29yZHMgYW5kIGdyb3VwIHRoZW0gaW50byBwcmVkZXRlcm1pbmVkIGJ1Y2tldHMuIA0KDQpBZnRlciBwbGF5aW5nIGFyb3VuZCB3aXRoIHRoZSBudW1iZXIgb2YgdG9waWNzLCBJIHNldHRsZWQgb24gNSB3aGljaCBnYXZlIHRoZSBsZWFzdCBhbW91bnQgb2Ygb3ZlcmxhcC4NCg0KYGBge3J9DQpkdG08LWFzLkRvY3VtZW50VGVybU1hdHJpeCh0ZG0pDQpyb3dUb3RhbHMgPC0gYXBwbHkoZHRtICwgMSwgc3VtKSANCmR0bV9jb3JyZWN0ZWQgICA8LSBkdG1bcm93VG90YWxzPiAwLCBdIA0KDQpsZGE8LUxEQShkdG1fY29ycmVjdGVkLCBjb250cm9sPWxpc3Qoc2VlZD0xMjMpICxrPTUpIA0KdGVybTwtdGVybXMobGRhLCA1KQ0KKHRlcm08LWFwcGx5KHRlcm0sIE1BUkdJTj0yLCBwYXN0ZSwgY29sbGFwc2U9IiwgIikpDQpgYGANCg0KVGhlIGFsZ29yaXRobSBpc24ndCBnb2luZyB0byBhc3NpZ24gb3VyIHRvcGljcyBuYW1lcy4gSGVyZSdzIG15IGF0dGVtcHQuDQoNClRvcGljIDEgYXBwZWFycyB0byBiZSBwcm9tb3RpbmcgZXZlbnRzIChMdW5hciBOZXcgWWVhciwgVmFsZW50aW5lJ3MgRGF5KS4NCg0KVG9waWMgMiBpcyBlLXNwb3J0cyBwcm9tb3Rpb24uDQoNClRvcGljIDMgbG9va3MgdG8gYmUgcmVsYXRlZCB0byBwbGF5cywgcG9zc2libHkgcHJvIHRlYW1zPyANCg0KVG9waWMgNCBpcyByZWxhdGVkIHRvIHRhbGtpbmcgYWJvdXQgdXBkYXRlcyB0byB0aGUgZ2FtZS4NCg0KVG9waWMgNSBpcyBwb3NzaWJseSBwcm9tb3Rpbmcgc3RyZWFtZXJzIGFuZCByZXdhcmRzLg0KDQpOb3RlIHRoZXJlIGlzIHNvbWUgb3ZlcmxhcCBiZXR3ZWVuIHRvcGljcy4NCg0KU28gXEBCbGl6ekhlcm9lcyBtYWlubHkgY29uY2VybnM6IHByb21vdGlvbmFsIGV2ZW50cywgcHJvbW90aW5nIGUtc3BvcnRzLCBzaGFyaW5nIHdpY2tlZCBwbGF5cywgaW5mb3JtaW5nIGFib3V0IHVwZGF0ZXMsIGFuZCBwcm9tb3Rpbmcgc3RyZWFtZXJzLg0KDQojI1NlbnRpbWVudCBBbmFseXNpcw0KDQpXaGF0IGlzIHRoZSBvdmVyYWxsIGF0dGl0dWRlIG9mIFxAQmxpenpIZXJvZXM/IEhlcmUgc2VudGltZW50IGFuYWx5c2lzIHNhdmVzIHRoZSBkYXkuIEl0IGNsYXNzaWZpZXMgZXZlcnkgdHdlZXQgYXMgZWl0aGVyICJuZWdhdGl2ZSIsICJuZXV0cmFsIiwgb3IgInBvc2l0aXZlIiBiYXNlZCBvbiB0aGUgYW1vdW50IG9mIHBvc2l0aXZlL25lZ2F0aXZlIHdvcmRzLg0KYGBge3J9DQpuZXdfdHdpdHRlcl9kZjwtZGF0YS5mcmFtZSh0ZXh0ID0gc2FwcGx5KGNvcnB1c0NvcHksIHBhc3RlLCBjb2xsYXBzZSA9ICIgIiksIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkNCg0Kc2VudGltZW50czwtc2VudGltZW50KG5ld190d2l0dGVyX2RmJHRleHQpDQp0YWJsZShzZW50aW1lbnRzJHBvbGFyaXR5KQ0KDQpzZW50aW1lbnRzJHNjb3JlPC0wDQpzZW50aW1lbnRzJHNjb3JlW3NlbnRpbWVudHMkcG9sYXJpdHk9PSJwb3NpdGl2ZSJdPC0gMQ0Kc2VudGltZW50cyRzY29yZVtzZW50aW1lbnRzJHBvbGFyaXR5PT0ibmVnYXRpdmUiXTwtIC0xDQpzZW50aW1lbnRzJGRhdGUgPC0gYXMuSURhdGUodHdlZXRzX2RmJGNyZWF0ZWQpDQoNCnJlc3VsdCA8LSBhZ2dyZWdhdGUoc2NvcmUgfiBkYXRlLCBkYXRhID0gc2VudGltZW50cywgc3VtKQ0KYGBgDQpXb3chIE1vc3Qgb2YgXEBCbGl6ekhlcm9lcyB0d2VldHMgYXJlIHF1aXRlIHBvc2l0aXZlLiBPbmx5IDQgd2VyZSBmbGFnZ2VkIGFzIG5lZ2F0aXZlLiBMZXQncyBpbnZlc3RpZ2F0ZSB3aGF0IHRob3NlIG5lZ2F0aXZlIHR3ZWV0cyB3ZXJlLg0KDQpgYGB7cn0NCnNlbnRpbWVudHNbc2VudGltZW50cyRwb2xhcml0eT09Im5lZ2F0aXZlIixdJHRleHQNCmBgYA0KDQpNb3N0IG9mIHRoZSBuZWdhdGl2ZSBzZW50aW1lbnQgY29tZXMgZnJvbSBhcG9sb2lnaXppbmcgZHVlIHRvIGdhbWUgaXNzdWVzL2J1Z3MuIA0KDQojI1Zpc3VhbGl6aW5nIFNlbnRpbWVudA0KDQpMZXQncyBleGFtaW5lIHRoZSBzZW50aW1lbnQgb3ZlciB0aW1lLg0KDQpgYGB7cn0NCmdncGxvdChkYXRhPXJlc3VsdCkgKyBnZW9tX3Ntb290aChhZXMoeD1kYXRlLCB5PXNjb3JlKSkreGxhYigiTW9udGgiKSt5bGFiKCJTY29yZSIpK2dndGl0bGUoIkBCbGl6ekhlcm9lcyBzZW50aW1lbnQgb3ZlciB0aW1lIikrdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSkpDQpgYGANCg0KRGlkIEJsaXp6YXJkIGdldCBhIGNhc2Ugb2YgdGhlIGhvbGlkYXkgYmx1ZXM/IFRoZWlyIHNlbnRpbWVudCBpcyBkZWNyZWFzaW5nIGFyb3VuZCBEZWNlbWJlci4gTW9zdCBsaWtlbHkgdGhpcyBpcyBkbyB0byBlbXBsb3llZXMgKHNvY2lhbCBtZWRpYSBtYW5hZ2VycykgdGFraW5nIGEgYnJlYWsgKGFuZCBzZW5kaW5nIGZld2VyIHBvc2l0aXZlbHkgZmxhZ2dlZCB0d2VldHMpLiANCg0KDQpGaW5hbGx5LCB3ZSB2aXN1YWxpemUgdGhlIHNlbnRpbWVudHMuDQoNCmBgYHtyfQ0KZ2dwbG90KHNlbnRpbWVudHMsIGFlcyh4PXBvbGFyaXR5KSkgKw0KICBnZW9tX2JhcihhZXMoeT0uLmNvdW50Li4sIGZpbGw9cG9sYXJpdHkpKSArDQogIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGU9IlJkR3kiKSArDQogIGxhYnMoeD0iUG9sYXJpdHkiLCB5PSJOdW1iZXIgb2YgVHdlZXRzIikgKw0KICBnZ3RpdGxlKCJTZW50aW1lbnQgQW5hbHlzaXMgb2YgQEJsaXp6SGVyb2VzIikrDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpKQ0KYGBgDQoNCk92ZXJhbGwsIFxAQmxpenpIZXJvZXMgaXMgYSB2ZXJ5IHBvc2l0aXZlIFR3aXR0ZXIgdXNlci4gR3JlYXQgam9iLCBcQEJsaXp6SGVyb2VzIQ0K