GameStats: Support public data for getpd and setpd
Some checks are pending
Build CI / build (push) Waiting to run

This commit is contained in:
Palapeli 2025-03-03 09:34:21 -05:00
parent 788bf7ff0b
commit f8339dbf87
No known key found for this signature in database
GPG Key ID: 1FFE8F556A474925
6 changed files with 244 additions and 15 deletions

View File

@ -3,6 +3,7 @@ package common
import (
"errors"
"fmt"
"strconv"
"strings"
)
@ -14,9 +15,10 @@ type GameSpyCommand struct {
var (
ErrInvalidGameSpyCommand = errors.New("invalid GameSpy command received")
ErrNoGameStatsDataLength = errors.New("no data length found in GameStats message")
)
func ParseGameSpyMessage(msg string) ([]GameSpyCommand, error) {
func parseGameSpyMessage(msg string, gameStats bool) ([]GameSpyCommand, error) {
if !strings.Contains(msg, `\final\`) {
return nil, ErrInvalidGameSpyCommand
}
@ -43,7 +45,26 @@ func ParseGameSpyMessage(msg string) ([]GameSpyCommand, error) {
break
}
if strings.Contains(msg, `\`) {
if gameStats && key == "data" {
if g.OtherValues["length"] == "" {
return nil, ErrNoGameStatsDataLength
}
dataLength, err := strconv.Atoi(g.OtherValues["length"])
if err != nil {
return nil, err
}
if len(msg) < dataLength+1 {
return nil, ErrInvalidGameSpyCommand
}
value = msg[:dataLength]
msg = msg[dataLength:]
if msg[0] == '\\' {
msg = msg[1:]
}
} else if strings.Contains(msg, `\`) {
if msg[0] != '\\' {
valueEnd := strings.Index(msg[1:], `\`)
value = msg[:valueEnd+1]
@ -70,12 +91,27 @@ func ParseGameSpyMessage(msg string) ([]GameSpyCommand, error) {
return commands, nil
}
func ParseGameSpyMessage(msg string) ([]GameSpyCommand, error) {
return parseGameSpyMessage(msg, false)
}
func ParseGameStatsMessage(msg string) ([]GameSpyCommand, error) {
return parseGameSpyMessage(msg, true)
}
func CreateGameSpyMessage(command GameSpyCommand) string {
query := ""
endQuery := ""
for k, v := range command.OtherValues {
query += fmt.Sprintf(`\%s\%s`, k, v)
if command.Command == "getpdr" && k == "data" {
endQuery += fmt.Sprintf(`\%s\%s`, k, v)
} else {
query += fmt.Sprintf(`\%s\%s`, k, v)
}
}
query += endQuery
if command.Command != "" {
query = fmt.Sprintf(`\%s\%s%s`, command.Command, command.CommandValue, query)
}

29
database/gamestats.go Normal file
View File

@ -0,0 +1,29 @@
package database
import (
"context"
"time"
"github.com/jackc/pgx/v4/pgxpool"
)
const (
queryGsGetPublicData = `SELECT modified_time, pdata FROM gamestats_public_data WHERE profile_id = $1 AND dindex = $2 AND ptype = $3`
queryGsInsertPublicData = `INSERT INTO gamestats_public_data (profile_id, dindex, ptype, pdata, modified_time) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) RETURNING modified_time`
queryGsUpdatePublicData = `UPDATE gamestats_public_data SET pdata = $4, modified_time = CURRENT_TIMESTAMP WHERE profile_id = $1 AND dindex = $2 AND ptype = $3 RETURNING modified_time`
)
func GetGameStatsPublicData(pool *pgxpool.Pool, ctx context.Context, profileId uint32, dindex string, ptype string) (modifiedTime time.Time, publicData string, err error) {
err = pool.QueryRow(ctx, queryGsGetPublicData, profileId, dindex, ptype).Scan(&modifiedTime, &publicData)
return
}
func CreateGameStatsPublicData(pool *pgxpool.Pool, ctx context.Context, profileId uint32, dindex string, ptype string, publicData string) (modifiedTime time.Time, err error) {
err = pool.QueryRow(ctx, queryGsInsertPublicData, profileId, dindex, ptype, publicData).Scan(&modifiedTime)
return
}
func UpdateGameStatsPublicData(pool *pgxpool.Pool, ctx context.Context, profileId uint32, dindex string, ptype string, publicData string) (modifiedTime time.Time, err error) {
err = pool.QueryRow(ctx, queryGsUpdatePublicData, profileId, dindex, ptype, publicData).Scan(&modifiedTime)
return
}

View File

@ -2,13 +2,70 @@ package gamestats
import (
"strconv"
"time"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
"github.com/jackc/pgx/v4"
"github.com/logrusorgru/aurora/v3"
)
func (g *GameStatsSession) getpd(command common.GameSpyCommand) {
// Temporary empty data, it's an embedded gamespy \key\value message excluding \final\
data := `\\`
errMsg := common.GameSpyCommand{
Command: "getpdr",
CommandValue: "0",
OtherValues: map[string]string{
"pid": command.OtherValues["pid"],
"lid": strconv.Itoa(g.LoginID),
},
}
profileIdStr, ok := command.OtherValues["pid"]
if !ok {
logging.Error(g.ModuleName, "Missing pid")
logging.Error(g.ModuleName, "Full command:", command)
g.Write(errMsg)
return
}
profileId, err := strconv.ParseUint(profileIdStr, 10, 32)
if err != nil {
logging.Error(g.ModuleName, "Invalid pid:", aurora.Cyan(profileIdStr))
logging.Error(g.ModuleName, "Full command:", command)
g.Write(errMsg)
return
}
dindex, ok := command.OtherValues["dindex"]
if !ok {
logging.Error(g.ModuleName, "Missing dindex")
logging.Error(g.ModuleName, "Full command:", command)
g.Write(errMsg)
return
}
ptype, ok := command.OtherValues["ptype"]
if !ok {
logging.Error(g.ModuleName, "Missing ptype")
logging.Error(g.ModuleName, "Full command:", command)
g.Write(errMsg)
return
}
logging.Info(g.ModuleName, "Get public data: PID:", aurora.Cyan(profileId), "Index:", aurora.Cyan(dindex), "Type:", aurora.Cyan(ptype))
modifiedTime, data, err := database.GetGameStatsPublicData(pool, ctx, uint32(profileId), dindex, ptype)
if err != nil {
if err != pgx.ErrNoRows {
logging.Error(g.ModuleName, "GetGameStatsPublicData returned", err)
g.Write(errMsg)
return
}
logging.Warn(g.ModuleName, "No data found")
g.Write(errMsg)
return
}
g.Write(common.GameSpyCommand{
Command: "getpdr",
@ -16,9 +73,9 @@ func (g *GameStatsSession) getpd(command common.GameSpyCommand) {
OtherValues: map[string]string{
"lid": strconv.Itoa(g.LoginID),
"pid": command.OtherValues["pid"],
"mod": strconv.Itoa(int(time.Now().Unix())),
"length": strconv.Itoa(len(data) + 1),
"data": `\` + data + `\`,
"mod": strconv.Itoa(int(modifiedTime.Unix())),
"length": strconv.Itoa(len(data)),
"data": data,
},
})
}

View File

@ -222,7 +222,7 @@ func HandlePacket(index uint64, data []byte) {
message := decrypted.String()
session.ReadBuffer = []byte{}
commands, err := common.ParseGameSpyMessage(message)
commands, err := common.ParseGameStatsMessage(message)
if err != nil {
logging.Error(session.ModuleName, "Error parsing message:", err.Error())
logging.Error(session.ModuleName, "Raw data:", message)

View File

@ -2,20 +2,113 @@ package gamestats
import (
"strconv"
"strings"
"time"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
"github.com/jackc/pgx/v4"
"github.com/logrusorgru/aurora/v3"
)
func (g *GameStatsSession) setpd(command common.GameSpyCommand) {
// Example (with formatting):
// \setpd\
// \pid\1000000004
// \ptype\3
// \dindex\0
// \kv\1
// \lid\0
// \length\149
// \data\
// \itast_friend_p\AFAAYQBsAGEAcABlAGwAaQAAAAAAANzmAFAAYQBsAGEAcABlAGwAaQAAAABSJYgbuuQEbA5pAASOoAk9JpJsjKhAFEmQTQCKAIolBAAAAAAAAAAAAAAAAAAAAAAAAAAAGps*\x00
// \final\
errMsg := common.GameSpyCommand{
Command: "setpdr",
CommandValue: "0",
OtherValues: map[string]string{
"pid": command.OtherValues["pid"],
"lid": strconv.Itoa(g.LoginID),
},
}
if command.OtherValues["pid"] != strconv.FormatUint(uint64(g.User.ProfileId), 10) {
logging.Error(g.ModuleName, "Invalid profile ID:", aurora.Cyan(command.OtherValues["pid"]))
g.Write(errMsg)
return
}
dindex, ok := command.OtherValues["dindex"]
if !ok {
logging.Error(g.ModuleName, "Missing dindex")
logging.Error(g.ModuleName, "Full command:", command)
g.Write(errMsg)
return
}
ptype, ok := command.OtherValues["ptype"]
if !ok {
logging.Error(g.ModuleName, "Missing ptype")
logging.Error(g.ModuleName, "Full command:", command)
g.Write(errMsg)
return
}
newData, ok := command.OtherValues["data"]
if !ok {
logging.Error(g.ModuleName, "Missing data")
logging.Error(g.ModuleName, "Full command:", command)
g.Write(errMsg)
return
}
logging.Info(g.ModuleName, "Set public data: PID:", aurora.Cyan(g.User.ProfileId), "Index:", aurora.Cyan(dindex), "Type:", aurora.Cyan(ptype), "Data:", aurora.Cyan(newData))
// Trim extra null byte at the end
if len(newData) > 0 && newData[len(newData)-1] == 0 {
newData = newData[:len(newData)-1]
}
if strings.ContainsRune(newData, 0) {
logging.Error(g.ModuleName, "Data contains null byte")
g.Write(errMsg)
return
}
var modifiedTime time.Time
_, _, err := database.GetGameStatsPublicData(pool, ctx, g.User.ProfileId, dindex, ptype)
if err != nil {
if err != pgx.ErrNoRows {
logging.Error(g.ModuleName, "GetGameStatsPublicData returned", err)
g.Write(errMsg)
return
}
modifiedTime, err = database.CreateGameStatsPublicData(pool, ctx, g.User.ProfileId, dindex, ptype, newData)
if err != nil {
logging.Error(g.ModuleName, "GetGameStatsPublicData returned", err)
g.Write(errMsg)
return
}
} else {
modifiedTime, err = database.UpdateGameStatsPublicData(pool, ctx, g.User.ProfileId, dindex, ptype, newData)
if err != nil {
logging.Error(g.ModuleName, "UpdateGameStatsPublicData returned", err)
g.Write(errMsg)
return
}
}
// TODO: Is mod supposed to be the last modified time or new modified time?
g.Write(common.GameSpyCommand{
Command: "setpdr",
CommandValue: "1",
OtherValues: map[string]string{
"lid": strconv.Itoa(g.LoginID),
"pid": command.OtherValues["pid"],
"mod": strconv.Itoa(int(time.Now().Unix())),
"length": "0",
"data": `\\`,
"lid": strconv.Itoa(g.LoginID),
"pid": command.OtherValues["pid"],
"mod": strconv.Itoa(int(modifiedTime.Unix())),
},
})
}

View File

@ -86,6 +86,20 @@ ALTER TABLE ONLY public.mario_kart_wii_sake
ALTER TABLE public.mario_kart_wii_sake OWNER TO wiilink;
--
-- Name: gamestats_public_data; Type: TABLE; Schema: public; Owner: wiilink
--
CREATE TABLE IF NOT EXISTS public.gamestats_public_data (
profile_id bigint NOT NULL,
dindex character varying NOT NULL,
ptype character varying NOT NULL,
pdata character varying NOT NULL,
modified_time timestamp without time zone NOT NULL,
CONSTRAINT one_pdata_constraint UNIQUE (profile_id, dindex, ptype)
);
--
-- Name: users_profile_id_seq; Type: SEQUENCE; Schema: public; Owner: wiilink
--