aboutsummaryrefslogtreecommitdiff
path: root/backend/handlers/map.go
diff options
context:
space:
mode:
authorNidboj132 <lol2s@vp.plm>2023-09-05 18:23:11 +0200
committerNidboj132 <lol2s@vp.plm>2023-09-05 18:23:11 +0200
commit3869cb67351ccf3bc45b076f31afdc7133292c39 (patch)
treedc03341e147dde0964bf6be84b14e13424c647b7 /backend/handlers/map.go
parentadded graph and fixed some css (diff)
parentfix: create map summary, why the fuck does this have to be a pointer integer?? (diff)
downloadlphub-3869cb67351ccf3bc45b076f31afdc7133292c39.tar.gz
lphub-3869cb67351ccf3bc45b076f31afdc7133292c39.tar.bz2
lphub-3869cb67351ccf3bc45b076f31afdc7133292c39.zip
Merge branch 'main' of https://github.com/pektezol/LeastPortalsHub
Former-commit-id: 221385f463b7f5b0fc43a093b2c7c46e68d46d68
Diffstat (limited to 'backend/handlers/map.go')
-rw-r--r--backend/handlers/map.go362
1 files changed, 362 insertions, 0 deletions
diff --git a/backend/handlers/map.go b/backend/handlers/map.go
new file mode 100644
index 0000000..1d9cee8
--- /dev/null
+++ b/backend/handlers/map.go
@@ -0,0 +1,362 @@
1package handlers
2
3import (
4 "net/http"
5 "strconv"
6 "time"
7
8 "github.com/gin-gonic/gin"
9 "github.com/pektezol/leastportalshub/backend/database"
10 "github.com/pektezol/leastportalshub/backend/models"
11)
12
13type MapSummaryResponse struct {
14 Map models.Map `json:"map"`
15 Summary models.MapSummary `json:"summary"`
16}
17
18type MapLeaderboardsResponse struct {
19 Map models.Map `json:"map"`
20 Records any `json:"records"`
21}
22
23type ChaptersResponse struct {
24 Game models.Game `json:"game"`
25 Chapters []models.Chapter `json:"chapters"`
26}
27
28type ChapterMapsResponse struct {
29 Chapter models.Chapter `json:"chapter"`
30 Maps []models.MapShort `json:"maps"`
31}
32
33type RecordSingleplayer struct {
34 Placement int `json:"placement"`
35 RecordID int `json:"record_id"`
36 ScoreCount int `json:"score_count"`
37 ScoreTime int `json:"score_time"`
38 User models.UserShortWithAvatar `json:"user"`
39 DemoID string `json:"demo_id"`
40 RecordDate time.Time `json:"record_date"`
41}
42
43type RecordMultiplayer struct {
44 Placement int `json:"placement"`
45 RecordID int `json:"record_id"`
46 ScoreCount int `json:"score_count"`
47 ScoreTime int `json:"score_time"`
48 Host models.UserShortWithAvatar `json:"host"`
49 Partner models.UserShortWithAvatar `json:"partner"`
50 HostDemoID string `json:"host_demo_id"`
51 PartnerDemoID string `json:"partner_demo_id"`
52 RecordDate time.Time `json:"record_date"`
53}
54
55// GET Map Summary
56//
57// @Description Get map summary with specified id.
58// @Tags maps
59// @Produce json
60// @Param id path int true "Map ID"
61// @Success 200 {object} models.Response{data=MapSummaryResponse}
62// @Failure 400 {object} models.Response
63// @Router /maps/{id}/summary [get]
64func FetchMapSummary(c *gin.Context) {
65 id := c.Param("id")
66 response := MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}}
67 intID, err := strconv.Atoi(id)
68 if err != nil {
69 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
70 return
71 }
72 // Get map data
73 response.Map.ID = intID
74 sql := `SELECT m.id, g.name, c.name, m.name, m.image, g.is_coop
75 FROM maps m
76 INNER JOIN games g ON m.game_id = g.id
77 INNER JOIN chapters c ON m.chapter_id = c.id
78 WHERE m.id = $1`
79 err = database.DB.QueryRow(sql, id).Scan(&response.Map.ID, &response.Map.GameName, &response.Map.ChapterName, &response.Map.MapName, &response.Map.Image, &response.Map.IsCoop)
80 if err != nil {
81 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
82 return
83 }
84 // Get map routes and histories
85 sql = `SELECT r.id, c.id, c.name, h.user_name, h.score_count, h.record_date, r.description, r.showcase, COALESCE(avg(rating), 0.0) FROM map_routes r
86 INNER JOIN categories c ON r.category_id = c.id
87 INNER JOIN map_history h ON r.map_id = h.map_id AND r.category_id = h.category_id
88 LEFT JOIN map_ratings rt ON r.map_id = rt.map_id AND r.category_id = rt.category_id
89 WHERE r.map_id = $1 AND h.score_count = r.score_count GROUP BY r.id, c.id, h.user_name, h.score_count, h.record_date, r.description, r.showcase
90 ORDER BY h.record_date ASC;`
91 rows, err := database.DB.Query(sql, id)
92 if err != nil {
93 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
94 return
95 }
96 for rows.Next() {
97 route := models.MapRoute{Category: models.Category{}, History: models.MapHistory{}}
98 err = rows.Scan(&route.RouteID, &route.Category.ID, &route.Category.Name, &route.History.RunnerName, &route.History.ScoreCount, &route.History.Date, &route.Description, &route.Showcase, &route.Rating)
99 if err != nil {
100 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
101 return
102 }
103 response.Summary.Routes = append(response.Summary.Routes, route)
104 }
105 // Return response
106 c.JSON(http.StatusOK, models.Response{
107 Success: true,
108 Message: "Successfully retrieved map summary.",
109 Data: response,
110 })
111}
112
113// GET Map Leaderboards
114//
115// @Description Get map leaderboards with specified id.
116// @Tags maps
117// @Produce json
118// @Param id path int true "Map ID"
119// @Success 200 {object} models.Response{data=MapLeaderboardsResponse}
120// @Failure 400 {object} models.Response
121// @Router /maps/{id}/leaderboards [get]
122func FetchMapLeaderboards(c *gin.Context) {
123 // TODO: make new response type
124 id := c.Param("id")
125 // Get map data
126 response := MapLeaderboardsResponse{Map: models.Map{}, Records: nil}
127 // var mapData models.Map
128 // var mapRecordsData models.MapRecords
129 var isDisabled bool
130 intID, err := strconv.Atoi(id)
131 if err != nil {
132 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
133 return
134 }
135 response.Map.ID = intID
136 sql := `SELECT g.name, c.name, m.name, is_disabled, m.image, g.is_coop
137 FROM maps m
138 INNER JOIN games g ON m.game_id = g.id
139 INNER JOIN chapters c ON m.chapter_id = c.id
140 WHERE m.id = $1`
141 err = database.DB.QueryRow(sql, id).Scan(&response.Map.GameName, &response.Map.ChapterName, &response.Map.MapName, &isDisabled, &response.Map.Image, &response.Map.IsCoop)
142 if err != nil {
143 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
144 return
145 }
146 if isDisabled {
147 c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards."))
148 return
149 }
150 // TODO: avatar and names for host & partner
151 if response.Map.GameName == "Portal 2 - Cooperative" {
152 records := []RecordMultiplayer{}
153 sql = `SELECT
154 sub.id,
155 sub.host_id,
156 host.user_name AS host_user_name,
157 host.avatar_link AS host_avatar_link,
158 sub.partner_id,
159 partner.user_name AS partner_user_name,
160 partner.avatar_link AS partner_avatar_link,
161 sub.score_count,
162 sub.score_time,
163 sub.host_demo_id,
164 sub.partner_demo_id,
165 sub.record_date
166 FROM (
167 SELECT
168 id,
169 host_id,
170 partner_id,
171 score_count,
172 score_time,
173 host_demo_id,
174 partner_demo_id,
175 record_date,
176 ROW_NUMBER() OVER (PARTITION BY host_id, partner_id ORDER BY score_count, score_time) AS rn
177 FROM records_mp
178 WHERE map_id = $1
179 ) sub
180 JOIN users AS host ON sub.host_id = host.steam_id
181 JOIN users AS partner ON sub.partner_id = partner.steam_id
182 WHERE sub.rn = 1;`
183 rows, err := database.DB.Query(sql, id)
184 if err != nil {
185 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
186 return
187 }
188 placement := 1
189 ties := 0
190 for rows.Next() {
191 var record RecordMultiplayer
192 err := rows.Scan(&record.RecordID, &record.Host.SteamID, &record.Host.UserName, &record.Host.AvatarLink, &record.Partner.SteamID, &record.Partner.UserName, &record.Partner.AvatarLink, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate)
193 if err != nil {
194 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
195 return
196 }
197 if len(records) != 0 && records[len(records)-1].ScoreTime == record.ScoreTime {
198 ties++
199 record.Placement = placement - ties
200 } else {
201 record.Placement = placement
202 }
203 records = append(records, record)
204 placement++
205 }
206 response.Records = records
207 } else {
208 records := []RecordSingleplayer{}
209 sql = `SELECT id, user_id, users.user_name, users.avatar_link, score_count, score_time, demo_id, record_date
210 FROM (
211 SELECT id, user_id, score_count, score_time, demo_id, record_date,
212 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY score_count, score_time) AS rn
213 FROM records_sp
214 WHERE map_id = $1
215 ) sub
216 INNER JOIN users ON user_id = users.steam_id
217 WHERE rn = 1`
218 rows, err := database.DB.Query(sql, id)
219 if err != nil {
220 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
221 return
222 }
223 placement := 1
224 ties := 0
225 for rows.Next() {
226 var record RecordSingleplayer
227 err := rows.Scan(&record.RecordID, &record.User.SteamID, &record.User.UserName, &record.User.AvatarLink, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate)
228 if err != nil {
229 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
230 return
231 }
232 if len(records) != 0 && records[len(records)-1].ScoreTime == record.ScoreTime {
233 ties++
234 record.Placement = placement - ties
235 } else {
236 record.Placement = placement
237 }
238 records = append(records, record)
239 placement++
240 }
241 response.Records = records
242 }
243 c.JSON(http.StatusOK, models.Response{
244 Success: true,
245 Message: "Successfully retrieved map leaderboards.",
246 Data: response,
247 })
248}
249
250// GET Games
251//
252// @Description Get games from the leaderboards.
253// @Tags games & chapters
254// @Produce json
255// @Success 200 {object} models.Response{data=[]models.Game}
256// @Failure 400 {object} models.Response
257// @Router /games [get]
258func FetchGames(c *gin.Context) {
259 rows, err := database.DB.Query(`SELECT id, name, is_coop FROM games`)
260 if err != nil {
261 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
262 return
263 }
264 var games []models.Game
265 for rows.Next() {
266 var game models.Game
267 if err := rows.Scan(&game.ID, &game.Name, &game.IsCoop); err != nil {
268 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
269 return
270 }
271 games = append(games, game)
272 }
273 c.JSON(http.StatusOK, models.Response{
274 Success: true,
275 Message: "Successfully retrieved games.",
276 Data: games,
277 })
278}
279
280// GET Chapters of a Game
281//
282// @Description Get chapters from the specified game id.
283// @Tags games & chapters
284// @Produce json
285// @Param id path int true "Game ID"
286// @Success 200 {object} models.Response{data=ChaptersResponse}
287// @Failure 400 {object} models.Response
288// @Router /games/{id} [get]
289func FetchChapters(c *gin.Context) {
290 gameID := c.Param("id")
291 intID, err := strconv.Atoi(gameID)
292 if err != nil {
293 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
294 return
295 }
296 var response ChaptersResponse
297 rows, err := database.DB.Query(`SELECT c.id, c.name, g.name FROM chapters c INNER JOIN games g ON c.game_id = g.id WHERE game_id = $1`, gameID)
298 if err != nil {
299 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
300 return
301 }
302 var chapters []models.Chapter
303 var gameName string
304 for rows.Next() {
305 var chapter models.Chapter
306 if err := rows.Scan(&chapter.ID, &chapter.Name, &gameName); err != nil {
307 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
308 return
309 }
310 chapters = append(chapters, chapter)
311 }
312 response.Game.ID = intID
313 response.Game.Name = gameName
314 response.Chapters = chapters
315 c.JSON(http.StatusOK, models.Response{
316 Success: true,
317 Message: "Successfully retrieved chapters.",
318 Data: response,
319 })
320}
321
322// GET Maps of a Chapter
323//
324// @Description Get maps from the specified chapter id.
325// @Tags games & chapters
326// @Produce json
327// @Param id path int true "Chapter ID"
328// @Success 200 {object} models.Response{data=ChapterMapsResponse}
329// @Failure 400 {object} models.Response
330// @Router /chapters/{id} [get]
331func FetchChapterMaps(c *gin.Context) {
332 chapterID := c.Param("id")
333 intID, err := strconv.Atoi(chapterID)
334 if err != nil {
335 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
336 return
337 }
338 var response ChapterMapsResponse
339 rows, err := database.DB.Query(`SELECT m.id, m.name, c.name FROM maps m INNER JOIN chapters c ON m.chapter_id = c.id WHERE chapter_id = $1`, chapterID)
340 if err != nil {
341 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
342 return
343 }
344 var maps []models.MapShort
345 var chapterName string
346 for rows.Next() {
347 var mapShort models.MapShort
348 if err := rows.Scan(&mapShort.ID, &mapShort.Name, &chapterName); err != nil {
349 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
350 return
351 }
352 maps = append(maps, mapShort)
353 }
354 response.Chapter.ID = intID
355 response.Chapter.Name = chapterName
356 response.Maps = maps
357 c.JSON(http.StatusOK, models.Response{
358 Success: true,
359 Message: "Successfully retrieved maps.",
360 Data: response,
361 })
362}