Skip to content

Commit

Permalink
RACE: Handle 'SubmitScores' requests
Browse files Browse the repository at this point in the history
Closes issue #43.
  • Loading branch information
MikeIsAStar authored and MikeIsAStar committed Sep 14, 2024
1 parent 3d1cdc7 commit 007771d
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 68 deletions.
196 changes: 169 additions & 27 deletions race/nintendo_racing_service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package race

import (
"bytes"
"encoding/binary"
"encoding/xml"
"io"
"net/http"
Expand All @@ -14,15 +16,14 @@ import (
)

type rankingsRequestEnvelope struct {
Body rankingsRequestBody
Body rankingsRequestBody `xml:"Body"`
}

type rankingsRequestBody struct {
Data rankingsRequestData `xml:",any"`
GetTopTenRankings rankingsRequestGetTopTenRankings `xml:"GetTopTenRankings"`
}

type rankingsRequestData struct {
XMLName xml.Name
type rankingsRequestGetTopTenRankings struct {
GameId int `xml:"gameid"`
RegionId common.MarioKartWiiLeaderboardRegionId `xml:"regionid"`
CourseId common.MarioKartWiiCourseId `xml:"courseid"`
Expand Down Expand Up @@ -56,7 +57,56 @@ type rankingsResponseRankingData struct {
UserData string `xml:"userdata"`
}

type submitScoresRequestEnvelope struct {
Body submitScoresRequestBody `xml:"Body"`
}

type submitScoresRequestBody struct {
SubmitScores submitScoresRequestSubmitScores `xml:"SubmitScores"`
}

type submitScoresRequestSubmitScores struct {
GameData submitScoresRequestGameData `xml:"gameData"`
}

type submitScoresRequestGameData struct {
RegionId common.MarioKartWiiLeaderboardRegionId `xml:"regionid"`
ProfileID int `xml:"profileid"`
GameId int `xml:"gameid"`
ScoreMode scoreMode `xml:"scoremode"`
ScoreDatas submitScoresRequestScoreDatas `xml:"ScoreDatas"`
}

type submitScoresRequestScoreDatas struct {
ScoreData []submitScoresRequestScoreData `xml:"ScoreData"`
}

type submitScoresRequestScoreData struct {
Time int `xml:"time"`
CourseID common.MarioKartWiiCourseId `xml:"courseid"`
PlayerInfoBase64 string `xml:"playerinfobase64"`
}

type submitScoresResponse struct {
XMLName xml.Name `xml:"SubmitScoresResult"`
XMLNSXSI string `xml:"xmlns:xsi,attr"`
XMLNSXSD string `xml:"xmlns:xsd,attr"`
XMLNS string `xml:"xmlns,attr"`
ResponseCode raceServiceResult `xml:"responseCode"`
}

type playerInfo struct {
MiiData common.Mii // 0x00
ControllerId byte // 0x4C
Unknown byte // 0x4D
StateCode byte // 0x4E
CountryCode byte // 0x4F
}

type raceServiceResult int
type scoreMode int

const playerInfoSize = 0x50

// https://github.com/GameProgressive/UniSpySDK/blob/master/webservices/RacingService.h
const (
Expand All @@ -66,6 +116,11 @@ const (
raceServiceResultInvalidParameters = 105
)

const (
scoreModeTimeTrials = iota // 0x00
scoreModeContest // 0x01
)

const (
xmlNamespaceXSI = "http://www.w3.org/2001/XMLSchema-instance"
xmlNamespaceXSD = "http://www.w3.org/2001/XMLSchema"
Expand All @@ -78,20 +133,17 @@ func handleNintendoRacingServiceRequest(moduleName string, responseWriter http.R
soapActionHeader := request.Header.Get("SOAPAction")
if soapActionHeader == "" {
logging.Error(moduleName, "No SOAPAction header")
writeErrorResponse(raceServiceResultParseError, responseWriter)
return
}

slashIndex := strings.LastIndex(soapActionHeader, "/")
if slashIndex == -1 {
logging.Error(moduleName, "Invalid SOAPAction header")
writeErrorResponse(raceServiceResultParseError, responseWriter)
return
}
quotationMarkIndex := strings.Index(soapActionHeader[slashIndex+1:], "\"")
if quotationMarkIndex == -1 {
logging.Error(moduleName, "Invalid SOAPAction header")
writeErrorResponse(raceServiceResultParseError, responseWriter)
return
}

Expand All @@ -104,10 +156,8 @@ func handleNintendoRacingServiceRequest(moduleName string, responseWriter http.R
switch soapAction {
case "GetTopTenRankings":
handleGetTopTenRankingsRequest(moduleName, responseWriter, requestBody)

// TODO SubmitScores
default:
logging.Info(moduleName, "Unhandled SOAPAction:", aurora.Cyan(soapAction))
case "SubmitScores":
handleSubmitScoresRequest(moduleName, responseWriter, requestBody)
}
}

Expand All @@ -116,37 +166,36 @@ func handleGetTopTenRankingsRequest(moduleName string, responseWriter http.Respo
err := xml.Unmarshal(requestBody, &requestXML)
if err != nil {
logging.Error(moduleName, "Got malformed XML")
writeErrorResponse(raceServiceResultParseError, responseWriter)
writeGetTop10RankingsResponse(raceServiceResultParseError, responseWriter, rankingsResponseDataArray{})
return
}

requestData := requestXML.Body.Data
getTopTenRankings := requestXML.Body.GetTopTenRankings

gameId := requestData.GameId
gameId := getTopTenRankings.GameId
if gameId != marioKartWiiGameID {
logging.Error(moduleName, "Wrong GameSpy game ID:", aurora.Cyan(gameId))
writeErrorResponse(raceServiceResultInvalidParameters, responseWriter)
writeGetTop10RankingsResponse(raceServiceResultInvalidParameters, responseWriter, rankingsResponseDataArray{})
return
}

regionId := requestData.RegionId
courseId := requestData.CourseId

regionId := getTopTenRankings.RegionId
if !regionId.IsValid() {
logging.Error(moduleName, "Invalid region ID:", aurora.Cyan(regionId))
writeErrorResponse(raceServiceResultInvalidParameters, responseWriter)
writeGetTop10RankingsResponse(raceServiceResultInvalidParameters, responseWriter, rankingsResponseDataArray{})
return
}
courseId := getTopTenRankings.CourseId
if courseId < common.MarioCircuit || courseId > 32767 {
logging.Error(moduleName, "Invalid course ID:", aurora.Cyan(courseId))
writeErrorResponse(raceServiceResultInvalidParameters, responseWriter)
writeGetTop10RankingsResponse(raceServiceResultInvalidParameters, responseWriter, rankingsResponseDataArray{})
return
}

topTenRankings, err := database.GetMarioKartWiiTopTenRankings(pool, ctx, regionId, courseId)
if err != nil {
logging.Error(moduleName, "Failed to get the Top 10 rankings:", err)
writeErrorResponse(raceServiceResultDatabaseError, responseWriter)
writeGetTop10RankingsResponse(raceServiceResultDatabaseError, responseWriter, rankingsResponseDataArray{})
return
}

Expand All @@ -172,30 +221,99 @@ func handleGetTopTenRankingsRequest(moduleName string, responseWriter http.Respo
Data: data,
}

writeGetTop10RankingsResponse(raceServiceResultSuccess, responseWriter, dataArray)
}

func handleSubmitScoresRequest(moduleName string, responseWriter http.ResponseWriter, requestBody []byte) {
requestXML := submitScoresRequestEnvelope{}
err := xml.Unmarshal(requestBody, &requestXML)
if err != nil {
logging.Error(moduleName, "Got malformed XML")
writeSubmitScoresResponse(raceServiceResultParseError, responseWriter)
return
}

gameData := requestXML.Body.SubmitScores.GameData

gameId := gameData.GameId
if gameId != marioKartWiiGameID {
logging.Error(moduleName, "Wrong GameSpy game ID:", aurora.Cyan(gameId))
writeSubmitScoresResponse(raceServiceResultInvalidParameters, responseWriter)
return
}

if gameData.ProfileID <= 0 {
logging.Error(moduleName, "Invalid profile ID:", aurora.Cyan(gameData.ProfileID))
writeSubmitScoresResponse(raceServiceResultInvalidParameters, responseWriter)
return
}

if !gameData.RegionId.IsValid() || gameData.RegionId == common.Worldwide {
logging.Error(moduleName, "Invalid region ID:", aurora.Cyan(gameData.RegionId))
writeSubmitScoresResponse(raceServiceResultInvalidParameters, responseWriter)
return
}

scoreMode := gameData.ScoreMode
if scoreMode != scoreModeTimeTrials && scoreMode != scoreModeContest {
logging.Error(moduleName, "Invalid score mode:", aurora.Cyan(scoreMode))
writeSubmitScoresResponse(raceServiceResultInvalidParameters, responseWriter)
return
}

for _, scoreData := range gameData.ScoreDatas.ScoreData {
if scoreData.Time <= 0 || scoreData.Time >= 360000 /* 6 minutes */ {
logging.Error(moduleName, "Invalid time:", aurora.Cyan(scoreData.Time))
writeSubmitScoresResponse(raceServiceResultInvalidParameters, responseWriter)
return
}

courseId := scoreData.CourseID
isContest := scoreMode == scoreModeContest
if courseId < common.MarioCircuit || isContest == courseId.IsValid() || courseId > 32767 {
logging.Error(moduleName, "Invalid course ID:", aurora.Cyan(courseId))
writeSubmitScoresResponse(raceServiceResultInvalidParameters, responseWriter)
return
}

if !IsPlayerInfoValid(scoreData.PlayerInfoBase64) {
logging.Error(moduleName, "Invalid player info:", aurora.Cyan(scoreData.PlayerInfoBase64))
writeSubmitScoresResponse(raceServiceResultInvalidParameters, responseWriter)
return
}
}

// While we recognize that we have received the time, in contrast to the original Race server,
// we require that a ghost accompanies a time in order to display it on the rankings.
writeSubmitScoresResponse(raceServiceResultSuccess, responseWriter)
}

func writeGetTop10RankingsResponse(raceServiceResult raceServiceResult, responseWriter http.ResponseWriter,
dataArray rankingsResponseDataArray) {
rankingDataResponse := rankingsResponseRankingDataResponse{
XMLNSXSI: xmlNamespaceXSI,
XMLNSXSD: xmlNamespaceXSD,
XMLNS: xmlNamespace,
ResponseCode: raceServiceResultSuccess,
ResponseCode: raceServiceResult,
DataArray: dataArray,
}

writeResponse(responseWriter, rankingDataResponse)
}

func writeErrorResponse(raceServiceResult raceServiceResult, responseWriter http.ResponseWriter) {
rankingDataResponse := rankingsResponseRankingDataResponse{
func writeSubmitScoresResponse(raceServiceResult raceServiceResult, responseWriter http.ResponseWriter) {
submitScoresResponse := submitScoresResponse{
XMLNSXSI: xmlNamespaceXSI,
XMLNSXSD: xmlNamespaceXSD,
XMLNS: xmlNamespace,
ResponseCode: raceServiceResult,
}

writeResponse(responseWriter, rankingDataResponse)
writeResponse(responseWriter, submitScoresResponse)
}

func writeResponse(responseWriter http.ResponseWriter, rankingDataResponse rankingsResponseRankingDataResponse) {
responseBody, err := xml.Marshal(rankingDataResponse)
func writeResponse(responseWriter http.ResponseWriter, data any) {
responseBody, err := xml.Marshal(data)
if err != nil {
panic(err)
}
Expand All @@ -206,3 +324,27 @@ func writeResponse(responseWriter http.ResponseWriter, rankingDataResponse ranki
responseWriter.Header().Set("Content-Type", "text/xml")
responseWriter.Write(responseBody)
}

func IsPlayerInfoValid(playerInfoString string) bool {
playerInfoByteArray, err := common.DecodeGameSpyBase64(playerInfoString, common.GameSpyBase64EncodingURLSafe)
if err != nil {
return false
}

if len(playerInfoByteArray) != playerInfoSize {
return false
}

var playerInfo playerInfo
reader := bytes.NewReader(playerInfoByteArray)
err = binary.Read(reader, binary.BigEndian, &playerInfo)
if err != nil {
return false
}

if playerInfo.MiiData.RFLCalculateCRC() != 0x0000 {
return false
}

return common.MarioKartWiiControllerId(playerInfo.ControllerId).IsValid()
}
45 changes: 4 additions & 41 deletions sake/mario_kart_wii.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sake

import (
"bytes"
"encoding/binary"
"fmt"
"io"
Expand All @@ -11,23 +10,12 @@ import (
"wwfc/common"
"wwfc/database"
"wwfc/logging"
"wwfc/race"

"github.com/logrusorgru/aurora/v3"
)

type playerInfo struct {
MiiData common.Mii // 0x00
ControllerId byte // 0x4C
Unknown byte // 0x4D
StateCode byte // 0x4E
CountryCode byte // 0x4F
}

const (
playerInfoSize = 0x50

rkgdFileName = "ghost.bin"
)
const rkgdFileName = "ghost.bin"

func handleMarioKartWiiFileDownloadRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request) {
if strings.HasSuffix(request.URL.Path, "ghostdownload.aspx") {
Expand Down Expand Up @@ -128,6 +116,7 @@ func handleMarioKartWiiGhostDownloadRequest(moduleName string, responseWriter ht
}

func handleMarioKartWiiFileUploadRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request) {
return
if strings.HasSuffix(request.URL.Path, "ghostupload.aspx") {
handleMarioKartWiiGhostUploadRequest(moduleName, responseWriter, request)
return
Expand Down Expand Up @@ -184,7 +173,7 @@ func handleMarioKartWiiGhostUploadRequest(moduleName string, responseWriter http
return
}

if !isPlayerInfoValid(playerInfo) {
if !race.IsPlayerInfoValid(playerInfo) {
logging.Error(moduleName, "Invalid player info:", aurora.Cyan(playerInfo))
responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultMissingParameter))
return
Expand Down Expand Up @@ -259,32 +248,6 @@ func downloadedGhostFileHeader() []byte {
return downloadedGhostFileHeader[:]
}

func isPlayerInfoValid(playerInfoString string) bool {
playerInfoByteArray, err := common.DecodeGameSpyBase64(playerInfoString, common.GameSpyBase64EncodingURLSafe)
if err != nil {
return false
}

if len(playerInfoByteArray) != playerInfoSize {
return false
}

var playerInfo playerInfo
reader := bytes.NewReader(playerInfoByteArray)
err = binary.Read(reader, binary.BigEndian, &playerInfo)
if err != nil {
return false
}

if playerInfo.MiiData.RFLCalculateCRC() != 0x0000 {
return false
}

controllerId := common.MarioKartWiiControllerId(playerInfo.ControllerId)

return controllerId.IsValid()
}

func getMultipartBoundary(contentType string) string {
startIndex := strings.Index(contentType, "boundary=")
if startIndex == -1 {
Expand Down

0 comments on commit 007771d

Please sign in to comment.