diff options
| -rw-r--r-- | backend/handlers/record.go | 94 | ||||
| -rw-r--r-- | backend/parser/parser.go | 196 | ||||
| -rw-r--r-- | frontend/src/App.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/api/Api.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/api/Maps.tsx | 26 | ||||
| -rw-r--r-- | frontend/src/components/UploadRunDialog.tsx | 70 | ||||
| -rw-r--r-- | frontend/src/types/Content.tsx | 1 |
7 files changed, 334 insertions, 59 deletions
diff --git a/backend/handlers/record.go b/backend/handlers/record.go index fea6b5d..2338097 100644 --- a/backend/handlers/record.go +++ b/backend/handlers/record.go | |||
| @@ -24,10 +24,9 @@ import ( | |||
| 24 | ) | 24 | ) |
| 25 | 25 | ||
| 26 | type RecordRequest struct { | 26 | type RecordRequest struct { |
| 27 | HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` | 27 | HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` |
| 28 | PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` | 28 | PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` |
| 29 | IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"` | 29 | PartnerID string `json:"partner_id" form:"partner_id"` |
| 30 | PartnerID string `json:"partner_id" form:"partner_id"` | ||
| 31 | } | 30 | } |
| 32 | 31 | ||
| 33 | type RecordResponse struct { | 32 | type RecordResponse struct { |
| @@ -50,7 +49,12 @@ type RecordResponse struct { | |||
| 50 | // @Success 200 {object} models.Response{data=RecordResponse} | 49 | // @Success 200 {object} models.Response{data=RecordResponse} |
| 51 | // @Router /maps/{mapid}/record [post] | 50 | // @Router /maps/{mapid}/record [post] |
| 52 | func CreateRecordWithDemo(c *gin.Context) { | 51 | func CreateRecordWithDemo(c *gin.Context) { |
| 53 | mapId := c.Param("mapid") | 52 | id := c.Param("mapid") |
| 53 | mapID, err := strconv.Atoi(id) | ||
| 54 | if err != nil { | ||
| 55 | c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) | ||
| 56 | return | ||
| 57 | } | ||
| 54 | // Check if user exists | 58 | // Check if user exists |
| 55 | user, exists := c.Get("user") | 59 | user, exists := c.Get("user") |
| 56 | if !exists { | 60 | if !exists { |
| @@ -62,7 +66,7 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 62 | var isCoop bool | 66 | var isCoop bool |
| 63 | var isDisabled bool | 67 | var isDisabled bool |
| 64 | sql := `SELECT g.name, m.is_disabled FROM maps m INNER JOIN games g ON m.game_id=g.id WHERE m.id = $1` | 68 | sql := `SELECT g.name, m.is_disabled FROM maps m INNER JOIN games g ON m.game_id=g.id WHERE m.id = $1` |
| 65 | err := database.DB.QueryRow(sql, mapId).Scan(&gameName, &isDisabled) | 69 | err = database.DB.QueryRow(sql, mapID).Scan(&gameName, &isDisabled) |
| 66 | if err != nil { | 70 | if err != nil { |
| 67 | c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) | 71 | c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) |
| 68 | return | 72 | return |
| @@ -94,8 +98,8 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 94 | var hostDemoUUID, hostDemoFileID, partnerDemoUUID, partnerDemoFileID string | 98 | var hostDemoUUID, hostDemoFileID, partnerDemoUUID, partnerDemoFileID string |
| 95 | var hostDemoScoreCount, hostDemoScoreTime int | 99 | var hostDemoScoreCount, hostDemoScoreTime int |
| 96 | var hostSteamID, partnerSteamID string | 100 | var hostSteamID, partnerSteamID string |
| 97 | client := serviceAccount() | 101 | var hostDemoServerNumber, partnerDemoServerNumber int |
| 98 | srv, err := drive.New(client) | 102 | srv, err := drive.New(serviceAccount()) |
| 99 | if err != nil { | 103 | if err != nil { |
| 100 | c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) | 104 | c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) |
| 101 | return | 105 | return |
| @@ -131,13 +135,22 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 131 | c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) | 135 | c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) |
| 132 | return | 136 | return |
| 133 | } | 137 | } |
| 134 | hostDemoScoreCount, hostDemoScoreTime, hostSteamID, partnerSteamID, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem") | 138 | parserResult, err := parser.ProcessDemo("backend/parser/" + uuid + ".dem") |
| 135 | if err != nil { | 139 | if err != nil { |
| 136 | deleteFile(srv, file.Id) | 140 | deleteFile(srv, file.Id) |
| 137 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionCreateRecordProcessDemoFail, err.Error()) | 141 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionCreateRecordProcessDemoFail, err.Error()) |
| 138 | c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) | 142 | c.JSON(http.StatusOK, models.ErrorResponse(err.Error())) |
| 139 | return | 143 | return |
| 140 | } | 144 | } |
| 145 | if mapID != parserResult.MapID { | ||
| 146 | deleteFile(srv, file.Id) | ||
| 147 | c.JSON(http.StatusOK, models.ErrorResponse("demo map does not match uploaded map id")) | ||
| 148 | return | ||
| 149 | } | ||
| 150 | hostDemoScoreCount = parserResult.PortalCount | ||
| 151 | hostDemoScoreTime = parserResult.TickCount | ||
| 152 | hostSteamID = parserResult.HostSteamID | ||
| 153 | partnerSteamID = parserResult.PartnerSteamID | ||
| 141 | if hostDemoScoreCount == 0 && hostDemoScoreTime == 0 { | 154 | if hostDemoScoreCount == 0 && hostDemoScoreTime == 0 { |
| 142 | deleteFile(srv, file.Id) | 155 | deleteFile(srv, file.Id) |
| 143 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionCreateRecordProcessDemoFail, err.Error()) | 156 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionCreateRecordProcessDemoFail, err.Error()) |
| @@ -145,30 +158,32 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 145 | return | 158 | return |
| 146 | } | 159 | } |
| 147 | if !isCoop { | 160 | if !isCoop { |
| 148 | hostSplit := strings.Split(hostSteamID, ":") | 161 | convertedSteamID := strconv.FormatInt(convertSteamID64(hostSteamID), 10) |
| 149 | if hostSplit[len(hostSplit)-1] != user.(models.User).SteamID { | 162 | if convertedSteamID != user.(models.User).SteamID { |
| 150 | c.JSON(http.StatusOK, models.ErrorResponse(fmt.Sprintf("Host SteamID from demo and request does not match! Check your submission and try again.\nDemo Host SteamID: %s\nRequest Host SteamID: %s", hostSplit[len(hostSplit)-1], user.(models.User).SteamID))) | 163 | deleteFile(srv, file.Id) |
| 164 | c.JSON(http.StatusOK, models.ErrorResponse(fmt.Sprintf("Host SteamID from demo and request does not match! Check your submission and try again.\nDemo Host SteamID: %s\nRequest Host SteamID: %s", convertedSteamID, user.(models.User).SteamID))) | ||
| 151 | return | 165 | return |
| 152 | } | 166 | } |
| 153 | } else { | 167 | } else { |
| 154 | partnerSplit := strings.Split(partnerSteamID, ":") | 168 | if parserResult.IsHost && i != 0 { |
| 155 | if partnerSplit[len(partnerSplit)-1] != record.PartnerID { | 169 | deleteFile(srv, file.Id) |
| 156 | c.JSON(http.StatusOK, models.ErrorResponse(fmt.Sprintf("Partner SteamID from demo and request does not match! Check your submission and try again.\nDemo Partner SteamID: %s\nRequest Partner SteamID: %s", partnerSplit[len(partnerSplit)-1], record.PartnerID))) | 170 | c.JSON(http.StatusOK, models.ErrorResponse("Given partner demo is a host demo.")) |
| 157 | return | 171 | return |
| 158 | } | 172 | } |
| 159 | var verifyCoopSteamID string | 173 | if !parserResult.IsHost && i == 0 { |
| 160 | database.DB.QueryRow("SELECT steam_id FROM users WHERE steam_id = $1", record.PartnerID).Scan(&verifyCoopSteamID) | 174 | deleteFile(srv, file.Id) |
| 161 | if verifyCoopSteamID != record.PartnerID { | 175 | c.JSON(http.StatusOK, models.ErrorResponse("Given host demo is a partner demo.")) |
| 162 | c.JSON(http.StatusOK, models.ErrorResponse("Given partner SteamID does not match an account on LPHUB.")) | ||
| 163 | return | 176 | return |
| 164 | } | 177 | } |
| 165 | } | 178 | } |
| 166 | if i == 0 { | 179 | if i == 0 { |
| 167 | hostDemoFileID = file.Id | 180 | hostDemoFileID = file.Id |
| 168 | hostDemoUUID = uuid | 181 | hostDemoUUID = uuid |
| 182 | hostDemoServerNumber = parserResult.ServerNumber | ||
| 169 | } else if i == 1 { | 183 | } else if i == 1 { |
| 170 | partnerDemoFileID = file.Id | 184 | partnerDemoFileID = file.Id |
| 171 | partnerDemoUUID = uuid | 185 | partnerDemoUUID = uuid |
| 186 | partnerDemoServerNumber = parserResult.ServerNumber | ||
| 172 | } | 187 | } |
| 173 | _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) | 188 | _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) |
| 174 | if err != nil { | 189 | if err != nil { |
| @@ -180,18 +195,37 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 180 | } | 195 | } |
| 181 | // Insert into records | 196 | // Insert into records |
| 182 | if isCoop { | 197 | if isCoop { |
| 198 | if hostDemoServerNumber != partnerDemoServerNumber { | ||
| 199 | deleteFile(srv, hostDemoFileID) | ||
| 200 | deleteFile(srv, partnerDemoFileID) | ||
| 201 | c.JSON(http.StatusOK, models.ErrorResponse(fmt.Sprintf("Host and partner demo server numbers (%d & %d) does not match!", hostDemoServerNumber, partnerDemoServerNumber))) | ||
| 202 | return | ||
| 203 | } | ||
| 204 | convertedHostSteamID := strconv.FormatInt(convertSteamID64(hostSteamID), 10) | ||
| 205 | if convertedHostSteamID != user.(models.User).SteamID && convertedHostSteamID != record.PartnerID { | ||
| 206 | deleteFile(srv, hostDemoFileID) | ||
| 207 | deleteFile(srv, partnerDemoFileID) | ||
| 208 | c.JSON(http.StatusOK, models.ErrorResponse(fmt.Sprintf("Host SteamID from demo and request does not match! Check your submission and try again.\nDemo Host SteamID: %s\nRequest Host SteamID: %s", convertedHostSteamID, user.(models.User).SteamID))) | ||
| 209 | return | ||
| 210 | } | ||
| 211 | convertedPartnerSteamID := strconv.FormatInt(convertSteamID64(partnerSteamID), 10) | ||
| 212 | if convertedPartnerSteamID != record.PartnerID && convertedPartnerSteamID != user.(models.User).SteamID { | ||
| 213 | deleteFile(srv, hostDemoFileID) | ||
| 214 | deleteFile(srv, partnerDemoFileID) | ||
| 215 | c.JSON(http.StatusOK, models.ErrorResponse(fmt.Sprintf("Partner SteamID from demo and request does not match! Check your submission and try again.\nDemo Partner SteamID: %s\nRequest Partner SteamID: %s", convertedPartnerSteamID, record.PartnerID))) | ||
| 216 | return | ||
| 217 | } | ||
| 218 | var verifyPartnerSteamID string | ||
| 219 | database.DB.QueryRow("SELECT steam_id FROM users WHERE steam_id = $1", record.PartnerID).Scan(&verifyPartnerSteamID) | ||
| 220 | if verifyPartnerSteamID != record.PartnerID { | ||
| 221 | deleteFile(srv, hostDemoFileID) | ||
| 222 | deleteFile(srv, partnerDemoFileID) | ||
| 223 | c.JSON(http.StatusOK, models.ErrorResponse("Given partner SteamID does not match an account on LPHUB.")) | ||
| 224 | return | ||
| 225 | } | ||
| 183 | sql := `INSERT INTO records_mp(map_id,score_count,score_time,host_id,partner_id,host_demo_id,partner_demo_id) | 226 | sql := `INSERT INTO records_mp(map_id,score_count,score_time,host_id,partner_id,host_demo_id,partner_demo_id) |
| 184 | VALUES($1, $2, $3, $4, $5, $6, $7)` | 227 | VALUES($1, $2, $3, $4, $5, $6, $7)` |
| 185 | var hostID string | 228 | _, err := tx.Exec(sql, mapID, hostDemoScoreCount, hostDemoScoreTime, strconv.FormatInt(convertSteamID64(hostSteamID), 10), strconv.FormatInt(convertSteamID64(partnerSteamID), 10), hostDemoUUID, partnerDemoUUID) |
| 186 | var partnerID string | ||
| 187 | if record.IsPartnerOrange { | ||
| 188 | hostID = user.(models.User).SteamID | ||
| 189 | partnerID = record.PartnerID | ||
| 190 | } else { | ||
| 191 | partnerID = user.(models.User).SteamID | ||
| 192 | hostID = record.PartnerID | ||
| 193 | } | ||
| 194 | _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, hostID, partnerID, hostDemoUUID, partnerDemoUUID) | ||
| 195 | if err != nil { | 229 | if err != nil { |
| 196 | deleteFile(srv, hostDemoFileID) | 230 | deleteFile(srv, hostDemoFileID) |
| 197 | deleteFile(srv, partnerDemoFileID) | 231 | deleteFile(srv, partnerDemoFileID) |
| @@ -202,7 +236,7 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 202 | } else { | 236 | } else { |
| 203 | sql := `INSERT INTO records_sp(map_id,score_count,score_time,user_id,demo_id) | 237 | sql := `INSERT INTO records_sp(map_id,score_count,score_time,user_id,demo_id) |
| 204 | VALUES($1, $2, $3, $4, $5)` | 238 | VALUES($1, $2, $3, $4, $5)` |
| 205 | _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) | 239 | _, err := tx.Exec(sql, mapID, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) |
| 206 | if err != nil { | 240 | if err != nil { |
| 207 | deleteFile(srv, hostDemoFileID) | 241 | deleteFile(srv, hostDemoFileID) |
| 208 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionCreateRecordInsertRecordFail, err.Error()) | 242 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionCreateRecordInsertRecordFail, err.Error()) |
diff --git a/backend/parser/parser.go b/backend/parser/parser.go index 1a80d4a..4605600 100644 --- a/backend/parser/parser.go +++ b/backend/parser/parser.go | |||
| @@ -4,30 +4,52 @@ import ( | |||
| 4 | "errors" | 4 | "errors" |
| 5 | "math" | 5 | "math" |
| 6 | "os" | 6 | "os" |
| 7 | "regexp" | ||
| 8 | "strconv" | ||
| 9 | "strings" | ||
| 7 | 10 | ||
| 8 | "github.com/pektezol/bitreader" | 11 | "github.com/pektezol/bitreader" |
| 9 | ) | 12 | ) |
| 10 | 13 | ||
| 14 | type Result struct { | ||
| 15 | MapID int | ||
| 16 | ServerNumber int | ||
| 17 | PortalCount int | ||
| 18 | TickCount int | ||
| 19 | HostSteamID string | ||
| 20 | PartnerSteamID string | ||
| 21 | IsHost bool | ||
| 22 | } | ||
| 23 | |||
| 11 | // Don't try to understand it, feel it. | 24 | // Don't try to understand it, feel it. |
| 12 | func ProcessDemo(filePath string) (portalCount int, tickCount int, hostSteamID string, partnerSteamID string, err error) { | 25 | func ProcessDemo(filePath string) (Result, error) { |
| 26 | var result Result | ||
| 13 | file, err := os.Open(filePath) | 27 | file, err := os.Open(filePath) |
| 14 | if err != nil { | 28 | if err != nil { |
| 15 | return 0, 0, "", "", err | 29 | return Result{}, err |
| 16 | } | 30 | } |
| 17 | reader := bitreader.NewReader(file, true) | 31 | reader := bitreader.NewReader(file, true) |
| 18 | demoFileStamp := reader.TryReadString() | 32 | demoFileStamp := reader.TryReadString() |
| 19 | demoProtocol := reader.TryReadSInt32() | 33 | demoProtocol := reader.TryReadSInt32() |
| 20 | networkProtocol := reader.TryReadSInt32() | 34 | networkProtocol := reader.TryReadSInt32() |
| 21 | reader.SkipBytes(1056) | 35 | serverName := reader.TryReadStringLength(260) |
| 36 | // clientName := reader.TryReadStringLength(260) | ||
| 37 | reader.SkipBytes(260) | ||
| 38 | mapName := reader.TryReadStringLength(260) | ||
| 39 | reader.SkipBytes(276) | ||
| 22 | if demoFileStamp != "HL2DEMO" { | 40 | if demoFileStamp != "HL2DEMO" { |
| 23 | return 0, 0, "", "", errors.New("invalid demo file stamp") | 41 | return Result{}, errors.New("invalid demo file stamp") |
| 24 | } | 42 | } |
| 25 | if demoProtocol != 4 { | 43 | if demoProtocol != 4 { |
| 26 | return 0, 0, "", "", errors.New("this parser only supports demos from new engine") | 44 | return Result{}, errors.New("this parser only supports demos from new engine") |
| 27 | } | 45 | } |
| 28 | if networkProtocol != 2001 { | 46 | if networkProtocol != 2001 { |
| 29 | return 0, 0, "", "", errors.New("this parser only supports demos from portal 2") | 47 | return Result{}, errors.New("this parser only supports demos from portal 2") |
| 48 | } | ||
| 49 | if mapDict[mapName] == 0 { | ||
| 50 | return Result{}, errors.New("demo recorded on an invalid map") | ||
| 30 | } | 51 | } |
| 52 | result.MapID = mapDict[mapName] | ||
| 31 | for { | 53 | for { |
| 32 | packetType := reader.TryReadUInt8() | 54 | packetType := reader.TryReadUInt8() |
| 33 | reader.SkipBits(40) | 55 | reader.SkipBits(40) |
| @@ -119,7 +141,17 @@ func ProcessDemo(filePath string) (portalCount int, tickCount int, hostSteamID s | |||
| 119 | packetReader.SkipBytes(2) | 141 | packetReader.SkipBytes(2) |
| 120 | packetReader.SkipBits(uint64(packetReader.TryReadUInt16())) | 142 | packetReader.SkipBits(uint64(packetReader.TryReadUInt16())) |
| 121 | case 16: | 143 | case 16: |
| 122 | packetReader.TryReadString() | 144 | print := packetReader.TryReadString() |
| 145 | re := regexp.MustCompile(`Server Number: (\d+)`) | ||
| 146 | match := re.FindStringSubmatch(print) | ||
| 147 | if len(match) >= 1 { | ||
| 148 | serverNumber := match[1] | ||
| 149 | n, err := strconv.Atoi(serverNumber) | ||
| 150 | if err != nil { | ||
| 151 | return Result{}, err | ||
| 152 | } | ||
| 153 | result.ServerNumber = n | ||
| 154 | } | ||
| 123 | case 17: | 155 | case 17: |
| 124 | var length uint16 | 156 | var length uint16 |
| 125 | if packetReader.TryReadBool() { | 157 | if packetReader.TryReadBool() { |
| @@ -180,8 +212,8 @@ func ProcessDemo(filePath string) (portalCount int, tickCount int, hostSteamID s | |||
| 180 | NumPortals: userMessageReader.TryReadSInt32(), | 212 | NumPortals: userMessageReader.TryReadSInt32(), |
| 181 | TimeTaken: userMessageReader.TryReadSInt32(), | 213 | TimeTaken: userMessageReader.TryReadSInt32(), |
| 182 | } | 214 | } |
| 183 | portalCount = int(scoreboardTempUpdate.NumPortals) | 215 | result.PortalCount = int(scoreboardTempUpdate.NumPortals) |
| 184 | tickCount = int(math.Round(float64((float32(scoreboardTempUpdate.TimeTaken) / 100.0) / float32(1.0/60.0)))) | 216 | result.TickCount = int(math.Round(float64((float32(scoreboardTempUpdate.TimeTaken) / 100.0) / float32(1.0/60.0)))) |
| 185 | } | 217 | } |
| 186 | case 24: | 218 | case 24: |
| 187 | packetReader.SkipBits(20) | 219 | packetReader.SkipBits(20) |
| @@ -216,8 +248,8 @@ func ProcessDemo(filePath string) (portalCount int, tickCount int, hostSteamID s | |||
| 216 | packetReader.SkipBytes(packetReader.TryReadBits(32)) | 248 | packetReader.SkipBytes(packetReader.TryReadBits(32)) |
| 217 | case 33: | 249 | case 33: |
| 218 | packetReader.SkipBits(packetReader.TryReadBits(32)) | 250 | packetReader.SkipBits(packetReader.TryReadBits(32)) |
| 219 | default: | 251 | // default: |
| 220 | return 0, 0, "", "", errors.New("unknown msg type") | 252 | // return Result{}, errors.New(fmt.Sprintf("unknown msg type %d", messageType)) |
| 221 | } | 253 | } |
| 222 | } | 254 | } |
| 223 | case 3, 7: | 255 | case 3, 7: |
| @@ -252,7 +284,7 @@ func ProcessDemo(filePath string) (portalCount int, tickCount int, hostSteamID s | |||
| 252 | if stringTableReader.TryReadBool() { | 284 | if stringTableReader.TryReadBool() { |
| 253 | byteLen, err := stringTableReader.ReadBits(16) | 285 | byteLen, err := stringTableReader.ReadBits(16) |
| 254 | if err != nil { | 286 | if err != nil { |
| 255 | return 0, 0, "", "", errors.New("error on reading entry length") | 287 | return Result{}, errors.New("error on reading entry length") |
| 256 | } | 288 | } |
| 257 | stringTableEntryReader := bitreader.NewReaderFromBytes(stringTableReader.TryReadBytesToSlice(byteLen), true) | 289 | stringTableEntryReader := bitreader.NewReaderFromBytes(stringTableReader.TryReadBytesToSlice(byteLen), true) |
| 258 | if tableName == "userinfo" { | 290 | if tableName == "userinfo" { |
| @@ -285,10 +317,14 @@ func ProcessDemo(filePath string) (portalCount int, tickCount int, hostSteamID s | |||
| 285 | userInfo.FilesDownloaded = stringTableEntryReader.TryReadUInt8() | 317 | userInfo.FilesDownloaded = stringTableEntryReader.TryReadUInt8() |
| 286 | stringTableEntryReader.SkipBytes(3) | 318 | stringTableEntryReader.SkipBytes(3) |
| 287 | if guidCount == 0 { | 319 | if guidCount == 0 { |
| 288 | hostSteamID = userInfo.GUID | 320 | result.HostSteamID = userInfo.GUID |
| 321 | if strings.Contains(serverName, "localhost") { | ||
| 322 | result.IsHost = true | ||
| 323 | } | ||
| 289 | } else if guidCount == 1 { | 324 | } else if guidCount == 1 { |
| 290 | partnerSteamID = userInfo.GUID | 325 | result.PartnerSteamID = userInfo.GUID |
| 291 | } | 326 | } |
| 327 | guidCount++ | ||
| 292 | } | 328 | } |
| 293 | } | 329 | } |
| 294 | } | 330 | } |
| @@ -303,11 +339,139 @@ func ProcessDemo(filePath string) (portalCount int, tickCount int, hostSteamID s | |||
| 303 | } | 339 | } |
| 304 | } | 340 | } |
| 305 | default: | 341 | default: |
| 306 | return 0, 0, "", "", errors.New("invalid packet type") | 342 | return Result{}, errors.New("invalid packet type") |
| 307 | } | 343 | } |
| 308 | if packetType == 7 { | 344 | if packetType == 7 { |
| 309 | break | 345 | break |
| 310 | } | 346 | } |
| 311 | } | 347 | } |
| 312 | return portalCount, tickCount, hostSteamID, partnerSteamID, nil | 348 | return result, nil |
| 349 | } | ||
| 350 | |||
| 351 | var mapDict = map[string]int{ | ||
| 352 | "sp_a1_intro1": 1, | ||
| 353 | "sp_a1_intro2": 2, | ||
| 354 | "sp_a1_intro3": 3, | ||
| 355 | "sp_a1_intro4": 4, | ||
| 356 | "sp_a1_intro5": 5, | ||
| 357 | "sp_a1_intro6": 6, | ||
| 358 | "sp_a1_intro7": 7, | ||
| 359 | "sp_a1_wakeup": 8, | ||
| 360 | "sp_a2_intro": 9, | ||
| 361 | |||
| 362 | "sp_a2_laser_intro": 10, | ||
| 363 | "sp_a2_laser_stairs": 11, | ||
| 364 | "sp_a2_dual_lasers": 12, | ||
| 365 | "sp_a2_laser_over_goo": 13, | ||
| 366 | "sp_a2_catapult_intro": 14, | ||
| 367 | "sp_a2_trust_fling": 15, | ||
| 368 | "sp_a2_pit_flings": 16, | ||
| 369 | "sp_a2_fizzler_intro": 17, | ||
| 370 | |||
| 371 | "sp_a2_sphere_peek": 18, | ||
| 372 | "sp_a2_ricochet": 19, | ||
| 373 | "sp_a2_bridge_intro": 20, | ||
| 374 | "sp_a2_bridge_the_gap": 21, | ||
| 375 | "sp_a2_turret_intro": 22, | ||
| 376 | "sp_a2_laser_relays": 23, | ||
| 377 | "sp_a2_turret_blocker": 24, | ||
| 378 | "sp_a2_laser_vs_turret": 25, | ||
| 379 | "sp_a2_pull_the_rug": 26, | ||
| 380 | |||
| 381 | "sp_a2_column_blocker": 27, | ||
| 382 | "sp_a2_laser_chaining": 28, | ||
| 383 | "sp_a2_triple_laser": 29, | ||
| 384 | "sp_a2_bts1": 30, | ||
| 385 | "sp_a2_bts2": 31, | ||
| 386 | |||
| 387 | "sp_a2_bts3": 32, | ||
| 388 | "sp_a2_bts4": 33, | ||
| 389 | "sp_a2_bts5": 34, | ||
| 390 | "sp_a2_core": 35, | ||
| 391 | |||
| 392 | "sp_a3_01": 36, | ||
| 393 | "sp_a3_03": 37, | ||
| 394 | "sp_a3_jump_intro": 38, | ||
| 395 | "sp_a3_bomb_flings": 39, | ||
| 396 | "sp_a3_crazy_box": 40, | ||
| 397 | "sp_a3_transition01": 41, | ||
| 398 | |||
| 399 | "sp_a3_speed_ramp": 42, | ||
| 400 | "sp_a3_speed_flings": 43, | ||
| 401 | "sp_a3_portal_intro": 44, | ||
| 402 | "sp_a3_end": 45, | ||
| 403 | |||
| 404 | "sp_a4_intro": 46, | ||
| 405 | "sp_a4_tb_intro": 47, | ||
| 406 | "sp_a4_tb_trust_drop": 48, | ||
| 407 | "sp_a4_tb_wall_button": 49, | ||
| 408 | "sp_a4_tb_polarity": 50, | ||
| 409 | "sp_a4_tb_catch": 51, | ||
| 410 | "sp_a4_stop_the_box": 52, | ||
| 411 | "sp_a4_laser_catapult": 53, | ||
| 412 | "sp_a4_laser_platform": 54, | ||
| 413 | "sp_a4_speed_catch": 55, | ||
| 414 | "sp_a4_jump_polarity": 56, | ||
| 415 | |||
| 416 | "sp_a4_finale1": 57, | ||
| 417 | "sp_a4_finale2": 58, | ||
| 418 | "sp_a4_finale3": 59, | ||
| 419 | "sp_a4_finale4": 60, | ||
| 420 | |||
| 421 | "mp_coop_start": 61, | ||
| 422 | "mp_coop_lobby_2": 62, | ||
| 423 | |||
| 424 | "mp_coop_doors": 63, | ||
| 425 | "mp_coop_race_2": 64, | ||
| 426 | "mp_coop_laser_2": 65, | ||
| 427 | "mp_coop_rat_maze": 66, | ||
| 428 | "mp_coop_laser_crusher": 67, | ||
| 429 | "mp_coop_teambts": 68, | ||
| 430 | |||
| 431 | "mp_coop_fling_3": 69, | ||
| 432 | "mp_coop_infinifling_train": 70, | ||
| 433 | "mp_coop_come_along": 71, | ||
| 434 | "mp_coop_fling_1": 72, | ||
| 435 | "mp_coop_catapult_1": 73, | ||
| 436 | "mp_coop_multifling_1": 74, | ||
| 437 | "mp_coop_fling_crushers": 75, | ||
| 438 | "mp_coop_fan": 76, | ||
| 439 | |||
| 440 | "mp_coop_wall_intro": 77, | ||
| 441 | "mp_coop_wall_2": 78, | ||
| 442 | "mp_coop_catapult_wall_intro": 79, | ||
| 443 | "mp_coop_wall_block": 80, | ||
| 444 | "mp_coop_catapult_2": 81, | ||
| 445 | "mp_coop_turret_walls": 82, | ||
| 446 | "mp_coop_turret_ball": 83, | ||
| 447 | "mp_coop_wall_5": 84, | ||
| 448 | |||
| 449 | "mp_coop_tbeam_redirect": 85, | ||
| 450 | "mp_coop_tbeam_drill": 86, | ||
| 451 | "mp_coop_tbeam_catch_grind_1": 87, | ||
| 452 | "mp_coop_tbeam_laser_1": 88, | ||
| 453 | "mp_coop_tbeam_polarity": 89, | ||
| 454 | "mp_coop_tbeam_polarity2": 90, | ||
| 455 | "mp_coop_tbeam_polarity3": 91, | ||
| 456 | "mp_coop_tbeam_maze": 92, | ||
| 457 | "mp_coop_tbeam_end": 93, | ||
| 458 | |||
| 459 | "mp_coop_paint_come_along": 94, | ||
| 460 | "mp_coop_paint_redirect": 95, | ||
| 461 | "mp_coop_paint_bridge": 96, | ||
| 462 | "mp_coop_paint_walljumps": 97, | ||
| 463 | "mp_coop_paint_speed_flings": 98, | ||
| 464 | "mp_coop_paint_red_racer": 99, | ||
| 465 | "mp_coop_paint_speed_catch": 100, | ||
| 466 | "mp_coop_paint_longjump_intro": 101, | ||
| 467 | |||
| 468 | "mp_coop_seperation_1": 102, | ||
| 469 | "mp_coop_tripleaxis": 103, | ||
| 470 | "mp_coop_catapult_catch": 104, | ||
| 471 | "mp_coop_2paints_1bridge": 105, | ||
| 472 | "mp_coop_paint_conversion": 106, | ||
| 473 | "mp_coop_bridge_catch": 107, | ||
| 474 | "mp_coop_laser_tbeam": 108, | ||
| 475 | "mp_coop_paint_rat_maze": 109, | ||
| 476 | "mp_coop_paint_crazy_box": 110, | ||
| 313 | } | 477 | } |
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3a7fa18..c34cbcf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx | |||
| @@ -75,7 +75,7 @@ const App: React.FC = () => { | |||
| 75 | 75 | ||
| 76 | return ( | 76 | return ( |
| 77 | <> | 77 | <> |
| 78 | <UploadRunDialog open={uploadRunDialog} onClose={() => setUploadRunDialog(false)} mapID={uploadRunDialogMapID} games={games} /> | 78 | <UploadRunDialog token={token} open={uploadRunDialog} onClose={() => setUploadRunDialog(false)} games={games} /> |
| 79 | <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} onUploadRun={() => setUploadRunDialog(true)} /> | 79 | <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} onUploadRun={() => setUploadRunDialog(true)} /> |
| 80 | <Routes> | 80 | <Routes> |
| 81 | <Route path="/" element={<Homepage />} /> | 81 | <Route path="/" element={<Homepage />} /> |
diff --git a/frontend/src/api/Api.tsx b/frontend/src/api/Api.tsx index 6731cb3..0f0c4d3 100644 --- a/frontend/src/api/Api.tsx +++ b/frontend/src/api/Api.tsx | |||
| @@ -3,8 +3,9 @@ import { delete_token, get_token } from './Auth'; | |||
| 3 | import { get_user, get_profile, post_profile } from './User'; | 3 | import { get_user, get_profile, post_profile } from './User'; |
| 4 | import { get_games, get_chapters, get_games_chapters, get_game_maps, get_search } from './Games'; | 4 | import { get_games, get_chapters, get_games_chapters, get_game_maps, get_search } from './Games'; |
| 5 | import { get_official_rankings, get_unofficial_rankings } from './Rankings'; | 5 | import { get_official_rankings, get_unofficial_rankings } from './Rankings'; |
| 6 | import { get_map_summary, get_map_leaderboard, get_map_discussions, get_map_discussion, post_map_discussion, post_map_discussion_comment, delete_map_discussion } from './Maps'; | 6 | import { get_map_summary, get_map_leaderboard, get_map_discussions, get_map_discussion, post_map_discussion, post_map_discussion_comment, delete_map_discussion, post_record } from './Maps'; |
| 7 | import { delete_map_summary, post_map_summary, put_map_image, put_map_summary } from './Mod'; | 7 | import { delete_map_summary, post_map_summary, put_map_image, put_map_summary } from './Mod'; |
| 8 | import { UploadRunContent } from '../types/Content'; | ||
| 8 | 9 | ||
| 9 | // add new api call function entries here | 10 | // add new api call function entries here |
| 10 | // example usage: API.get_games(); | 11 | // example usage: API.get_games(); |
| @@ -34,6 +35,7 @@ export const API = { | |||
| 34 | 35 | ||
| 35 | post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content), | 36 | post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content), |
| 36 | post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment), | 37 | post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment), |
| 38 | post_record: (token: string, run: UploadRunContent) => post_record(token, run), | ||
| 37 | 39 | ||
| 38 | delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id), | 40 | delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id), |
| 39 | // Mod | 41 | // Mod |
diff --git a/frontend/src/api/Maps.tsx b/frontend/src/api/Maps.tsx index fbad78c..6bdc3e6 100644 --- a/frontend/src/api/Maps.tsx +++ b/frontend/src/api/Maps.tsx | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | import axios from "axios"; | 1 | import axios from "axios"; |
| 2 | import { url } from "./Api"; | 2 | import { url } from "./Api"; |
| 3 | import { MapDiscussionContent } from "../types/Content"; | 3 | import { MapDiscussionContent, UploadRunContent } from "../types/Content"; |
| 4 | import { MapSummary, MapLeaderboard, MapDiscussions, MapDiscussion } from "../types/Map"; | 4 | import { MapSummary, MapLeaderboard, MapDiscussions, MapDiscussion } from "../types/Map"; |
| 5 | 5 | ||
| 6 | export const get_map_summary = async (map_id: string): Promise<MapSummary> => { | 6 | export const get_map_summary = async (map_id: string): Promise<MapSummary> => { |
| @@ -74,3 +74,27 @@ export const delete_map_discussion = async (token: string, map_id: string, discu | |||
| 74 | }); | 74 | }); |
| 75 | return response.data.success; | 75 | return response.data.success; |
| 76 | }; | 76 | }; |
| 77 | |||
| 78 | export const post_record = async (token: string, run: UploadRunContent): Promise<[string]> => { | ||
| 79 | if (run.partner_demo && run.partner_id) { | ||
| 80 | const response = await axios.postForm(url(`maps/${run.map_id}/record`), { | ||
| 81 | "host_demo": run.host_demo, | ||
| 82 | "partner_demo": run.partner_demo, | ||
| 83 | "partner_id": run.partner_id, | ||
| 84 | }, { | ||
| 85 | headers: { | ||
| 86 | "Authorization": token, | ||
| 87 | } | ||
| 88 | }); | ||
| 89 | return response.data.message; | ||
| 90 | } else { | ||
| 91 | const response = await axios.postForm(url(`maps/${run.map_id}/record`), { | ||
| 92 | "host_demo": run.host_demo, | ||
| 93 | }, { | ||
| 94 | headers: { | ||
| 95 | "Authorization": token, | ||
| 96 | } | ||
| 97 | }); | ||
| 98 | return response.data.message; | ||
| 99 | } | ||
| 100 | }; | ||
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx index fb146ba..08d4108 100644 --- a/frontend/src/components/UploadRunDialog.tsx +++ b/frontend/src/components/UploadRunDialog.tsx | |||
| @@ -3,25 +3,26 @@ import { UploadRunContent } from '../types/Content'; | |||
| 3 | 3 | ||
| 4 | import '../css/UploadRunDialog.css'; | 4 | import '../css/UploadRunDialog.css'; |
| 5 | import { Game } from '../types/Game'; | 5 | import { Game } from '../types/Game'; |
| 6 | import Games from '../pages/Games'; | ||
| 7 | import { Map } from '../types/Map'; | 6 | import { Map } from '../types/Map'; |
| 8 | import { API } from '../api/Api'; | 7 | import { API } from '../api/Api'; |
| 8 | import { useNavigate } from 'react-router-dom'; | ||
| 9 | 9 | ||
| 10 | interface UploadRunDialogProps { | 10 | interface UploadRunDialogProps { |
| 11 | token?: string; | ||
| 11 | open: boolean; | 12 | open: boolean; |
| 12 | onClose: () => void; | 13 | onClose: () => void; |
| 13 | mapID?: number; | ||
| 14 | games: Game[]; | 14 | games: Game[]; |
| 15 | } | 15 | } |
| 16 | 16 | ||
| 17 | const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ open, onClose, mapID, games }) => { | 17 | const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, games }) => { |
| 18 | |||
| 19 | const navigate = useNavigate(); | ||
| 18 | 20 | ||
| 19 | const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ | 21 | const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ |
| 20 | map_id: 0, | 22 | map_id: 0, |
| 21 | host_demo: null, | 23 | host_demo: null, |
| 22 | partner_demo: null, | 24 | partner_demo: null, |
| 23 | partner_id: undefined, | 25 | partner_id: undefined, |
| 24 | is_partner_orange: undefined, | ||
| 25 | }); | 26 | }); |
| 26 | 27 | ||
| 27 | const [currentMap, setCurrentMap] = React.useState<string>(""); | 28 | const [currentMap, setCurrentMap] = React.useState<string>(""); |
| @@ -56,8 +57,10 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ open, onClose, mapID, | |||
| 56 | const gameMaps = await API.get_game_maps(game_id); | 57 | const gameMaps = await API.get_game_maps(game_id); |
| 57 | setSelectedGameMaps(gameMaps); | 58 | setSelectedGameMaps(gameMaps); |
| 58 | setUploadRunContent({ | 59 | setUploadRunContent({ |
| 59 | ...uploadRunContent, | ||
| 60 | map_id: gameMaps[0].id, | 60 | map_id: gameMaps[0].id, |
| 61 | host_demo: null, | ||
| 62 | partner_demo: null, | ||
| 63 | partner_id: undefined, | ||
| 61 | }); | 64 | }); |
| 62 | _set_current_map(gameMaps[0].name); | 65 | _set_current_map(gameMaps[0].name); |
| 63 | setSelectedGameID(parseInt(game_id) - 1); | 66 | setSelectedGameID(parseInt(game_id) - 1); |
| @@ -65,6 +68,50 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ open, onClose, mapID, | |||
| 65 | setLoading(false); | 68 | setLoading(false); |
| 66 | }; | 69 | }; |
| 67 | 70 | ||
| 71 | const _handle_file_change = async (e: React.ChangeEvent<HTMLInputElement>, host: boolean) => { | ||
| 72 | if (e.target.files) { | ||
| 73 | if (host) { | ||
| 74 | setUploadRunContent({ | ||
| 75 | ...uploadRunContent, | ||
| 76 | host_demo: e.target.files[0], | ||
| 77 | }); | ||
| 78 | } else { | ||
| 79 | setUploadRunContent({ | ||
| 80 | ...uploadRunContent, | ||
| 81 | partner_demo: e.target.files[0], | ||
| 82 | }); | ||
| 83 | } | ||
| 84 | } | ||
| 85 | }; | ||
| 86 | |||
| 87 | const _upload_run = async () => { | ||
| 88 | if (token) { | ||
| 89 | if (games[selectedGameID].is_coop) { | ||
| 90 | if (uploadRunContent.host_demo === null) { | ||
| 91 | alert("You must select a host demo to upload.") | ||
| 92 | return | ||
| 93 | } else if (uploadRunContent.partner_demo === null) { | ||
| 94 | alert("You must select a partner demo to upload.") | ||
| 95 | return | ||
| 96 | } else if (uploadRunContent.partner_id === undefined) { | ||
| 97 | alert("You must specify your partner.") | ||
| 98 | return | ||
| 99 | } | ||
| 100 | } else { | ||
| 101 | if (uploadRunContent.host_demo === null) { | ||
| 102 | alert("You must select a demo to upload.") | ||
| 103 | return | ||
| 104 | } | ||
| 105 | } | ||
| 106 | if (window.confirm("Are you sure you want to submit this run to LPHUB?")) { | ||
| 107 | const message = await API.post_record(token, uploadRunContent); | ||
| 108 | alert(message); | ||
| 109 | navigate(0); | ||
| 110 | onClose(); | ||
| 111 | } | ||
| 112 | } | ||
| 113 | }; | ||
| 114 | |||
| 68 | React.useEffect(() => { | 115 | React.useEffect(() => { |
| 69 | if (open) { | 116 | if (open) { |
| 70 | _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?. | 117 | _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?. |
| @@ -105,23 +152,28 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ open, onClose, mapID, | |||
| 105 | <div> | 152 | <div> |
| 106 | <div id='dropdown2' className={dropdown2Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> | 153 | <div id='dropdown2' className={dropdown2Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> |
| 107 | {selectedGameMaps && selectedGameMaps.map((gameMap) => ( | 154 | {selectedGameMaps && selectedGameMaps.map((gameMap) => ( |
| 108 | <div onClick={() => { setUploadRunContent({...uploadRunContent, map_id: parseInt(gameMap.name)}); _set_current_map(gameMap.name); _handle_dropdowns(2); }} key={gameMap.id}>{gameMap.name}</div> | 155 | <div onClick={() => { setUploadRunContent({...uploadRunContent, map_id: gameMap.id}); _set_current_map(gameMap.name); _handle_dropdowns(2); }} key={gameMap.id}>{gameMap.name}</div> |
| 109 | ))} | 156 | ))} |
| 110 | </div> | 157 | </div> |
| 111 | </div> | 158 | </div> |
| 112 | <span>Host Demo</span> | 159 | <span>Host Demo</span> |
| 113 | <input type="file" name="host_demo" id="host_demo" accept=".dem" /> | 160 | <input type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e, true)} /> |
| 114 | { | 161 | { |
| 115 | games[selectedGameID].is_coop && | 162 | games[selectedGameID].is_coop && |
| 116 | ( | 163 | ( |
| 117 | <> | 164 | <> |
| 118 | <span>Partner Demo</span> | 165 | <span>Partner Demo</span> |
| 119 | <input type="file" name="partner_demo" id="partner_demo" accept=".dem" /> | 166 | <input type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e, false)} /> |
| 167 | <span>Partner ID</span> | ||
| 168 | <input type="text" name="partner_id" id="partner_id" onChange={(e) => setUploadRunContent({ | ||
| 169 | ...uploadRunContent, | ||
| 170 | partner_id: e.target.value, | ||
| 171 | })} /> | ||
| 120 | </> | 172 | </> |
| 121 | ) | 173 | ) |
| 122 | } | 174 | } |
| 123 | <div className='upload-run-buttons-container'> | 175 | <div className='upload-run-buttons-container'> |
| 124 | <button onClick={() => onClose()}>Submit</button> | 176 | <button onClick={_upload_run}>Submit</button> |
| 125 | <button onClick={() => onClose()}>Cancel</button> | 177 | <button onClick={() => onClose()}>Cancel</button> |
| 126 | </div> | 178 | </div> |
| 127 | </div> | 179 | </div> |
diff --git a/frontend/src/types/Content.tsx b/frontend/src/types/Content.tsx index 5733f1f..e6e0cb1 100644 --- a/frontend/src/types/Content.tsx +++ b/frontend/src/types/Content.tsx | |||
| @@ -22,5 +22,4 @@ export interface UploadRunContent { | |||
| 22 | host_demo: File | null; | 22 | host_demo: File | null; |
| 23 | partner_demo: File | null; | 23 | partner_demo: File | null; |
| 24 | partner_id?: string; | 24 | partner_id?: string; |
| 25 | is_partner_orange?: boolean; | ||
| 26 | }; | 25 | }; |