This commit is contained in:
David Nwokoye 2026-03-02 13:15:38 +00:00 committed by GitHub
commit cb166edc8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 296 additions and 6 deletions

2
.gitignore vendored
View File

@ -20,6 +20,8 @@ state
# Executables
*.exe
*.exe~
wwfc
# Editor files
.vscode
.github

58
api/mkw_rr.go Normal file
View File

@ -0,0 +1,58 @@
package api
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"wwfc/qr2"
)
type RaceResultInfo struct {
Results map[int][]qr2.RaceResult `json:"results"`
}
func HandleMKWRR(w http.ResponseWriter, r *http.Request) {
u, err := url.Parse(r.URL.String())
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
query, err := url.ParseQuery(u.RawQuery)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
groupNames := query["id"]
if len(groupNames) != 1 {
w.WriteHeader(http.StatusBadRequest)
return
}
results := qr2.GetRaceResultsForGroup(query["id"][0])
if results == nil {
w.WriteHeader(http.StatusNotFound)
return
}
var jsonData []byte
if len(results) == 0 {
w.WriteHeader(http.StatusNotFound)
return
} else {
jsonData, err = json.Marshal(RaceResultInfo{
Results: results,
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
}

View File

@ -56,9 +56,7 @@ func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error {
return pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick).Scan(&user.ProfileId)
}
if user.ProfileId >= 1000000000 {
return ErrReservedProfileIDRange
}
// Reserved profile ID check removed; all profile IDs allowed
var exists bool
err := pool.QueryRow(ctx, IsProfileIDInUse, user.ProfileId).Scan(&exists)
@ -75,9 +73,7 @@ func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error {
}
func (user *User) UpdateProfileID(pool *pgxpool.Pool, ctx context.Context, newProfileId uint32) error {
if newProfileId >= 1000000000 {
return ErrReservedProfileIDRange
}
// Reserved profile ID check removed; all profile IDs allowed
var exists bool
err := pool.QueryRow(ctx, IsProfileIDInUse, newProfileId).Scan(&exists)

View File

@ -1,7 +1,9 @@
package gpcm
import (
"encoding/json"
"strconv"
"time"
"wwfc/common"
"wwfc/logging"
"wwfc/qr2"
@ -9,6 +11,19 @@ import (
"github.com/logrusorgru/aurora/v3"
)
type RaceResultPlayer struct {
Pid int `json:"pid"`
FinishTimeMs int `json:"finish_time_ms"`
CharacterId int `json:"character_id"`
KartId int `json:"kart_id"`
PlayerCount int `json:"player_count"`
}
type RaceResult struct {
ClientReportVersion string `json:"client_report_version"`
Player *RaceResultPlayer `json:"player"`
}
func (g *GameSpySession) handleWWFCReport(command common.GameSpyCommand) {
for key, value := range command.OtherValues {
logging.Info(g.ModuleName, "WiiLink Report:", aurora.Yellow(key))
@ -63,6 +78,69 @@ func (g *GameSpySession) handleWWFCReport(command common.GameSpyCommand) {
}
qr2.ProcessMKWSelectRecord(g.User.ProfileId, key, value)
case "wl:mkw_race_result":
if g.GameName != "mariokartwii" {
logging.Warn(g.ModuleName, "Ignoring", keyColored, "from wrong game")
continue
}
logging.Info(g.ModuleName, "Received race result from profile", aurora.BrightCyan(strconv.FormatUint(uint64(g.User.ProfileId), 10)))
var raceResult RaceResult
err := json.Unmarshal([]byte(value), &raceResult)
if err != nil {
logging.Error(g.ModuleName, "Error parsing race result JSON:", err.Error())
logging.Info(g.ModuleName, "Raw payload:", aurora.BrightMagenta(value))
continue
}
logging.Info(g.ModuleName, "Race result version:", aurora.Yellow(raceResult.ClientReportVersion))
player := raceResult.Player
logging.Info(g.ModuleName,
"Player",
"- PID:", aurora.Cyan(strconv.Itoa(player.Pid)),
"Time:", aurora.Cyan(strconv.Itoa(player.FinishTimeMs)), "ms",
"Char:", aurora.Cyan(strconv.Itoa(player.CharacterId)),
"Kart:", aurora.Cyan(strconv.Itoa(player.KartId)),
"Count:", aurora.Cyan(strconv.Itoa(player.PlayerCount)))
// Hand off to qr2 for processing
qr2.ProcessMKWRaceResult(g.User.ProfileId, player.Pid, player.FinishTimeMs, player.CharacterId, player.KartId, player.PlayerCount)
case "wl:mkw_race_start_time":
serverTime := time.Now().UnixMilli()
logging.Info(g.ModuleName,
"Race start time:", aurora.Yellow(value),
"Server time:", aurora.Yellow(strconv.FormatInt(serverTime, 10)))
// Parse client timestamp
clientTime, err := strconv.ParseInt(value, 10, 64)
if err != nil {
logging.Error(g.ModuleName, "Failed to parse client timestamp:", err.Error())
return
}
// Store timing data in qr2 module
qr2.StoreRaceStartTime(g.User.ProfileId, clientTime, serverTime)
case "wl:mkw_race_finish_time":
serverTime := time.Now().UnixMilli()
logging.Info(g.ModuleName,
"Race finish time:", aurora.Yellow(value),
"Server time:", aurora.Yellow(strconv.FormatInt(serverTime, 10)))
// Parse client timestamp
clientTime, err := strconv.ParseInt(value, 10, 64)
if err != nil {
logging.Error(g.ModuleName, "Failed to parse client finish timestamp:", err.Error())
return
}
// Store timing data in qr2 module
qr2.StoreRaceFinishTime(g.User.ProfileId, clientTime, serverTime)
}
}
}

View File

@ -177,6 +177,12 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
return
}
// Check for /api/mkw_rr
if r.URL.Path == "/api/mkw_rr" {
api.HandleMKWRR(w, r)
return
}
logging.Info("NAS", aurora.Yellow(r.Method), aurora.Cyan(r.URL), "via", aurora.Cyan(r.Host), "from", aurora.BrightCyan(r.RemoteAddr))
replyHTTPError(w, 404, "404 Not Found")
}

View File

@ -31,6 +31,39 @@ type Group struct {
MKWEngineClassID int
}
type RaceResultPlayer struct {
Pid int `json:"pid"`
FinishTimeMs int `json:"finish_time_ms"`
CharacterId int `json:"character_id"`
KartId int `json:"kart_id"`
}
type RaceResult struct {
ProfileID uint32
PlayerID int
FinishTime uint32
CharacterID uint32
VehicleID uint32
PlayerCount uint32
FinishPos int
CourseID int
EngineClassID int
Delta int
}
var raceResults = map[string]map[int][]RaceResult{} // GroupName -> RaceNumber -> []RaceResult
// Timing storage for delta calculation using start/finish times
var raceStartTimings = map[uint32]struct {
ClientTime int64
ServerTime int64
}{}
var raceFinishTimings = map[uint32]struct {
ClientTime int64
ServerTime int64
}{}
var groups = map[string]*Group{}
func processResvOK(moduleName string, matchVersion int, reservation common.MatchCommandDataReservation, resvOK common.MatchCommandDataResvOK, sender, destination *Session) bool {
@ -310,6 +343,22 @@ func CheckGPReservationAllowed(senderIP uint64, senderPid uint32, destPid uint32
return checkReservationAllowed(moduleName, from, to, joinType)
}
// StoreRaceStartTime stores the start timing data for delta calculation
func StoreRaceStartTime(profileId uint32, clientTime, serverTime int64) {
raceStartTimings[profileId] = struct {
ClientTime int64
ServerTime int64
}{ClientTime: clientTime, ServerTime: serverTime}
}
// StoreRaceFinishTime stores the finish timing data for delta calculation
func StoreRaceFinishTime(profileId uint32, clientTime, serverTime int64) {
raceFinishTimings[profileId] = struct {
ClientTime int64
ServerTime int64
}{ClientTime: clientTime, ServerTime: serverTime}
}
func ProcessNATNEGReport(result byte, ip1 string, ip2 string) {
moduleName := "QR2:NATNEGReport"
@ -525,6 +574,107 @@ func ProcessMKWSelectRecord(profileId uint32, key string, value string) {
}
func ProcessMKWRaceResult(profileId uint32, playerPid int, finishTimeMs int, characterId int, kartId int, playerCount int) {
moduleName := "QR2:MKWRaceResult:" + strconv.FormatUint(uint64(profileId), 10)
mutex.Lock()
login := logins[profileId]
if login == nil {
mutex.Unlock()
logging.Warn(moduleName, "Received race result from non-existent profile ID", aurora.Cyan(profileId))
return
}
session := login.session
if session == nil {
mutex.Unlock()
logging.Warn(moduleName, "Received race result from profile ID", aurora.Cyan(profileId), "but no session exists")
return
}
mutex.Unlock()
group := session.groupPointer
if group == nil {
return
}
if group.MKWRaceNumber == 0 {
logging.Error(moduleName, "Received race result but no races have been started")
return
}
// Calculate delta using start/finish times
var delta int
if startTiming, exists := raceStartTimings[profileId]; exists {
if finishTiming, exists := raceFinishTimings[profileId]; exists {
clientElapsedTime := finishTiming.ClientTime - startTiming.ClientTime
serverElapsedTime := finishTiming.ServerTime - startTiming.ServerTime
delta = int(serverElapsedTime - clientElapsedTime)
}
}
// Calculate finish position based on current race results
finishPos := 1
if raceResults[group.GroupName] != nil && len(raceResults[group.GroupName][group.MKWRaceNumber]) > 0 {
// Get current race results for this race number
currentResults := raceResults[group.GroupName][group.MKWRaceNumber]
// Count how many players have finished with better times
for _, existingResult := range currentResults {
if existingResult.FinishTime < uint32(finishTimeMs) {
finishPos++
}
}
}
// Convert race result data to internal format
raceResultData := RaceResult{
ProfileID: profileId,
PlayerID: playerPid,
FinishTime: uint32(finishTimeMs),
CharacterID: uint32(characterId),
VehicleID: uint32(kartId),
PlayerCount: uint32(playerCount),
FinishPos: finishPos,
CourseID: group.MKWCourseID,
EngineClassID: group.MKWEngineClassID,
Delta: delta,
}
mutex.Lock()
defer mutex.Unlock()
if raceResults[group.GroupName] == nil {
raceResults[group.GroupName] = map[int][]RaceResult{}
}
raceResults[group.GroupName][group.MKWRaceNumber] = append(raceResults[group.GroupName][group.MKWRaceNumber], raceResultData)
logging.Info(moduleName, "Stored race result for profile", aurora.BrightCyan(strconv.FormatUint(uint64(profileId), 10)),
"Race #:", aurora.Cyan(strconv.Itoa(group.MKWRaceNumber)),
"Course:", aurora.Cyan(strconv.Itoa(group.MKWCourseID)),
"Delta:", aurora.Cyan(strconv.Itoa(delta)))
}
func GetRaceResultsForGroup(groupName string) map[int][]RaceResult {
mutex.Lock()
defer mutex.Unlock()
groupResults, ok := raceResults[groupName]
if !ok {
return nil
}
// Return a copy to prevent external modification
copiedRaceResults := make(map[int][]RaceResult)
for raceNumber, results := range groupResults {
copiedRaceResults[raceNumber] = make([]RaceResult, len(results))
copy(copiedRaceResults[raceNumber], results)
}
return copiedRaceResults
}
// saveGroups saves the current groups state to disk.
// Expects the mutex to be locked.
func saveGroups() error {