diff options
| author | Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> | 2024-09-12 00:25:15 +0300 |
|---|---|---|
| committer | Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> | 2024-09-12 00:25:15 +0300 |
| commit | df6f6cb5ff8957be8f01d58d60857da2c094a3d9 (patch) | |
| tree | 5ec5a8a95633d7fa6cce62654a9bc6fc6204f788 /rankings | |
| parent | refactor: fix module ver (diff) | |
| download | lphub-df6f6cb5ff8957be8f01d58d60857da2c094a3d9.tar.gz lphub-df6f6cb5ff8957be8f01d58d60857da2c094a3d9.tar.bz2 lphub-df6f6cb5ff8957be8f01d58d60857da2c094a3d9.zip | |
refactor: unofficial rankings implementation
Diffstat (limited to 'rankings')
| -rw-r--r-- | rankings/.env.example | 1 | ||||
| -rw-r--r-- | rankings/.gitignore | 2 | ||||
| -rw-r--r-- | rankings/README.md | 35 | ||||
| -rw-r--r-- | rankings/export.go | 21 | ||||
| -rw-r--r-- | rankings/fetch.go | 185 | ||||
| -rw-r--r-- | rankings/filter.go | 81 | ||||
| -rw-r--r-- | rankings/go.mod | 8 | ||||
| -rw-r--r-- | rankings/go.sum | 4 | ||||
| -rw-r--r-- | rankings/input/overrides.json | 44 | ||||
| -rw-r--r-- | rankings/input/records.json | 604 | ||||
| -rw-r--r-- | rankings/main.go | 45 | ||||
| -rw-r--r-- | rankings/models.go | 51 | ||||
| -rw-r--r-- | rankings/prefetch.go | 44 |
13 files changed, 1125 insertions, 0 deletions
diff --git a/rankings/.env.example b/rankings/.env.example new file mode 100644 index 0000000..48204cd --- /dev/null +++ b/rankings/.env.example | |||
| @@ -0,0 +1 @@ | |||
| API_KEY= | |||
diff --git a/rankings/.gitignore b/rankings/.gitignore new file mode 100644 index 0000000..764d23d --- /dev/null +++ b/rankings/.gitignore | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | .env | ||
| 2 | output/ \ No newline at end of file | ||
diff --git a/rankings/README.md b/rankings/README.md new file mode 100644 index 0000000..5532c88 --- /dev/null +++ b/rankings/README.md | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | # Rankings Algorithm | ||
| 2 | |||
| 3 | Unofficial rankings are fetched from Steam. The reason that LPHUB considers Steam leaderboards unofficial is that entries do not require proof, and it is very easy to cheat the portal count in terms of in-game commands and/or otherwise. | ||
| 4 | |||
| 5 | The algorithm is close clone of [@NeKz](https://github.com/NeKzor)'s implementation of their lp boards, without including the video showcases and tie counts for each map. | ||
| 6 | |||
| 7 | ## Fetch Manual Inputs | ||
| 8 | - records.json | ||
| 9 | - Contains all map ids and wrs | ||
| 10 | - overrides.json | ||
| 11 | - Used to replace invalid scores by legit players | ||
| 12 | |||
| 13 | ## Fetch 5000 Scores for Each Map | ||
| 14 | - Dictionary of players and map entries are created during the period of fetching all of the maps. | ||
| 15 | - First initialize the players dictionary with all players that finished Portal Gun and Doors in their limit portal count. This results in ~200K players as of 2024 Q4. | ||
| 16 | - Iterate over the rest of the maps and increase the category score and iteration count for each player. | ||
| 17 | - If a player from an entry does not exist in the initial dictionary, they are skipped. | ||
| 18 | - If a player has a score that is lower than the WR, they are skipped since the score is invalid. | ||
| 19 | - If they have an override however, their entry is overriden and becomes valid. | ||
| 20 | - If there are more than 5000 scores that have WR for that map, the search goes on until every WR holder is fetched. | ||
| 21 | - If there is a specified map limit in the records.json, then all of the scores up to and including that map limit is fetched. Any score above the map limit is skipped. | ||
| 22 | |||
| 23 | ## Filtering Players Dictionary | ||
| 24 | - Create seperate arrays for singleplayer, multiplayer, overall rankings. | ||
| 25 | - Iterate over the players dictionary and add players that completed at least one category to their respective arrays. | ||
| 26 | - If player has 51 sp entries, add to sp rankings. | ||
| 27 | - If player has 48 mp entries, add to mp rankings. | ||
| 28 | - If player has 51 sp entries and 48 mp entries, add to overall rankings. | ||
| 29 | - If none of the above, remove player from the dictionary. | ||
| 30 | - Iterate over the dictionary to get Steam data for each player that has at least one category complete. This results in one API call for each ~300 players as of 2024 Q4. | ||
| 31 | - Sort the sp, mp, overall rankings arrays by score counts of their respective category. | ||
| 32 | - Iterate over each rankings arrays and calculate the ranks for each player. | ||
| 33 | |||
| 34 | ## Exporting Rankings Arrays | ||
| 35 | - Marshall each array into JSON and output into JSON files. \ No newline at end of file | ||
diff --git a/rankings/export.go b/rankings/export.go new file mode 100644 index 0000000..20dfebe --- /dev/null +++ b/rankings/export.go | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "encoding/json" | ||
| 5 | "os" | ||
| 6 | ) | ||
| 7 | |||
| 8 | func exportAll(spRankings, mpRankings, overallRankings *[]*Player) { | ||
| 9 | sp, _ := os.Create("./output/sp.json") | ||
| 10 | spRankingsOut, _ := json.Marshal(*spRankings) | ||
| 11 | sp.Write(spRankingsOut) | ||
| 12 | sp.Close() | ||
| 13 | mp, _ := os.Create("./output/mp.json") | ||
| 14 | mpRankingsOut, _ := json.Marshal(*mpRankings) | ||
| 15 | mp.Write(mpRankingsOut) | ||
| 16 | mp.Close() | ||
| 17 | overall, _ := os.Create("./output/overall.json") | ||
| 18 | overallRankingsOut, _ := json.Marshal(*overallRankings) | ||
| 19 | overall.Write(overallRankingsOut) | ||
| 20 | overall.Close() | ||
| 21 | } | ||
diff --git a/rankings/fetch.go b/rankings/fetch.go new file mode 100644 index 0000000..ee5d5bb --- /dev/null +++ b/rankings/fetch.go | |||
| @@ -0,0 +1,185 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "encoding/json" | ||
| 5 | "encoding/xml" | ||
| 6 | "fmt" | ||
| 7 | "io" | ||
| 8 | "log" | ||
| 9 | "net/http" | ||
| 10 | "os" | ||
| 11 | "strconv" | ||
| 12 | ) | ||
| 13 | |||
| 14 | func fetchLeaderboard(records *[]Record, overrides *map[string]map[string]int) *map[string]*Player { | ||
| 15 | log.Println("fetching leaderboard") | ||
| 16 | players := map[string]*Player{} | ||
| 17 | // first init players map with records from portal gun and doors | ||
| 18 | fetchAnotherPage := true | ||
| 19 | start := 0 | ||
| 20 | end := 5000 | ||
| 21 | |||
| 22 | for fetchAnotherPage { | ||
| 23 | portalGunEntries := fetchRecordsFromMap(47459, 0, 5000) | ||
| 24 | fetchAnotherPage = portalGunEntries.needsAnotherPage(&(*records)[0]) | ||
| 25 | if fetchAnotherPage { | ||
| 26 | start = end + 1 | ||
| 27 | end = start + 5000 | ||
| 28 | } | ||
| 29 | for _, entry := range portalGunEntries.Entries.Entry { | ||
| 30 | if entry.Score < 0 { | ||
| 31 | continue // ban | ||
| 32 | } | ||
| 33 | players[entry.SteamID] = &Player{ | ||
| 34 | SteamID: entry.SteamID, | ||
| 35 | Entries: []PlayerEntry{ | ||
| 36 | { | ||
| 37 | MapID: 47459, | ||
| 38 | MapScore: entry.Score, | ||
| 39 | }, | ||
| 40 | }, | ||
| 41 | SpScoreCount: entry.Score, | ||
| 42 | SpIterations: 1, | ||
| 43 | } | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | fetchAnotherPage = true | ||
| 48 | start = 0 | ||
| 49 | end = 5000 | ||
| 50 | |||
| 51 | for fetchAnotherPage { | ||
| 52 | doorsEntries := fetchRecordsFromMap(47740, start, end) | ||
| 53 | fetchAnotherPage = doorsEntries.needsAnotherPage(&(*records)[51]) | ||
| 54 | if fetchAnotherPage { | ||
| 55 | start = end + 1 | ||
| 56 | end = start + 5000 | ||
| 57 | } | ||
| 58 | for _, entry := range doorsEntries.Entries.Entry { | ||
| 59 | if entry.Score < 0 { | ||
| 60 | continue // ban | ||
| 61 | } | ||
| 62 | player, ok := players[entry.SteamID] | ||
| 63 | if !ok { | ||
| 64 | players[entry.SteamID] = &Player{ | ||
| 65 | SteamID: entry.SteamID, | ||
| 66 | Entries: []PlayerEntry{ | ||
| 67 | { | ||
| 68 | MapID: 47740, | ||
| 69 | MapScore: entry.Score, | ||
| 70 | }, | ||
| 71 | }, | ||
| 72 | MpScoreCount: entry.Score, | ||
| 73 | MpIterations: 1, | ||
| 74 | } | ||
| 75 | } else { | ||
| 76 | player.Entries = append(player.Entries, PlayerEntry{ | ||
| 77 | MapID: 47740, | ||
| 78 | MapScore: entry.Score, | ||
| 79 | }) | ||
| 80 | player.MpScoreCount = entry.Score | ||
| 81 | player.MpIterations++ | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | for _, record := range *records { | ||
| 87 | if record.MapID == 47459 || record.MapID == 47740 { | ||
| 88 | continue | ||
| 89 | } | ||
| 90 | |||
| 91 | fetchAnotherPage := true | ||
| 92 | start := 0 | ||
| 93 | end := 5000 | ||
| 94 | |||
| 95 | for fetchAnotherPage { | ||
| 96 | entries := fetchRecordsFromMap(record.MapID, start, end) | ||
| 97 | fetchAnotherPage = entries.needsAnotherPage(&record) | ||
| 98 | if fetchAnotherPage { | ||
| 99 | start = end + 1 | ||
| 100 | end = start + 5000 | ||
| 101 | } | ||
| 102 | for _, entry := range (*entries).Entries.Entry { | ||
| 103 | player, ok := players[entry.SteamID] | ||
| 104 | if !ok { | ||
| 105 | continue | ||
| 106 | } | ||
| 107 | score := entry.Score | ||
| 108 | if entry.Score < record.MapWR { | ||
| 109 | _, ok := (*overrides)[entry.SteamID] | ||
| 110 | if ok { | ||
| 111 | _, ok := (*overrides)[entry.SteamID][strconv.Itoa(record.MapID)] | ||
| 112 | if ok { | ||
| 113 | score = (*overrides)[entry.SteamID][strconv.Itoa(record.MapID)] | ||
| 114 | } else { | ||
| 115 | continue // ban | ||
| 116 | } | ||
| 117 | } else { | ||
| 118 | continue // ban | ||
| 119 | } | ||
| 120 | } | ||
| 121 | if record.MapLimit != nil && score > *record.MapLimit { | ||
| 122 | continue // ignore above limit | ||
| 123 | } | ||
| 124 | player.Entries = append(player.Entries, PlayerEntry{ | ||
| 125 | MapID: record.MapID, | ||
| 126 | MapScore: score, | ||
| 127 | }) | ||
| 128 | if record.MapMode == 1 { | ||
| 129 | player.SpScoreCount += score | ||
| 130 | player.SpIterations++ | ||
| 131 | } else if record.MapMode == 2 { | ||
| 132 | player.MpScoreCount += score | ||
| 133 | player.MpIterations++ | ||
| 134 | } | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | } | ||
| 139 | return &players | ||
| 140 | } | ||
| 141 | |||
| 142 | func fetchRecordsFromMap(mapID int, start int, end int) *Leaderboard { | ||
| 143 | resp, err := http.Get(fmt.Sprintf("https://steamcommunity.com/stats/Portal2/leaderboards/%d?xml=1&start=%d&end=%d", mapID, start, end)) | ||
| 144 | if err != nil { | ||
| 145 | log.Fatalln(err.Error()) | ||
| 146 | } | ||
| 147 | respBytes, err := io.ReadAll(resp.Body) | ||
| 148 | if err != nil { | ||
| 149 | log.Fatalln(err.Error()) | ||
| 150 | } | ||
| 151 | leaderboard := Leaderboard{} | ||
| 152 | err = xml.Unmarshal(respBytes, &leaderboard) | ||
| 153 | if err != nil { | ||
| 154 | log.Fatalln(err.Error()) | ||
| 155 | } | ||
| 156 | return &leaderboard | ||
| 157 | } | ||
| 158 | |||
| 159 | func fetchPlayerInfo(player *Player) { | ||
| 160 | url := fmt.Sprintf("http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=%s&steamids=%s", os.Getenv("API_KEY"), player.SteamID) | ||
| 161 | resp, err := http.Get(url) | ||
| 162 | if err != nil { | ||
| 163 | log.Fatalln(err.Error()) | ||
| 164 | } | ||
| 165 | body, err := io.ReadAll(resp.Body) | ||
| 166 | if err != nil { | ||
| 167 | log.Fatalln(err.Error()) | ||
| 168 | } | ||
| 169 | type PlayerSummary struct { | ||
| 170 | PersonaName string `json:"personaname"` | ||
| 171 | AvatarFull string `json:"avatarfull"` | ||
| 172 | } | ||
| 173 | |||
| 174 | type Result struct { | ||
| 175 | Response struct { | ||
| 176 | Players []PlayerSummary `json:"players"` | ||
| 177 | } `json:"response"` | ||
| 178 | } | ||
| 179 | var data Result | ||
| 180 | if err := json.Unmarshal(body, &data); err != nil { | ||
| 181 | log.Fatalln(err.Error()) | ||
| 182 | } | ||
| 183 | player.AvatarLink = data.Response.Players[0].AvatarFull | ||
| 184 | player.Username = data.Response.Players[0].PersonaName | ||
| 185 | } | ||
diff --git a/rankings/filter.go b/rankings/filter.go new file mode 100644 index 0000000..5f54a81 --- /dev/null +++ b/rankings/filter.go | |||
| @@ -0,0 +1,81 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "log" | ||
| 5 | "sort" | ||
| 6 | ) | ||
| 7 | |||
| 8 | func filterRankings(spRankings, mpRankings, overallRankings *[]*Player, players *map[string]*Player) { | ||
| 9 | for k, p := range *players { | ||
| 10 | if p.SpIterations == 51 { | ||
| 11 | *spRankings = append(*spRankings, p) | ||
| 12 | } | ||
| 13 | if p.MpIterations == 48 { | ||
| 14 | *mpRankings = append(*mpRankings, p) | ||
| 15 | } | ||
| 16 | if p.SpIterations == 51 && p.MpIterations == 48 { | ||
| 17 | p.OverallScoreCount = p.SpScoreCount + p.MpScoreCount | ||
| 18 | *overallRankings = append(*overallRankings, p) | ||
| 19 | } | ||
| 20 | if p.SpIterations < 51 && p.MpIterations < 48 { | ||
| 21 | delete(*players, k) | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | log.Println("getting player summaries") | ||
| 26 | for _, v := range *players { | ||
| 27 | fetchPlayerInfo(v) | ||
| 28 | } | ||
| 29 | |||
| 30 | log.Println("sorting the ranks") | ||
| 31 | sort.Slice(*spRankings, func(i, j int) bool { | ||
| 32 | return (*spRankings)[i].SpScoreCount < (*spRankings)[j].SpScoreCount | ||
| 33 | }) | ||
| 34 | |||
| 35 | rank := 1 | ||
| 36 | |||
| 37 | for idx := 0; idx < len(*spRankings); idx++ { | ||
| 38 | if idx == 0 { | ||
| 39 | (*spRankings)[idx].SpRank = rank | ||
| 40 | continue | ||
| 41 | } | ||
| 42 | if (*spRankings)[idx-1].SpScoreCount != (*spRankings)[idx].SpScoreCount { | ||
| 43 | rank++ | ||
| 44 | } | ||
| 45 | (*spRankings)[idx].SpRank = rank | ||
| 46 | } | ||
| 47 | |||
| 48 | sort.Slice(*mpRankings, func(i, j int) bool { | ||
| 49 | return (*mpRankings)[i].MpScoreCount < (*mpRankings)[j].MpScoreCount | ||
| 50 | }) | ||
| 51 | |||
| 52 | rank = 1 | ||
| 53 | |||
| 54 | for idx := 0; idx < len(*mpRankings); idx++ { | ||
| 55 | if idx == 0 { | ||
| 56 | (*mpRankings)[idx].MpRank = rank | ||
| 57 | continue | ||
| 58 | } | ||
| 59 | if (*mpRankings)[idx-1].MpScoreCount != (*mpRankings)[idx].MpScoreCount { | ||
| 60 | rank++ | ||
| 61 | } | ||
| 62 | (*mpRankings)[idx].MpRank = rank | ||
| 63 | } | ||
| 64 | |||
| 65 | sort.Slice(*overallRankings, func(i, j int) bool { | ||
| 66 | return (*overallRankings)[i].OverallScoreCount < (*overallRankings)[j].OverallScoreCount | ||
| 67 | }) | ||
| 68 | |||
| 69 | rank = 1 | ||
| 70 | |||
| 71 | for idx := 0; idx < len(*overallRankings); idx++ { | ||
| 72 | if idx == 0 { | ||
| 73 | (*overallRankings)[idx].OverallRank = rank | ||
| 74 | continue | ||
| 75 | } | ||
| 76 | if (*overallRankings)[idx-1].OverallScoreCount != (*overallRankings)[idx].OverallScoreCount { | ||
| 77 | rank++ | ||
| 78 | } | ||
| 79 | (*overallRankings)[idx].OverallRank = rank | ||
| 80 | } | ||
| 81 | } | ||
diff --git a/rankings/go.mod b/rankings/go.mod new file mode 100644 index 0000000..2a395e0 --- /dev/null +++ b/rankings/go.mod | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | module rankings | ||
| 2 | |||
| 3 | go 1.23.0 | ||
| 4 | |||
| 5 | require ( | ||
| 6 | github.com/joho/godotenv v1.5.1 | ||
| 7 | github.com/robfig/cron/v3 v3.0.1 | ||
| 8 | ) | ||
diff --git a/rankings/go.sum b/rankings/go.sum new file mode 100644 index 0000000..cb2af12 --- /dev/null +++ b/rankings/go.sum | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||
| 2 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||
| 3 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= | ||
| 4 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= | ||
diff --git a/rankings/input/overrides.json b/rankings/input/overrides.json new file mode 100644 index 0000000..9c2e2a3 --- /dev/null +++ b/rankings/input/overrides.json | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | { | ||
| 2 | "76561198997027314": { | ||
| 3 | "47467": 3 | ||
| 4 | }, | ||
| 5 | "76561198103821970": { | ||
| 6 | "47750": 2, | ||
| 7 | "47753": 4 | ||
| 8 | }, | ||
| 9 | "76561198389681125": { | ||
| 10 | "47827": 4, | ||
| 11 | "52716": 4 | ||
| 12 | }, | ||
| 13 | "76561198136477838": { | ||
| 14 | "47838": 2, | ||
| 15 | "47850": 3, | ||
| 16 | "47467": 13, | ||
| 17 | "47753": 5, | ||
| 18 | "47749": 8, | ||
| 19 | "47747": 2, | ||
| 20 | "47107": 3, | ||
| 21 | "47772": 2, | ||
| 22 | "47781": 3, | ||
| 23 | "47750": 2, | ||
| 24 | "47745": 2, | ||
| 25 | "47777": 6, | ||
| 26 | "47778": 7, | ||
| 27 | "47782": 7, | ||
| 28 | "47822": 4, | ||
| 29 | "47823": 9, | ||
| 30 | "47457": 9, | ||
| 31 | "47809": 2, | ||
| 32 | "47816": 16, | ||
| 33 | "47471": 8, | ||
| 34 | "47792": 4, | ||
| 35 | "47820": 4, | ||
| 36 | "47805": 2, | ||
| 37 | "47785": 9, | ||
| 38 | "47799": 5, | ||
| 39 | "47794": 2, | ||
| 40 | "47796": 29, | ||
| 41 | "47754": 5, | ||
| 42 | "47767": 12 | ||
| 43 | } | ||
| 44 | } | ||
diff --git a/rankings/input/records.json b/rankings/input/records.json new file mode 100644 index 0000000..276310f --- /dev/null +++ b/rankings/input/records.json | |||
| @@ -0,0 +1,604 @@ | |||
| 1 | [ | ||
| 2 | { | ||
| 3 | "id": 47459, | ||
| 4 | "name": "Portal Gun", | ||
| 5 | "mode": 1, | ||
| 6 | "wr": 0 | ||
| 7 | }, | ||
| 8 | { | ||
| 9 | "id": 47454, | ||
| 10 | "name": "Smooth Jazz", | ||
| 11 | "mode": 1, | ||
| 12 | "wr": 0, | ||
| 13 | "limit": 1 | ||
| 14 | }, | ||
| 15 | { | ||
| 16 | "id": 47451, | ||
| 17 | "name": "Cube Momentum", | ||
| 18 | "mode": 1, | ||
| 19 | "wr": 1 | ||
| 20 | }, | ||
| 21 | { | ||
| 22 | "id": 47107, | ||
| 23 | "name": "Future Starter", | ||
| 24 | "mode": 1, | ||
| 25 | "wr": 2 | ||
| 26 | }, | ||
| 27 | { | ||
| 28 | "id": 47734, | ||
| 29 | "name": "Incinerator", | ||
| 30 | "mode": 1, | ||
| 31 | "wr": 0 | ||
| 32 | }, | ||
| 33 | { | ||
| 34 | "id": 47737, | ||
| 35 | "name": "Laser Stairs", | ||
| 36 | "mode": 1, | ||
| 37 | "wr": 0 | ||
| 38 | }, | ||
| 39 | { | ||
| 40 | "id": 47739, | ||
| 41 | "name": "Dual Lasers", | ||
| 42 | "mode": 1, | ||
| 43 | "wr": 2 | ||
| 44 | }, | ||
| 45 | { | ||
| 46 | "id": 47743, | ||
| 47 | "name": "Laser Over Goo", | ||
| 48 | "mode": 1, | ||
| 49 | "wr": 0 | ||
| 50 | }, | ||
| 51 | { | ||
| 52 | "id": 47745, | ||
| 53 | "name": "Trust Fling", | ||
| 54 | "mode": 1, | ||
| 55 | "wr": 0, | ||
| 56 | "limit": 2 | ||
| 57 | }, | ||
| 58 | { | ||
| 59 | "id": 47466, | ||
| 60 | "name": "Pit Flings", | ||
| 61 | "mode": 1, | ||
| 62 | "wr": 0 | ||
| 63 | }, | ||
| 64 | { | ||
| 65 | "id": 47747, | ||
| 66 | "name": "Fizzler Intro", | ||
| 67 | "mode": 1, | ||
| 68 | "wr": 0, | ||
| 69 | "limit": 2 | ||
| 70 | }, | ||
| 71 | { | ||
| 72 | "id": 47749, | ||
| 73 | "name": "Ceiling Catapult", | ||
| 74 | "mode": 1, | ||
| 75 | "wr": 0 | ||
| 76 | }, | ||
| 77 | { | ||
| 78 | "id": 47750, | ||
| 79 | "name": "Ricochet", | ||
| 80 | "mode": 1, | ||
| 81 | "wr": 0 | ||
| 82 | }, | ||
| 83 | { | ||
| 84 | "id": 47753, | ||
| 85 | "name": "Bridge Intro", | ||
| 86 | "mode": 1, | ||
| 87 | "wr": 2 | ||
| 88 | }, | ||
| 89 | { | ||
| 90 | "id": 47754, | ||
| 91 | "name": "Bridge the Gap", | ||
| 92 | "mode": 1, | ||
| 93 | "wr": 0 | ||
| 94 | }, | ||
| 95 | { | ||
| 96 | "id": 47757, | ||
| 97 | "name": "Turret Intro", | ||
| 98 | "mode": 1, | ||
| 99 | "wr": 0 | ||
| 100 | }, | ||
| 101 | { | ||
| 102 | "id": 47758, | ||
| 103 | "name": "Laser Relays", | ||
| 104 | "mode": 1, | ||
| 105 | "wr": 0, | ||
| 106 | "limit": 2 | ||
| 107 | }, | ||
| 108 | { | ||
| 109 | "id": 47761, | ||
| 110 | "name": "Turret Blocker", | ||
| 111 | "mode": 1, | ||
| 112 | "wr": 0 | ||
| 113 | }, | ||
| 114 | { | ||
| 115 | "id": 47762, | ||
| 116 | "name": "Laser vs Turret", | ||
| 117 | "mode": 1, | ||
| 118 | "wr": 0 | ||
| 119 | }, | ||
| 120 | { | ||
| 121 | "id": 47765, | ||
| 122 | "name": "Pull the Rug", | ||
| 123 | "mode": 1, | ||
| 124 | "wr": 0, | ||
| 125 | "limit": 3 | ||
| 126 | }, | ||
| 127 | { | ||
| 128 | "id": 47767, | ||
| 129 | "name": "Column Blocker", | ||
| 130 | "mode": 1, | ||
| 131 | "wr": 0, | ||
| 132 | "limit": 2 | ||
| 133 | }, | ||
| 134 | { | ||
| 135 | "id": 47769, | ||
| 136 | "name": "Laser Chaining", | ||
| 137 | "mode": 1, | ||
| 138 | "wr": 0 | ||
| 139 | }, | ||
| 140 | { | ||
| 141 | "id": 47771, | ||
| 142 | "name": "Triple Laser", | ||
| 143 | "mode": 1, | ||
| 144 | "wr": 0 | ||
| 145 | }, | ||
| 146 | { | ||
| 147 | "id": 47772, | ||
| 148 | "name": "Jail Break", | ||
| 149 | "mode": 1, | ||
| 150 | "wr": 2 | ||
| 151 | }, | ||
| 152 | { | ||
| 153 | "id": 47775, | ||
| 154 | "name": "Escape", | ||
| 155 | "mode": 1, | ||
| 156 | "wr": 0 | ||
| 157 | }, | ||
| 158 | { | ||
| 159 | "id": 47777, | ||
| 160 | "name": "Turret Factory", | ||
| 161 | "mode": 1, | ||
| 162 | "wr": 5 | ||
| 163 | }, | ||
| 164 | { | ||
| 165 | "id": 47778, | ||
| 166 | "name": "Turret Sabotage", | ||
| 167 | "mode": 1, | ||
| 168 | "wr": 4 | ||
| 169 | }, | ||
| 170 | { | ||
| 171 | "id": 47781, | ||
| 172 | "name": "Neurotoxin Sabotage", | ||
| 173 | "mode": 1, | ||
| 174 | "wr": 0, | ||
| 175 | "limit": 3 | ||
| 176 | }, | ||
| 177 | { | ||
| 178 | "id": 47782, | ||
| 179 | "name": "Underground", | ||
| 180 | "mode": 1, | ||
| 181 | "wr": 2 | ||
| 182 | }, | ||
| 183 | { | ||
| 184 | "id": 47785, | ||
| 185 | "name": "Cave Johnson", | ||
| 186 | "mode": 1, | ||
| 187 | "wr": 4 | ||
| 188 | }, | ||
| 189 | { | ||
| 190 | "id": 47786, | ||
| 191 | "name": "Repulsion Intro", | ||
| 192 | "mode": 1, | ||
| 193 | "wr": 0 | ||
| 194 | }, | ||
| 195 | { | ||
| 196 | "id": 47467, | ||
| 197 | "name": "Bomb Flings", | ||
| 198 | "mode": 1, | ||
| 199 | "wr": 3 | ||
| 200 | }, | ||
| 201 | { | ||
| 202 | "id": 47470, | ||
| 203 | "name": "Crazy Box", | ||
| 204 | "mode": 1, | ||
| 205 | "wr": 0 | ||
| 206 | }, | ||
| 207 | { | ||
| 208 | "id": 47471, | ||
| 209 | "name": "PotatOS", | ||
| 210 | "mode": 1, | ||
| 211 | "wr": 5 | ||
| 212 | }, | ||
| 213 | { | ||
| 214 | "id": 47792, | ||
| 215 | "name": "Propulsion Intro", | ||
| 216 | "mode": 1, | ||
| 217 | "wr": 2 | ||
| 218 | }, | ||
| 219 | { | ||
| 220 | "id": 47794, | ||
| 221 | "name": "Propulsion Flings", | ||
| 222 | "mode": 1, | ||
| 223 | "wr": 0 | ||
| 224 | }, | ||
| 225 | { | ||
| 226 | "id": 47796, | ||
| 227 | "name": "Conversion Intro", | ||
| 228 | "mode": 1, | ||
| 229 | "wr": 8 | ||
| 230 | }, | ||
| 231 | { | ||
| 232 | "id": 47799, | ||
| 233 | "name": "Three Gels", | ||
| 234 | "mode": 1, | ||
| 235 | "wr": 4 | ||
| 236 | }, | ||
| 237 | { | ||
| 238 | "id": 47801, | ||
| 239 | "name": "Funnel Intro", | ||
| 240 | "mode": 1, | ||
| 241 | "wr": 0 | ||
| 242 | }, | ||
| 243 | { | ||
| 244 | "id": 47803, | ||
| 245 | "name": "Ceiling Button", | ||
| 246 | "mode": 1, | ||
| 247 | "wr": 0 | ||
| 248 | }, | ||
| 249 | { | ||
| 250 | "id": 47805, | ||
| 251 | "name": "Wall Button", | ||
| 252 | "mode": 1, | ||
| 253 | "wr": 0 | ||
| 254 | }, | ||
| 255 | { | ||
| 256 | "id": 47807, | ||
| 257 | "name": "Polarity", | ||
| 258 | "mode": 1, | ||
| 259 | "wr": 0 | ||
| 260 | }, | ||
| 261 | { | ||
| 262 | "id": 47809, | ||
| 263 | "name": "Funnel Catch", | ||
| 264 | "mode": 1, | ||
| 265 | "wr": 2 | ||
| 266 | }, | ||
| 267 | { | ||
| 268 | "id": 47812, | ||
| 269 | "name": "Stop the Box", | ||
| 270 | "mode": 1, | ||
| 271 | "wr": 0 | ||
| 272 | }, | ||
| 273 | { | ||
| 274 | "id": 47814, | ||
| 275 | "name": "Laser Catapult", | ||
| 276 | "mode": 1, | ||
| 277 | "wr": 0 | ||
| 278 | }, | ||
| 279 | { | ||
| 280 | "id": 47816, | ||
| 281 | "name": "Laser Platform", | ||
| 282 | "mode": 1, | ||
| 283 | "wr": 3 | ||
| 284 | }, | ||
| 285 | { | ||
| 286 | "id": 47818, | ||
| 287 | "name": "Propulsion Catch", | ||
| 288 | "mode": 1, | ||
| 289 | "wr": 0 | ||
| 290 | }, | ||
| 291 | { | ||
| 292 | "id": 47820, | ||
| 293 | "name": "Repulsion Polarity", | ||
| 294 | "mode": 1, | ||
| 295 | "wr": 2 | ||
| 296 | }, | ||
| 297 | { | ||
| 298 | "id": 47822, | ||
| 299 | "name": "Finale 2", | ||
| 300 | "mode": 1, | ||
| 301 | "wr": 2 | ||
| 302 | }, | ||
| 303 | { | ||
| 304 | "id": 47823, | ||
| 305 | "name": "Finale 3", | ||
| 306 | "mode": 1, | ||
| 307 | "wr": 6 | ||
| 308 | }, | ||
| 309 | { | ||
| 310 | "id": 47457, | ||
| 311 | "name": "Finale 4", | ||
| 312 | "mode": 1, | ||
| 313 | "wr": 4 | ||
| 314 | }, | ||
| 315 | { | ||
| 316 | "id": 47740, | ||
| 317 | "name": "Doors", | ||
| 318 | "mode": 2, | ||
| 319 | "wr": 0, | ||
| 320 | "limit": 2 | ||
| 321 | }, | ||
| 322 | { | ||
| 323 | "id": 47826, | ||
| 324 | "name": "Buttons", | ||
| 325 | "mode": 2, | ||
| 326 | "wr": 2 | ||
| 327 | }, | ||
| 328 | { | ||
| 329 | "id": 47827, | ||
| 330 | "name": "Lasers", | ||
| 331 | "mode": 2, | ||
| 332 | "wr": 2 | ||
| 333 | }, | ||
| 334 | { | ||
| 335 | "id": 47830, | ||
| 336 | "name": "Rat Maze", | ||
| 337 | "mode": 2, | ||
| 338 | "wr": 2 | ||
| 339 | }, | ||
| 340 | { | ||
| 341 | "id": 45466, | ||
| 342 | "name": "Laser Crusher", | ||
| 343 | "mode": 2, | ||
| 344 | "wr": 0 | ||
| 345 | }, | ||
| 346 | { | ||
| 347 | "id": 46361, | ||
| 348 | "name": "Behind The Scenes", | ||
| 349 | "mode": 2, | ||
| 350 | "wr": 0 | ||
| 351 | }, | ||
| 352 | { | ||
| 353 | "id": 47832, | ||
| 354 | "name": "Flings", | ||
| 355 | "mode": 2, | ||
| 356 | "wr": 4 | ||
| 357 | }, | ||
| 358 | { | ||
| 359 | "id": 47834, | ||
| 360 | "name": "Infinifling", | ||
| 361 | "mode": 2, | ||
| 362 | "wr": 0 | ||
| 363 | }, | ||
| 364 | { | ||
| 365 | "id": 47836, | ||
| 366 | "name": "Team Retrieval", | ||
| 367 | "mode": 2, | ||
| 368 | "wr": 0 | ||
| 369 | }, | ||
| 370 | { | ||
| 371 | "id": 47838, | ||
| 372 | "name": "Vertical Flings", | ||
| 373 | "mode": 2, | ||
| 374 | "wr": 2 | ||
| 375 | }, | ||
| 376 | { | ||
| 377 | "id": 47839, | ||
| 378 | "name": "Catapults", | ||
| 379 | "mode": 2, | ||
| 380 | "wr": 4 | ||
| 381 | }, | ||
| 382 | { | ||
| 383 | "id": 47842, | ||
| 384 | "name": "Multifling", | ||
| 385 | "mode": 2, | ||
| 386 | "wr": 2 | ||
| 387 | }, | ||
| 388 | { | ||
| 389 | "id": 47843, | ||
| 390 | "name": "Fling Crushers", | ||
| 391 | "mode": 2, | ||
| 392 | "wr": 0 | ||
| 393 | }, | ||
| 394 | { | ||
| 395 | "id": 47846, | ||
| 396 | "name": "Industrial Fan", | ||
| 397 | "mode": 2, | ||
| 398 | "wr": 0 | ||
| 399 | }, | ||
| 400 | { | ||
| 401 | "id": 47847, | ||
| 402 | "name": "Cooperative Bridges", | ||
| 403 | "mode": 2, | ||
| 404 | "wr": 3 | ||
| 405 | }, | ||
| 406 | { | ||
| 407 | "id": 47850, | ||
| 408 | "name": "Bridge Swap", | ||
| 409 | "mode": 2, | ||
| 410 | "wr": 2 | ||
| 411 | }, | ||
| 412 | { | ||
| 413 | "id": 47855, | ||
| 414 | "name": "Fling Block", | ||
| 415 | "mode": 2, | ||
| 416 | "wr": 0 | ||
| 417 | }, | ||
| 418 | { | ||
| 419 | "id": 47857, | ||
| 420 | "name": "Catapult Block", | ||
| 421 | "mode": 2, | ||
| 422 | "wr": 4 | ||
| 423 | }, | ||
| 424 | { | ||
| 425 | "id": 47859, | ||
| 426 | "name": "Bridge Fling", | ||
| 427 | "mode": 2, | ||
| 428 | "wr": 2 | ||
| 429 | }, | ||
| 430 | { | ||
| 431 | "id": 47860, | ||
| 432 | "name": "Turret Walls", | ||
| 433 | "mode": 2, | ||
| 434 | "wr": 4 | ||
| 435 | }, | ||
| 436 | { | ||
| 437 | "id": 52641, | ||
| 438 | "name": "Turret Assassin", | ||
| 439 | "mode": 2, | ||
| 440 | "wr": 0 | ||
| 441 | }, | ||
| 442 | { | ||
| 443 | "id": 52659, | ||
| 444 | "name": "Bridge Testing", | ||
| 445 | "mode": 2, | ||
| 446 | "wr": 0 | ||
| 447 | }, | ||
| 448 | { | ||
| 449 | "id": 52661, | ||
| 450 | "name": "Cooperative Funnels", | ||
| 451 | "mode": 2, | ||
| 452 | "wr": 0 | ||
| 453 | }, | ||
| 454 | { | ||
| 455 | "id": 52664, | ||
| 456 | "name": "Funnel Drill", | ||
| 457 | "mode": 2, | ||
| 458 | "wr": 0 | ||
| 459 | }, | ||
| 460 | { | ||
| 461 | "id": 52666, | ||
| 462 | "name": "Funnel Catch", | ||
| 463 | "mode": 2, | ||
| 464 | "wr": 0 | ||
| 465 | }, | ||
| 466 | { | ||
| 467 | "id": 52668, | ||
| 468 | "name": "Funnel Laser", | ||
| 469 | "mode": 2, | ||
| 470 | "wr": 0 | ||
| 471 | }, | ||
| 472 | { | ||
| 473 | "id": 52672, | ||
| 474 | "name": "Cooperative Polarity", | ||
| 475 | "mode": 2, | ||
| 476 | "wr": 0 | ||
| 477 | }, | ||
| 478 | { | ||
| 479 | "id": 52688, | ||
| 480 | "name": "Funnel Hop", | ||
| 481 | "mode": 2, | ||
| 482 | "wr": 0 | ||
| 483 | }, | ||
| 484 | { | ||
| 485 | "id": 52690, | ||
| 486 | "name": "Advanced Polarity", | ||
| 487 | "mode": 2, | ||
| 488 | "wr": 0 | ||
| 489 | }, | ||
| 490 | { | ||
| 491 | "id": 52692, | ||
| 492 | "name": "Funnel Maze", | ||
| 493 | "mode": 2, | ||
| 494 | "wr": 0 | ||
| 495 | }, | ||
| 496 | { | ||
| 497 | "id": 52778, | ||
| 498 | "name": "Turret Warehouse", | ||
| 499 | "mode": 2, | ||
| 500 | "wr": 0 | ||
| 501 | }, | ||
| 502 | { | ||
| 503 | "id": 52693, | ||
| 504 | "name": "Repulsion Jumps", | ||
| 505 | "mode": 2, | ||
| 506 | "wr": 0 | ||
| 507 | }, | ||
| 508 | { | ||
| 509 | "id": 52712, | ||
| 510 | "name": "Double Bounce", | ||
| 511 | "mode": 2, | ||
| 512 | "wr": 2 | ||
| 513 | }, | ||
| 514 | { | ||
| 515 | "id": 52713, | ||
| 516 | "name": "Bridge Repulsion", | ||
| 517 | "mode": 2, | ||
| 518 | "wr": 2 | ||
| 519 | }, | ||
| 520 | { | ||
| 521 | "id": 52716, | ||
| 522 | "name": "Wall Repulsion", | ||
| 523 | "mode": 2, | ||
| 524 | "wr": 2 | ||
| 525 | }, | ||
| 526 | { | ||
| 527 | "id": 52718, | ||
| 528 | "name": "Propulsion Crushers", | ||
| 529 | "mode": 2, | ||
| 530 | "wr": 0 | ||
| 531 | }, | ||
| 532 | { | ||
| 533 | "id": 52736, | ||
| 534 | "name": "Turret Ninja", | ||
| 535 | "mode": 2, | ||
| 536 | "wr": 0 | ||
| 537 | }, | ||
| 538 | { | ||
| 539 | "id": 52737, | ||
| 540 | "name": "Propulsion Retrieval", | ||
| 541 | "mode": 2, | ||
| 542 | "wr": 0 | ||
| 543 | }, | ||
| 544 | { | ||
| 545 | "id": 52739, | ||
| 546 | "name": "Vault Entrance", | ||
| 547 | "mode": 2, | ||
| 548 | "wr": 0 | ||
| 549 | }, | ||
| 550 | { | ||
| 551 | "id": 49342, | ||
| 552 | "name": "Separation", | ||
| 553 | "mode": 2, | ||
| 554 | "wr": 0 | ||
| 555 | }, | ||
| 556 | { | ||
| 557 | "id": 49344, | ||
| 558 | "name": "Triple Axis", | ||
| 559 | "mode": 2, | ||
| 560 | "wr": 0 | ||
| 561 | }, | ||
| 562 | { | ||
| 563 | "id": 49346, | ||
| 564 | "name": "Catapult Catch", | ||
| 565 | "mode": 2, | ||
| 566 | "wr": 0 | ||
| 567 | }, | ||
| 568 | { | ||
| 569 | "id": 49348, | ||
| 570 | "name": "Bridge Gels", | ||
| 571 | "mode": 2, | ||
| 572 | "wr": 2 | ||
| 573 | }, | ||
| 574 | { | ||
| 575 | "id": 49350, | ||
| 576 | "name": "Maintenance", | ||
| 577 | "mode": 2, | ||
| 578 | "wr": 0 | ||
| 579 | }, | ||
| 580 | { | ||
| 581 | "id": 49352, | ||
| 582 | "name": "Bridge Catch", | ||
| 583 | "mode": 2, | ||
| 584 | "wr": 0 | ||
| 585 | }, | ||
| 586 | { | ||
| 587 | "id": 52758, | ||
| 588 | "name": "Double Lift", | ||
| 589 | "mode": 2, | ||
| 590 | "wr": 0 | ||
| 591 | }, | ||
| 592 | { | ||
| 593 | "id": 52760, | ||
| 594 | "name": "Gel Maze", | ||
| 595 | "mode": 2, | ||
| 596 | "wr": 0 | ||
| 597 | }, | ||
| 598 | { | ||
| 599 | "id": 48288, | ||
| 600 | "name": "Crazier Box", | ||
| 601 | "mode": 2, | ||
| 602 | "wr": 0 | ||
| 603 | } | ||
| 604 | ] \ No newline at end of file | ||
diff --git a/rankings/main.go b/rankings/main.go new file mode 100644 index 0000000..dfafb0c --- /dev/null +++ b/rankings/main.go | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "log" | ||
| 5 | "os" | ||
| 6 | "os/signal" | ||
| 7 | "syscall" | ||
| 8 | |||
| 9 | "github.com/joho/godotenv" | ||
| 10 | "github.com/robfig/cron/v3" | ||
| 11 | ) | ||
| 12 | |||
| 13 | func main() { | ||
| 14 | err := godotenv.Load() | ||
| 15 | if err != nil { | ||
| 16 | log.Fatalln("Error loading .env file:", err.Error()) | ||
| 17 | } | ||
| 18 | c := cron.New() | ||
| 19 | _, err = c.AddFunc("0 0 * * *", run) | ||
| 20 | if err != nil { | ||
| 21 | log.Fatalln("Error scheduling daily reminder:", err.Error()) | ||
| 22 | } | ||
| 23 | c.Start() | ||
| 24 | log.Println("ready for jobs") | ||
| 25 | sc := make(chan os.Signal, 1) | ||
| 26 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) | ||
| 27 | <-sc | ||
| 28 | } | ||
| 29 | |||
| 30 | func run() { | ||
| 31 | log.Println("started job") | ||
| 32 | records := readRecords() | ||
| 33 | overrides := readOverrides() | ||
| 34 | players := fetchLeaderboard(records, overrides) | ||
| 35 | |||
| 36 | spRankings := []*Player{} | ||
| 37 | mpRankings := []*Player{} | ||
| 38 | overallRankings := []*Player{} | ||
| 39 | |||
| 40 | log.Println("filtering rankings") | ||
| 41 | filterRankings(&spRankings, &mpRankings, &overallRankings, players) | ||
| 42 | |||
| 43 | log.Println("exporting jsons") | ||
| 44 | exportAll(&spRankings, &mpRankings, &overallRankings) | ||
| 45 | } | ||
diff --git a/rankings/models.go b/rankings/models.go new file mode 100644 index 0000000..1b349b0 --- /dev/null +++ b/rankings/models.go | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | type Record struct { | ||
| 4 | MapID int `json:"id"` | ||
| 5 | MapName string `json:"name"` | ||
| 6 | MapMode int `json:"mode"` | ||
| 7 | MapWR int `json:"wr"` | ||
| 8 | MapLimit *int `json:"limit"` | ||
| 9 | } | ||
| 10 | |||
| 11 | type Leaderboard struct { | ||
| 12 | Entries LeaderboardEntries `xml:"entries"` | ||
| 13 | } | ||
| 14 | |||
| 15 | func (l *Leaderboard) needsAnotherPage(record *Record) bool { | ||
| 16 | if l.Entries.Entry[len(l.Entries.Entry)-1].Score == record.MapWR { | ||
| 17 | return true | ||
| 18 | } else if record.MapLimit != nil && l.Entries.Entry[len(l.Entries.Entry)-1].Score <= *record.MapLimit { | ||
| 19 | return true | ||
| 20 | } | ||
| 21 | return false | ||
| 22 | } | ||
| 23 | |||
| 24 | type LeaderboardEntries struct { | ||
| 25 | Entry []LeaderboardEntry `xml:"entry"` | ||
| 26 | } | ||
| 27 | |||
| 28 | type LeaderboardEntry struct { | ||
| 29 | SteamID string `xml:"steamid"` | ||
| 30 | Score int `xml:"score"` | ||
| 31 | } | ||
| 32 | |||
| 33 | type Player struct { | ||
| 34 | Username string `json:"user_name"` | ||
| 35 | AvatarLink string `json:"avatar_link"` | ||
| 36 | SteamID string `json:"steam_id"` | ||
| 37 | Entries []PlayerEntry `json:"-"` | ||
| 38 | SpScoreCount int `json:"sp_score"` | ||
| 39 | MpScoreCount int `json:"mp_score"` | ||
| 40 | OverallScoreCount int `json:"overall_score"` | ||
| 41 | SpRank int `json:"sp_rank"` | ||
| 42 | MpRank int `json:"mp_rank"` | ||
| 43 | OverallRank int `json:"overall_rank"` | ||
| 44 | SpIterations int `json:"-"` | ||
| 45 | MpIterations int `json:"-"` | ||
| 46 | } | ||
| 47 | |||
| 48 | type PlayerEntry struct { | ||
| 49 | MapID int | ||
| 50 | MapScore int | ||
| 51 | } | ||
diff --git a/rankings/prefetch.go b/rankings/prefetch.go new file mode 100644 index 0000000..487a76f --- /dev/null +++ b/rankings/prefetch.go | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "encoding/json" | ||
| 5 | "io" | ||
| 6 | "log" | ||
| 7 | "os" | ||
| 8 | ) | ||
| 9 | |||
| 10 | func readRecords() *[]Record { | ||
| 11 | recordsFile, err := os.Open("./input/records.json") | ||
| 12 | if err != nil { | ||
| 13 | log.Fatalln(err.Error()) | ||
| 14 | } | ||
| 15 | defer recordsFile.Close() | ||
| 16 | recordFileBytes, err := io.ReadAll(recordsFile) | ||
| 17 | if err != nil { | ||
| 18 | log.Fatalln(err.Error()) | ||
| 19 | } | ||
| 20 | records := []Record{} | ||
| 21 | err = json.Unmarshal(recordFileBytes, &records) | ||
| 22 | if err != nil { | ||
| 23 | log.Fatalln(err.Error()) | ||
| 24 | } | ||
| 25 | return &records | ||
| 26 | } | ||
| 27 | |||
| 28 | func readOverrides() *map[string]map[string]int { | ||
| 29 | overridesFile, err := os.Open("./input/overrides.json") | ||
| 30 | if err != nil { | ||
| 31 | log.Fatalln(err.Error()) | ||
| 32 | } | ||
| 33 | defer overridesFile.Close() | ||
| 34 | overridesFileBytes, err := io.ReadAll(overridesFile) | ||
| 35 | if err != nil { | ||
| 36 | log.Fatalln(err.Error()) | ||
| 37 | } | ||
| 38 | overrides := map[string]map[string]int{} | ||
| 39 | err = json.Unmarshal(overridesFileBytes, &overrides) | ||
| 40 | if err != nil { | ||
| 41 | log.Fatalln(err.Error()) | ||
| 42 | } | ||
| 43 | return &overrides | ||
| 44 | } | ||