aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--backend/api/routes.go6
-rw-r--r--backend/docs/docs.go90
-rw-r--r--backend/docs/swagger.json90
-rw-r--r--backend/docs/swagger.yaml57
-rw-r--r--backend/handlers/home.go100
-rw-r--r--rankings/.env.example1
-rw-r--r--rankings/.gitignore2
-rw-r--r--rankings/README.md35
-rw-r--r--rankings/export.go21
-rw-r--r--rankings/fetch.go185
-rw-r--r--rankings/filter.go81
-rw-r--r--rankings/go.mod8
-rw-r--r--rankings/go.sum4
-rw-r--r--rankings/input/overrides.json44
-rw-r--r--rankings/input/records.json604
-rw-r--r--rankings/main.go45
-rw-r--r--rankings/models.go51
-rw-r--r--rankings/prefetch.go44
18 files changed, 1454 insertions, 14 deletions
diff --git a/backend/api/routes.go b/backend/api/routes.go
index 050a3bd..81f1ec6 100644
--- a/backend/api/routes.go
+++ b/backend/api/routes.go
@@ -25,7 +25,8 @@ const (
25 mapRecordIDPath string = "/maps/:mapid/record/:recordid" 25 mapRecordIDPath string = "/maps/:mapid/record/:recordid"
26 mapDiscussionsPath string = "/maps/:mapid/discussions" 26 mapDiscussionsPath string = "/maps/:mapid/discussions"
27 mapDiscussionIDPath string = "/maps/:mapid/discussions/:discussionid" 27 mapDiscussionIDPath string = "/maps/:mapid/discussions/:discussionid"
28 rankingsPath string = "/rankings" 28 rankingsLPHUBPath string = "/rankings/lphub"
29 rankingsSteamPath string = "/rankings/steam"
29 searchPath string = "/search" 30 searchPath string = "/search"
30 gamesPath string = "/games" 31 gamesPath string = "/games"
31 chaptersPath string = "/games/:gameid" 32 chaptersPath string = "/games/:gameid"
@@ -73,7 +74,8 @@ func InitRoutes(router *gin.Engine) {
73 v1.PUT(mapDiscussionIDPath, CheckAuth, handlers.EditMapDiscussion) 74 v1.PUT(mapDiscussionIDPath, CheckAuth, handlers.EditMapDiscussion)
74 v1.DELETE(mapDiscussionIDPath, CheckAuth, handlers.DeleteMapDiscussion) 75 v1.DELETE(mapDiscussionIDPath, CheckAuth, handlers.DeleteMapDiscussion)
75 // Rankings, search 76 // Rankings, search
76 v1.GET(rankingsPath, handlers.Rankings) 77 v1.GET(rankingsLPHUBPath, handlers.RankingsLPHUB)
78 v1.GET(rankingsSteamPath, handlers.RankingsSteam)
77 v1.GET(searchPath, handlers.SearchWithQuery) 79 v1.GET(searchPath, handlers.SearchWithQuery)
78 // Games, chapters, maps 80 // Games, chapters, maps
79 v1.GET(gamesPath, handlers.FetchGames) 81 v1.GET(gamesPath, handlers.FetchGames)
diff --git a/backend/docs/docs.go b/backend/docs/docs.go
index f652a1e..c4b2801 100644
--- a/backend/docs/docs.go
+++ b/backend/docs/docs.go
@@ -1171,9 +1171,9 @@ const docTemplate = `{
1171 } 1171 }
1172 } 1172 }
1173 }, 1173 },
1174 "/rankings": { 1174 "/rankings/lphub": {
1175 "get": { 1175 "get": {
1176 "description": "Get rankings of every player.", 1176 "description": "Get rankings of every player from LPHUB.",
1177 "produces": [ 1177 "produces": [
1178 "application/json" 1178 "application/json"
1179 ], 1179 ],
@@ -1202,6 +1202,37 @@ const docTemplate = `{
1202 } 1202 }
1203 } 1203 }
1204 }, 1204 },
1205 "/rankings/steam": {
1206 "get": {
1207 "description": "Get rankings of every player from Steam.",
1208 "produces": [
1209 "application/json"
1210 ],
1211 "tags": [
1212 "rankings"
1213 ],
1214 "responses": {
1215 "200": {
1216 "description": "OK",
1217 "schema": {
1218 "allOf": [
1219 {
1220 "$ref": "#/definitions/models.Response"
1221 },
1222 {
1223 "type": "object",
1224 "properties": {
1225 "data": {
1226 "$ref": "#/definitions/handlers.RankingsSteamResponse"
1227 }
1228 }
1229 }
1230 ]
1231 }
1232 }
1233 }
1234 }
1235 },
1205 "/search": { 1236 "/search": {
1206 "get": { 1237 "get": {
1207 "description": "Get all user and map data matching to the query.", 1238 "description": "Get all user and map data matching to the query.",
@@ -1789,6 +1820,29 @@ const docTemplate = `{
1789 } 1820 }
1790 } 1821 }
1791 }, 1822 },
1823 "handlers.RankingsSteamResponse": {
1824 "type": "object",
1825 "properties": {
1826 "rankings_multiplayer": {
1827 "type": "array",
1828 "items": {
1829 "$ref": "#/definitions/handlers.SteamUserRanking"
1830 }
1831 },
1832 "rankings_overall": {
1833 "type": "array",
1834 "items": {
1835 "$ref": "#/definitions/handlers.SteamUserRanking"
1836 }
1837 },
1838 "rankings_singleplayer": {
1839 "type": "array",
1840 "items": {
1841 "$ref": "#/definitions/handlers.SteamUserRanking"
1842 }
1843 }
1844 }
1845 },
1792 "handlers.RecordResponse": { 1846 "handlers.RecordResponse": {
1793 "type": "object", 1847 "type": "object",
1794 "properties": { 1848 "properties": {
@@ -1854,6 +1908,38 @@ const docTemplate = `{
1854 } 1908 }
1855 } 1909 }
1856 }, 1910 },
1911 "handlers.SteamUserRanking": {
1912 "type": "object",
1913 "properties": {
1914 "avatar_link": {
1915 "type": "string"
1916 },
1917 "mp_rank": {
1918 "type": "integer"
1919 },
1920 "mp_score": {
1921 "type": "integer"
1922 },
1923 "overall_rank": {
1924 "type": "integer"
1925 },
1926 "overall_score": {
1927 "type": "integer"
1928 },
1929 "sp_rank": {
1930 "type": "integer"
1931 },
1932 "sp_score": {
1933 "type": "integer"
1934 },
1935 "steam_id": {
1936 "type": "string"
1937 },
1938 "user_name": {
1939 "type": "string"
1940 }
1941 }
1942 },
1857 "models.Category": { 1943 "models.Category": {
1858 "type": "object", 1944 "type": "object",
1859 "properties": { 1945 "properties": {
diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json
index 6de5978..7613d2c 100644
--- a/backend/docs/swagger.json
+++ b/backend/docs/swagger.json
@@ -1165,9 +1165,9 @@
1165 } 1165 }
1166 } 1166 }
1167 }, 1167 },
1168 "/rankings": { 1168 "/rankings/lphub": {
1169 "get": { 1169 "get": {
1170 "description": "Get rankings of every player.", 1170 "description": "Get rankings of every player from LPHUB.",
1171 "produces": [ 1171 "produces": [
1172 "application/json" 1172 "application/json"
1173 ], 1173 ],
@@ -1196,6 +1196,37 @@
1196 } 1196 }
1197 } 1197 }
1198 }, 1198 },
1199 "/rankings/steam": {
1200 "get": {
1201 "description": "Get rankings of every player from Steam.",
1202 "produces": [
1203 "application/json"
1204 ],
1205 "tags": [
1206 "rankings"
1207 ],
1208 "responses": {
1209 "200": {
1210 "description": "OK",
1211 "schema": {
1212 "allOf": [
1213 {
1214 "$ref": "#/definitions/models.Response"
1215 },
1216 {
1217 "type": "object",
1218 "properties": {
1219 "data": {
1220 "$ref": "#/definitions/handlers.RankingsSteamResponse"
1221 }
1222 }
1223 }
1224 ]
1225 }
1226 }
1227 }
1228 }
1229 },
1199 "/search": { 1230 "/search": {
1200 "get": { 1231 "get": {
1201 "description": "Get all user and map data matching to the query.", 1232 "description": "Get all user and map data matching to the query.",
@@ -1783,6 +1814,29 @@
1783 } 1814 }
1784 } 1815 }
1785 }, 1816 },
1817 "handlers.RankingsSteamResponse": {
1818 "type": "object",
1819 "properties": {
1820 "rankings_multiplayer": {
1821 "type": "array",
1822 "items": {
1823 "$ref": "#/definitions/handlers.SteamUserRanking"
1824 }
1825 },
1826 "rankings_overall": {
1827 "type": "array",
1828 "items": {
1829 "$ref": "#/definitions/handlers.SteamUserRanking"
1830 }
1831 },
1832 "rankings_singleplayer": {
1833 "type": "array",
1834 "items": {
1835 "$ref": "#/definitions/handlers.SteamUserRanking"
1836 }
1837 }
1838 }
1839 },
1786 "handlers.RecordResponse": { 1840 "handlers.RecordResponse": {
1787 "type": "object", 1841 "type": "object",
1788 "properties": { 1842 "properties": {
@@ -1848,6 +1902,38 @@
1848 } 1902 }
1849 } 1903 }
1850 }, 1904 },
1905 "handlers.SteamUserRanking": {
1906 "type": "object",
1907 "properties": {
1908 "avatar_link": {
1909 "type": "string"
1910 },
1911 "mp_rank": {
1912 "type": "integer"
1913 },
1914 "mp_score": {
1915 "type": "integer"
1916 },
1917 "overall_rank": {
1918 "type": "integer"
1919 },
1920 "overall_score": {
1921 "type": "integer"
1922 },
1923 "sp_rank": {
1924 "type": "integer"
1925 },
1926 "sp_score": {
1927 "type": "integer"
1928 },
1929 "steam_id": {
1930 "type": "string"
1931 },
1932 "user_name": {
1933 "type": "string"
1934 }
1935 }
1936 },
1851 "models.Category": { 1937 "models.Category": {
1852 "type": "object", 1938 "type": "object",
1853 "properties": { 1939 "properties": {
diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml
index 853b3b9..22651e3 100644
--- a/backend/docs/swagger.yaml
+++ b/backend/docs/swagger.yaml
@@ -283,6 +283,21 @@ definitions:
283 $ref: '#/definitions/models.UserRanking' 283 $ref: '#/definitions/models.UserRanking'
284 type: array 284 type: array
285 type: object 285 type: object
286 handlers.RankingsSteamResponse:
287 properties:
288 rankings_multiplayer:
289 items:
290 $ref: '#/definitions/handlers.SteamUserRanking'
291 type: array
292 rankings_overall:
293 items:
294 $ref: '#/definitions/handlers.SteamUserRanking'
295 type: array
296 rankings_singleplayer:
297 items:
298 $ref: '#/definitions/handlers.SteamUserRanking'
299 type: array
300 type: object
286 handlers.RecordResponse: 301 handlers.RecordResponse:
287 properties: 302 properties:
288 score_count: 303 score_count:
@@ -325,6 +340,27 @@ definitions:
325 $ref: '#/definitions/models.UserShortWithAvatar' 340 $ref: '#/definitions/models.UserShortWithAvatar'
326 type: array 341 type: array
327 type: object 342 type: object
343 handlers.SteamUserRanking:
344 properties:
345 avatar_link:
346 type: string
347 mp_rank:
348 type: integer
349 mp_score:
350 type: integer
351 overall_rank:
352 type: integer
353 overall_score:
354 type: integer
355 sp_rank:
356 type: integer
357 sp_score:
358 type: integer
359 steam_id:
360 type: string
361 user_name:
362 type: string
363 type: object
328 models.Category: 364 models.Category:
329 properties: 365 properties:
330 id: 366 id:
@@ -1216,9 +1252,9 @@ paths:
1216 $ref: '#/definitions/models.Response' 1252 $ref: '#/definitions/models.Response'
1217 tags: 1253 tags:
1218 - users 1254 - users
1219 /rankings: 1255 /rankings/lphub:
1220 get: 1256 get:
1221 description: Get rankings of every player. 1257 description: Get rankings of every player from LPHUB.
1222 produces: 1258 produces:
1223 - application/json 1259 - application/json
1224 responses: 1260 responses:
@@ -1233,6 +1269,23 @@ paths:
1233 type: object 1269 type: object
1234 tags: 1270 tags:
1235 - rankings 1271 - rankings
1272 /rankings/steam:
1273 get:
1274 description: Get rankings of every player from Steam.
1275 produces:
1276 - application/json
1277 responses:
1278 "200":
1279 description: OK
1280 schema:
1281 allOf:
1282 - $ref: '#/definitions/models.Response'
1283 - properties:
1284 data:
1285 $ref: '#/definitions/handlers.RankingsSteamResponse'
1286 type: object
1287 tags:
1288 - rankings
1236 /search: 1289 /search:
1237 get: 1290 get:
1238 description: Get all user and map data matching to the query. 1291 description: Get all user and map data matching to the query.
diff --git a/backend/handlers/home.go b/backend/handlers/home.go
index 1734d28..fd7c6c0 100644
--- a/backend/handlers/home.go
+++ b/backend/handlers/home.go
@@ -1,8 +1,11 @@
1package handlers 1package handlers
2 2
3import ( 3import (
4 "encoding/json"
5 "io"
4 "log" 6 "log"
5 "net/http" 7 "net/http"
8 "os"
6 "sort" 9 "sort"
7 "strings" 10 "strings"
8 11
@@ -18,9 +21,26 @@ type SearchResponse struct {
18} 21}
19 22
20type RankingsResponse struct { 23type RankingsResponse struct {
21 Overall []models.UserRanking `json:"rankings_overall"`
22 Singleplayer []models.UserRanking `json:"rankings_singleplayer"` 24 Singleplayer []models.UserRanking `json:"rankings_singleplayer"`
23 Multiplayer []models.UserRanking `json:"rankings_multiplayer"` 25 Multiplayer []models.UserRanking `json:"rankings_multiplayer"`
26 Overall []models.UserRanking `json:"rankings_overall"`
27}
28
29type SteamUserRanking struct {
30 UserName string `json:"user_name"`
31 AvatarLink string `json:"avatar_link"`
32 SteamID string `json:"steam_id"`
33 SpScore int `json:"sp_score"`
34 MpScore int `json:"mp_score"`
35 OverallScore int `json:"overall_score"`
36 SpRank int `json:"sp_rank"`
37 MpRank int `json:"mp_rank"`
38 OverallRank int `json:"overall_rank"`
39}
40type RankingsSteamResponse struct {
41 Singleplayer []SteamUserRanking `json:"rankings_singleplayer"`
42 Multiplayer []SteamUserRanking `json:"rankings_multiplayer"`
43 Overall []SteamUserRanking `json:"rankings_overall"`
24} 44}
25 45
26type MapShortWithGame struct { 46type MapShortWithGame struct {
@@ -30,18 +50,18 @@ type MapShortWithGame struct {
30 Map string `json:"map"` 50 Map string `json:"map"`
31} 51}
32 52
33// GET Rankings 53// GET Rankings LPHUB
34// 54//
35// @Description Get rankings of every player. 55// @Description Get rankings of every player from LPHUB.
36// @Tags rankings 56// @Tags rankings
37// @Produce json 57// @Produce json
38// @Success 200 {object} models.Response{data=RankingsResponse} 58// @Success 200 {object} models.Response{data=RankingsResponse}
39// @Router /rankings [get] 59// @Router /rankings/lphub [get]
40func Rankings(c *gin.Context) { 60func RankingsLPHUB(c *gin.Context) {
41 response := RankingsResponse{ 61 response := RankingsResponse{
42 Overall: []models.UserRanking{},
43 Singleplayer: []models.UserRanking{}, 62 Singleplayer: []models.UserRanking{},
44 Multiplayer: []models.UserRanking{}, 63 Multiplayer: []models.UserRanking{},
64 Overall: []models.UserRanking{},
45 } 65 }
46 // Singleplayer rankings 66 // Singleplayer rankings
47 sql := `SELECT u.steam_id, u.user_name, u.avatar_link, COUNT(DISTINCT map_id), 67 sql := `SELECT u.steam_id, u.user_name, u.avatar_link, COUNT(DISTINCT map_id),
@@ -171,6 +191,74 @@ func Rankings(c *gin.Context) {
171 }) 191 })
172} 192}
173 193
194// GET Rankings Steam
195//
196// @Description Get rankings of every player from Steam.
197// @Tags rankings
198// @Produce json
199// @Success 200 {object} models.Response{data=RankingsSteamResponse}
200// @Router /rankings/steam [get]
201func RankingsSteam(c *gin.Context) {
202 response := RankingsSteamResponse{
203 Singleplayer: []SteamUserRanking{},
204 Multiplayer: []SteamUserRanking{},
205 Overall: []SteamUserRanking{},
206 }
207 spJson, err := os.Open("../rankings/output/sp.json")
208 if err != nil {
209 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
210 return
211 }
212 defer spJson.Close()
213 spJsonBytes, err := io.ReadAll(spJson)
214 if err != nil {
215 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
216 return
217 }
218 err = json.Unmarshal(spJsonBytes, &response.Singleplayer)
219 if err != nil {
220 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
221 return
222 }
223 mpJson, err := os.Open("../rankings/output/mp.json")
224 if err != nil {
225 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
226 return
227 }
228 defer mpJson.Close()
229 mpJsonBytes, err := io.ReadAll(mpJson)
230 if err != nil {
231 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
232 return
233 }
234 err = json.Unmarshal(mpJsonBytes, &response.Multiplayer)
235 if err != nil {
236 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
237 return
238 }
239 overallJson, err := os.Open("../rankings/output/overall.json")
240 if err != nil {
241 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
242 return
243 }
244 defer overallJson.Close()
245 overallJsonBytes, err := io.ReadAll(overallJson)
246 if err != nil {
247 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
248 return
249 }
250 err = json.Unmarshal(overallJsonBytes, &response.Overall)
251 if err != nil {
252 c.JSON(http.StatusOK, models.ErrorResponse(err.Error()))
253 return
254 }
255 c.JSON(http.StatusOK, models.Response{
256 Success: true,
257 Message: "Successfully retrieved rankings.",
258 Data: response,
259 })
260}
261
174// GET Search With Query 262// GET Search With Query
175// 263//
176// @Description Get all user and map data matching to the query. 264// @Description Get all user and map data matching to the query.
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
2output/ \ 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
3Unofficial 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
5The 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 @@
1package main
2
3import (
4 "encoding/json"
5 "os"
6)
7
8func 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 @@
1package main
2
3import (
4 "encoding/json"
5 "encoding/xml"
6 "fmt"
7 "io"
8 "log"
9 "net/http"
10 "os"
11 "strconv"
12)
13
14func 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
142func 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
159func 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 @@
1package main
2
3import (
4 "log"
5 "sort"
6)
7
8func 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 @@
1module rankings
2
3go 1.23.0
4
5require (
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 @@
1github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
2github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
3github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
4github.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 @@
1package main
2
3import (
4 "log"
5 "os"
6 "os/signal"
7 "syscall"
8
9 "github.com/joho/godotenv"
10 "github.com/robfig/cron/v3"
11)
12
13func 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
30func 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 @@
1package main
2
3type 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
11type Leaderboard struct {
12 Entries LeaderboardEntries `xml:"entries"`
13}
14
15func (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
24type LeaderboardEntries struct {
25 Entry []LeaderboardEntry `xml:"entry"`
26}
27
28type LeaderboardEntry struct {
29 SteamID string `xml:"steamid"`
30 Score int `xml:"score"`
31}
32
33type 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
48type 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 @@
1package main
2
3import (
4 "encoding/json"
5 "io"
6 "log"
7 "os"
8)
9
10func 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
28func 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}