diff --git a/common/mario_kart_wii.go b/common/mario_kart_wii.go new file mode 100644 index 0000000..c431444 --- /dev/null +++ b/common/mario_kart_wii.go @@ -0,0 +1,110 @@ +package common + +type MarioKartWiiRegionID uint +type MarioKartWiiCourseID uint + +const MarioKartWiiGameSpyGameID uint = 1687 + +const ( + Worldwide = iota // 0 + Japan = iota // 1 + UnitedStates = iota // 2 + Europe = iota // 3 + Australia = iota // 4 + Taiwan = iota // 5 + Korea = iota // 6 + China = iota // 7 +) + +const ( + MarioCircuit = iota // 0x00 + MooMooMeadows = iota // 0x01 + MushroomGorge = iota // 0x02 + GrumbleVolcano = iota // 0x03 + ToadsFactory = iota // 0x04 + CoconutMall = iota // 0x05 + DKSummit = iota // 0x06 + WarioGoldMine = iota // 0x07 + LuigiCircuit = iota // 0x08 + DaisyCircuit = iota // 0x09 + MoonviewHighway = iota // 0x0A + MapleTreeway = iota // 0x0B + BowsersCastle = iota // 0x0C + RainbowRoad = iota // 0x0D + DryDryRuins = iota // 0x0E + KoopaCape = iota // 0x0F + GCNPeachBeach = iota // 0x10 + GCNMarioCircuit = iota // 0x11 + GCNWaluigiStadium = iota // 0x12 + GCNDKMountain = iota // 0x13 + DSYoshiFalls = iota // 0x14 + DSDesertHills = iota // 0x15 + DSPeachGardens = iota // 0x16 + DSDelfinoSquare = iota // 0x17 + SNESMarioCircuit3 = iota // 0x18 + SNESGhostValley2 = iota // 0x19 + N64MarioRaceway = iota // 0x1A + N64SherbetLand = iota // 0x1B + N64BowsersCastle = iota // 0x1C + N64DKsJungleParkway = iota // 0x1D + GBABowserCastle3 = iota // 0x1E + GBAShyGuyBeach = iota // 0x1F +) + +func (regionId MarioKartWiiRegionID) IsValid() bool { + return regionId >= Worldwide && regionId <= China +} + +func (courseId MarioKartWiiCourseID) IsValid() bool { + return courseId >= MarioCircuit && courseId <= GBAShyGuyBeach +} + +func (regionId MarioKartWiiRegionID) ToString() string { + return [...]string{ + "Worldwide", + "Japan", + "United States", + "Europe", + "Australia", + "Taiwan", + "Korea", + "China", + }[regionId] +} + +func (courseId MarioKartWiiCourseID) ToString() string { + return [...]string{ + "Mario Circuit", + "Moo Moo Meadows", + "Mushroom Gorge", + "Grumble Volcano", + "Toad's Factory", + "Coconut Mall", + "DK Summit", + "Wario's Gold Mine", + "Luigi Circuit", + "Daisy Circuit", + "Moonview Highway", + "Maple Treeway", + "Bowser's Castle", + "Rainbow Road", + "Dry Dry Ruins", + "Koopa Cape", + "GCN Peach Beach", + "GCN Mario Circuit", + "GCN Waluigi Stadium", + "GCN DK Mountain", + "DS Yoshi Falls", + "DS Desert Hills", + "DS Peach Gardens", + "DS Delfino Square", + "SNES Mario Circuit 3", + "SNES Ghost Valley 2", + "N64 Mario Raceway", + "N64 Sherbet Land", + "N64 Bowser's Castle", + "N64 DK's Jungle Parkway", + "GBA Bowser Castle 3", + "GBA Shy Guy Beach", + }[courseId] +} diff --git a/main.go b/main.go index 45cdd79..98fec0a 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,7 @@ import ( "wwfc/nas" "wwfc/natneg" "wwfc/qr2" + "wwfc/race" "wwfc/sake" "wwfc/serverbrowser" @@ -99,7 +100,7 @@ func backendMain(noSignal, noReload bool) { } wg := &sync.WaitGroup{} - actions := []func(bool){nas.StartServer, gpcm.StartServer, qr2.StartServer, gpsp.StartServer, serverbrowser.StartServer, sake.StartServer, natneg.StartServer, api.StartServer, gamestats.StartServer} + actions := []func(bool){nas.StartServer, gpcm.StartServer, qr2.StartServer, gpsp.StartServer, serverbrowser.StartServer, race.StartServer, sake.StartServer, natneg.StartServer, api.StartServer, gamestats.StartServer} wg.Add(len(actions)) for _, action := range actions { go func(ac func(bool)) { @@ -215,7 +216,7 @@ func (r *RPCPacket) Shutdown(stateUuid string, _ *struct{}) error { } wg := &sync.WaitGroup{} - actions := []func(){nas.Shutdown, gpcm.Shutdown, qr2.Shutdown, gpsp.Shutdown, serverbrowser.Shutdown, sake.Shutdown, natneg.Shutdown, api.Shutdown, gamestats.Shutdown} + actions := []func(){nas.Shutdown, gpcm.Shutdown, qr2.Shutdown, gpsp.Shutdown, serverbrowser.Shutdown, race.Shutdown, sake.Shutdown, natneg.Shutdown, api.Shutdown, gamestats.Shutdown} wg.Add(len(actions)) for _, action := range actions { go func(ac func()) { diff --git a/nas/main.go b/nas/main.go index 9cd39ae..e16f541 100644 --- a/nas/main.go +++ b/nas/main.go @@ -13,6 +13,7 @@ import ( "wwfc/gamestats" "wwfc/logging" "wwfc/nhttp" + "wwfc/race" "wwfc/sake" "github.com/logrusorgru/aurora/v3" @@ -71,6 +72,7 @@ func Shutdown() { } } +var regexRaceHost = regexp.MustCompile(`^([a-z\-]+\.)?race\.gs\.`) var regexSakeHost = regexp.MustCompile(`^([a-z\-]+\.)?sake\.gs\.`) var regexGamestatsHost = regexp.MustCompile(`^([a-z\-]+\.)?gamestats2?\.gs\.`) var regexStage1URL = regexp.MustCompile(`^/w([0-9])$`) @@ -90,6 +92,13 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { return } + // Check for *.race.gs.* or race.gs.* + if regexRaceHost.MatchString(r.Host) { + // Redirect to the race server + race.HandleRequest(w, r) + return + } + moduleName := "NAS:" + r.RemoteAddr // Handle conntest server diff --git a/race/main.go b/race/main.go new file mode 100644 index 0000000..b0c919d --- /dev/null +++ b/race/main.go @@ -0,0 +1,25 @@ +package race + +import ( + "net/http" + "path" + "wwfc/logging" + + "github.com/logrusorgru/aurora/v3" +) + +func StartServer(reload bool) { +} + +func Shutdown() { +} + +func HandleRequest(responseWriter http.ResponseWriter, request *http.Request) { + logging.Info("RACE", aurora.Yellow(request.Method), aurora.Cyan(request.URL), "via", aurora.Cyan(request.Host), "from", aurora.BrightCyan(request.RemoteAddr)) + + switch path.Base(request.URL.Path) { + case "NintendoRacingService.asmx": + moduleName := "RACE:RacingService:" + request.RemoteAddr + handleNintendoRacingServiceRequest(moduleName, responseWriter, request) + } +} diff --git a/race/nintendo_racing_service.go b/race/nintendo_racing_service.go new file mode 100644 index 0000000..9781444 --- /dev/null +++ b/race/nintendo_racing_service.go @@ -0,0 +1,73 @@ +package race + +import ( + "encoding/xml" + "io" + "net/http" + "wwfc/common" + "wwfc/logging" + + "github.com/logrusorgru/aurora/v3" +) + +type rankingsRequestEnvelope struct { + XMLName xml.Name + Body rankingsRequestBody +} + +type rankingsRequestBody struct { + XMLName xml.Name + Data rankingsRequestData `xml:",any"` +} + +type rankingsRequestData struct { + XMLName xml.Name + GameId uint `xml:"gameid"` + RegionId common.MarioKartWiiRegionID `xml:"regionid"` + CourseId common.MarioKartWiiCourseID `xml:"courseid"` +} + +func handleNintendoRacingServiceRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request) { + soapActionHeader := request.Header.Get("SOAPAction") + if soapActionHeader == "" { + logging.Error(moduleName, "No SOAPAction header") + return + } + + requestBody, err := io.ReadAll(request.Body) + if err != nil { + panic(err) + } + + soapMessage := rankingsRequestEnvelope{} + err = xml.Unmarshal(requestBody, &soapMessage) + if err != nil { + logging.Error(moduleName, "Malformed XML") + return + } + soapMessageData := soapMessage.Body.Data + + gameId := soapMessageData.GameId + if gameId != common.MarioKartWiiGameSpyGameID { + logging.Error(moduleName, "Wrong GameSpy game id") + return + } + + soapAction := soapMessageData.XMLName.Local + switch soapAction { + case "GetTopTenRankings": + regionId := soapMessageData.RegionId + courseId := soapMessageData.CourseId + + if !regionId.IsValid() { + logging.Error(moduleName, "Invalid region id") + return + } + if !courseId.IsValid() { + logging.Error(moduleName, "Invalid course id") + return + } + + logging.Info(moduleName, "Received a request for the Top 10 of", aurora.BrightCyan(courseId.ToString())) + } +}