From 3ccda9ed2618577ca0efe5bc12668e1d9561d97e Mon Sep 17 00:00:00 2001 From: Maple Nebel Date: Wed, 29 Jul 2026 00:29:18 +0200 Subject: [PATCH] feat: implement splatoon ranking functions --- .../database/get_user_score_entries.go | 38 ++++++++++++ nex/ranking/database/get_vote_count.go | 23 ++++++++ nex/ranking/database/get_win_count.go | 28 +++++++++ nex/ranking/database/setup.go | 37 ++++++++++++ .../database/store_user_match_result.go | 25 ++++++++ nex/ranking/database/store_user_score.go | 33 +++++++++++ nex/ranking/get_competition_ranking_score.go | 58 ++++++++++++++++++- .../competition_ranking_upload_score_param.go | 14 ++--- .../upload_competition_ranking_score.go | 6 +- ...register_common_secure_server_protocols.go | 12 ++++ 10 files changed, 265 insertions(+), 9 deletions(-) create mode 100644 nex/ranking/database/get_user_score_entries.go create mode 100644 nex/ranking/database/get_vote_count.go create mode 100644 nex/ranking/database/get_win_count.go create mode 100644 nex/ranking/database/setup.go create mode 100644 nex/ranking/database/store_user_match_result.go create mode 100644 nex/ranking/database/store_user_score.go diff --git a/nex/ranking/database/get_user_score_entries.go b/nex/ranking/database/get_user_score_entries.go new file mode 100644 index 0000000..920033a --- /dev/null +++ b/nex/ranking/database/get_user_score_entries.go @@ -0,0 +1,38 @@ +package database + +import ( + "github.com/PretendoNetwork/splatoon/globals" + ranking_splatoon_types "github.com/PretendoNetwork/splatoon/nex/ranking/types" +) + +func GetScoreDataEntries(splatfestID uint32) ([]ranking_splatoon_types.CompetitionRankingScoreData, error) { + list := []ranking_splatoon_types.CompetitionRankingScoreData{} + + stream, err := globals.Postgres.Query(`SELECT + uploader_pid, + score, + last_updated, + app_data + FROM ranking_splatoon.user_scores WHERE splatfest_id=$1`, uint64(splatfestID)) + + if err != nil { + return nil, err + } + + for stream.Next() { + competitionRankingScoreData := ranking_splatoon_types.NewCompetitionRankingScoreData() + err = stream.Scan(&competitionRankingScoreData.PID, &competitionRankingScoreData.Score, &competitionRankingScoreData.Modified, &competitionRankingScoreData.AppData) + if err != nil { + stream.Close() + return nil, err + } + list = append(list, competitionRankingScoreData) + } + + err = stream.Err() + if err != nil { + return nil, err + } + + return list, nil +} diff --git a/nex/ranking/database/get_vote_count.go b/nex/ranking/database/get_vote_count.go new file mode 100644 index 0000000..9cb74b7 --- /dev/null +++ b/nex/ranking/database/get_vote_count.go @@ -0,0 +1,23 @@ +package database + +import ( + "github.com/PretendoNetwork/splatoon/globals" +) + +func GetVoteCount(splatfestID uint32, team_id uint8) (uint32, error) { + row := globals.Postgres.QueryRow(`SELECT + COUNT(user_id) + FROM ranking_splatoon.user_scores WHERE splatfest_id=$1 AND team_id=$2`, uint64(splatfestID), int16(team_id)) + + if row.Err() != nil { + return 0, row.Err() + } + var count uint32 + + err := row.Scan(&count) + if err != nil { + return 0, err + } + + return count, nil +} diff --git a/nex/ranking/database/get_win_count.go b/nex/ranking/database/get_win_count.go new file mode 100644 index 0000000..504b788 --- /dev/null +++ b/nex/ranking/database/get_win_count.go @@ -0,0 +1,28 @@ +package database + +import ( + "github.com/PretendoNetwork/splatoon/globals" +) + +func GetWinCount(splatfestID uint32, team_id uint8) (uint32, error) { + row := globals.Postgres.QueryRow(`SELECT + COUNT(upload_id) + FROM ranking_splatoon.results + WHERE splatfest_id=$1 + AND ( + (has_won = false AND team_id NOT EQUAL $2) OR + (has_won = true AND team_id=$2) + )`, uint64(splatfestID), int16(team_id)) + + if row.Err() != nil { + return 0, row.Err() + } + var count uint32 + + err := row.Scan(&count) + if err != nil { + return 0, err + } + + return count, nil +} diff --git a/nex/ranking/database/setup.go b/nex/ranking/database/setup.go new file mode 100644 index 0000000..509f4c7 --- /dev/null +++ b/nex/ranking/database/setup.go @@ -0,0 +1,37 @@ +package database + +import ( + "github.com/PretendoNetwork/splatoon/globals" +) + +func Setup() { + var err error + + _, err = globals.Postgres.Exec(`CREATE SCHEMA IF NOT EXISTS ranking_splatoon`) + if err != nil { + globals.Logger.Error(err.Error()) + return + } + + _, err = globals.Postgres.Exec(`CREATE TABLE IF NOT EXISTS ranking_splatoon.results ( + upload_id bigserial primary key not null, + uploader_pid int8 not null, + splatfest_id int8 not null, + team_id int2 not null, + has_won int2 not null, + app_data bytea not null, + )`) + _, err = globals.Postgres.Exec(`CREATE TABLE IF NOT EXISTS ranking_splatoon.user_scores ( + uploader_pid int8 not null, + splatfest_id int8 not null, + score int8 not null, + team_id boolean not null, + user_data bytea not null, + last_updated int8 not null + PRIMARY KEY(uploader_pid, splatfest_id) + )`) + if err != nil { + globals.Logger.Error(err.Error()) + return + } +} diff --git a/nex/ranking/database/store_user_match_result.go b/nex/ranking/database/store_user_match_result.go new file mode 100644 index 0000000..f7b87c3 --- /dev/null +++ b/nex/ranking/database/store_user_match_result.go @@ -0,0 +1,25 @@ +package database + +import ( + "github.com/PretendoNetwork/nex-go/v2/types" + "github.com/PretendoNetwork/splatoon/globals" + ranking_splatoon_types "github.com/PretendoNetwork/splatoon/nex/ranking/types" +) + +func StoreUserMatchResult(userPID types.PID, data ranking_splatoon_types.CompetitionRankingUploadScoreParam) error { + var err error + _, err = globals.Postgres.Exec(` + INSERT INTO ranking_splatoon.results( + uploader_pid, + splatfest_id, + score, + team_id, + has_won + ) VALUES ($1, $2, $3, $4, $5) + `, int64(userPID), int64(data.SplatfestId), int64(data.Score), int16(data.TeamId), bool(data.HasWon)) + + if err != nil { + return err + } + return nil +} diff --git a/nex/ranking/database/store_user_score.go b/nex/ranking/database/store_user_score.go new file mode 100644 index 0000000..c9c215a --- /dev/null +++ b/nex/ranking/database/store_user_score.go @@ -0,0 +1,33 @@ +package database + +import ( + "github.com/PretendoNetwork/nex-go/v2/types" + "github.com/PretendoNetwork/splatoon/globals" + ranking_splatoon_types "github.com/PretendoNetwork/splatoon/nex/ranking/types" +) + +func StoreUserScore(userPID types.PID, data ranking_splatoon_types.CompetitionRankingUploadScoreParam) error { + var err error + time := types.NewDateTime(0).Now() + _, err = globals.Postgres.Exec(` + INSERT INTO ranking_splatoon.user_scores( + uploader_pid, + splatfest_id, + score, + team_id, + last_updated, + app_data + ) VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (uploader_pid, splatfest_id) + DO UPDATE SET + score = EXCLUDED.score, + last_updated = EXCLUDED.last_updated + team_id = EXCLUDED.team_id + app_data = EXCLUDED.app_data + `, int64(userPID), int64(data.SplatfestId), int64(data.Score), int16(data.TeamId), int64(time), ([]byte)(data.AppData)) + + if err != nil { + return err + } + return nil +} diff --git a/nex/ranking/get_competition_ranking_score.go b/nex/ranking/get_competition_ranking_score.go index ac9d047..327202a 100644 --- a/nex/ranking/get_competition_ranking_score.go +++ b/nex/ranking/get_competition_ranking_score.go @@ -6,9 +6,54 @@ import ( common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" ranking "github.com/PretendoNetwork/nex-protocols-go/v2/ranking/splatoon" "github.com/PretendoNetwork/splatoon/globals" + "github.com/PretendoNetwork/splatoon/nex/ranking/database" ranking_splatoon_types "github.com/PretendoNetwork/splatoon/nex/ranking/types" ) +func GetSingleCompetitionRankingScore(splatfestID uint32) (ranking_splatoon_types.CompetitionRankingScoreInfo, error) { + var err error + info := ranking_splatoon_types.NewCompetitionRankingScoreInfo() + + info.FestId = types.UInt32(splatfestID) + + var team_alpha_count uint32 + team_alpha_count, err = database.GetVoteCount(splatfestID, 0) + if err != nil { + return info, err + } + info.TeamVotes = append(info.TeamVotes, types.UInt32(team_alpha_count)) + + var team_bravo_count uint32 + team_bravo_count, err = database.GetVoteCount(splatfestID, 1) + if err != nil { + return info, err + } + info.TeamVotes = append(info.TeamVotes, types.UInt32(team_bravo_count)) + + var team_alpha_wins uint32 + team_alpha_wins, err = database.GetWinCount(splatfestID, 0) + if err != nil { + return info, err + } + info.TeamWins = append(info.TeamWins, types.UInt32(team_alpha_wins)) + + var team_bravo_wins uint32 + team_bravo_wins, err = database.GetVoteCount(splatfestID, 1) + if err != nil { + return info, err + } + info.TeamWins = append(info.TeamWins, types.UInt32(team_bravo_wins)) + + var scoreDataEntries []ranking_splatoon_types.CompetitionRankingScoreData + scoreDataEntries, err = database.GetScoreDataEntries(splatfestID) + if err != nil { + return info, err + } + info.ScoreData = scoreDataEntries + + return info, nil +} + func GetCompetitionRankingScore(err error, packet nex.PacketInterface, callID uint32, packetPayload []byte) (*nex.RMCMessage, *nex.Error) { rmcResponseStream := nex.NewByteStreamOut(globals.SecureServer.LibraryVersions, globals.SecureServer.ByteStreamSettings) @@ -24,8 +69,19 @@ func GetCompetitionRankingScore(err error, packet nex.PacketInterface, callID ui return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, err.Error()) } - // todo: actually implement this function retVal := types.NewList[ranking_splatoon_types.CompetitionRankingScoreInfo]() + var festListIndex uint32 + for festListIndex = 0; festListIndex < uint32(params.ResultRange.Length); festListIndex++ { + if festListIndex >= uint32(len(params.FestivalIds)) { + return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "out of resultrange is out of bounds") + } + festId := params.FestivalIds[festListIndex+uint32(params.ResultRange.Offset)] + info, err := GetSingleCompetitionRankingScore(uint32(festId)) + if err != nil { + return nil, nex.NewError(nex.ResultCodes.Core.Exception, "error retrieving ranking scores") + } + retVal = append(retVal, info) + } retVal.WriteTo(rmcResponseStream) diff --git a/nex/ranking/types/competition_ranking_upload_score_param.go b/nex/ranking/types/competition_ranking_upload_score_param.go index aba1924..9b40e1f 100644 --- a/nex/ranking/types/competition_ranking_upload_score_param.go +++ b/nex/ranking/types/competition_ranking_upload_score_param.go @@ -16,7 +16,7 @@ type CompetitionRankingUploadScoreParam struct { Unknown2 types.UInt32 Score types.UInt32 TeamId types.UInt8 - TeamWin types.UInt8 + HasWon types.Bool IsFirstUpload types.Bool AppData types.QBuffer } @@ -30,7 +30,7 @@ func (crusp CompetitionRankingUploadScoreParam) WriteTo(writable types.Writable) crusp.Unknown2.WriteTo(contentWritable) crusp.Score.WriteTo(contentWritable) crusp.TeamId.WriteTo(contentWritable) - crusp.TeamWin.WriteTo(contentWritable) + crusp.HasWon.WriteTo(contentWritable) crusp.IsFirstUpload.WriteTo(contentWritable) crusp.AppData.WriteTo(contentWritable) @@ -62,7 +62,7 @@ func (crusp CompetitionRankingUploadScoreParam) ExtractFrom(readable types.Reada if err := crusp.TeamId.ExtractFrom(readable); err != nil { return fmt.Errorf("failed to extract CompetitionRankingUploadScoreParam.TeamId. %s", err.Error()) } - if err := crusp.TeamWin.ExtractFrom(readable); err != nil { + if err := crusp.HasWon.ExtractFrom(readable); err != nil { return fmt.Errorf("failed to extract CompetitionRankingUploadScoreParam.TeamWin. %s", err.Error()) } if err := crusp.IsFirstUpload.ExtractFrom(readable); err != nil { @@ -85,7 +85,7 @@ func (crusp CompetitionRankingUploadScoreParam) Copy() types.RVType { copied.Unknown2 = crusp.Unknown2.Copy().(types.UInt32) copied.Score = crusp.Score.Copy().(types.UInt32) copied.TeamId = crusp.TeamId.Copy().(types.UInt8) - copied.TeamWin = crusp.TeamWin.Copy().(types.UInt8) + copied.HasWon = crusp.HasWon.Copy().(types.Bool) copied.IsFirstUpload = crusp.IsFirstUpload.Copy().(types.Bool) copied.AppData = crusp.AppData.Copy().(types.QBuffer) @@ -119,7 +119,7 @@ func (crusp CompetitionRankingUploadScoreParam) Equals(o types.RVType) bool { if !crusp.TeamId.Equals(other.TeamId) { return false } - if !crusp.TeamWin.Equals(other.TeamWin) { + if !crusp.HasWon.Equals(other.HasWon) { return false } if !crusp.IsFirstUpload.Equals(other.IsFirstUpload) { @@ -161,7 +161,7 @@ func (crusp CompetitionRankingUploadScoreParam) FormatToString(indentationLevel fmt.Fprintf(&b, "%sUnknown2: %s,\n", indentationValues, crusp.Unknown2) fmt.Fprintf(&b, "%sScore: %s,\n", indentationValues, crusp.Score) fmt.Fprintf(&b, "%sTeamId: %s,\n", indentationValues, crusp.TeamId) - fmt.Fprintf(&b, "%sTeamWin: %s,\n", indentationValues, crusp.TeamWin) + fmt.Fprintf(&b, "%sHasWon: %s,\n", indentationValues, crusp.HasWon) fmt.Fprintf(&b, "%sIsFirstUpload: %s,\n", indentationValues, crusp.IsFirstUpload) fmt.Fprintf(&b, "%sAppData: %s,\n", indentationValues, crusp.AppData) fmt.Fprintf(&b, "%s}", indentationEnd) @@ -177,7 +177,7 @@ func NewCompetitionRankingUploadScoreParam() CompetitionRankingUploadScoreParam Unknown2: types.NewUInt32(0), Score: types.NewUInt32(0), TeamId: types.NewUInt8(0), - TeamWin: types.NewUInt8(0), + HasWon: types.NewBool(false), IsFirstUpload: types.NewBool(false), AppData: types.NewQBuffer([]byte{}), } diff --git a/nex/ranking/upload_competition_ranking_score.go b/nex/ranking/upload_competition_ranking_score.go index 5d1e3db..885707f 100644 --- a/nex/ranking/upload_competition_ranking_score.go +++ b/nex/ranking/upload_competition_ranking_score.go @@ -6,6 +6,7 @@ import ( common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" ranking "github.com/PretendoNetwork/nex-protocols-go/v2/ranking/splatoon" "github.com/PretendoNetwork/splatoon/globals" + "github.com/PretendoNetwork/splatoon/nex/ranking/database" ranking_splatoon_types "github.com/PretendoNetwork/splatoon/nex/ranking/types" ) @@ -24,7 +25,10 @@ func UploadCompetitionRankingScore(err error, packet nex.PacketInterface, callID return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, err.Error()) } - types.NewBool(false).WriteTo(rmcResponseStream) + database.StoreUserMatchResult(packet.Sender().PID(), params) + database.StoreUserScore(packet.Sender().PID(), params) + + types.NewBool(true).WriteTo(rmcResponseStream) rmcResponse := nex.NewRMCSuccess(globals.SecureEndpoint, rmcResponseStream.Bytes()) rmcResponse.ProtocolID = ranking.ProtocolID diff --git a/nex/register_common_secure_server_protocols.go b/nex/register_common_secure_server_protocols.go index 48be6fd..185e72a 100644 --- a/nex/register_common_secure_server_protocols.go +++ b/nex/register_common_secure_server_protocols.go @@ -1,6 +1,9 @@ package nex import ( + "slices" + + "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" commonmatchmaking "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making" commonmatchmakingext "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making-ext" @@ -17,6 +20,7 @@ import ( secure "github.com/PretendoNetwork/nex-protocols-go/v2/secure-connection" "github.com/PretendoNetwork/splatoon/globals" ranking_impl "github.com/PretendoNetwork/splatoon/nex/ranking" + "github.com/PretendoNetwork/splatoon/nex/ranking/database" ) func CreateReportDBRecord(_ types.PID, _ types.UInt32, _ types.QBuffer) error { @@ -29,11 +33,19 @@ func cleanupMatchmakeSessionSearchCriteriasHandler(searchCriterias types.List[ma } } +func adjustPublicStationFirst(packet nex.PacketInterface, urls types.List[types.StationURL], dataHolder types.DataHolder) { + connection := packet.Sender().(*nex.PRUDPConnection) + slices.Reverse(connection.StationURLs) +} + func registerCommonSecureServerProtocols() { + database.Setup() + secureProtocol := secure.NewProtocol() globals.SecureEndpoint.RegisterServiceProtocol(secureProtocol) commonSecureProtocol := commonsecure.NewCommonProtocol(secureProtocol) commonSecureProtocol.EnableInsecureRegister() + commonSecureProtocol.OnAfterRegisterEx = adjustPublicStationFirst commonSecureProtocol.CreateReportDBRecord = CreateReportDBRecord natTraversalProtocol := nattraversal.NewProtocol()