diff options
| -rw-r--r-- | backend/api/routes.go | 6 | ||||
| -rw-r--r-- | backend/docs/docs.go | 90 | ||||
| -rw-r--r-- | backend/docs/swagger.json | 90 | ||||
| -rw-r--r-- | backend/docs/swagger.yaml | 57 | ||||
| -rw-r--r-- | backend/handlers/home.go | 100 | ||||
| -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 |
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 @@ | |||
| 1 | package handlers | 1 | package handlers |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 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 | ||
| 20 | type RankingsResponse struct { | 23 | type 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 | |||
| 29 | type 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 | } | ||
| 40 | type RankingsSteamResponse struct { | ||
| 41 | Singleplayer []SteamUserRanking `json:"rankings_singleplayer"` | ||
| 42 | Multiplayer []SteamUserRanking `json:"rankings_multiplayer"` | ||
| 43 | Overall []SteamUserRanking `json:"rankings_overall"` | ||
| 24 | } | 44 | } |
| 25 | 45 | ||
| 26 | type MapShortWithGame struct { | 46 | type 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] |
| 40 | func Rankings(c *gin.Context) { | 60 | func 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] | ||
| 201 | func 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 | ||
| 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 | } | ||