├── .gitignore ├── LICENSE ├── README.md ├── authentication ├── npticket.go └── ticketverifier.go ├── database ├── database.go ├── housekeeping_tasks.go └── utils.go ├── go.mod ├── go.sum ├── models ├── accomplishments.go ├── band.go ├── character.go ├── config.go ├── gathering.go ├── machine.go ├── motdInfo.go ├── score.go ├── setlist.go └── user.go ├── protocols └── jsonproto │ ├── manager.go │ ├── marshaler │ ├── request.go │ ├── response.go │ └── utils.go │ └── services │ ├── accomplishment │ └── record.go │ ├── accountlink │ └── linkstatus.go │ ├── battles │ ├── create.go │ ├── getclosed.go │ └── limitcheck.go │ ├── config │ └── config.go │ ├── entities │ ├── band │ │ └── update.go │ ├── character │ │ ├── namecheck.go │ │ └── update.go │ └── linkcode.go │ ├── leaderboards │ ├── acc_maxrank.go │ ├── acc_player_get.go │ ├── acc_rankrange.go │ ├── battle_maxrank.go │ ├── battle_player_get.go │ ├── battle_rankrange.go │ ├── friends_update.go │ ├── maxrank.go │ ├── player.go │ ├── playerranks.go │ └── rankrange.go │ ├── misc │ ├── get_accounts_setlist_creation_status.go │ ├── option_data.go │ └── sync_available_songs.go │ ├── music_library │ └── sort_and_filters.go │ ├── performance │ └── record.go │ ├── scores │ ├── battles_record.go │ └── record.go │ ├── setlists │ ├── sync.go │ └── update.go │ ├── songlists │ └── get.go │ ├── stats │ └── pad_user.go │ └── ticker │ └── info.go ├── quazal └── exceptions.go ├── restapi └── restapi.go ├── serialization └── gathering │ ├── gathering_deserializer.go │ └── harmonixgathering.go ├── server.go ├── servers ├── customfind.go ├── deleteaccount.go ├── findbynamelike.go ├── findbysingleid.go ├── getbinarydata.go ├── getconsoleusernames.go ├── getmessageheaders.go ├── getstatus.go ├── jsonrequest.go ├── launchsession.go ├── login.go ├── lookuporcreateaccount.go ├── nexservers.go ├── participate.go ├── registerex.go ├── registergathering.go ├── requestprobeinitiation.go ├── requestticket.go ├── requesturls.go ├── savebinarydata.go ├── setstate.go ├── setstatus.go ├── terminategathering.go ├── unparticipate.go ├── updategathering.go └── validation.go └── utils └── clientInfo.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.bat 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | .vscode/* 18 | 19 | *.log 20 | *.env 21 | 22 | binary_data/* 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoCentral 2 | A Rock Band 3 master server re-implementation written in Golang using MongoDB as the database layer and my Quazal Rendez-vous-compatible forks of [nex-go](https://github.com/ihatecompvir/nex-go)/[nex-protocols-go](https://github.com/ihatecompvir/nex-protocols-go) as the underlying server layer. 3 | 4 | Note that this only aims to replicate what the game calls "Rock Central", support for the Music Store is _not_ here and will never be added. Just buy the songs through the Xbox Live Marketplace or PlayStation Store instead. 5 | 6 | ## Platform Compatibility 7 | - PS3 (real hardware and RPCS3) 8 | - Wii (real hardware and Dolphin) 9 | - Xbox 360 (real hardware, requires RB3Enhanced) 10 | 11 | ## Setup and Usage 12 | ### Connecting on PS3 (Real Hardware) 13 | 1. Set your console's DNS settings to primary 45.33.44.103, secondary 1.1.1.1. 14 | ### Connecting on PS3 (RPCS3) 15 | 1. Ensure you have RPCN set up in RPCS3 and an account on RPCN. 16 | 2. In Settings->Network, make sure status is "Connected" and PSN status is "RPCN". 17 | 3. In "IP/Hosts switches", add `rb3ps3live.hmxservices.com=45.33.44.103` 18 | ### Connecting on Wii (RB3Enhanced) 19 | 1. Make sure RB3Enhanced 0.7 or later is installed. https://rb3e.rbenhanced.rocks/ 20 | 2. GoCentral is enabled by default! If you need to enable it yourself: 21 | * Open rb3.ini, change GoCentralAddress to `gocentral-wii.rbenhanced.rocks` 22 | ### Connecting on Wii (Gecko/Ocarina Code) - Dolphin too! 23 | 1. Download the code from https://rb3e.rbenhanced.rocks/gocentral_gecko.txt 24 | 2. Copy this code to wherever you store Gecko/Ocarina codes on your SD card. This is often in txtcodes/SZBx69.txt, where x is your region (P for Europe, E for America). 25 | 26 | (If on Dolphin, right click the game's properties and enter the code into the Gecko Codes tab.) 27 | ### Connecting on Wii (USB Loader GX) 28 | 1. Set your console's DNS settings to primary 45.33.44.103, secondary 1.1.1.1. 29 | 2. In your loader settings for Rock Band 3, enable the "NoSSL only" option for custom servers. 30 | If you are using another loader, check with your loader on enabling a NoSSL patch. Will add more instructions later on. 31 | ### Connecting on Xbox 360 (RB3Enhanced) 32 | 1. Make sure RB3Enhanced 0.7 or later is installed. https://rb3e.rbenhanced.rocks/ 33 | 2. GoCentral is enabled by default! If you need to enable it yourself: 34 | * Open rb3.ini, change GoCentralAddress to `gocentral-xbox.rbenhanced.rocks` 35 | 36 | For the most reliable experience, port forward port 9103 (UDP) to your console in your router's settings, or if on RPCS3, enable UPnP. 37 | 38 | (Do note that by changing DNS settings, you may be unable to play other games or use other services. Some ISPs may block custom DNS servers.) 39 | 40 | ## Features Implemented 41 | - Message of the Day 42 | - Online Matchmaking 43 | - Leaderboards 44 | - Entity storage (characters, bands) 45 | - Linked account spoofing to unlock the "Link Your Account to Rockband.com" goal/achievement 46 | - Battle of the Bands 47 | - Setlist Challenges 48 | - Setlist Sharing 49 | - Global rank calculation 50 | - Instaranks ("You are ranked #4 on the Guitar Leaderboard" on the post-song stats screen) 51 | 52 | ## Features Coming In the Future 53 | - [Crossplay between PS3 and Wii](https://www.youtube.com/watch?v=KW5NrjDsv00) (requires RB3Enhanced) 54 | 55 | ## Special Thanks 56 | The following users made contributions to GoCentral, but aren't listed in the Contributors tab on GitHub, so they are listed here instead. 57 | - [@knvtva](https://github.com/knvtva) 58 | - [@li1lypad](https://github.com/li1lypad) 59 | -------------------------------------------------------------------------------- /authentication/npticket.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | type NPTicket struct { 9 | 10 | // this is little endian 11 | Size1 uint32 // the size of the NPTicket 12 | Size2 uint32 // the size of the NPTicket again? (always seems to be the same as above) 13 | 14 | // everything after is big endian 15 | Version uint8 16 | Unknown uint8 17 | Unknown2 uint8 18 | Unknown3 uint8 19 | TicketSize uint32 20 | BodyType uint16 // type as in datatype 21 | BodySize uint16 22 | Body []byte 23 | FooterType uint16 24 | FooterSize uint16 25 | Footer NPTicketFooter 26 | } 27 | 28 | type NPTicketFooter struct { 29 | // everything is big endian 30 | CipherIDType uint16 // type as in datatype 31 | CipherIDSize uint16 32 | CipherID uint32 33 | SignatureType uint16 34 | SignatureSize uint16 35 | Signature []byte // asn.1 encoded 36 | } 37 | 38 | func (t *NPTicket) Bytes() []byte { 39 | size1Bytes := make([]byte, 4) 40 | binary.LittleEndian.PutUint32(size1Bytes, t.Size1) 41 | size2Bytes := make([]byte, 4) 42 | binary.LittleEndian.PutUint32(size2Bytes, t.Size2) 43 | ticketSizeBytes := make([]byte, 4) 44 | binary.BigEndian.PutUint32(ticketSizeBytes, t.TicketSize) 45 | bodyTypeBytes := make([]byte, 2) 46 | binary.BigEndian.PutUint16(bodyTypeBytes, t.BodyType) 47 | bodySizeBytes := make([]byte, 2) 48 | binary.BigEndian.PutUint16(bodySizeBytes, t.BodySize) 49 | footerTypeBytes := make([]byte, 2) 50 | binary.BigEndian.PutUint16(footerTypeBytes, t.FooterType) 51 | footerSizeBytes := make([]byte, 2) 52 | binary.BigEndian.PutUint16(footerSizeBytes, t.FooterSize) 53 | 54 | buf := bytes.Buffer{} 55 | buf.Write(size1Bytes) 56 | buf.Write(size2Bytes) 57 | buf.WriteByte(t.Version) 58 | buf.WriteByte(t.Unknown) 59 | buf.WriteByte(t.Unknown2) 60 | buf.WriteByte(t.Unknown3) 61 | buf.Write(ticketSizeBytes) 62 | buf.Write(bodyTypeBytes) 63 | buf.Write(bodySizeBytes) 64 | buf.Write(t.Body) 65 | buf.Write(footerTypeBytes) 66 | buf.Write(footerSizeBytes) 67 | footerBytes := t.Footer.Bytes() 68 | buf.Write(footerBytes) 69 | 70 | return buf.Bytes() 71 | } 72 | 73 | func (f *NPTicketFooter) Bytes() []byte { 74 | cipherIDTypeBytes := make([]byte, 2) 75 | binary.BigEndian.PutUint16(cipherIDTypeBytes, f.CipherIDType) 76 | cipherIDSizeBytes := make([]byte, 2) 77 | binary.BigEndian.PutUint16(cipherIDSizeBytes, f.CipherIDSize) 78 | cipherIDBytes := make([]byte, 4) 79 | binary.BigEndian.PutUint32(cipherIDBytes, f.CipherID) 80 | signatureTypeBytes := make([]byte, 2) 81 | binary.BigEndian.PutUint16(signatureTypeBytes, f.SignatureType) 82 | signatureSizeBytes := make([]byte, 2) 83 | binary.BigEndian.PutUint16(signatureSizeBytes, f.SignatureSize) 84 | 85 | buf := bytes.Buffer{} 86 | buf.Write(cipherIDTypeBytes) 87 | buf.Write(cipherIDSizeBytes) 88 | buf.Write(cipherIDBytes) 89 | buf.Write(signatureTypeBytes) 90 | buf.Write(signatureSizeBytes) 91 | buf.Write(f.Signature) 92 | 93 | return buf.Bytes() 94 | } 95 | 96 | type NPTicketDeserializer struct{} 97 | 98 | func (d *NPTicketDeserializer) Deserialize(data []byte) (NPTicket, error) { 99 | ticket := &NPTicket{} 100 | ticket.Size1 = binary.LittleEndian.Uint32(data[:4]) 101 | ticket.Size2 = binary.LittleEndian.Uint32(data[4:8]) 102 | ticket.Version = uint8(data[8]) 103 | ticket.Unknown = uint8(data[9]) 104 | ticket.Unknown2 = uint8(data[10]) 105 | ticket.Unknown3 = uint8(data[11]) 106 | ticket.TicketSize = binary.BigEndian.Uint32(data[12:16]) 107 | ticket.BodyType = binary.BigEndian.Uint16(data[16:18]) 108 | ticket.BodySize = binary.BigEndian.Uint16(data[18:20]) 109 | ticket.Body = data[20 : 20+ticket.BodySize] 110 | footerStart := 20 + ticket.BodySize 111 | ticket.FooterType = binary.BigEndian.Uint16(data[footerStart : footerStart+2]) 112 | ticket.FooterSize = binary.BigEndian.Uint16(data[footerStart+2 : footerStart+4]) 113 | footerData := data[footerStart+4 : footerStart+4+ticket.FooterSize] 114 | footer := &NPTicketFooter{} 115 | footer.CipherIDType = binary.BigEndian.Uint16(footerData[:2]) 116 | footer.CipherIDSize = binary.BigEndian.Uint16(footerData[2:4]) 117 | footer.CipherID = binary.BigEndian.Uint32(footerData[4:8]) 118 | footer.SignatureType = binary.BigEndian.Uint16(footerData[8:10]) 119 | footer.SignatureSize = binary.BigEndian.Uint16(footerData[10:12]) 120 | footer.Signature = footerData[12 : 12+footer.SignatureSize] 121 | ticket.Footer = *footer 122 | return *ticket, nil 123 | } 124 | -------------------------------------------------------------------------------- /authentication/ticketverifier.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | type TicketVerifier struct { 12 | TicketVerifierEndpoint string 13 | } 14 | 15 | func (verifier *TicketVerifier) VerifyTicket(ticketData []byte, consoleType int) bool { 16 | encodedTicket := base64.StdEncoding.EncodeToString(ticketData) 17 | urlEncodedTicket := url.QueryEscape(encodedTicket) 18 | 19 | verifyURL := fmt.Sprintf("%s?ticket=%s&platform=%d", verifier.TicketVerifierEndpoint, urlEncodedTicket, consoleType) 20 | resp, err := http.Get(verifyURL) 21 | if err != nil { 22 | log.Println("Failed to verify ticket:", err) 23 | return false 24 | } 25 | defer resp.Body.Close() 26 | 27 | if resp.StatusCode == http.StatusOK { 28 | return true 29 | } else { 30 | return false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "go.mongodb.org/mongo-driver/mongo" 4 | 5 | // mongoDB singleton 6 | var GocentralDatabase *mongo.Database 7 | -------------------------------------------------------------------------------- /database/housekeeping_tasks.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "time" 8 | 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | ) 12 | 13 | func CleanupDuplicateScores() { 14 | scoresCollection := GocentralDatabase.Collection("scores") 15 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 16 | defer cancel() 17 | 18 | pipeline := mongo.Pipeline{ 19 | {{"$group", bson.D{ 20 | {"_id", bson.D{ 21 | {"pid", "$pid"}, 22 | {"role_id", "$role_id"}, 23 | {"song_id", "$song_id"}, 24 | {"boi", "$boi"}, 25 | {"diff_id", "$diff_id"}, 26 | {"instrument_mask", "$instrument_mask"}, 27 | {"notespct", "$notespct"}, 28 | {"score", "$score"}, 29 | {"stars", "$stars"}, 30 | }}, 31 | {"count", bson.D{{"$sum", 1}}}, 32 | {"docs", bson.D{{"$push", "$$ROOT"}}}, 33 | }}}, 34 | {{"$match", bson.D{{"count", bson.D{{"$gt", 1}}}}}}, 35 | } 36 | 37 | cursor, err := scoresCollection.Aggregate(ctx, pipeline) 38 | if err != nil { 39 | log.Println("Could not aggregate duplicate scores: ", err) 40 | return 41 | } 42 | defer cursor.Close(ctx) 43 | 44 | var results []bson.M 45 | if err = cursor.All(ctx, &results); err != nil { 46 | log.Println("Could not decode aggregation results: ", err) 47 | return 48 | } 49 | 50 | deletedCount := 0 51 | 52 | for _, result := range results { 53 | docs := result["docs"].(bson.A) 54 | for i := 1; i < len(docs); i++ { // skip the first document to keep one 55 | doc := docs[i].(bson.M) 56 | _, err := scoresCollection.DeleteOne(ctx, bson.M{"_id": doc["_id"]}) 57 | if err != nil { 58 | log.Println("Could not delete duplicate score: ", err) 59 | } else { 60 | deletedCount++ 61 | } 62 | } 63 | } 64 | 65 | if deletedCount != 0 { 66 | log.Printf("Deleted %d duplicate scores.\n", deletedCount) 67 | } 68 | } 69 | 70 | func PruneOldSessions() { 71 | 72 | gatherings := GocentralDatabase.Collection("gatherings") 73 | 74 | // find any gatherings which haven't had their "updated" field updated in the last hour and delete them 75 | // technically speaking, someone playing a song longer than one hour could have their gathering deleted, but this is such an extreme and unlikely edge case that it's not worth worrying about 76 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 77 | defer cancel() 78 | 79 | // Calculate the Unix time for 1 hour ago 80 | cutoff := time.Now().Add(-1 * time.Hour).Unix() 81 | 82 | _, err := gatherings.DeleteMany(ctx, bson.M{"last_updated": bson.M{"$lt": cutoff}}) 83 | if err != nil { 84 | log.Println("Could not delete old gatherings: ", err) 85 | } 86 | } 87 | 88 | func CleanupInvalidScores() { 89 | scoresCollection := GocentralDatabase.Collection("scores") 90 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 91 | defer cancel() 92 | 93 | deletedCount := 0 94 | 95 | // safe deletion function 96 | deleteInvalidScores := func(filter bson.M) { 97 | result, err := scoresCollection.DeleteMany(ctx, filter) 98 | if err != nil { 99 | log.Println("Could not delete invalid scores: ", err) 100 | return 101 | } 102 | if result != nil { 103 | deletedCount += int(result.DeletedCount) 104 | } 105 | } 106 | 107 | // Delete scores based on various conditions 108 | deleteInvalidScores(bson.M{"song_id": 0}) // Invalid song ID 109 | deleteInvalidScores(bson.M{"role_id": bson.M{"$gt": 10}}) // Role ID greater than 10 110 | deleteInvalidScores(bson.M{"score": bson.M{"$lte": 0}}) // Score less than or equal to 0 111 | deleteInvalidScores(bson.M{"stars": bson.M{"$gt": 6}}) // Stars greater than 6 112 | deleteInvalidScores(bson.M{"diff_id": bson.M{"$gt": 4}}) // Difficulty ID greater than 4 113 | deleteInvalidScores(bson.M{"notespct": bson.M{"$gt": 100}}) // Percentage greater than 100 114 | 115 | if deletedCount != 0 { 116 | log.Printf("Deleted %d invalid scores.\n", deletedCount) 117 | } 118 | } 119 | 120 | func DeleteExpiredBattles() { 121 | setlistsCollection := GocentralDatabase.Collection("setlists") 122 | 123 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 124 | defer cancel() 125 | 126 | cursor, err := setlistsCollection.Find(ctx, bson.M{"type": bson.M{"$in": []int{1000, 1001, 1002}}}) 127 | if err != nil { 128 | log.Println("Could not get setlists for deletion: ", err) 129 | return 130 | } 131 | defer cursor.Close(ctx) 132 | 133 | deletedCount := 0 134 | 135 | for cursor.Next(ctx) { 136 | var setlist models.Setlist 137 | cursor.Decode(&setlist) 138 | 139 | isExpired, expiryTime := GetBattleExpiryInfo(setlist.SetlistID) 140 | 141 | if isExpired { 142 | // allow players 3 days to view the leaderboards of the setlist before it is nuked 143 | // the game itself should prevent recording scores at this time, but we should add a check for this in the battle score record too 144 | expiredTime := expiryTime.Add(3 * 24 * time.Hour) 145 | 146 | if time.Now().After(expiredTime) { 147 | _, err := setlistsCollection.DeleteOne(ctx, bson.M{"setlist_id": setlist.SetlistID}) 148 | if err != nil { 149 | log.Println("Could not delete expired battle: ", err) 150 | } else { 151 | deletedCount++ 152 | } 153 | 154 | // delete all scores associated with this setlist 155 | scoresCollection := GocentralDatabase.Collection("scores") 156 | _, err = scoresCollection.DeleteMany(ctx, bson.M{"setlist_id": setlist.SetlistID}) 157 | 158 | if err != nil { 159 | log.Println("Could not delete scores associated with expired battle: ", err) 160 | } 161 | } 162 | } 163 | } 164 | 165 | if deletedCount != 0 { 166 | log.Printf("Deleted %d expired battles.\n", deletedCount) 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module rb3server 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.3 6 | 7 | require ( 8 | github.com/ihatecompvir/nex-go v0.0.0-20250131035452-0e0eff533457 9 | github.com/ihatecompvir/nex-protocols-go v0.0.0-20250512202801-4e47526c2ea3 10 | github.com/jinzhu/copier v0.4.0 11 | go.mongodb.org/mongo-driver v1.16.0 12 | ) 13 | 14 | require ( 15 | github.com/creack/pty v1.1.9 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/go-chi/chi/v5 v5.1.0 // indirect 18 | github.com/golang/snappy v0.0.4 // indirect 19 | github.com/google/go-cmp v0.6.0 // indirect 20 | github.com/joho/godotenv v1.5.1 // indirect 21 | github.com/klauspost/compress v1.17.9 // indirect 22 | github.com/kr/pretty v0.3.1 // indirect 23 | github.com/kr/pty v1.1.1 // indirect 24 | github.com/kr/text v0.2.0 // indirect 25 | github.com/montanaflynn/stats v0.7.1 // indirect 26 | github.com/natefinch/lumberjack v2.0.0+incompatible // indirect 27 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect 28 | github.com/pkg/errors v0.9.1 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | github.com/rogpeppe/go-internal v1.9.0 // indirect 31 | github.com/stretchr/objx v0.5.0 // indirect 32 | github.com/stretchr/testify v1.8.2 // indirect 33 | github.com/superwhiskers/crunch/v3 v3.5.7 // indirect 34 | github.com/tidwall/pretty v1.0.0 // indirect 35 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 36 | github.com/xdg-go/scram v1.1.2 // indirect 37 | github.com/xdg-go/stringprep v1.0.4 // indirect 38 | github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect 39 | github.com/yuin/goldmark v1.4.13 // indirect 40 | golang.org/x/crypto v0.25.0 // indirect 41 | golang.org/x/mod v0.17.0 // indirect 42 | golang.org/x/net v0.25.0 // indirect 43 | golang.org/x/sync v0.7.0 // indirect 44 | golang.org/x/sys v0.22.0 // indirect 45 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 // indirect 46 | golang.org/x/term v0.22.0 // indirect 47 | golang.org/x/text v0.16.0 // indirect 48 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 49 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect 50 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /models/accomplishments.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type AccomplishmentScoreEntry struct { 4 | PID int `json:"pid" bson:"pid"` 5 | Score int `json:"score" bson:"score"` 6 | } 7 | 8 | type Accomplishments struct { 9 | LBGoalValueCampaignMetascore []AccomplishmentScoreEntry `json:"lb_goal_value_campaign_metascore" bson:"lb_goal_value_campaign_metascore"` 10 | LBGoalValueAccTourgoldlocal1 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldlocal1" bson:"lb_goal_value_acc_tourgoldlocal1"` 11 | LBGoalValueAccTourgoldlocal2 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldlocal2" bson:"lb_goal_value_acc_tourgoldlocal2"` 12 | LBGoalValueAccTourgoldregional1 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldregional1" bson:"lb_goal_value_acc_tourgoldregional1"` 13 | LBGoalValueAccTourgoldregional2 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldregional2" bson:"lb_goal_value_acc_tourgoldregional2"` 14 | LBGoalValueAccTourgoldcontinental1 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldcontinental1" bson:"lb_goal_value_acc_tourgoldcontinental1"` 15 | LBGoalValueAccTourgoldcontinental2 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldcontinental2" bson:"lb_goal_value_acc_tourgoldcontinental2"` 16 | LBGoalValueAccTourgoldcontinental3 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldcontinental3" bson:"lb_goal_value_acc_tourgoldcontinental3"` 17 | LBGoalValueAccTourgoldglobal1 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldglobal1" bson:"lb_goal_value_acc_tourgoldglobal1"` 18 | LBGoalValueAccTourgoldglobal2 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldglobal2" bson:"lb_goal_value_acc_tourgoldglobal2"` 19 | LBGoalValueAccTourgoldglobal3 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tourgoldglobal3" bson:"lb_goal_value_acc_tourgoldglobal3"` 20 | LBGoalValueAccOverdrivemaintain3 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_overdrivemaintain3" bson:"lb_goal_value_acc_overdrivemaintain3"` 21 | LBGoalValueAccOverdrivecareer []AccomplishmentScoreEntry `json:"lb_goal_value_acc_overdrivecareer" bson:"lb_goal_value_acc_overdrivecareer"` 22 | LBGoalValueAccCareersaves []AccomplishmentScoreEntry `json:"lb_goal_value_acc_careersaves" bson:"lb_goal_value_acc_careersaves"` 23 | LBGoalValueAccMillionpoints []AccomplishmentScoreEntry `json:"lb_goal_value_acc_millionpoints" bson:"lb_goal_value_acc_millionpoints"` 24 | LBGoalValueAccBassstreaklarge []AccomplishmentScoreEntry `json:"lb_goal_value_acc_bassstreaklarge" bson:"lb_goal_value_acc_bassstreaklarge"` 25 | LBGoalValueAccHopothreehundredbass []AccomplishmentScoreEntry `json:"lb_goal_value_acc_hopothreehundredbass" bson:"lb_goal_value_acc_hopothreehundredbass"` 26 | LBGoalValueAccDrumfill170 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_drumfill170" bson:"lb_goal_value_acc_drumfill170"` 27 | LBGoalValueAccDrumstreaklong []AccomplishmentScoreEntry `json:"lb_goal_value_acc_drumstreaklong" bson:"lb_goal_value_acc_drumstreaklong"` 28 | LBGoalValueAccDeployguitarfour []AccomplishmentScoreEntry `json:"lb_goal_value_acc_deployguitarfour" bson:"lb_goal_value_acc_deployguitarfour"` 29 | LBGoalValueAccGuitarstreaklarge []AccomplishmentScoreEntry `json:"lb_goal_value_acc_guitarstreaklarge" bson:"lb_goal_value_acc_guitarstreaklarge"` 30 | LBGoalValueAccHopoonethousand []AccomplishmentScoreEntry `json:"lb_goal_value_acc_hopoonethousand" bson:"lb_goal_value_acc_hopoonethousand"` 31 | LBGoalValueAccDoubleawesomealot []AccomplishmentScoreEntry `json:"lb_goal_value_acc_doubleawesomealot" bson:"lb_goal_value_acc_doubleawesomealot"` 32 | LBGoalValueAccTripleawesomealot []AccomplishmentScoreEntry `json:"lb_goal_value_acc_tripleawesomealot" bson:"lb_goal_value_acc_tripleawesomealot"` 33 | LBGoalValueAccKeystreaklong []AccomplishmentScoreEntry `json:"lb_goal_value_acc_keystreaklong" bson:"lb_goal_value_acc_keystreaklong"` 34 | LBGoalValueAccProbassstreakepic []AccomplishmentScoreEntry `json:"lb_goal_value_acc_probassstreakepic" bson:"lb_goal_value_acc_probassstreakepic"` 35 | LBGoalValueAccProdrumroll3 []AccomplishmentScoreEntry `json:"lb_goal_value_acc_prodrumroll3" bson:"lb_goal_value_acc_prodrumroll3"` 36 | LBGoalValueAccProdrumstreaklong []AccomplishmentScoreEntry `json:"lb_goal_value_acc_prodrumstreaklong" bson:"lb_goal_value_acc_prodrumstreaklong"` 37 | LBGoalValueAccProguitarstreakepic []AccomplishmentScoreEntry `json:"lb_goal_value_acc_proguitarstreakepic" bson:"lb_goal_value_acc_proguitarstreakepic"` 38 | LBGoalValueAccProkeystreaklong []AccomplishmentScoreEntry `json:"lb_goal_value_acc_prokeystreaklong" bson:"lb_goal_value_acc_prokeystreaklong"` 39 | LBGoalValueAccDeployvocals []AccomplishmentScoreEntry `json:"lb_goal_value_acc_deployvocals" bson:"lb_goal_value_acc_deployvocals"` 40 | LBGoalValueAccDeployvocalsonehundred []AccomplishmentScoreEntry `json:"lb_goal_value_acc_deployvocalsonehundred" bson:"lb_goal_value_acc_deployvocalsonehundred"` 41 | } 42 | -------------------------------------------------------------------------------- /models/band.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "go.mongodb.org/mongo-driver/bson/primitive" 4 | 5 | type Band struct { 6 | ID primitive.ObjectID `json:"_id" bson:"_id"` 7 | Art []byte `json:"art" bson:"art"` 8 | Name string `json:"name" bson:"name"` 9 | OwnerPID int `json:"owner_pid" bson:"owner_pid"` 10 | BandID int `json:"band_id" bson:"band_id"` 11 | ConsoleType int `json:"console_type" bson:"console_type"` 12 | } 13 | -------------------------------------------------------------------------------- /models/character.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "go.mongodb.org/mongo-driver/bson/primitive" 4 | 5 | type Character struct { 6 | ID primitive.ObjectID `json:"_id" bson:"_id"` 7 | CharData []byte `json:"char_data" bson:"char_data"` 8 | Name string `json:"name" bson:"name"` 9 | OwnerPID int `json:"owner_pid" bson:"owner_pid"` 10 | GUID string `json:"guid" bson:"guid"` 11 | CharacterID int `json:"character_id" bson:"character_id"` 12 | } 13 | -------------------------------------------------------------------------------- /models/config.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "go.mongodb.org/mongo-driver/bson/primitive" 4 | 5 | type Config struct { 6 | ID primitive.ObjectID `json:"_id" bson:"_id"` 7 | LastPID int `json:"last_pid" bson:"last_pid"` 8 | LastBandID int `json:"last_band_id" bson:"last_band_id"` 9 | LastCharacterID int `json:"last_character_id" bson:"last_character_id"` 10 | LastSetlistID int `json:"last_setlist_id" bson:"last_setlist_id"` 11 | ProfanityList []string `json:"profanity_list" bson:"profanity_list"` 12 | BattleLimit int `json:"battle_limit" bson:"battle_limit"` 13 | LastMachineID int `json:"last_machine_id" bson:"last_machine_id"` 14 | } 15 | -------------------------------------------------------------------------------- /models/gathering.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "go.mongodb.org/mongo-driver/bson/primitive" 4 | 5 | /// This is different from the model inside of the serialization folder and refers to the MongoDB representation of a gathering 6 | type Gathering struct { 7 | ID primitive.ObjectID `json:"_id" bson:"_id"` 8 | GatheringID int `json:"gathering_id" bson:"gathering_id"` 9 | Creator string `json:"creator" bson:"creator"` 10 | Contents []byte `json:"contents" bson:"contents"` 11 | State uint32 `json:"state" bson:"state"` 12 | LastUpdated int64 `json:"last_updated" bson:"last_updated"` 13 | ConsoleType uint32 `json:"console_type" bson:"console_type"` 14 | Public uint32 `json:"public" bson:"public"` 15 | } 16 | -------------------------------------------------------------------------------- /models/machine.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Machine struct { 4 | ConsoleType int `json:"console_type" bson:"console_type"` 5 | MachineID int `json:"machine_id" bson:"machine_id"` 6 | Status string `json:"status" bson:"status"` 7 | WiiFriendCode string `json:"wii_friend_code" bson:"wii_friend_code"` 8 | StationURL string `json:"station_url" bson:"station_url"` 9 | IntStationURL string `json:"int_station_url" bson:"int_station_url"` 10 | } 11 | -------------------------------------------------------------------------------- /models/motdInfo.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "go.mongodb.org/mongo-driver/bson/primitive" 4 | 5 | type MOTDInfo struct { 6 | ID primitive.ObjectID `json:"_id" bson:"_id"` 7 | DTA string `json:"dta" bson:"dta"` 8 | } 9 | -------------------------------------------------------------------------------- /models/score.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Score struct { 4 | SongID int `bson:"song_id"` 5 | OwnerPID int `bson:"pid"` 6 | RoleID int `bson:"role_id"` 7 | Score int `bson:"score"` 8 | NotesPercent int `bson:"notespct"` 9 | Stars int `bson:"stars"` 10 | DiffID int `bson:"diff_id"` 11 | BOI int `bson:"boi"` 12 | InstrumentMask int `bson:"instrument_mask"` 13 | BattleID int `bson:"battle_id"` 14 | } 15 | -------------------------------------------------------------------------------- /models/setlist.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // represents a battle score entry in the database 4 | type BattleScoreEntry struct { 5 | PID int `bson:"pid"` 6 | Score int `bson:"score"` 7 | } 8 | 9 | // represents a Setlist in the database 10 | // this is used for both setlists and battles, battles are really just setlists with a type of 1000/1001/1002 11 | type Setlist struct { 12 | Created int64 `bson:"created"` // unix timestamp of when the setlist was created 13 | SetlistID int `bson:"setlist_id"` 14 | PID int `bson:"pid"` 15 | Title string `bson:"title"` 16 | Desc string `bson:"desc"` 17 | Type int `bson:"type"` 18 | Owner string `bson:"owner"` 19 | OwnerGUID string `bson:"owner_guid"` 20 | GUID string `bson:"guid"` 21 | ArtURL string `bson:"art_url"` 22 | Shared string `bson:"shared"` 23 | SongIDs []int `bson:"s_ids"` 24 | SongNames []string `bson:"s_names"` 25 | 26 | // battle fields 27 | TimeEndVal int `bson:"time_end_val"` 28 | TimeEndUnits string `bson:"time_end_units"` 29 | Flags int `bson:"flags"` 30 | Instrument int `bson:"instrument"` 31 | } 32 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "go.mongodb.org/mongo-driver/bson/primitive" 4 | 5 | type User struct { 6 | ID primitive.ObjectID `json:"_id" bson:"_id"` 7 | Username string `json:"username" bson:"username"` 8 | PID uint32 `json:"pid" bson:"pid"` 9 | StationURL string `json:"station_url" bson:"station_url"` 10 | IntStationURL string `json:"int_station_url" bson:"int_station_url"` 11 | ConsoleType int `json:"console_type" bson:"console_type"` 12 | GUID string `json:"guid" bson:"guid"` 13 | LinkCode string `json:"link_code" bson:"link_code"` 14 | Friends []int `json:"friends" bson:"friends"` 15 | Groups []string `json:"groups" bson:"groups"` 16 | 17 | // machine stuff 18 | CreatedByMachineID int `json:"created_by_machine_id" bson:"created_by_machine_id"` 19 | } 20 | -------------------------------------------------------------------------------- /protocols/jsonproto/manager.go: -------------------------------------------------------------------------------- 1 | package jsonproto 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "rb3server/database" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/protocols/jsonproto/services/accomplishment" 9 | "rb3server/protocols/jsonproto/services/accountlink" 10 | "rb3server/protocols/jsonproto/services/battles" 11 | "rb3server/protocols/jsonproto/services/config" 12 | "rb3server/protocols/jsonproto/services/entities" 13 | "rb3server/protocols/jsonproto/services/entities/band" 14 | "rb3server/protocols/jsonproto/services/entities/character" 15 | leaderboard "rb3server/protocols/jsonproto/services/leaderboards" 16 | "rb3server/protocols/jsonproto/services/misc" 17 | "rb3server/protocols/jsonproto/services/music_library" 18 | "rb3server/protocols/jsonproto/services/performance" 19 | "rb3server/protocols/jsonproto/services/scores" 20 | "rb3server/protocols/jsonproto/services/setlists" 21 | "rb3server/protocols/jsonproto/services/songlists" 22 | "rb3server/protocols/jsonproto/services/stats" 23 | "rb3server/protocols/jsonproto/services/ticker" 24 | 25 | "github.com/ihatecompvir/nex-go" 26 | "go.mongodb.org/mongo-driver/mongo" 27 | ) 28 | 29 | type Service interface { 30 | // the unique path to lookup the service by 31 | Path() string 32 | 33 | // function to process request 34 | Handle(string, *mongo.Database, *nex.Client) (string, error) 35 | } 36 | 37 | type ServicesManager struct { 38 | services map[string]Service 39 | } 40 | 41 | // Creates new services manager 42 | func NewServicesManager() *ServicesManager { 43 | mgr := &ServicesManager{ 44 | services: make(map[string]Service), 45 | } 46 | 47 | // register all services 48 | mgr.registerAll() 49 | 50 | return mgr 51 | 52 | } 53 | 54 | // register all services 55 | func (mgr *ServicesManager) registerAll() { 56 | // config 57 | mgr.register(config.ConfigService{}) 58 | 59 | // setlist creation 60 | mgr.register(setlists.SetlistSyncService{}) 61 | mgr.register(setlists.SetlistUpdateService{}) 62 | 63 | // account linking 64 | mgr.register(accountlink.AccountLinkService{}) 65 | 66 | // ticker for instaranks and etc. 67 | mgr.register(ticker.TickerInfoService{}) 68 | 69 | // entities 70 | mgr.register(character.CharacterUpdateService{}) 71 | mgr.register(band.BandUpdateService{}) 72 | mgr.register(entities.GetLinkcodeService{}) 73 | 74 | // performance 75 | mgr.register(performance.PerformanceRecordService{}) 76 | 77 | // accomplishments 78 | mgr.register(accomplishment.AccomplishmentRecordService{}) 79 | 80 | // leaderboards 81 | mgr.register(leaderboard.MaxrankGetService{}) 82 | mgr.register(leaderboard.PlayerGetService{}) 83 | mgr.register(leaderboard.AccPlayerGetService{}) 84 | mgr.register(leaderboard.AccMaxrankGetService{}) 85 | mgr.register(leaderboard.AccRankRangeGetService{}) 86 | mgr.register(leaderboard.RankRangeGetService{}) 87 | mgr.register(leaderboard.BattleMaxrankGetService{}) 88 | mgr.register(leaderboard.BattlePlayerGetService{}) 89 | mgr.register(leaderboard.BattleRankRangeGetService{}) 90 | mgr.register(leaderboard.PlayerranksGetService{}) 91 | mgr.register(leaderboard.FriendsUpdateService{}) 92 | 93 | // songlists 94 | mgr.register(songlists.GetSonglistsService{}) 95 | 96 | // battles 97 | mgr.register(battles.GetBattlesClosedService{}) 98 | mgr.register(battles.LimitCheckService{}) 99 | mgr.register(battles.BattleCreateService{}) 100 | 101 | // score recording 102 | mgr.register(scores.ScoreRecordService{}) 103 | mgr.register(scores.BattleScoreRecordService{}) 104 | 105 | // stats 106 | mgr.register(stats.StatsPadService{}) 107 | 108 | // misc 109 | mgr.register(misc.MiscSyncAvailableSongsService{}) 110 | mgr.register(misc.SetlistCreationStatusService{}) 111 | mgr.register(misc.OptionDataService{}) 112 | 113 | // music_library 114 | mgr.register(music_library.SortAndFiltersService{}) 115 | 116 | } 117 | 118 | // register a single service 119 | func (mgr *ServicesManager) register(service Service) { 120 | mgr.services[service.Path()] = service 121 | } 122 | 123 | // delegates the request to the proper service 124 | func (mgr ServicesManager) Handle(jsonStr string, client *nex.Client) (string, error) { 125 | 126 | methodPath, err := marshaler.GetRequestName(jsonStr) 127 | if err != nil { 128 | return "", err 129 | } 130 | 131 | // check service is implemented 132 | service, exists := mgr.services[methodPath] 133 | if !exists { 134 | log.Printf("Unimplemented JSON service for path: %s\n", methodPath) 135 | return "", fmt.Errorf("unimplemented service for path:%s\n", methodPath) 136 | } 137 | 138 | return service.Handle(jsonStr, database.GocentralDatabase, client) 139 | 140 | } 141 | -------------------------------------------------------------------------------- /protocols/jsonproto/marshaler/request.go: -------------------------------------------------------------------------------- 1 | package marshaler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "regexp" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | // gets the request name from the json data 13 | func GetRequestName(data string) (string, error) { 14 | var out [][]interface{} 15 | err := json.Unmarshal([]byte(data), &out) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | if len(out) != 1 { 21 | return "", fmt.Errorf("received bad length:%d\n", len(out)) 22 | } 23 | 24 | first := out[0] 25 | if len(first) != 2 { 26 | return "", fmt.Errorf("received bad length:%d\n", len(first)) 27 | } 28 | 29 | name, ok := first[0].(string) 30 | if !ok { 31 | panic("bad name") 32 | } 33 | 34 | return name, nil 35 | } 36 | 37 | // extracts the index from dynamic fields (pid000, pid001, etc.) 38 | func extractIndex(key, prefix string) (int, error) { 39 | re := regexp.MustCompile(`\d+$`) 40 | indexStr := re.FindString(key[len(prefix):]) 41 | var index int 42 | _, err := fmt.Sscanf(indexStr, "%d", &index) 43 | return index, err 44 | } 45 | 46 | // Converts JSON list to struct 47 | func UnmarshalRequest(data string, out interface{}) error { 48 | normalized, err := normalizeJson(data) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | outValue := reflect.ValueOf(out).Elem() 54 | outType := outValue.Type() 55 | 56 | dynamicFields := make(map[string]map[int]interface{}) 57 | 58 | for i := 0; i < outType.NumField(); i++ { 59 | field := outType.Field(i) 60 | jsonTag := field.Tag.Get("json") 61 | if jsonTag == "" || jsonTag == "-" { 62 | continue 63 | } 64 | 65 | // Handle static fields 66 | if value, ok := normalized[jsonTag]; ok { 67 | fieldValue := outValue.Field(i) 68 | if fieldValue.CanSet() { 69 | switch fieldValue.Kind() { 70 | case reflect.Int: 71 | if v, ok := value.(float64); ok { 72 | fieldValue.SetInt(int64(v)) 73 | } 74 | case reflect.String: 75 | if v, ok := value.(string); ok { 76 | fieldValue.SetString(v) 77 | } 78 | default: 79 | fieldValue.Set(reflect.ValueOf(value)) 80 | } 81 | } 82 | delete(normalized, jsonTag) 83 | } 84 | 85 | // Handle dynamic fields (pidXXX, role_idXXX, etc.) 86 | if strings.Contains(jsonTag, "XXX") { 87 | fieldNamePrefix := strings.TrimSuffix(jsonTag, "XXX") 88 | for key, value := range normalized { 89 | if strings.HasPrefix(key, fieldNamePrefix) { 90 | index, err := extractIndex(key, fieldNamePrefix) 91 | if err != nil { 92 | continue 93 | } 94 | if _, ok := dynamicFields[jsonTag]; !ok { 95 | dynamicFields[jsonTag] = make(map[int]interface{}) 96 | } 97 | dynamicFields[jsonTag][index] = value 98 | delete(normalized, key) 99 | } 100 | } 101 | } 102 | } 103 | 104 | for i := 0; i < outType.NumField(); i++ { 105 | field := outType.Field(i) 106 | jsonTag := field.Tag.Get("json") 107 | if values, ok := dynamicFields[jsonTag]; ok { 108 | fieldValue := outValue.Field(i) 109 | if fieldValue.Kind() == reflect.Slice { 110 | keys := make([]int, 0, len(values)) 111 | for k := range values { 112 | keys = append(keys, k) 113 | } 114 | sort.Ints(keys) 115 | for _, k := range keys { 116 | value := values[k] 117 | switch fieldValue.Type().Elem().Kind() { 118 | case reflect.Int: 119 | if v, ok := value.(float64); ok { 120 | fieldValue.Set(reflect.Append(fieldValue, reflect.ValueOf(int(v)))) 121 | } 122 | case reflect.String: 123 | if v, ok := value.(string); ok { 124 | fieldValue.Set(reflect.Append(fieldValue, reflect.ValueOf(v))) 125 | } 126 | default: 127 | fieldValue.Set(reflect.Append(fieldValue, reflect.ValueOf(value))) 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func normalizeJson(data string) (map[string]interface{}, error) { 138 | var out [][]interface{} 139 | err := json.Unmarshal([]byte(data), &out) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | if len(out) != 1 { 145 | return nil, fmt.Errorf("received bad length:%d\n", len(out)) 146 | } 147 | 148 | outer := out[0] 149 | if len(outer) != 2 { 150 | return nil, fmt.Errorf("received bad length:%d\n", len(outer)) 151 | } 152 | 153 | inner, ok := outer[1].([]interface{}) 154 | if !ok { 155 | return nil, fmt.Errorf("bad inner") 156 | } 157 | 158 | fields, ok := inner[0].([]interface{}) 159 | if !ok { 160 | return nil, fmt.Errorf("bad fields") 161 | } 162 | 163 | fieldLen := len(fields) 164 | 165 | values, ok := inner[1].([]interface{}) 166 | if !ok { 167 | return nil, fmt.Errorf("bad values") 168 | } 169 | 170 | m := make(map[string]interface{}, fieldLen) 171 | for i := 0; i < fieldLen; i++ { 172 | field, ok := fields[i].(string) 173 | if !ok { 174 | return nil, fmt.Errorf("bad field name") 175 | } 176 | m[field] = values[i] 177 | } 178 | 179 | return m, nil 180 | } 181 | -------------------------------------------------------------------------------- /protocols/jsonproto/marshaler/response.go: -------------------------------------------------------------------------------- 1 | package marshaler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | // builds the response for the service 11 | func MarshalResponse(path string, obj interface{}) (string, error) { 12 | var names []string 13 | var types string 14 | var values interface{} 15 | 16 | v := reflect.ValueOf(obj) 17 | 18 | if v.Type().Kind() == reflect.Slice { 19 | len := v.Len() 20 | if len < 1 { 21 | panic("too small") 22 | } 23 | 24 | names, types = buildNamesAndTypesLists(v.Index(0)) 25 | 26 | temp := make([]interface{}, len) 27 | for i := 0; i < len; i++ { 28 | temp[i] = buildValuesList(v.Index(i)) 29 | } 30 | values = temp 31 | 32 | } else { 33 | names, types = buildNamesAndTypesLists(v) 34 | values = buildValuesList(v) 35 | } 36 | 37 | res := [][]interface{}{{path, types, names, values}} 38 | 39 | out, err := json.Marshal(res) 40 | if err != nil { 41 | return "", err 42 | } 43 | return string(out), nil 44 | } 45 | 46 | func buildNamesAndTypesLists(v reflect.Value) ([]string, string) { 47 | tp := v.Type() 48 | 49 | if tp.Kind() == reflect.Slice { 50 | panic(fmt.Errorf("slices should not be passed here:%+v", v)) 51 | } 52 | 53 | fieldCount := v.NumField() 54 | 55 | names := make([]string, 0, fieldCount) 56 | var types strings.Builder 57 | 58 | for i := 0; i < fieldCount; i++ { 59 | field := tp.Field(i) 60 | 61 | if field.Type.Kind() == reflect.Slice { 62 | for j := 0; j < v.Field(i).Len(); j++ { 63 | name := fmt.Sprintf("%s%03d", strings.TrimSuffix(field.Tag.Get("json"), "XXX"), j) 64 | names = append(names, name) 65 | switch v.Field(i).Index(j).Interface().(type) { 66 | case string: 67 | types.WriteString("s") 68 | case int: 69 | types.WriteString("d") 70 | default: 71 | panic(fmt.Errorf("unsupported type in slice:%+v\n", field.Type)) 72 | } 73 | } 74 | } else { 75 | name := field.Tag.Get("json") 76 | if name == "" { 77 | name = field.Name 78 | } 79 | 80 | names = append(names, name) 81 | 82 | switch v.Field(i).Interface().(type) { 83 | case string: 84 | types.WriteString("s") 85 | case int: 86 | types.WriteString("d") 87 | default: 88 | panic(fmt.Errorf("unsupported type:%+v\n", field.Type)) 89 | } 90 | } 91 | } 92 | 93 | return names, types.String() 94 | } 95 | 96 | func buildValuesList(v reflect.Value) []interface{} { 97 | if v.Type().Kind() == reflect.Slice { 98 | panic(fmt.Errorf("slices should not be passed here:%+v", v)) 99 | } 100 | 101 | fieldCount := v.NumField() 102 | 103 | values := make([]interface{}, 0, fieldCount) 104 | 105 | for i := 0; i < fieldCount; i++ { 106 | if v.Field(i).Type().Kind() == reflect.Slice { 107 | for j := 0; j < v.Field(i).Len(); j++ { 108 | values = append(values, v.Field(i).Index(j).Interface()) 109 | } 110 | } else { 111 | values = append(values, v.Field(i).Interface()) 112 | } 113 | } 114 | 115 | return values 116 | } 117 | -------------------------------------------------------------------------------- /protocols/jsonproto/marshaler/utils.go: -------------------------------------------------------------------------------- 1 | package marshaler 2 | 3 | import "encoding/json" 4 | 5 | // CombineJSONMethods combines multiple json method responses into a singular one 6 | // used by the game when the structure of the JSON needs to be different in each response (i.e. setlists, battles, etc.) 7 | func CombineJSONMethods(jsonStrings []string) (string, error) { 8 | var finalOutput [][]interface{} 9 | 10 | for _, jsonString := range jsonStrings { 11 | var tempOutput [][]interface{} 12 | if err := json.Unmarshal([]byte(jsonString), &tempOutput); err != nil { 13 | return "", err 14 | } 15 | finalOutput = append(finalOutput, tempOutput...) 16 | } 17 | 18 | output, err := json.Marshal(finalOutput) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | return string(output), nil 24 | } 25 | 26 | // GenerateEmptyJSONResponse generates an empty JSON response for the given service name 27 | // this is useful for things like leaderboards or battles or etc. so we can make the game show there are no scores or what not 28 | func GenerateEmptyJSONResponse(serviceName string) string { 29 | return "[[\"" + serviceName + "\", \"\", [], []]]" 30 | } 31 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/accountlink/linkstatus.go: -------------------------------------------------------------------------------- 1 | package accountlink 2 | 3 | import ( 4 | "log" 5 | "rb3server/protocols/jsonproto/marshaler" 6 | "rb3server/utils" 7 | 8 | "github.com/ihatecompvir/nex-go" 9 | "go.mongodb.org/mongo-driver/mongo" 10 | ) 11 | 12 | type AccountLinkRequest struct { 13 | Region string `json:"region"` 14 | SystemMS int `json:"system_ms"` 15 | MachineID string `json:"machine_id"` 16 | SessionGUID string `json:"session_guid"` 17 | PID int `json:"pid"` 18 | } 19 | 20 | type AccountLinkResponse struct { 21 | PID int `json:"pid"` 22 | Linked int `json:"linked"` 23 | } 24 | 25 | type AccountLinkService struct { 26 | } 27 | 28 | func (service AccountLinkService) Path() string { 29 | return "misc/get_accounts_web_linked_status" 30 | } 31 | 32 | func (service AccountLinkService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 33 | var req AccountLinkRequest 34 | err := marshaler.UnmarshalRequest(data, &req) 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 40 | 41 | if !validPIDres { 42 | log.Println("Client is attempting to check link status without a valid server-assigned PID, rejecting call") 43 | return "", err 44 | } 45 | 46 | res := []AccountLinkResponse{{ 47 | req.PID, 48 | 1, 49 | }} 50 | 51 | return marshaler.MarshalResponse(service.Path(), res) 52 | } 53 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/battles/create.go: -------------------------------------------------------------------------------- 1 | package battles 2 | 3 | import ( 4 | "context" 5 | "log" 6 | db "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/protocols/jsonproto/marshaler" 9 | "rb3server/utils" 10 | "strings" 11 | "time" 12 | 13 | "github.com/ihatecompvir/nex-go" 14 | "go.mongodb.org/mongo-driver/bson" 15 | "go.mongodb.org/mongo-driver/mongo" 16 | ) 17 | 18 | type BattleCreateRequest struct { 19 | Type int `json:"type"` 20 | Name string `json:"name"` 21 | Region string `json:"region"` 22 | Description string `json:"description"` 23 | Flags int `json:"flags"` 24 | Instrument int `json:"instrument"` 25 | SystemMS int `json:"system_ms"` 26 | MachineID string `json:"machine_id"` 27 | SessionGUID string `json:"session_guid"` 28 | PID int `json:"pid"` 29 | TimeEndVal int `json:"time_end_val"` 30 | TimeEndUnits string `json:"time_end_units"` 31 | SongIDs []int `json:"song_idXXX"` 32 | } 33 | 34 | type BattleCreateResponse struct { 35 | Success int `json:"success"` 36 | BattleID int `json:"battle_id"` 37 | } 38 | 39 | type BattleCreateService struct { 40 | } 41 | 42 | func (service BattleCreateService) Path() string { 43 | return "battles/create" 44 | } 45 | 46 | func (service BattleCreateService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 47 | var req BattleCreateRequest 48 | err := marshaler.UnmarshalRequest(data, &req) 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 54 | 55 | if !validPIDres { 56 | log.Println("Client is attempting to create a battle without a valid server-assigned PID, rejecting call") 57 | return "", err 58 | } 59 | 60 | // do a profanity check before updating the setlist 61 | var config models.Config 62 | configCollection := database.Collection("config") 63 | err = configCollection.FindOne(context.TODO(), bson.M{}).Decode(&config) 64 | if err != nil { 65 | log.Printf("Could not get config %v\n", err) 66 | } 67 | 68 | // check if the setlist name or description contain anything in the profanity list 69 | // NOTE: exercise caution with the profanity list. Putting "ass" on the list would mean that a setlist name like "Band Assistant" is not allowed. 70 | // use your best judgment, it's up to you to define your own profanity list as a server host, GoCentral does not and will not ship with one 71 | for _, profanity := range config.ProfanityList { 72 | if profanity != "" && req.Name != "" && len(req.Name) >= len(profanity) { 73 | lowerName := strings.ToLower(req.Name) 74 | lowerProfanity := strings.ToLower(profanity) 75 | 76 | if lowerName == lowerProfanity { 77 | return marshaler.MarshalResponse(service.Path(), []BattleCreateResponse{{0xF, -1}}) 78 | } 79 | 80 | if strings.Contains(lowerName, lowerProfanity) { 81 | return marshaler.MarshalResponse(service.Path(), []BattleCreateResponse{{0xF, -1}}) 82 | } 83 | } 84 | 85 | // check description too 86 | if profanity != "" && req.Description != "" && len(req.Description) >= len(profanity) { 87 | lowerName := strings.ToLower(req.Description) 88 | lowerProfanity := strings.ToLower(profanity) 89 | 90 | if lowerName == lowerProfanity { 91 | return marshaler.MarshalResponse(service.Path(), []BattleCreateResponse{{0x10, -1}}) 92 | } 93 | 94 | if strings.Contains(lowerName, lowerProfanity) { 95 | return marshaler.MarshalResponse(service.Path(), []BattleCreateResponse{{0x10, -1}}) 96 | } 97 | } 98 | } 99 | 100 | _, err = configCollection.UpdateOne( 101 | context.TODO(), 102 | bson.M{}, 103 | bson.D{ 104 | {"$set", bson.D{{"last_setlist_id", config.LastSetlistID + 1}}}, 105 | }, 106 | ) 107 | 108 | if err != nil { 109 | log.Println("Could not update config in database while creating battle: ", err) 110 | } 111 | 112 | config.LastSetlistID += 1 113 | 114 | users := database.Collection("users") 115 | var user models.User 116 | err = users.FindOne(context.TODO(), bson.M{"pid": req.PID}).Decode(&user) 117 | 118 | if err != nil { 119 | log.Printf("Could not find user with PID %d, defaulting to \"Player\": %v", req.PID, err) 120 | user.Username = "Player" 121 | } 122 | 123 | // write setlist to database 124 | setlistCollection := database.Collection("setlists") 125 | 126 | var setlist models.Setlist 127 | setlist.ArtURL = "" 128 | setlist.Desc = req.Description 129 | setlist.Title = req.Name 130 | setlist.Type = 1000 131 | setlist.Owner = user.Username 132 | setlist.OwnerGUID = user.GUID 133 | setlist.SetlistID = config.LastSetlistID 134 | setlist.PID = req.PID 135 | setlist.SongIDs = req.SongIDs 136 | 137 | // battle-specific fields 138 | setlist.TimeEndVal = req.TimeEndVal 139 | setlist.TimeEndUnits = req.TimeEndUnits 140 | setlist.Flags = req.Flags 141 | setlist.Instrument = req.Instrument 142 | 143 | setlist.Created = time.Now().Unix() 144 | 145 | // if user is a battles admin, they will create Harmonix battles 146 | if db.IsPIDInGroup(req.PID, "battle_admin") { 147 | setlist.Type = 1002 148 | } 149 | 150 | // create song names that are just empty strings for now 151 | // TODO: create a song ID DB so we can store the proper names 152 | // perhaps there is some way we can automatically create this, but I don't think the game ever sends song names 153 | setlist.SongNames = make([]string, len(req.SongIDs)) 154 | 155 | update := bson.D{ 156 | {Key: "art_url", Value: setlist.ArtURL}, 157 | {Key: "desc", Value: setlist.Desc}, 158 | {Key: "title", Value: setlist.Title}, 159 | {Key: "type", Value: setlist.Type}, 160 | {Key: "owner", Value: setlist.Owner}, 161 | {Key: "owner_guid", Value: setlist.OwnerGUID}, 162 | {Key: "setlist_id", Value: setlist.SetlistID}, 163 | {Key: "pid", Value: setlist.PID}, 164 | {Key: "s_ids", Value: setlist.SongIDs}, 165 | {Key: "s_names", Value: setlist.SongNames}, 166 | {Key: "shared", Value: "t"}, 167 | {Key: "time_end_val", Value: setlist.TimeEndVal}, 168 | {Key: "time_end_units", Value: setlist.TimeEndUnits}, 169 | {Key: "flags", Value: setlist.Flags}, 170 | {Key: "instrument", Value: setlist.Instrument}, 171 | {Key: "created", Value: setlist.Created}, 172 | } 173 | 174 | _, err = setlistCollection.InsertOne(context.TODO(), update) 175 | if err != nil { 176 | log.Printf("Error inserting battle to DB: %s", err) 177 | } 178 | 179 | res := []BattleCreateResponse{{ 180 | 0, 181 | config.LastSetlistID, 182 | }} 183 | 184 | return marshaler.MarshalResponse(service.Path(), res) 185 | } 186 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/battles/getclosed.go: -------------------------------------------------------------------------------- 1 | package battles 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | 10 | db "rb3server/database" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | ) 16 | 17 | type GetBattlesClosedRequest struct { 18 | Region string `json:"region"` 19 | Locale string `json:"locale"` 20 | SystemMS int `json:"system_ms"` 21 | MachineID string `json:"machine_id"` 22 | SessionGUID string `json:"session_guid"` 23 | PID000 int `json:"pid000"` 24 | } 25 | 26 | type GetBattlesClosedResponse struct { 27 | ID int `json:"id"` 28 | PID int `json:"pid"` 29 | Title string `json:"title"` 30 | Desc string `json:"desc"` 31 | Type int `json:"type"` 32 | Owner string `json:"owner"` 33 | OwnerGUID string `json:"owner_guid"` 34 | GUID string `json:"guid"` 35 | ArtURL string `json:"art_url"` 36 | SongIDs []int `json:"s_idXXX"` 37 | SongNames []string `json:"s_nameXXX"` 38 | } 39 | 40 | type GetBattlesClosedService struct { 41 | } 42 | 43 | func (service GetBattlesClosedService) Path() string { 44 | return "battles/closed/get" 45 | } 46 | 47 | func (service GetBattlesClosedService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 48 | var req GetBattlesClosedRequest 49 | 50 | err := marshaler.UnmarshalRequest(data, &req) 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 56 | 57 | if !validPIDres { 58 | log.Println("Client is attempting to get closed battles without a valid server-assigned PID, rejecting call") 59 | return "", err 60 | } 61 | 62 | setlistCollection := database.Collection("setlists") 63 | 64 | setlistCursor, err := setlistCollection.Find(context.TODO(), bson.D{{"shared", "t"}, {"pid", bson.D{{"$ne", req.PID000}}}}) 65 | 66 | if err != nil { 67 | log.Printf("Error getting closed battles: %s", err) 68 | } 69 | 70 | jsonStrings := []string{} 71 | 72 | for setlistCursor.Next(context.TODO()) { 73 | var setlistToCopy models.Setlist 74 | 75 | setlistCursor.Decode(&setlistToCopy) 76 | 77 | // battle setlist 78 | if setlistToCopy.Type == 1000 || setlistToCopy.Type == 1001 || setlistToCopy.Type == 1002 { 79 | var battle GetBattlesClosedResponse 80 | battle.ArtURL = setlistToCopy.ArtURL 81 | battle.Desc = setlistToCopy.Desc 82 | battle.GUID = setlistToCopy.GUID 83 | battle.Owner = setlistToCopy.Owner 84 | battle.OwnerGUID = setlistToCopy.OwnerGUID 85 | battle.PID = setlistToCopy.PID 86 | battle.Title = setlistToCopy.Title 87 | battle.Type = setlistToCopy.Type 88 | battle.SongIDs = append(battle.SongIDs, setlistToCopy.SongIDs...) 89 | battle.SongNames = append(battle.SongNames, setlistToCopy.SongNames...) 90 | 91 | isExpired, _ := db.GetBattleExpiryInfo(setlistToCopy.SetlistID) 92 | 93 | // if the battle is closed, add it, otherwise skip 94 | if isExpired { 95 | resString, _ := marshaler.MarshalResponse(service.Path(), []GetBattlesClosedResponse{battle}) 96 | 97 | jsonStrings = append(jsonStrings, resString) 98 | } else { 99 | continue 100 | } 101 | } 102 | } 103 | 104 | if len(jsonStrings) == 0 { 105 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 106 | } else { 107 | resString, _ := marshaler.CombineJSONMethods(jsonStrings) 108 | return resString, nil 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/battles/limitcheck.go: -------------------------------------------------------------------------------- 1 | package battles 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | 9 | db "rb3server/database" 10 | 11 | "github.com/ihatecompvir/nex-go" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | ) 15 | 16 | type LimitCheckRequest struct { 17 | Region string `json:"region"` 18 | SystemMS int `json:"system_ms"` 19 | MachineID string `json:"machine_id"` 20 | SessionGUID string `json:"session_guid"` 21 | PID int `json:"pid"` 22 | } 23 | 24 | type LimitCheckResponse struct { 25 | Success int `json:"success"` 26 | } 27 | 28 | type LimitCheckService struct { 29 | } 30 | 31 | func (service LimitCheckService) Path() string { 32 | return "battles/limit/check" 33 | } 34 | 35 | func (service LimitCheckService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 36 | var req LimitCheckRequest 37 | 38 | err := marshaler.UnmarshalRequest(data, &req) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | if req.PID != int(client.PlayerID()) { 44 | log.Println("Client-supplied PID did not match server-assigned PID, rejecting battle limit check") 45 | return "", err 46 | } 47 | 48 | var config models.Config 49 | configCollection := database.Collection("config") 50 | err = configCollection.FindOne(context.TODO(), bson.M{}).Decode(&config) 51 | if err != nil { 52 | log.Printf("Could not get config %v\n", err) 53 | } 54 | 55 | users := database.Collection("users") 56 | var user models.User 57 | err = users.FindOne(context.TODO(), bson.M{"pid": req.PID}).Decode(&user) 58 | 59 | if err != nil { 60 | log.Printf("Could not find user with PID %d, could not check limit", req.PID) 61 | return marshaler.MarshalResponse(service.Path(), []LimitCheckResponse{{0x16}}) 62 | } 63 | 64 | if db.IsPIDInGroup(req.PID, "battle_admin") { 65 | // if the user is a battle administrator, they can create as many battles as they want 66 | // so do not check the limit 67 | return marshaler.MarshalResponse(service.Path(), []LimitCheckResponse{{0}}) 68 | } 69 | 70 | // find how many battles this user has created 71 | // type must be either 1000, 1001, or 1002 so we don't catch normal setlists 72 | setlistsCollection := database.Collection("setlists") 73 | count, err := setlistsCollection.CountDocuments(context.TODO(), bson.M{"pid": req.PID, "type": bson.M{"$in": []int{1000, 1001, 1002}}}) 74 | if err != nil { 75 | log.Printf("Could not count setlists for user %d, could not check battle limit", req.PID) 76 | return marshaler.MarshalResponse(service.Path(), []LimitCheckResponse{{0x16}}) 77 | } 78 | 79 | if int(count) >= config.BattleLimit { 80 | return marshaler.MarshalResponse(service.Path(), []LimitCheckResponse{{0x16}}) 81 | } 82 | 83 | res := []LimitCheckResponse{{0}} 84 | 85 | return marshaler.MarshalResponse(service.Path(), res) 86 | } 87 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "rb3server/models" 5 | "rb3server/protocols/jsonproto/marshaler" 6 | 7 | "github.com/ihatecompvir/nex-go" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/mongo" 10 | ) 11 | 12 | type ConfigRequest struct { 13 | Region string `json:"region"` 14 | Locale string `json:"locale"` 15 | SystemMS int `json:"system_ms"` 16 | MachineID string `json:"machine_id"` 17 | SessionGUID string `json:"session_guid"` 18 | } 19 | 20 | type ConfigResponse struct { 21 | OutDta string `json:"out_dta"` 22 | Version string `json:"version"` 23 | } 24 | 25 | type ConfigService struct { 26 | } 27 | 28 | func (service ConfigService) Path() string { 29 | return "config/get" 30 | } 31 | 32 | func (service ConfigService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 33 | var req ConfigRequest 34 | 35 | var motdInfo models.MOTDInfo 36 | 37 | motdCollection := database.Collection("motd") 38 | 39 | motdCollection.FindOne(nil, bson.D{}).Decode(&motdInfo) 40 | 41 | err := marshaler.UnmarshalRequest(data, &req) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | res := []ConfigResponse{{ 47 | motdInfo.DTA, 48 | "3", 49 | }} 50 | 51 | return marshaler.MarshalResponse(service.Path(), res) 52 | } 53 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/entities/band/update.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "log" 7 | "rb3server/models" 8 | "rb3server/protocols/jsonproto/marshaler" 9 | "rb3server/utils" 10 | "strings" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | ) 16 | 17 | type BandUpdateRequest struct { 18 | Name string `json:"name"` 19 | Region string `json:"region"` 20 | Flags int `json:"flags"` 21 | SystemMS int `json:"system_ms"` 22 | MachineID string `json:"machine_id"` 23 | SessionGUID string `json:"session_guid"` 24 | PID int `json:"pid"` 25 | Art string `json:"art"` 26 | } 27 | 28 | type BandUpdateResponse struct { 29 | RetCode int `json:"ret_code"` 30 | } 31 | 32 | type BandUpdateService struct { 33 | } 34 | 35 | func (service BandUpdateService) Path() string { 36 | return "entities/band/update" 37 | } 38 | 39 | func (service BandUpdateService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 40 | var req BandUpdateRequest 41 | err := marshaler.UnmarshalRequest(data, &req) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | // do a profanity check before updating the band 47 | var config models.Config 48 | configCollection := database.Collection("config") 49 | err = configCollection.FindOne(context.TODO(), bson.M{}).Decode(&config) 50 | if err != nil { 51 | log.Printf("Could not get config %v\n", err) 52 | } 53 | 54 | // check if the band name contains anything in the profanity list 55 | // NOTE: exercise caution with the profanity list. Putting "ass" on the list would mean that a name like "Band Assistant" is not allowed. 56 | // use your best judgment, it's up to you to define your own profanity list as a server host, GoCentral does not and will not ship with one 57 | for _, profanity := range config.ProfanityList { 58 | if profanity != "" && req.Name != "" && len(req.Name) >= len(profanity) { 59 | lowerName := strings.ToLower(req.Name) 60 | lowerProfanity := strings.ToLower(profanity) 61 | 62 | if lowerName == lowerProfanity { 63 | return marshaler.MarshalResponse(service.Path(), []BandUpdateResponse{{2}}) 64 | } 65 | 66 | if strings.Contains(lowerName, lowerProfanity) { 67 | return marshaler.MarshalResponse(service.Path(), []BandUpdateResponse{{2}}) 68 | } 69 | } 70 | } 71 | 72 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 73 | 74 | if !validPIDres { 75 | log.Println("Client is attempting to update a band without a valid server-assigned PID, rejecting call") 76 | return "", err 77 | } 78 | 79 | artBytes, err := hex.DecodeString(req.Art) 80 | if err != nil { 81 | log.Printf("Could not update band %s for PID %v: %s\n", req.Name, req.PID, err) 82 | return marshaler.MarshalResponse(service.Path(), []BandUpdateResponse{{1}}) 83 | } 84 | 85 | bands := database.Collection("bands") 86 | var band models.Band 87 | err = bands.FindOne(nil, bson.M{"owner_pid": req.PID}).Decode(&band) 88 | 89 | if err != nil { 90 | 91 | _, err = configCollection.UpdateOne( 92 | nil, 93 | bson.M{}, 94 | bson.D{ 95 | {"$set", bson.D{{"last_band_id", config.LastBandID + 1}}}, 96 | }, 97 | ) 98 | 99 | config.LastBandID += 1 100 | 101 | if err != nil { 102 | log.Println("Could not update config in database while updating band: ", err) 103 | } 104 | 105 | _, err = bands.InsertOne(nil, bson.D{ 106 | {Key: "art", Value: artBytes}, 107 | {Key: "name", Value: req.Name}, 108 | {Key: "owner_pid", Value: req.PID}, 109 | {Key: "band_id", Value: config.LastBandID}, 110 | }) 111 | 112 | if err != nil { 113 | log.Printf("Could not update band %s for PID %v: %s\n", req.Name, req.PID, err) 114 | return marshaler.MarshalResponse(service.Path(), []BandUpdateResponse{{0}}) 115 | } 116 | 117 | return marshaler.MarshalResponse(service.Path(), []BandUpdateResponse{{1}}) 118 | } 119 | 120 | _, err = bands.UpdateOne(nil, bson.M{"owner_pid": req.PID}, bson.M{"$set": bson.M{ 121 | "art": artBytes, 122 | "name": req.Name, 123 | }}) 124 | 125 | if err != nil { 126 | log.Printf("Could not update band %s for PID %v: %s\n", req.Name, req.PID, err) 127 | return marshaler.MarshalResponse(service.Path(), []BandUpdateResponse{{0}}) 128 | } 129 | 130 | return marshaler.MarshalResponse(service.Path(), []BandUpdateResponse{{1}}) 131 | } 132 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/entities/character/namecheck.go: -------------------------------------------------------------------------------- 1 | package character 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | "strings" 10 | 11 | "github.com/ihatecompvir/nex-go" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | ) 15 | 16 | type CharacterNameCheckRequest struct { 17 | Name string `json:"name"` 18 | Region string `json:"region"` 19 | Flags int `json:"flags"` 20 | PID int `json:"pid"` 21 | SystemMS int `json:"system_ms"` 22 | MachineID string `json:"machine_id"` 23 | SessionGUID string `json:"session_guid"` 24 | } 25 | 26 | type CharacterNameCheckResponse struct { 27 | RetCode int `json:"ret_code"` 28 | } 29 | 30 | type CharacterNameCheckService struct { 31 | } 32 | 33 | func (service CharacterNameCheckService) Path() string { 34 | return "entities/character/update" 35 | } 36 | 37 | func (service CharacterNameCheckService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 38 | var req CharacterNameCheckRequest 39 | err := marshaler.UnmarshalRequest(data, &req) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 45 | 46 | if !validPIDres { 47 | log.Println("Client is attempting to namecheck for a character without a valid server-assigned PID, rejecting call") 48 | return "", err 49 | } 50 | 51 | // do a profanity check before updating the band 52 | var config models.Config 53 | configCollection := database.Collection("config") 54 | err = configCollection.FindOne(context.TODO(), bson.M{}).Decode(&config) 55 | if err != nil { 56 | log.Printf("Could not get config %v\n", err) 57 | } 58 | 59 | // check if the band name contains anything in the profanity list 60 | // NOTE: exercise caution with the profanity list. Putting "ass" on the list would mean that a name like "Band Assistant" is not allowed. 61 | // use your best judgment, it's up to you to define your own profanity list as a server host, GoCentral does not and will not ship with one 62 | for _, profanity := range config.ProfanityList { 63 | if profanity != "" && req.Name != "" && len(req.Name) >= len(profanity) { 64 | lowerName := strings.ToLower(req.Name) 65 | lowerProfanity := strings.ToLower(profanity) 66 | 67 | if lowerName == lowerProfanity { 68 | return marshaler.MarshalResponse(service.Path(), []CharacterNameCheckResponse{{2}}) 69 | } 70 | 71 | if strings.Contains(lowerName, lowerProfanity) { 72 | return marshaler.MarshalResponse(service.Path(), []CharacterNameCheckResponse{{2}}) 73 | } 74 | } 75 | } 76 | 77 | return marshaler.MarshalResponse(service.Path(), []CharacterNameCheckResponse{{1}}) 78 | } 79 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/entities/character/update.go: -------------------------------------------------------------------------------- 1 | package character 2 | 3 | import ( 4 | "encoding/hex" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | ) 14 | 15 | type CharacterUpdateRequest struct { 16 | Name string `json:"name"` 17 | Region string `json:"region"` 18 | Flags int `json:"flags"` 19 | SystemMS int `json:"system_ms"` 20 | MachineID string `json:"machine_id"` 21 | SessionGUID string `json:"session_guid"` 22 | PID int `json:"pid"` 23 | GUID string `json:"guid"` 24 | CharData string `json:"char_data"` 25 | } 26 | 27 | type CharacterUpdateResponse struct { 28 | RetCode int `json:"ret_code"` 29 | } 30 | 31 | type CharacterUpdateService struct { 32 | } 33 | 34 | func (service CharacterUpdateService) Path() string { 35 | return "entities/character/update" 36 | } 37 | 38 | func (service CharacterUpdateService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 39 | var req CharacterUpdateRequest 40 | err := marshaler.UnmarshalRequest(data, &req) 41 | if err != nil { 42 | return "", err 43 | } 44 | 45 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 46 | 47 | if !validPIDres { 48 | log.Println("Client is attempting to update a character without a valid server-assigned PID, rejecting call") 49 | return "", err 50 | } 51 | 52 | characterBytes, err := hex.DecodeString(req.CharData) 53 | if err != nil { 54 | log.Printf("Could not update character %s with GUID %s for PID %v: %s\n", req.Name, req.GUID, req.PID, err) 55 | return marshaler.MarshalResponse(service.Path(), []CharacterUpdateResponse{{0}}) 56 | } 57 | 58 | var config models.Config 59 | configCollection := database.Collection("config") 60 | err = configCollection.FindOne(nil, bson.M{}).Decode(&config) 61 | if err != nil { 62 | log.Printf("Could not get config %v\n", err) 63 | } 64 | 65 | characters := database.Collection("characters") 66 | var character models.Character 67 | err = characters.FindOne(nil, bson.M{"guid": req.GUID}).Decode(&character) 68 | 69 | if err != nil { 70 | 71 | _, err = configCollection.UpdateOne( 72 | nil, 73 | bson.M{}, 74 | bson.D{ 75 | {"$set", bson.D{{"last_character_id", config.LastCharacterID + 1}}}, 76 | }, 77 | ) 78 | 79 | if err != nil { 80 | log.Println("Could not update config in database while updating character: ", err) 81 | } 82 | 83 | config.LastCharacterID += 1 84 | 85 | _, err = characters.InsertOne(nil, bson.D{ 86 | {Key: "guid", Value: req.GUID}, 87 | {Key: "char_data", Value: characterBytes}, 88 | {Key: "name", Value: req.Name}, 89 | {Key: "owner_pid", Value: req.PID}, 90 | {Key: "character_id", Value: config.LastCharacterID + 1}, 91 | }) 92 | if err != nil { 93 | log.Printf("Could not update character %s with GUID %s for PID %v: %s\n", req.Name, req.GUID, req.PID, err) 94 | } 95 | return marshaler.MarshalResponse(service.Path(), []CharacterUpdateResponse{{1}}) 96 | } 97 | 98 | _, err = characters.UpdateOne(nil, bson.M{"guid": req.GUID}, bson.M{"$set": bson.M{ 99 | "char_data": characterBytes, 100 | "name": req.Name, 101 | }}) 102 | 103 | if err != nil { 104 | log.Printf("Could not update character %s with GUID %s for PID %v: %s\n", req.Name, req.GUID, req.PID, err) 105 | return marshaler.MarshalResponse(service.Path(), []CharacterUpdateResponse{{0}}) 106 | } 107 | 108 | return marshaler.MarshalResponse(service.Path(), []CharacterUpdateResponse{{1}}) 109 | } 110 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/entities/linkcode.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "log" 5 | "rb3server/models" 6 | "rb3server/protocols/jsonproto/marshaler" 7 | "rb3server/utils" 8 | 9 | "github.com/ihatecompvir/nex-go" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | ) 13 | 14 | type GetLinkcodeRequest struct { 15 | Region string `json:"region"` 16 | SystemMS int `json:"system_ms"` 17 | MachineID string `json:"machine_id"` 18 | SessionGUID string `json:"session_guid"` 19 | PID int `json:"pid"` 20 | } 21 | 22 | type GetLinkcodeResponse struct { 23 | Code string `json:"code"` 24 | } 25 | 26 | type GetLinkcodeService struct { 27 | } 28 | 29 | func (service GetLinkcodeService) Path() string { 30 | return "entities/linkcode/get" 31 | } 32 | 33 | func (service GetLinkcodeService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 34 | var req GetLinkcodeRequest 35 | err := marshaler.UnmarshalRequest(data, &req) 36 | 37 | res := []GetLinkcodeResponse{{}} 38 | 39 | if err != nil { 40 | log.Println("Failed to unmarshal GetLinkcodeRequest:", err) 41 | res = []GetLinkcodeResponse{{ 42 | "Could not get link code, please try again later", 43 | }} 44 | } 45 | 46 | // make sure the client is asking for their own link code 47 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 48 | 49 | if !validPIDres { 50 | log.Println("Client is attempting to get a link code without a valid server-assigned PID, rejecting call") 51 | return "", err 52 | } 53 | 54 | usersCollection := database.Collection("users") 55 | var user models.User 56 | 57 | result := usersCollection.FindOne(nil, bson.M{"pid": req.PID}) 58 | 59 | if result.Err() != nil { 60 | log.Println("Could not find user with PID", req.PID) 61 | res = []GetLinkcodeResponse{{ 62 | "Could not get link code, please try again later", 63 | }} 64 | } 65 | 66 | err = result.Decode(&user) 67 | 68 | if err != nil { 69 | log.Println("Could not decode user with PID", req.PID) 70 | res = []GetLinkcodeResponse{{ 71 | "Could not get link code, please try again later", 72 | }} 73 | } 74 | 75 | // Spoof account linking status, 12345 pid 76 | res = []GetLinkcodeResponse{{ 77 | user.LinkCode, 78 | }} 79 | 80 | return marshaler.MarshalResponse(service.Path(), res) 81 | } 82 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/acc_maxrank.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | ) 14 | 15 | type AccMaxrankGetRequest struct { 16 | Region string `json:"region"` 17 | SystemMS int `json:"system_ms"` 18 | AccID string `json:"acc_id"` 19 | MachineID string `json:"machine_id"` 20 | SessionGUID string `json:"session_guid"` 21 | PID000 int `json:"pid000"` 22 | } 23 | 24 | type AccMaxrankGetResponse struct { 25 | MaxRank int `json:"max_rank"` 26 | } 27 | 28 | type AccMaxrankGetService struct { 29 | } 30 | 31 | func (service AccMaxrankGetService) Path() string { 32 | return "leaderboards/acc_maxrank/get" 33 | } 34 | 35 | func (service AccMaxrankGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 36 | var req AccMaxrankGetRequest 37 | 38 | err := marshaler.UnmarshalRequest(data, &req) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 44 | 45 | if !validPIDres { 46 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 47 | return "", err 48 | } 49 | 50 | accomplishmentsCollection := database.Collection("accomplishments") 51 | 52 | var accomplishments models.Accomplishments 53 | err = accomplishmentsCollection.FindOne(context.TODO(), bson.M{"acc_id": req.AccID}).Decode(&accomplishments) 54 | 55 | if err != nil { 56 | return marshaler.MarshalResponse(service.Path(), []AccMaxrankGetResponse{{ 57 | 0, 58 | }}) 59 | } 60 | 61 | // return the number of scores, aka the "max rank" 62 | res := []AccMaxrankGetResponse{{ 63 | len(getAccomplishmentField(req.AccID, accomplishments)), 64 | }} 65 | 66 | return marshaler.MarshalResponse(service.Path(), res) 67 | } 68 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/acc_player_get.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | db "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/protocols/jsonproto/marshaler" 9 | "rb3server/utils" 10 | "sort" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | ) 16 | 17 | type AccPlayerGetRequest struct { 18 | Region string `json:"region"` 19 | SystemMS int `json:"system_ms"` 20 | AccID string `json:"acc_id"` 21 | MachineID string `json:"machine_id"` 22 | SessionGUID string `json:"session_guid"` 23 | PID000 int `json:"pid000"` 24 | } 25 | 26 | type AccPlayerGetResponse struct { 27 | PID int `json:"pid"` 28 | Name string `json:"name"` 29 | DiffID int `json:"diff_id"` 30 | Rank int `json:"rank"` 31 | Score int `json:"score"` 32 | IsPercentile int `json:"is_percentile"` 33 | InstMask int `json:"inst_mask"` 34 | NotesPct int `json:"notes_pct"` 35 | IsFriend int `json:"is_friend"` 36 | UnnamedBand int `json:"unnamed_band"` 37 | PGUID string `json:"pguid"` 38 | ORank int `json:"orank"` 39 | } 40 | 41 | type AccPlayerGetService struct { 42 | } 43 | 44 | // a function that gets the acc_id and returns the proper field of the Accomplishments model 45 | // for example, campaign_metascore is LBGoalValueCampaignMetascore in models.Accomplishments 46 | func getAccomplishmentField(accID string, accomplishments models.Accomplishments) []models.AccomplishmentScoreEntry { 47 | switch accID { 48 | case "campaign_metascore": 49 | return accomplishments.LBGoalValueCampaignMetascore 50 | case "acc_tourgoldlocal1": 51 | return accomplishments.LBGoalValueAccTourgoldlocal1 52 | case "acc_tourgoldlocal2": 53 | return accomplishments.LBGoalValueAccTourgoldlocal2 54 | case "acc_tourgoldregional1": 55 | return accomplishments.LBGoalValueAccTourgoldregional1 56 | case "acc_tourgoldregional2": 57 | return accomplishments.LBGoalValueAccTourgoldregional2 58 | case "acc_tourgoldcontinental1": 59 | return accomplishments.LBGoalValueAccTourgoldcontinental1 60 | case "acc_tourgoldcontinental2": 61 | return accomplishments.LBGoalValueAccTourgoldcontinental2 62 | case "acc_tourgoldglobal1": 63 | return accomplishments.LBGoalValueAccTourgoldglobal1 64 | case "acc_tourgoldglobal2": 65 | return accomplishments.LBGoalValueAccTourgoldglobal2 66 | case "acc_tourgoldglobal3": 67 | return accomplishments.LBGoalValueAccTourgoldglobal3 68 | case "acc_overdrivemaintain3": 69 | return accomplishments.LBGoalValueAccOverdrivemaintain3 70 | case "acc_overdrivecareer": 71 | return accomplishments.LBGoalValueAccOverdrivecareer 72 | case "acc_careersaves": 73 | return accomplishments.LBGoalValueAccCareersaves 74 | case "acc_millionpoints": 75 | return accomplishments.LBGoalValueAccMillionpoints 76 | case "acc_bassstreaklarge": 77 | return accomplishments.LBGoalValueAccBassstreaklarge 78 | case "acc_hopothreehundredbass": 79 | return accomplishments.LBGoalValueAccHopothreehundredbass 80 | case "acc_drumfill170": 81 | return accomplishments.LBGoalValueAccDrumfill170 82 | case "acc_drumstreaklong": 83 | return accomplishments.LBGoalValueAccDrumstreaklong 84 | case "acc_deployguitarfour": 85 | return accomplishments.LBGoalValueAccDeployguitarfour 86 | case "acc_guitarstreaklarge": 87 | return accomplishments.LBGoalValueAccGuitarstreaklarge 88 | case "acc_keystreaklong": 89 | return accomplishments.LBGoalValueAccKeystreaklong 90 | case "acc_hopoonethousand": 91 | return accomplishments.LBGoalValueAccHopoonethousand 92 | case "acc_doubleawesomealot": 93 | return accomplishments.LBGoalValueAccDoubleawesomealot 94 | case "acc_tripleawesomealot": 95 | return accomplishments.LBGoalValueAccTripleawesomealot 96 | case "acc_probassstreakepic": 97 | return accomplishments.LBGoalValueAccProbassstreakepic 98 | case "acc_prodrumroll3": 99 | return accomplishments.LBGoalValueAccProdrumroll3 100 | case "acc_prodrumstreaklong": 101 | return accomplishments.LBGoalValueAccProdrumstreaklong 102 | case "acc_proguitarstreakepic": 103 | return accomplishments.LBGoalValueAccProguitarstreakepic 104 | case "acc_prokeystreaklong": 105 | return accomplishments.LBGoalValueAccProkeystreaklong 106 | case "acc_deployvocals": 107 | return accomplishments.LBGoalValueAccDeployvocals 108 | case "acc_deployvocalsonehundred": 109 | return accomplishments.LBGoalValueAccDeployvocalsonehundred 110 | default: 111 | return []models.AccomplishmentScoreEntry{} 112 | } 113 | } 114 | 115 | func (service AccPlayerGetService) Path() string { 116 | return "leaderboards/acc_player/get" 117 | } 118 | 119 | func (service AccPlayerGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 120 | var req AccPlayerGetRequest 121 | 122 | err := marshaler.UnmarshalRequest(data, &req) 123 | if err != nil { 124 | return "", err 125 | } 126 | 127 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 128 | 129 | if !validPIDres { 130 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 131 | return "", err 132 | } 133 | 134 | accomplishmentsCollection := database.Collection("accomplishments") 135 | 136 | // FindOne the accomplishment scores 137 | var accomplishments models.Accomplishments 138 | err = accomplishmentsCollection.FindOne(context.TODO(), bson.M{}).Decode(&accomplishments) 139 | 140 | if err != nil { 141 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 142 | } 143 | 144 | res := []AccPlayerGetResponse{} 145 | 146 | accSlice := getAccomplishmentField(req.AccID, accomplishments) 147 | 148 | // sort acc scores by score 149 | sort.Slice(accSlice, func(i, j int) bool { 150 | return accSlice[i].Score > accSlice[j].Score 151 | }) 152 | 153 | // find the player's score idx in the sorted list 154 | // if the player has no scores, just start with the first score 155 | playerScoreIdx := 0 156 | for idx, score := range accSlice { 157 | if score.PID == req.PID000 { 158 | playerScoreIdx = idx 159 | break 160 | } 161 | } 162 | 163 | // start and end idx must be in a window size of 20 otherwise the UI will act a bit buggy 164 | startIdx := (playerScoreIdx / 20) * 20 165 | endIdx := min(len(accSlice), startIdx+20) 166 | 167 | for i := startIdx; i < endIdx; i++ { 168 | score := accSlice[i] 169 | res = append(res, AccPlayerGetResponse{ 170 | PID: score.PID, 171 | Score: score.Score, 172 | DiffID: 0, 173 | Name: db.GetConsolePrefixedUsernameForPID(score.PID), 174 | IsPercentile: 0, 175 | IsFriend: 0, 176 | InstMask: 0, 177 | NotesPct: 0, 178 | UnnamedBand: 0, 179 | PGUID: "", 180 | Rank: i + 1, 181 | ORank: i + 1, 182 | }) 183 | } 184 | 185 | if len(res) == 0 { 186 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 187 | } else { 188 | return marshaler.MarshalResponse(service.Path(), res) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/acc_rankrange.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | db "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/protocols/jsonproto/marshaler" 9 | "rb3server/utils" 10 | "sort" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | ) 16 | 17 | type AccRankRangeGetRequest struct { 18 | Region string `json:"region"` 19 | SystemMS int `json:"system_ms"` 20 | AccID string `json:"acc_id"` 21 | MachineID string `json:"machine_id"` 22 | SessionGUID string `json:"session_guid"` 23 | PID000 int `json:"pid000"` 24 | StartRank int `json:"start_rank"` 25 | EndRank int `json:"end_rank"` 26 | } 27 | 28 | type AccRankRangeGetResponse struct { 29 | PID int `json:"pid"` 30 | Name string `json:"name"` 31 | DiffID int `json:"diff_id"` 32 | Rank int `json:"rank"` 33 | Score int `json:"score"` 34 | IsPercentile int `json:"is_percentile"` 35 | InstMask int `json:"inst_mask"` 36 | NotesPct int `json:"notes_pct"` 37 | IsFriend int `json:"is_friend"` 38 | UnnamedBand int `json:"unnamed_band"` 39 | PGUID string `json:"pguid"` 40 | ORank int `json:"orank"` 41 | } 42 | 43 | type AccRankRangeGetService struct { 44 | } 45 | 46 | func (service AccRankRangeGetService) Path() string { 47 | return "leaderboards/acc_rankrange/get" 48 | } 49 | 50 | func (service AccRankRangeGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 51 | var req AccRankRangeGetRequest 52 | 53 | err := marshaler.UnmarshalRequest(data, &req) 54 | if err != nil { 55 | return "", err 56 | } 57 | 58 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 59 | 60 | if !validPIDres { 61 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 62 | return "", err 63 | } 64 | 65 | accomplishmentsCollection := database.Collection("accomplishments") 66 | 67 | // FindOne the accomplishment scores 68 | var accomplishments models.Accomplishments 69 | err = accomplishmentsCollection.FindOne(context.TODO(), bson.M{}).Decode(&accomplishments) 70 | 71 | if err != nil { 72 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 73 | } 74 | 75 | res := []AccRankRangeGetResponse{} 76 | 77 | accSlice := getAccomplishmentField(req.AccID, accomplishments) 78 | 79 | // sort acc scores by score 80 | sort.Slice(accSlice, func(i, j int) bool { 81 | return accSlice[i].Score > accSlice[j].Score 82 | }) 83 | 84 | // get the scores in the range, and append them to the response 85 | for i := req.StartRank - 1; i < req.EndRank-1; i++ { 86 | if i >= len(accSlice) { 87 | break 88 | } 89 | 90 | score := accSlice[i] 91 | res = append(res, AccRankRangeGetResponse{ 92 | PID: score.PID, 93 | Name: db.GetConsolePrefixedUsernameForPID(score.PID), 94 | DiffID: 0, 95 | Rank: i + 1, 96 | Score: score.Score, 97 | IsPercentile: 0, 98 | InstMask: 0, 99 | NotesPct: 0, 100 | IsFriend: 0, 101 | UnnamedBand: 0, 102 | PGUID: "", 103 | ORank: i + 1, 104 | }) 105 | } 106 | 107 | if len(res) == 0 { 108 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 109 | } else { 110 | return marshaler.MarshalResponse(service.Path(), res) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/battle_maxrank.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/protocols/jsonproto/marshaler" 7 | "rb3server/utils" 8 | 9 | "github.com/ihatecompvir/nex-go" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | ) 13 | 14 | type BattleMaxrankGetRequest struct { 15 | Region string `json:"region"` 16 | SystemMS int `json:"system_ms"` 17 | BattleID int `json:"battle_id"` 18 | MachineID string `json:"machine_id"` 19 | SessionGUID string `json:"session_guid"` 20 | PID000 int `json:"pid000"` 21 | } 22 | 23 | type BattleMaxrankGetResponse struct { 24 | MaxRank int `json:"max_rank"` 25 | } 26 | 27 | type BattleMaxrankGetService struct { 28 | } 29 | 30 | func (service BattleMaxrankGetService) Path() string { 31 | return "leaderboards/battle_maxrank/get" 32 | } 33 | 34 | func (service BattleMaxrankGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 35 | var req BattleMaxrankGetRequest 36 | err := marshaler.UnmarshalRequest(data, &req) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 42 | 43 | if !validPIDres { 44 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 45 | return "", err 46 | } 47 | 48 | scoresCollection := database.Collection("scores") 49 | 50 | numScores, err := scoresCollection.CountDocuments(context.TODO(), bson.M{"battle_id": req.BattleID}) 51 | if err != nil { 52 | return marshaler.MarshalResponse(service.Path(), []BattleMaxrankGetResponse{{ 53 | 0, 54 | }}) 55 | } 56 | 57 | res := []BattleMaxrankGetResponse{{ 58 | int(numScores), 59 | }} 60 | 61 | return marshaler.MarshalResponse(service.Path(), res) 62 | } 63 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/battle_player_get.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | 10 | db "rb3server/database" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | "go.mongodb.org/mongo-driver/mongo/options" 16 | ) 17 | 18 | type BattlePlayerGetRequest struct { 19 | Region string `json:"region"` 20 | SystemMS int `json:"system_ms"` 21 | BattleID int `json:"battle_id"` 22 | MachineID string `json:"machine_id"` 23 | SessionGUID string `json:"session_guid"` 24 | PID000 int `json:"pid000"` 25 | LBMode int `json:"lb_mode"` 26 | NumRows int `json:"num_rows"` 27 | } 28 | 29 | type BattlePlayerGetResponse struct { 30 | PID int `json:"pid"` 31 | Name string `json:"name"` 32 | DiffID int `json:"diff_id"` 33 | Rank int `json:"rank"` 34 | Score int `json:"score"` 35 | IsPercentile int `json:"is_percentile"` 36 | InstMask int `json:"inst_mask"` 37 | NotesPct int `json:"notes_pct"` 38 | IsFriend int `json:"is_friend"` 39 | UnnamedBand int `json:"unnamed_band"` 40 | PGUID string `json:"pguid"` 41 | ORank int `json:"orank"` 42 | } 43 | 44 | type BattlePlayerGetService struct { 45 | } 46 | 47 | func (service BattlePlayerGetService) Path() string { 48 | return "leaderboards/battle_player/get" 49 | } 50 | 51 | func (service BattlePlayerGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 52 | var req BattlePlayerGetRequest 53 | 54 | err := marshaler.UnmarshalRequest(data, &req) 55 | if err != nil { 56 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 57 | } 58 | 59 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 60 | 61 | if !validPIDres { 62 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 63 | return "", err 64 | } 65 | 66 | scoresCollection := database.Collection("scores") 67 | 68 | var playerScore models.Score 69 | err = scoresCollection.FindOne(context.TODO(), bson.M{"battle_id": req.BattleID, "pid": req.PID000}).Decode(&playerScore) 70 | if err != nil && err != mongo.ErrNoDocuments { 71 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 72 | } 73 | 74 | if err == mongo.ErrNoDocuments { 75 | err = scoresCollection.FindOne(context.TODO(), bson.M{"battle_id": req.BattleID}, &options.FindOneOptions{ 76 | Sort: bson.M{"score": -1}, 77 | }).Decode(&playerScore) 78 | if err != nil && err != mongo.ErrNoDocuments { 79 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 80 | } 81 | } 82 | 83 | playerScoreIdx, err := scoresCollection.CountDocuments(context.TODO(), bson.M{"battle_id": req.BattleID, "score": bson.M{"$gt": playerScore.Score}}) 84 | if err != nil { 85 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 86 | } 87 | 88 | startRank := playerScoreIdx - (playerScoreIdx % 19) 89 | endRank := startRank + 19 90 | 91 | cursor, err := scoresCollection.Find(context.TODO(), bson.M{"battle_id": req.BattleID}, &options.FindOptions{ 92 | Skip: &startRank, 93 | Limit: &endRank, 94 | Sort: bson.M{"score": -1}, 95 | }) 96 | 97 | if err != nil { 98 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 99 | } 100 | 101 | var res []BattlePlayerGetResponse 102 | 103 | var idx int64 = startRank + 1 104 | 105 | for cursor.Next(context.Background()) { 106 | var score models.Score 107 | err := cursor.Decode(&score) 108 | 109 | if err != nil { 110 | log.Println("Failed to decode score:", err) 111 | continue 112 | } 113 | 114 | isBandScore := score.RoleID == 10 115 | 116 | if isBandScore { 117 | res = append(res, BattlePlayerGetResponse{ 118 | PID: score.OwnerPID, 119 | Name: db.GetBandNameForBandID(score.OwnerPID), 120 | DiffID: score.DiffID, 121 | Rank: int(idx), 122 | Score: score.Score, 123 | IsPercentile: 0, 124 | InstMask: score.InstrumentMask, 125 | NotesPct: score.NotesPercent, 126 | IsFriend: 0, 127 | UnnamedBand: 0, 128 | PGUID: "", 129 | ORank: int(idx), 130 | }) 131 | } else { 132 | res = append(res, BattlePlayerGetResponse{ 133 | PID: score.OwnerPID, 134 | Name: db.GetConsolePrefixedUsernameForPID(score.OwnerPID), 135 | DiffID: score.DiffID, 136 | Rank: int(idx), 137 | Score: score.Score, 138 | IsPercentile: 0, 139 | InstMask: score.InstrumentMask, 140 | NotesPct: score.NotesPercent, 141 | IsFriend: 0, 142 | UnnamedBand: 0, 143 | PGUID: "", 144 | ORank: int(idx), 145 | }) 146 | } 147 | 148 | idx++ 149 | } 150 | 151 | if len(res) == 0 { 152 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 153 | } else { 154 | return marshaler.MarshalResponse(service.Path(), res) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/battle_rankrange.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | 10 | db "rb3server/database" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | "go.mongodb.org/mongo-driver/mongo/options" 16 | ) 17 | 18 | type BattleRankRangeGetRequest struct { 19 | Region string `json:"region"` 20 | SystemMS int `json:"system_ms"` 21 | MachineID string `json:"machine_id"` 22 | SessionGUID string `json:"session_guid"` 23 | PID000 int `json:"pid000"` 24 | BattleID int `json:"battle_id"` 25 | StartRank int `json:"start_rank"` 26 | EndRank int `json:"end_rank"` 27 | } 28 | 29 | type BattleRankRangeGetResponse struct { 30 | PID int `json:"pid"` 31 | Name string `json:"name"` 32 | DiffID int `json:"diff_id"` 33 | Rank int `json:"rank"` 34 | Score int `json:"score"` 35 | IsPercentile int `json:"is_percentile"` 36 | InstMask int `json:"inst_mask"` 37 | NotesPct int `json:"notes_pct"` 38 | IsFriend int `json:"is_friend"` 39 | UnnamedBand int `json:"unnamed_band"` 40 | PGUID string `json:"pguid"` 41 | ORank int `json:"orank"` 42 | } 43 | 44 | type BattleRankRangeGetService struct { 45 | } 46 | 47 | func (service BattleRankRangeGetService) Path() string { 48 | return "leaderboards/battle_rankrange/get" 49 | } 50 | 51 | func (service BattleRankRangeGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 52 | var req BattleRankRangeGetRequest 53 | 54 | err := marshaler.UnmarshalRequest(data, &req) 55 | if err != nil { 56 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 57 | } 58 | 59 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 60 | 61 | if !validPIDres { 62 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 63 | return "", err 64 | } 65 | 66 | scoresCollection := database.Collection("scores") 67 | 68 | startRank := int64(req.StartRank - 1) 69 | endRank := int64((req.EndRank - req.StartRank) - 1) 70 | 71 | // get cursor of scores filtered by song_id and role_id, with starting and ending ranks as indices (like scores[start_rank:end_rank]), sorted by biggest to smallest 72 | // so if start_rank is 1 and end_rank is 10, we get the top 10 scores 73 | cursor, err := scoresCollection.Find(context.TODO(), bson.M{"battle_id": req.BattleID}, &options.FindOptions{ 74 | Skip: &startRank, 75 | Limit: &endRank, 76 | Sort: bson.M{"score": -1}, 77 | }) 78 | 79 | if err != nil { 80 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 81 | } 82 | 83 | var res []BattleRankRangeGetResponse 84 | 85 | var idx int = req.StartRank 86 | 87 | // iterate through the cursor and append each score to the response 88 | for cursor.Next(context.Background()) { 89 | var score models.Score 90 | err := cursor.Decode(&score) 91 | 92 | if err != nil { 93 | log.Println("Failed to decode score:", err) 94 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 95 | } 96 | 97 | isBandScore := score.RoleID == 10 98 | 99 | if isBandScore { 100 | res = append(res, BattleRankRangeGetResponse{ 101 | PID: score.OwnerPID, 102 | Name: db.GetBandNameForBandID(score.OwnerPID), 103 | DiffID: score.DiffID, 104 | Rank: idx, 105 | Score: score.Score, 106 | IsPercentile: 0, 107 | InstMask: score.InstrumentMask, 108 | NotesPct: score.NotesPercent, 109 | IsFriend: 0, 110 | UnnamedBand: 0, 111 | PGUID: "", 112 | ORank: idx, 113 | }) 114 | } else { 115 | res = append(res, BattleRankRangeGetResponse{ 116 | PID: score.OwnerPID, 117 | Name: db.GetConsolePrefixedUsernameForPID(score.OwnerPID), 118 | DiffID: score.DiffID, 119 | Rank: idx, 120 | Score: score.Score, 121 | IsPercentile: 0, 122 | InstMask: score.InstrumentMask, 123 | NotesPct: score.NotesPercent, 124 | IsFriend: 0, 125 | UnnamedBand: 0, 126 | PGUID: "", 127 | ORank: idx, 128 | }) 129 | } 130 | 131 | idx++ 132 | } 133 | 134 | if len(res) == 0 { 135 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 136 | } else { 137 | return marshaler.MarshalResponse(service.Path(), res) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/friends_update.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | 14 | db "rb3server/database" 15 | ) 16 | 17 | type FriendsUpdateRequest struct { 18 | Region string `json:"region"` 19 | SystemMS int `json:"system_ms"` 20 | MachineID string `json:"machine_id"` 21 | SessionGUID string `json:"session_guid"` 22 | PID int `json:"pid"` 23 | Names []string `json:"nameXXX"` 24 | GUIDs []string `json:"guidXXX"` 25 | } 26 | 27 | type FriendsUpdateResponse struct { 28 | Success int `json:"success"` 29 | } 30 | 31 | type FriendsUpdateService struct { 32 | } 33 | 34 | func (service FriendsUpdateService) Path() string { 35 | return "leaderboards/friends/update" 36 | } 37 | 38 | func (service FriendsUpdateService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 39 | var req FriendsUpdateRequest 40 | 41 | err := marshaler.UnmarshalRequest(data, &req) 42 | if err != nil { 43 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 44 | } 45 | 46 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 47 | 48 | if !validPIDres { 49 | log.Println("Client is attempting to update friends without a valid server-assigned PID, rejecting call") 50 | return "", err 51 | } 52 | 53 | log.Println("Updating friends list for player ", req.PID) 54 | 55 | // Lookup the user by their PID 56 | var user models.User 57 | err = database.Collection("users").FindOne(context.Background(), bson.M{"pid": req.PID}).Decode(&user) 58 | if err != nil { 59 | log.Println("Failed to find user with PID ", req.PID, ": ", err) 60 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 61 | } 62 | 63 | // loop through every name and guid and update the friends list 64 | for _, name := range req.Names { 65 | var pid int = 0 66 | pid = db.GetPIDForUsername(name) 67 | 68 | if pid == 0 { 69 | continue 70 | } 71 | 72 | // add the pid to the "friends" int array in the user's document if it is not already there 73 | filter := bson.M{"pid": req.PID} 74 | update := bson.M{ 75 | "$addToSet": bson.M{"friends": pid}, 76 | } 77 | 78 | _, err := database.Collection("users").UpdateOne(context.Background(), filter, update) 79 | if err != nil { 80 | log.Println("Failed to update friends list for player ", req.PID, " with friend PID ", pid, ": ", err) 81 | continue 82 | } 83 | } 84 | 85 | res := []FriendsUpdateResponse{{0}} 86 | 87 | return marshaler.MarshalResponse(service.Path(), res) 88 | } 89 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/maxrank.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/protocols/jsonproto/marshaler" 7 | "rb3server/utils" 8 | 9 | "github.com/ihatecompvir/nex-go" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | ) 13 | 14 | type MaxrankGetRequest struct { 15 | Region string `json:"region"` 16 | SystemMS int `json:"system_ms"` 17 | SongID int `json:"song_id"` 18 | MachineID string `json:"machine_id"` 19 | SessionGUID string `json:"session_guid"` 20 | RoleID int `json:"role_id"` 21 | PID000 int `json:"pid000"` 22 | LBType int `json:"lb_type"` 23 | } 24 | 25 | type MaxrankGetResponse struct { 26 | MaxRank int `json:"max_rank"` 27 | } 28 | 29 | type MaxrankGetService struct { 30 | } 31 | 32 | func (service MaxrankGetService) Path() string { 33 | return "leaderboards/maxrank/get" 34 | } 35 | 36 | func (service MaxrankGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 37 | var req MaxrankGetRequest 38 | err := marshaler.UnmarshalRequest(data, &req) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 44 | 45 | if !validPIDres { 46 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 47 | return "", err 48 | } 49 | 50 | scoresCollection := database.Collection("scores") 51 | 52 | numScores, err := scoresCollection.CountDocuments(context.TODO(), bson.M{"song_id": req.SongID, "role_id": req.RoleID}) 53 | if err != nil { 54 | return marshaler.MarshalResponse(service.Path(), []MaxrankGetResponse{{ 55 | 0, 56 | }}) 57 | } 58 | 59 | res := []MaxrankGetResponse{{ 60 | int(numScores), 61 | }} 62 | 63 | return marshaler.MarshalResponse(service.Path(), res) 64 | } 65 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/player.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | db "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/protocols/jsonproto/marshaler" 9 | "rb3server/utils" 10 | 11 | "github.com/ihatecompvir/nex-go" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | "go.mongodb.org/mongo-driver/mongo/options" 15 | ) 16 | 17 | type PlayerGetRequest struct { 18 | Region string `json:"region"` 19 | SystemMS int `json:"system_ms"` 20 | SongID int `json:"song_id"` 21 | MachineID string `json:"machine_id"` 22 | SessionGUID string `json:"session_guid"` 23 | PID000 int `json:"pid000"` 24 | RoleID int `json:"role_id"` 25 | LBType int `json:"lb_type"` 26 | LBMode int `json:"lb_mode"` 27 | NumRows int `json:"num_rows"` 28 | } 29 | 30 | type PlayerGetResponse struct { 31 | PID int `json:"pid"` 32 | Name string `json:"name"` 33 | DiffID int `json:"diff_id"` 34 | Rank int `json:"rank"` 35 | Score int `json:"score"` 36 | IsPercentile int `json:"is_percentile"` 37 | InstMask int `json:"inst_mask"` 38 | NotesPct int `json:"notes_pct"` 39 | IsFriend int `json:"is_friend"` 40 | UnnamedBand int `json:"unnamed_band"` 41 | PGUID string `json:"pguid"` 42 | ORank int `json:"orank"` 43 | } 44 | 45 | type PlayerGetService struct { 46 | } 47 | 48 | func (service PlayerGetService) Path() string { 49 | return "leaderboards/player/get" 50 | } 51 | 52 | func (service PlayerGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 53 | var req PlayerGetRequest 54 | 55 | err := marshaler.UnmarshalRequest(data, &req) 56 | if err != nil { 57 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 58 | } 59 | 60 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 61 | 62 | if !validPIDres { 63 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 64 | return "", err 65 | } 66 | 67 | scoresCollection := database.Collection("scores") 68 | 69 | var playerScore models.Score 70 | err = scoresCollection.FindOne(context.TODO(), bson.M{"song_id": req.SongID, "role_id": req.RoleID, "pid": req.PID000}).Decode(&playerScore) 71 | if err != nil && err != mongo.ErrNoDocuments { 72 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 73 | } 74 | 75 | if err == mongo.ErrNoDocuments { 76 | err = scoresCollection.FindOne(context.TODO(), bson.M{"song_id": req.SongID, "role_id": req.RoleID}, &options.FindOneOptions{ 77 | Sort: bson.M{"score": -1}, 78 | }).Decode(&playerScore) 79 | if err != nil && err != mongo.ErrNoDocuments { 80 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 81 | } 82 | } 83 | 84 | playerScoreIdx, err := scoresCollection.CountDocuments(context.TODO(), bson.M{"song_id": req.SongID, "role_id": req.RoleID, "score": bson.M{"$gt": playerScore.Score}}) 85 | if err != nil { 86 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 87 | } 88 | 89 | startRank := playerScoreIdx - (playerScoreIdx % 19) 90 | endRank := startRank + 19 91 | 92 | cursor, err := scoresCollection.Find(context.TODO(), bson.M{"song_id": req.SongID, "role_id": req.RoleID}, &options.FindOptions{ 93 | Skip: &startRank, 94 | Limit: &endRank, 95 | Sort: bson.M{"score": -1}, 96 | }) 97 | 98 | if err != nil { 99 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 100 | } 101 | 102 | var res []PlayerGetResponse 103 | 104 | var idx int64 = startRank + 1 105 | 106 | for cursor.Next(context.Background()) { 107 | var score models.Score 108 | err := cursor.Decode(&score) 109 | 110 | if err != nil { 111 | log.Println("Failed to decode score:", err) 112 | continue 113 | } 114 | 115 | isBandScore := score.RoleID == 10 116 | 117 | if isBandScore { 118 | res = append(res, PlayerGetResponse{ 119 | PID: score.OwnerPID, 120 | Name: db.GetBandNameForBandID(score.OwnerPID), 121 | DiffID: score.DiffID, 122 | Rank: int(idx), 123 | Score: score.Score, 124 | IsPercentile: 0, 125 | InstMask: score.InstrumentMask, 126 | NotesPct: score.NotesPercent, 127 | IsFriend: 0, 128 | UnnamedBand: 0, 129 | PGUID: "", 130 | ORank: int(idx), 131 | }) 132 | } else { 133 | res = append(res, PlayerGetResponse{ 134 | PID: score.OwnerPID, 135 | Name: db.GetConsolePrefixedUsernameForPID(score.OwnerPID), 136 | DiffID: score.DiffID, 137 | Rank: int(idx), 138 | Score: score.Score, 139 | IsPercentile: 0, 140 | InstMask: score.InstrumentMask, 141 | NotesPct: score.NotesPercent, 142 | IsFriend: 0, 143 | UnnamedBand: 0, 144 | PGUID: "", 145 | ORank: int(idx), 146 | }) 147 | } 148 | 149 | idx++ 150 | } 151 | 152 | if len(res) == 0 { 153 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 154 | } else { 155 | return marshaler.MarshalResponse(service.Path(), res) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/playerranks.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | "go.mongodb.org/mongo-driver/mongo/options" 14 | ) 15 | 16 | type PlayerranksGetRequest struct { 17 | Region string `json:"region"` 18 | SystemMS int `json:"system_ms"` 19 | MachineID string `json:"machine_id"` 20 | SessionGUID string `json:"session_guid"` 21 | PID int `json:"pid"` 22 | RoleID int `json:"role_id"` 23 | SongIDs []int `json:"song_idXXX"` 24 | } 25 | 26 | type PlayerranksGetResponse struct { 27 | SongID int `json:"song_id"` 28 | Rank int `json:"rank"` 29 | IsPercentile int `json:"is_percentile"` 30 | } 31 | 32 | type PlayerranksGetService struct { 33 | } 34 | 35 | func (service PlayerranksGetService) Path() string { 36 | return "leaderboards/playerranks/get" 37 | } 38 | 39 | func (service PlayerranksGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 40 | var req PlayerranksGetRequest 41 | 42 | err := marshaler.UnmarshalRequest(data, &req) 43 | if err != nil { 44 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 45 | } 46 | 47 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 48 | 49 | if !validPIDres { 50 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 51 | return "", err 52 | } 53 | 54 | scoresCollection := database.Collection("scores") 55 | 56 | res := []PlayerranksGetResponse{} 57 | 58 | for _, id := range req.SongIDs { 59 | var playerScore models.Score 60 | err = scoresCollection.FindOne(context.TODO(), bson.M{"song_id": id, "role_id": req.RoleID, "pid": req.PID}).Decode(&playerScore) 61 | if err != nil && err != mongo.ErrNoDocuments { 62 | log.Println(err) 63 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 64 | } 65 | 66 | if err == mongo.ErrNoDocuments { 67 | err = scoresCollection.FindOne(context.TODO(), bson.M{"song_id": id, "role_id": req.RoleID}, &options.FindOneOptions{ 68 | Sort: bson.M{"score": -1}, 69 | }).Decode(&playerScore) 70 | if err != nil && err != mongo.ErrNoDocuments { 71 | log.Println(err) 72 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 73 | } 74 | } 75 | 76 | playerScoreIdx, err := scoresCollection.CountDocuments(context.TODO(), bson.M{"song_id": id, "role_id": req.RoleID, "score": bson.M{"$gt": playerScore.Score}}) 77 | if err != nil { 78 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 79 | } 80 | 81 | var response PlayerranksGetResponse 82 | response.SongID = id 83 | response.Rank = int(playerScoreIdx) 84 | response.IsPercentile = 0 85 | 86 | res = append(res, response) 87 | } 88 | 89 | if len(res) == 0 { 90 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 91 | } else { 92 | return marshaler.MarshalResponse(service.Path(), res) 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/leaderboards/rankrange.go: -------------------------------------------------------------------------------- 1 | package leaderboard 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | 10 | db "rb3server/database" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | "go.mongodb.org/mongo-driver/mongo/options" 16 | ) 17 | 18 | type RankRangeGetRequest struct { 19 | Region string `json:"region"` 20 | SystemMS int `json:"system_ms"` 21 | SongID int `json:"song_id"` 22 | MachineID string `json:"machine_id"` 23 | SessionGUID string `json:"session_guid"` 24 | PID000 int `json:"pid000"` 25 | RoleID int `json:"role_id"` 26 | LBType int `json:"lb_type"` 27 | StartRank int `json:"start_rank"` 28 | EndRank int `json:"end_rank"` 29 | } 30 | 31 | type RankRangeGetResponse struct { 32 | PID int `json:"pid"` 33 | Name string `json:"name"` 34 | DiffID int `json:"diff_id"` 35 | Rank int `json:"rank"` 36 | Score int `json:"score"` 37 | IsPercentile int `json:"is_percentile"` 38 | InstMask int `json:"inst_mask"` 39 | NotesPct int `json:"notes_pct"` 40 | IsFriend int `json:"is_friend"` 41 | UnnamedBand int `json:"unnamed_band"` 42 | PGUID string `json:"pguid"` 43 | ORank int `json:"orank"` 44 | } 45 | 46 | type RankRangeGetService struct { 47 | } 48 | 49 | func (service RankRangeGetService) Path() string { 50 | return "leaderboards/rankrange/get" 51 | } 52 | 53 | func (service RankRangeGetService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 54 | var req RankRangeGetRequest 55 | 56 | err := marshaler.UnmarshalRequest(data, &req) 57 | if err != nil { 58 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 59 | } 60 | 61 | validPIDres, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 62 | 63 | if !validPIDres { 64 | log.Println("Client is attempting to get leaderboards without a valid server-assigned PID, rejecting call") 65 | return "", err 66 | } 67 | 68 | if req.LBType == 1 { 69 | // friends leaderboard not supported 70 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 71 | } 72 | 73 | scoresCollection := database.Collection("scores") 74 | 75 | startRank := int64(req.StartRank - 1) 76 | endRank := int64((req.EndRank - req.StartRank) - 1) 77 | 78 | // get cursor of scores filtered by song_id and role_id, with starting and ending ranks as indices (like scores[start_rank:end_rank]), sorted by biggest to smallest 79 | // so if start_rank is 1 and end_rank is 10, we get the top 10 scores 80 | cursor, err := scoresCollection.Find(context.TODO(), bson.M{"song_id": req.SongID, "role_id": req.RoleID}, &options.FindOptions{ 81 | Skip: &startRank, 82 | Limit: &endRank, 83 | Sort: bson.M{"score": -1}, 84 | }) 85 | 86 | if err != nil { 87 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 88 | } 89 | 90 | var res []RankRangeGetResponse 91 | 92 | var idx int = req.StartRank 93 | 94 | // iterate through the cursor and append each score to the response 95 | for cursor.Next(context.Background()) { 96 | var score models.Score 97 | err := cursor.Decode(&score) 98 | 99 | if err != nil { 100 | log.Println("Failed to decode score:", err) 101 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 102 | } 103 | 104 | isBandScore := score.RoleID == 10 105 | 106 | if isBandScore { 107 | res = append(res, RankRangeGetResponse{ 108 | PID: score.OwnerPID, 109 | Name: db.GetBandNameForBandID(score.OwnerPID), 110 | DiffID: score.DiffID, 111 | Rank: idx, 112 | Score: score.Score, 113 | IsPercentile: 0, 114 | InstMask: score.InstrumentMask, 115 | NotesPct: score.NotesPercent, 116 | IsFriend: 0, 117 | UnnamedBand: 0, 118 | PGUID: "", 119 | ORank: idx, 120 | }) 121 | } else { 122 | res = append(res, RankRangeGetResponse{ 123 | PID: score.OwnerPID, 124 | Name: db.GetConsolePrefixedUsernameForPID(score.OwnerPID), 125 | DiffID: score.DiffID, 126 | Rank: idx, 127 | Score: score.Score, 128 | IsPercentile: 0, 129 | InstMask: score.InstrumentMask, 130 | NotesPct: score.NotesPercent, 131 | IsFriend: 0, 132 | UnnamedBand: 0, 133 | PGUID: "", 134 | ORank: idx, 135 | }) 136 | } 137 | 138 | idx++ 139 | } 140 | 141 | if len(res) == 0 { 142 | return marshaler.GenerateEmptyJSONResponse(service.Path()), nil 143 | } else { 144 | return marshaler.MarshalResponse(service.Path(), res) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/misc/get_accounts_setlist_creation_status.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "log" 5 | "rb3server/protocols/jsonproto/marshaler" 6 | 7 | "github.com/ihatecompvir/nex-go" 8 | "go.mongodb.org/mongo-driver/mongo" 9 | ) 10 | 11 | type SetlistCreationStatusRequest struct { 12 | Region string `json:"region"` 13 | SystemMS int `json:"system_ms"` 14 | MachineID string `json:"machine_id"` 15 | SessionGUID string `json:"session_guid"` 16 | PID int `json:"pid"` 17 | } 18 | 19 | type SetlistCreationStatusResponse struct { 20 | PID int `json:"pid"` 21 | Creator int `json:"creator"` 22 | } 23 | 24 | type SetlistCreationStatusService struct { 25 | } 26 | 27 | func (service SetlistCreationStatusService) Path() string { 28 | return "misc/get_accounts_setlist_creation_status" 29 | } 30 | 31 | func (service SetlistCreationStatusService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 32 | var req SetlistCreationStatusRequest 33 | err := marshaler.UnmarshalRequest(data, &req) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | if req.PID != int(client.PlayerID()) { 39 | log.Println("Client-supplied PID did not match server-assigned PID, rejecting setlist creation request") 40 | return "", err 41 | } 42 | 43 | res := []SetlistCreationStatusResponse{{ 44 | req.PID, 45 | 0, 46 | }} 47 | 48 | return marshaler.MarshalResponse(service.Path(), res) 49 | } 50 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/misc/option_data.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "github.com/ihatecompvir/nex-go" 5 | "go.mongodb.org/mongo-driver/mongo" 6 | ) 7 | 8 | type OptionDataRequest struct { 9 | Region string `json:"region"` 10 | SystemMS int `json:"system_ms"` 11 | MachineID string `json:"machine_id"` 12 | SessionGUID string `json:"session_guid"` 13 | PID int `json:"pid"` 14 | } 15 | 16 | type OptionDataService struct { 17 | } 18 | 19 | func (service OptionDataService) Path() string { 20 | return "misc/option_data" 21 | } 22 | 23 | func (service OptionDataService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 24 | // no practical reason to store players options on the server side since they won't even sync if they go to a different console, so this is only here to reduce errors in the server log 25 | return "", nil 26 | } 27 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/misc/sync_available_songs.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "log" 5 | "rb3server/protocols/jsonproto/marshaler" 6 | "rb3server/utils" 7 | 8 | "github.com/ihatecompvir/nex-go" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | ) 12 | 13 | type MiscSyncAvailableSongsRequest struct { 14 | Region string `json:"region"` 15 | SystemMS int `json:"system_ms"` 16 | MachineID string `json:"machine_id"` 17 | SessionGUID string `json:"session_guid"` 18 | PIDs []int `json:"pidXXX"` 19 | SIDs string `json:"sids"` 20 | USIDs string `json:"usids"` 21 | } 22 | 23 | type MiscSyncAvailableSongsResponse struct { 24 | RetCode int `json:"ret_code"` 25 | } 26 | 27 | type MiscSyncAvailableSongsService struct { 28 | } 29 | 30 | func (service MiscSyncAvailableSongsService) Path() string { 31 | return "misc/sync_available_songs" 32 | } 33 | 34 | func (service MiscSyncAvailableSongsService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 35 | var req MiscSyncAvailableSongsRequest 36 | err := marshaler.UnmarshalRequest(data, &req) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | // make sure the pids array is not empty 42 | if len(req.PIDs) == 0 { 43 | log.Println("PID array is empty, rejecting available song sync") 44 | return "", err 45 | } 46 | 47 | if req.PIDs[0] == 0 { 48 | // it is a machine, not a player, so just respond with a blank response 49 | return marshaler.MarshalResponse(service.Path(), []MiscSyncAvailableSongsResponse{{0}}) 50 | } 51 | 52 | res, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PIDs[0])) 53 | 54 | if !res { 55 | log.Println("Client is attempting to sync available songs without a valid server-assigned PID, rejecting call") 56 | return "", err 57 | } 58 | 59 | usersCollection := database.Collection("users") 60 | 61 | for _, pid := range req.PIDs { 62 | // update sids and usids fields on user with pid 63 | _, err := usersCollection.UpdateOne(nil, bson.M{"pid": pid}, bson.M{"$set": bson.M{"sids": req.SIDs, "usids": req.USIDs}}) 64 | 65 | if err != nil { 66 | log.Printf("Could not update songlist for PID %v: %v\n", pid, err) 67 | return marshaler.MarshalResponse(service.Path(), []MiscSyncAvailableSongsResponse{{0}}) 68 | } 69 | } 70 | 71 | return marshaler.MarshalResponse(service.Path(), []MiscSyncAvailableSongsResponse{{1}}) 72 | } 73 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/music_library/sort_and_filters.go: -------------------------------------------------------------------------------- 1 | package music_library 2 | 3 | import ( 4 | "rb3server/protocols/jsonproto/marshaler" 5 | 6 | "github.com/ihatecompvir/nex-go" 7 | "go.mongodb.org/mongo-driver/mongo" 8 | ) 9 | 10 | type SortAndFiltersRequest struct { 11 | Region string `json:"region"` 12 | SystemMS int `json:"system_ms"` 13 | MachineID string `json:"machine_id"` 14 | SessionGUID string `json:"session_guid"` 15 | Filters string `json:"filters"` 16 | Sort string `json:"sort"` 17 | Mode string `json:"mode"` 18 | } 19 | 20 | type SortAndFiltersResponse struct { 21 | Success int `json:"success"` 22 | } 23 | 24 | type SortAndFiltersService struct { 25 | } 26 | 27 | func (service SortAndFiltersService) Path() string { 28 | return "music_library/sort_and_filters" 29 | } 30 | 31 | func (service SortAndFiltersService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 32 | // no practical reason to log which filters and sorting options players are using. this is only here to reduce errors in the server log 33 | return marshaler.MarshalResponse(service.Path(), SortAndFiltersResponse{1}) 34 | } 35 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/scores/battles_record.go: -------------------------------------------------------------------------------- 1 | package scores 2 | 3 | import ( 4 | "context" 5 | "log" 6 | db "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/protocols/jsonproto/marshaler" 9 | "rb3server/utils" 10 | "strconv" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | "go.mongodb.org/mongo-driver/mongo/options" 16 | ) 17 | 18 | type BattleScoreRecordRequest struct { 19 | Region string `json:"region"` 20 | Score int `json:"score"` 21 | SystemMS int `json:"system_ms"` 22 | MachineID string `json:"machine_id"` 23 | SessionGUID string `json:"session_guid"` 24 | PIDs []int `json:"pidXXX"` 25 | BattleID int `json:"battle_id"` 26 | Slots []int `json:"slotXXX"` 27 | } 28 | 29 | type BattleScoreRecordResponse struct { 30 | ID int `json:"id"` 31 | IsBOI int `json:"is_boi"` 32 | InstaRank int `json:"insta_rank"` 33 | IsPercentile int `json:"is_percentile"` 34 | Part1 string `json:"part_1"` 35 | Part2 string `json:"part_2"` 36 | } 37 | 38 | type BattleScoreRecordService struct { 39 | } 40 | 41 | func (service BattleScoreRecordService) Path() string { 42 | return "battles/record" 43 | } 44 | 45 | func (service BattleScoreRecordService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 46 | var req BattleScoreRecordRequest 47 | 48 | err := marshaler.UnmarshalRequest(data, &req) 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | // make sure the pids array is not empty 54 | if len(req.PIDs) == 0 { 55 | log.Println("PID array is empty, rejecting battle score record") 56 | return "", err 57 | } 58 | 59 | pidRes, _ := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PIDs[0])) 60 | 61 | if !pidRes { 62 | log.Println("Client is attempting to record a score without a valid server-assigned PID, rejecting call") 63 | return "", err 64 | } 65 | 66 | // make sure the player is not trying to submit a score for a battle that is expired 67 | isExpired, _ := db.GetBattleExpiryInfo(req.BattleID) 68 | if isExpired { 69 | log.Println("Battle", req.BattleID, "is expired, rejecting battle score record") 70 | return "", err 71 | } 72 | 73 | scoresCollection := database.Collection("scores") 74 | 75 | scoreHigher := []bool{} 76 | currentScore := []int{} 77 | 78 | for _, pid := range req.PIDs { 79 | var score models.Score 80 | score.OwnerPID = pid 81 | score.BattleID = req.BattleID 82 | score.Score = req.Score 83 | 84 | // Retrieve the existing score 85 | var existingScore models.Score 86 | err := scoresCollection.FindOne(context.TODO(), bson.M{"battle_id": req.BattleID, "pid": score.OwnerPID}).Decode(&existingScore) 87 | 88 | isNewScoreHigher := err == mongo.ErrNoDocuments || score.Score > existingScore.Score 89 | scoreHigher = append(scoreHigher, isNewScoreHigher) 90 | 91 | // Only update if the new score is higher 92 | if isNewScoreHigher { 93 | _, err = scoresCollection.UpdateOne( 94 | nil, 95 | bson.M{"battle_id": req.BattleID, "pid": score.OwnerPID}, 96 | bson.D{ 97 | {"$set", bson.D{ 98 | {"battle_id", score.BattleID}, 99 | {"pid", score.OwnerPID}, 100 | {"score", score.Score}, 101 | }}, 102 | }, 103 | options.Update().SetUpsert(true), 104 | ) 105 | 106 | currentScore = append(currentScore, score.Score) 107 | } else { 108 | currentScore = append(currentScore, existingScore.Score) 109 | } 110 | } 111 | 112 | res := []BattleScoreRecordResponse{} 113 | 114 | numPids := len(req.PIDs) 115 | 116 | for i := 0; i < (numPids / 2); i++ { 117 | playerScoreIdx, _ := scoresCollection.CountDocuments(context.TODO(), bson.M{"battle_id": req.BattleID, "score": bson.M{"$gt": req.Score}}) 118 | 119 | // Find the next highest score 120 | var nextHighestScore models.Score 121 | err = scoresCollection.FindOne(context.TODO(), bson.M{ 122 | "battle_id": req.BattleID, 123 | "score": bson.M{"$gt": req.Score}, 124 | }, options.FindOne().SetSort(bson.D{{"score", 1}})).Decode(&nextHighestScore) 125 | 126 | if scoreHigher[i] { 127 | instaRankString := "b" 128 | 129 | // Get the name of the player who has the next highest score 130 | var name string = db.GetUsernameForPID(nextHighestScore.OwnerPID) 131 | var nextScoreDiff int 132 | if err == mongo.ErrNoDocuments { 133 | name = "N/A" 134 | nextScoreDiff = 0 // No higher score exists 135 | } else { 136 | name = db.GetUsernameForPID(nextHighestScore.OwnerPID) 137 | nextScoreDiff = nextHighestScore.Score - req.Score 138 | } 139 | 140 | if nextScoreDiff < 2000 && nextScoreDiff > 0 { 141 | instaRankString = "i|" + strconv.Itoa(nextScoreDiff) + "|" + name 142 | } 143 | 144 | instarank := BattleScoreRecordResponse{ 145 | req.BattleID, 146 | 1, 147 | int(playerScoreIdx + 1), 148 | 0, 149 | "b", 150 | instaRankString, 151 | } 152 | 153 | res = append(res, instarank) 154 | } else { 155 | instarank := BattleScoreRecordResponse{ 156 | req.BattleID, 157 | 1, 158 | int(playerScoreIdx + 1), 159 | 0, 160 | "c|" + strconv.Itoa(currentScore[i]), 161 | "f", 162 | } 163 | res = append(res, instarank) 164 | } 165 | } 166 | 167 | for i := numPids / 2; i < numPids; i++ { 168 | playerScoreIdx, _ := scoresCollection.CountDocuments(context.TODO(), bson.M{"battle_id": req.BattleID, "score": bson.M{"$gt": req.Score}}) 169 | 170 | // Find the next highest score 171 | var nextHighestScore models.Score 172 | err = scoresCollection.FindOne(context.TODO(), bson.M{ 173 | "battle_id": req.BattleID, 174 | "score": bson.M{"$gt": req.Score}, 175 | }, options.FindOne().SetSort(bson.D{{"score", 1}})).Decode(&nextHighestScore) 176 | 177 | if scoreHigher[i] { 178 | instaRankString := "b" 179 | 180 | // Get the name of the player who has the next highest score 181 | var name string = db.GetUsernameForPID(nextHighestScore.OwnerPID) 182 | var nextScoreDiff int 183 | if err == mongo.ErrNoDocuments { 184 | name = "N/A" 185 | nextScoreDiff = 0 // No higher score exists 186 | } else { 187 | name = db.GetUsernameForPID(nextHighestScore.OwnerPID) 188 | nextScoreDiff = nextHighestScore.Score - req.Score 189 | } 190 | 191 | if nextScoreDiff < 2000 && nextScoreDiff > 0 { 192 | instaRankString = "i|" + strconv.Itoa(nextScoreDiff) + "|" + name 193 | } 194 | 195 | instarank := BattleScoreRecordResponse{ 196 | req.BattleID, 197 | 0, 198 | int(playerScoreIdx + 1), 199 | 0, 200 | "b", 201 | instaRankString, 202 | } 203 | res = append(res, instarank) 204 | } else { 205 | instarank := BattleScoreRecordResponse{ 206 | req.BattleID, 207 | 0, 208 | int(playerScoreIdx + 1), 209 | 0, 210 | "c|" + strconv.Itoa(currentScore[i]), 211 | "f", 212 | } 213 | 214 | res = append(res, instarank) 215 | } 216 | } 217 | 218 | return marshaler.MarshalResponse(service.Path(), res) 219 | } 220 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/setlists/sync.go: -------------------------------------------------------------------------------- 1 | package setlists 2 | 3 | import ( 4 | "rb3server/protocols/jsonproto/marshaler" 5 | 6 | "github.com/ihatecompvir/nex-go" 7 | "go.mongodb.org/mongo-driver/mongo" 8 | ) 9 | 10 | type SetlistSyncRequest struct { 11 | Region string `json:"region"` 12 | SystemMS int `json:"system_ms"` 13 | MachineID string `json:"machine_id"` 14 | SessionGUID string `json:"session_guid"` 15 | PIDs []int `json:"pidXXX"` 16 | } 17 | 18 | type SetlistSyncResponse struct { 19 | PID int `json:"pid"` 20 | Creator int `json:"creator"` 21 | } 22 | 23 | type SetlistSyncService struct { 24 | } 25 | 26 | func (service SetlistSyncService) Path() string { 27 | return "setlists/sync" 28 | } 29 | 30 | func (service SetlistSyncService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 31 | return marshaler.GenerateEmptyJSONResponse("setlists/sync"), nil 32 | } 33 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/setlists/update.go: -------------------------------------------------------------------------------- 1 | package setlists 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | "strings" 10 | "time" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | "go.mongodb.org/mongo-driver/mongo/options" 16 | ) 17 | 18 | type SetlistUpdateRequest struct { 19 | Type int `json:"type"` 20 | Name string `json:"name"` 21 | Region string `json:"region"` 22 | Description string `json:"description"` 23 | Flags int `json:"flags"` 24 | SystemMS int `json:"system_ms"` 25 | MachineID string `json:"machine_id"` 26 | SessionGUID string `json:"session_guid"` 27 | PID int `json:"pid"` 28 | Shared string `json:"shared"` 29 | ListGUID string `json:"list_guid"` 30 | SongIDs []int `json:"song_idXXX"` 31 | } 32 | 33 | type SetlistUpdateResponse struct { 34 | RetCode int `json:"ret_code"` 35 | } 36 | 37 | type SetlistUpdateService struct { 38 | } 39 | 40 | func (service SetlistUpdateService) Path() string { 41 | return "setlists/update" 42 | } 43 | 44 | func (service SetlistUpdateService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 45 | var req SetlistUpdateRequest 46 | err := marshaler.UnmarshalRequest(data, &req) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | validPIDres, _ := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 52 | 53 | if !validPIDres { 54 | log.Println("Client is attempting to update setlist without a valid server-assigned PID, rejecting call") 55 | return "", nil 56 | } 57 | 58 | // Do a profanity check before updating the setlist 59 | var config models.Config 60 | configCollection := database.Collection("config") 61 | err = configCollection.FindOne(context.TODO(), bson.M{}).Decode(&config) 62 | if err != nil { 63 | log.Printf("Could not get config %v\n", err) 64 | } 65 | 66 | // Check if the setlist name or description contain anything in the profanity list 67 | for _, profanity := range config.ProfanityList { 68 | if profanity != "" && req.Name != "" && len(req.Name) >= len(profanity) { 69 | lowerName := strings.ToLower(req.Name) 70 | lowerProfanity := strings.ToLower(profanity) 71 | 72 | if lowerName == lowerProfanity || strings.Contains(lowerName, lowerProfanity) { 73 | return marshaler.MarshalResponse(service.Path(), []SetlistUpdateResponse{{0xF}}) 74 | } 75 | } 76 | 77 | if profanity != "" && req.Description != "" && len(req.Description) >= len(profanity) { 78 | lowerDesc := strings.ToLower(req.Description) 79 | lowerProfanity := strings.ToLower(profanity) 80 | 81 | if lowerDesc == lowerProfanity || strings.Contains(lowerDesc, lowerProfanity) { 82 | return marshaler.MarshalResponse(service.Path(), []SetlistUpdateResponse{{0x10}}) 83 | } 84 | } 85 | } 86 | 87 | users := database.Collection("users") 88 | var user models.User 89 | err = users.FindOne(context.TODO(), bson.M{"pid": req.PID}).Decode(&user) 90 | if err != nil { 91 | log.Printf("Could not find user with PID %d, defaulting to \"Player\": %v", req.PID, err) 92 | user.Username = "Player" 93 | } 94 | 95 | // Write setlist to database 96 | setlistCollection := database.Collection("setlists") 97 | 98 | var setlist models.Setlist 99 | err = setlistCollection.FindOne(context.TODO(), bson.M{"guid": req.ListGUID}).Decode(&setlist) 100 | if err != nil && err != mongo.ErrNoDocuments { 101 | log.Printf("Error finding setlist: %s", err) 102 | return "", err 103 | } 104 | 105 | // look for the mongo no documents error to determine if we are inserting a new setlist 106 | // maybe there is a better way to do this? 107 | isNewSetlist := err == mongo.ErrNoDocuments 108 | 109 | // If it's an existing setlist, perform access control checks 110 | if !isNewSetlist { 111 | // Check if the setlist we are attempting to update is not a battle, as battles cannot be updated after-the-fact 112 | if setlist.Type == 1000 || setlist.Type == 1001 || setlist.Type == 1002 { 113 | log.Printf("Player with PID %d attempted to update a battle setlist, rejecting", req.PID) 114 | return marshaler.MarshalResponse(service.Path(), []SetlistUpdateResponse{{0x16}}) 115 | } 116 | 117 | // Access controls so that only the owner of the setlist can update it 118 | if setlist.Owner != client.Username || setlist.PID != int(client.PlayerID()) { 119 | log.Printf("Player with PID %d attempted to update a setlist that does not belong to them, rejecting", req.PID) 120 | return marshaler.MarshalResponse(service.Path(), []SetlistUpdateResponse{{0x16}}) 121 | } 122 | } 123 | 124 | if isNewSetlist { 125 | // Only update last_setlist_id if we are inserting a new setlist 126 | config.LastSetlistID += 1 127 | 128 | _, err = configCollection.UpdateOne( 129 | context.TODO(), 130 | bson.M{}, 131 | bson.D{ 132 | {"$set", bson.D{{"last_setlist_id", config.LastSetlistID}}}, 133 | }, 134 | ) 135 | 136 | if err != nil { 137 | log.Println("Could not update config in database while updating character: ", err) 138 | } 139 | 140 | setlist.SetlistID = config.LastSetlistID 141 | } 142 | 143 | setlist.ArtURL = "" 144 | setlist.Desc = req.Description 145 | setlist.Title = req.Name 146 | setlist.Type = 0 147 | setlist.Owner = user.Username 148 | setlist.OwnerGUID = user.GUID 149 | setlist.PID = req.PID 150 | setlist.SongIDs = req.SongIDs 151 | 152 | if setlist.Created == 0 { 153 | setlist.Created = time.Now().Unix() 154 | } 155 | 156 | setlist.SongNames = make([]string, len(req.SongIDs)) 157 | 158 | filter := bson.M{"guid": req.ListGUID} 159 | update := bson.M{ 160 | "$set": bson.M{ 161 | "art_url": setlist.ArtURL, 162 | "desc": setlist.Desc, 163 | "title": setlist.Title, 164 | "type": setlist.Type, 165 | "owner": setlist.Owner, 166 | "owner_guid": setlist.OwnerGUID, 167 | "setlist_id": setlist.SetlistID, 168 | "pid": setlist.PID, 169 | "s_ids": setlist.SongIDs, 170 | "s_names": setlist.SongNames, 171 | "guid": req.ListGUID, 172 | "shared": req.Shared, 173 | "created": setlist.Created, 174 | }, 175 | } 176 | 177 | opts := options.Update().SetUpsert(true) 178 | 179 | _, err = setlistCollection.UpdateOne(context.TODO(), filter, update, opts) 180 | if err != nil { 181 | log.Printf("Error upserting setlist: %s", err) 182 | } 183 | 184 | res := []SetlistUpdateResponse{{ 185 | 0, 186 | }} 187 | 188 | return marshaler.MarshalResponse(service.Path(), res) 189 | } 190 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/songlists/get.go: -------------------------------------------------------------------------------- 1 | package songlists 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | "time" 10 | 11 | "github.com/ihatecompvir/nex-go" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | 15 | db "rb3server/database" 16 | ) 17 | 18 | type GetSonglistsRequest struct { 19 | Region string `json:"region"` 20 | Locale string `json:"locale"` 21 | SystemMS int `json:"system_ms"` 22 | MachineID string `json:"machine_id"` 23 | SessionGUID string `json:"session_guid"` 24 | PID000 int `json:"pid000"` 25 | } 26 | 27 | type GetSonglistResponse struct { 28 | SetlistID int `json:"id"` 29 | PID int `json:"pid"` 30 | Title string `json:"title"` 31 | Desc string `json:"desc"` 32 | Type int `json:"type"` 33 | Owner string `json:"owner"` 34 | OwnerGUID string `json:"owner_guid"` 35 | GUID string `json:"guid"` 36 | ArtURL string `json:"art_url"` 37 | SongIDs []int `json:"s_idXXX"` 38 | SongNames []string `json:"s_nameXXX"` 39 | } 40 | 41 | type GetBattleSonglistResponse struct { 42 | SetlistID int `json:"id"` 43 | PID int `json:"pid"` 44 | Title string `json:"title"` 45 | Desc string `json:"desc"` 46 | Type int `json:"type"` 47 | Owner string `json:"owner"` 48 | OwnerGUID string `json:"owner_guid"` 49 | GUID string `json:"guid"` 50 | ArtURL string `json:"art_url"` 51 | SecondsLeft int `json:"seconds_left"` 52 | ValidInstr int `json:"valid_instr"` 53 | SongIDs []int `json:"s_idXXX"` 54 | SongNames []string `json:"s_nameXXX"` 55 | } 56 | 57 | type GetSonglistsService struct { 58 | } 59 | 60 | func (service GetSonglistsService) Path() string { 61 | return "songlists/get" 62 | } 63 | 64 | func (service GetSonglistsService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 65 | var req GetSonglistsRequest 66 | 67 | err := marshaler.UnmarshalRequest(data, &req) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | res, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID000)) 73 | 74 | if !res { 75 | log.Println("Client is attempting to get songlists without a valid server-assigned PID, rejecting call") 76 | return "", err 77 | } 78 | 79 | setlistCollection := database.Collection("setlists") 80 | 81 | setlistCursor, err := setlistCollection.Find(context.TODO(), bson.D{{"shared", "t"}}) 82 | 83 | if err != nil { 84 | log.Printf("Error getting songlists: %s", err) 85 | } 86 | 87 | jsonStrings := []string{} 88 | 89 | for setlistCursor.Next(context.TODO()) { 90 | var setlistToCopy models.Setlist 91 | 92 | setlistCursor.Decode(&setlistToCopy) 93 | 94 | // normal setlist 95 | if setlistToCopy.Type == 1 || setlistToCopy.Type == 2 || setlistToCopy.Type == 0 { 96 | 97 | // always show "Harmonix Recommends" aka server-provided setlists which are intended to be global for all players 98 | if setlistToCopy.Type != 2 { 99 | // make sure we only get setlists created by our friends 100 | isFriendCreated, err := db.IsPIDAFriendOfPID(req.PID000, setlistToCopy.PID) 101 | 102 | if err != nil { 103 | continue 104 | } 105 | 106 | if !isFriendCreated { 107 | continue 108 | } 109 | } 110 | 111 | var setlist GetSonglistResponse 112 | setlist.ArtURL = setlistToCopy.ArtURL 113 | setlist.Desc = setlistToCopy.Desc 114 | setlist.GUID = setlistToCopy.GUID 115 | setlist.Owner = setlistToCopy.Owner 116 | setlist.OwnerGUID = setlistToCopy.OwnerGUID 117 | setlist.PID = setlistToCopy.PID 118 | setlist.SetlistID = setlistToCopy.SetlistID 119 | setlist.Title = setlistToCopy.Title 120 | setlist.Type = setlistToCopy.Type 121 | setlist.SongIDs = append(setlist.SongIDs, setlistToCopy.SongIDs...) 122 | setlist.SongNames = append(setlist.SongNames, setlistToCopy.SongNames...) 123 | 124 | resString, _ := marshaler.MarshalResponse(service.Path(), []GetSonglistResponse{setlist}) 125 | 126 | jsonStrings = append(jsonStrings, resString) 127 | } 128 | 129 | // battle setlist 130 | if setlistToCopy.Type == 1000 || setlistToCopy.Type == 1001 || setlistToCopy.Type == 1002 { 131 | 132 | // always show server-provided battles 133 | if setlistToCopy.Type != 1002 { 134 | // make sure we only get battles created by our friends 135 | isFriendCreated, err := db.IsPIDAFriendOfPID(req.PID000, setlistToCopy.PID) 136 | 137 | if err != nil { 138 | continue 139 | } 140 | 141 | if !isFriendCreated { 142 | continue 143 | } 144 | } 145 | 146 | var battle GetBattleSonglistResponse 147 | battle.ArtURL = setlistToCopy.ArtURL 148 | battle.Desc = setlistToCopy.Desc 149 | battle.GUID = setlistToCopy.GUID 150 | battle.Owner = setlistToCopy.Owner 151 | battle.OwnerGUID = setlistToCopy.OwnerGUID 152 | battle.PID = setlistToCopy.PID 153 | battle.SetlistID = setlistToCopy.SetlistID 154 | battle.Title = setlistToCopy.Title 155 | battle.Type = setlistToCopy.Type 156 | battle.SongIDs = append(battle.SongIDs, setlistToCopy.SongIDs...) 157 | battle.SongNames = append(battle.SongNames, setlistToCopy.SongNames...) 158 | 159 | switch setlistToCopy.TimeEndUnits { 160 | case "seconds": 161 | battle.SecondsLeft = int(setlistToCopy.Created + int64(setlistToCopy.TimeEndVal) - (time.Now().Unix())) 162 | case "minutes": 163 | battle.SecondsLeft = int(setlistToCopy.Created + int64(setlistToCopy.TimeEndVal*60) - (time.Now().Unix())) 164 | case "hours": 165 | battle.SecondsLeft = int(setlistToCopy.Created + int64(setlistToCopy.TimeEndVal*3600) - (time.Now().Unix())) 166 | case "days": 167 | battle.SecondsLeft = int(setlistToCopy.Created + int64(setlistToCopy.TimeEndVal*86400) - (time.Now().Unix())) 168 | case "weeks": 169 | battle.SecondsLeft = int(setlistToCopy.Created + int64(setlistToCopy.TimeEndVal*604800) - (time.Now().Unix())) 170 | default: 171 | battle.SecondsLeft = 60 * 60 // default to 1 hour if there is nothing, but this should ideally never happen 172 | } 173 | 174 | battle.ValidInstr = setlistToCopy.Instrument 175 | 176 | resString, _ := marshaler.MarshalResponse(service.Path(), []GetBattleSonglistResponse{battle}) 177 | 178 | jsonStrings = append(jsonStrings, resString) 179 | } 180 | } 181 | 182 | resString, _ := marshaler.CombineJSONMethods(jsonStrings) 183 | return resString, nil 184 | } 185 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/stats/pad_user.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "rb3server/protocols/jsonproto/marshaler" 5 | 6 | "github.com/ihatecompvir/nex-go" 7 | "go.mongodb.org/mongo-driver/mongo" 8 | ) 9 | 10 | type StatsPadRequest struct { 11 | Name string `json:"name"` 12 | Region string `json:"region"` 13 | SystemMS int `json:"system_ms"` 14 | MachineID string `json:"machine_id"` 15 | SessionGUID string `json:"session_guid"` 16 | Pad0 string `json:"pad_0"` 17 | } 18 | 19 | type StatsPadResponse struct { 20 | PID int `json:"pid"` 21 | Creator int `json:"creator"` 22 | } 23 | 24 | type StatsPadService struct { 25 | } 26 | 27 | func (service StatsPadService) Path() string { 28 | return "stats/pad_user" 29 | } 30 | 31 | func (service StatsPadService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 32 | var req StatsPadRequest 33 | err := marshaler.UnmarshalRequest(data, &req) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | res := []StatsPadResponse{{ 39 | 123, 40 | 0, 41 | }} 42 | 43 | return marshaler.MarshalResponse(service.Path(), res) 44 | } 45 | -------------------------------------------------------------------------------- /protocols/jsonproto/services/ticker/info.go: -------------------------------------------------------------------------------- 1 | package ticker 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/models" 7 | "rb3server/protocols/jsonproto/marshaler" 8 | "rb3server/utils" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | 14 | db "rb3server/database" 15 | ) 16 | 17 | type TickerInfoRequest struct { 18 | Region string `json:"region"` 19 | Locale string `json:"locale"` 20 | SystemMS int `json:"system_ms"` 21 | MachineID string `json:"machine_id"` 22 | SessionGUID string `json:"session_guid"` 23 | PID int `json:"pid"` 24 | RoleID int `json:"role_id"` // current instrument? 25 | } 26 | 27 | type TickerInfoResponse struct { 28 | PID int `json:"pid"` 29 | MOTD string `json:"motd"` 30 | BattleCount int `json:"battle_count"` 31 | RoleID int `json:"role_id"` 32 | RoleRank int `json:"role_rank"` 33 | RoleIsGlobal int `json:"role_is_global"` 34 | RoleIsPercentile int `json:"role_is_percentile"` 35 | BandID int `json:"band_id"` 36 | BandRank int `json:"band_rank"` 37 | BankIsGlobal int `json:"band_is_global"` 38 | BandIsPercentile int `json:"band_is_percentile"` 39 | } 40 | 41 | type TickerInfoService struct { 42 | } 43 | 44 | func (service TickerInfoService) Path() string { 45 | return "ticker/info/get" 46 | } 47 | 48 | func (service TickerInfoService) Handle(data string, database *mongo.Database, client *nex.Client) (string, error) { 49 | var req TickerInfoRequest 50 | err := marshaler.UnmarshalRequest(data, &req) 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | validPIDres, _ := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), uint32(req.PID)) 56 | 57 | if !validPIDres { 58 | log.Println("Client is attempting to get ticker stats without a valid server-assigned PID, rejecting call") 59 | return "", nil 60 | } 61 | 62 | bandsCollection := database.Collection("bands") 63 | var band models.Band 64 | err = bandsCollection.FindOne(nil, bson.M{"pid": req.PID}).Decode(&band) 65 | 66 | setlistsCollection := database.Collection("setlists") 67 | 68 | // count the number of setlists with a type of 1000, 1001, or 1002 69 | battleCount, err := setlistsCollection.CountDocuments(nil, bson.M{"type": bson.M{"$in": []int{1000, 1001, 1002}}}) 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | scoresCollection := database.Collection("scores") 75 | 76 | // Aggregation to get total scores for each player across all instruments 77 | // mongo actually the GOAT for this 78 | pipeline := mongo.Pipeline{ 79 | {{"$group", bson.D{{"_id", "$pid"}, {"totalScore", bson.D{{"$sum", "$score"}}}}}}, 80 | {{"$sort", bson.D{{"totalScore", -1}}}}, 81 | } 82 | 83 | cursor, err := scoresCollection.Aggregate(context.TODO(), pipeline) 84 | if err != nil { 85 | return "", err 86 | } 87 | defer cursor.Close(context.TODO()) 88 | 89 | var results []struct { 90 | ID int `bson:"_id"` 91 | TotalScore int `bson:"totalScore"` 92 | } 93 | if err := cursor.All(context.TODO(), &results); err != nil { 94 | return "", err 95 | } 96 | 97 | // Calculate overall rank 98 | totalScoreRank := 0 99 | for i, result := range results { 100 | if result.ID == req.PID { 101 | totalScoreRank = i + 1 102 | break 103 | } 104 | } 105 | if totalScoreRank == 0 { 106 | totalScoreRank = len(results) + 1 107 | } 108 | 109 | // Aggregation to get total scores for the specified instrument 110 | rolePipeline := mongo.Pipeline{ 111 | {{"$match", bson.D{{"role_id", req.RoleID}}}}, 112 | {{"$group", bson.D{{"_id", "$pid"}, {"totalScore", bson.D{{"$sum", "$score"}}}}}}, 113 | {{"$sort", bson.D{{"totalScore", -1}}}}, 114 | } 115 | 116 | roleCursor, err := scoresCollection.Aggregate(context.TODO(), rolePipeline) 117 | if err != nil { 118 | return "", err 119 | } 120 | defer roleCursor.Close(context.TODO()) 121 | 122 | var roleResults []struct { 123 | ID int `bson:"_id"` 124 | TotalScore int `bson:"totalScore"` 125 | } 126 | if err := roleCursor.All(context.TODO(), &roleResults); err != nil { 127 | return "", err 128 | } 129 | 130 | roleRank := 0 131 | for i, result := range roleResults { 132 | if result.ID == req.PID { 133 | roleRank = i + 1 134 | break 135 | } 136 | } 137 | if roleRank == 0 { 138 | roleRank = len(roleResults) + 1 139 | } 140 | 141 | res := []TickerInfoResponse{{ 142 | req.PID, 143 | db.GetCoolFact(), 144 | int(battleCount), 145 | req.RoleID, 146 | roleRank, 147 | 1, 148 | 0, 149 | band.BandID, 150 | totalScoreRank, 151 | 1, 152 | 0, 153 | }} 154 | 155 | return marshaler.MarshalResponse(service.Path(), res) 156 | } 157 | -------------------------------------------------------------------------------- /quazal/exceptions.go: -------------------------------------------------------------------------------- 1 | package quazal 2 | 3 | // Combined exception values 4 | const ( 5 | UnknownError = 0x00010001 6 | OperationAborted = 0x00010004 7 | AccessDenied = 0x00010006 8 | InvalidArgument = 0x0001000A 9 | Timeout = 0x0001000B 10 | InitializationFailure = 0x0001000C 11 | ConnectionFailureTypeOne = 0x00050002 12 | ConnectionFailureTypeThree = 0x00030002 13 | AccountDisabled = 0x00030067 14 | InvalidUsername = 0x00030064 15 | NotAuthenticated = 0x00030002 16 | InvalidPassword = 0x00030066 17 | UsernameAlreadyExists = 0x00030068 18 | InvalidPID = 0x0003006B 19 | ConcurrentLoginDenied = 0x00030069 20 | AccountExpired = 0x00030068 21 | EncryptionFailure = 0x0003006A 22 | InvalidOperationInLiveEnvironment = 0x0003006F 23 | PythonException = 0x00040001 24 | TypeError = 0x00040002 25 | IndexError = 0x00040003 26 | InvalidReference = 0x00040004 27 | CallFailure = 0x00040005 28 | MemoryError = 0x00040006 29 | KeyError = 0x00040007 30 | OperationError = 0x00040008 31 | ConversionError = 0x00040009 32 | ValidationError = 0x0004000A 33 | ) 34 | -------------------------------------------------------------------------------- /serialization/gathering/gathering_deserializer.go: -------------------------------------------------------------------------------- 1 | package serialization 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type GatheringDeserializer struct{} 9 | 10 | // TODO: Redo this to use Go's marshaling because that would probably be better 11 | func (d *GatheringDeserializer) Deserialize(data []byte) (RVGathering, error) { 12 | if len(data) < 54 { 13 | return RVGathering{}, fmt.Errorf("insufficient data to deserialize Gathering, expected at least 54 bytes but got %d", len(data)) 14 | } 15 | 16 | var g RVGathering 17 | 18 | // Read fixed-size fields 19 | g.IDMyself = binary.LittleEndian.Uint32(data[0:4]) 20 | g.IDOwner = binary.LittleEndian.Uint32(data[4:8]) 21 | g.IDHost = binary.LittleEndian.Uint32(data[8:12]) 22 | g.MinParticipants = binary.LittleEndian.Uint16(data[12:14]) 23 | g.MaxParticipants = binary.LittleEndian.Uint16(data[14:16]) 24 | g.ParticipationPolicy = binary.LittleEndian.Uint32(data[16:20]) 25 | g.PolicyArgument = binary.LittleEndian.Uint32(data[20:24]) 26 | g.Flags = binary.LittleEndian.Uint32(data[24:28]) 27 | g.State = binary.LittleEndian.Uint32(data[28:32]) 28 | g.DescriptionCount = binary.LittleEndian.Uint32(data[32:36]) 29 | g.DescriptionString = data[36] 30 | 31 | // Read HarmonixGathering struct 32 | var h HmxGathering 33 | h.Public = data[37] 34 | h.Prop0 = binary.LittleEndian.Uint32(data[38:42]) 35 | h.Prop1 = binary.LittleEndian.Uint32(data[42:46]) 36 | h.Prop2 = binary.LittleEndian.Uint32(data[46:50]) 37 | h.Prop3 = binary.LittleEndian.Uint32(data[50:54]) 38 | h.Prop4 = binary.LittleEndian.Uint32(data[54:58]) 39 | h.Prop5 = binary.LittleEndian.Uint32(data[58:62]) 40 | h.Prop6 = binary.LittleEndian.Uint32(data[62:66]) 41 | h.Prop7 = binary.LittleEndian.Uint32(data[66:70]) 42 | h.Prop8 = binary.LittleEndian.Uint32(data[70:74]) 43 | h.Prop9 = binary.LittleEndian.Uint32(data[74:78]) 44 | h.Prop10 = binary.LittleEndian.Uint32(data[78:82]) 45 | h.Buffer = binary.LittleEndian.Uint32(data[82:86]) 46 | 47 | g.HarmonixGathering = h 48 | 49 | return g, nil 50 | } 51 | -------------------------------------------------------------------------------- /serialization/gathering/harmonixgathering.go: -------------------------------------------------------------------------------- 1 | package serialization 2 | 3 | // not sure what these fields are, but probably related to Overshell slots, instruments, and etc. 4 | // would be good to eventually completely reverse this 5 | type HmxGathering struct { 6 | Public byte 7 | Prop0 uint32 8 | Prop1 uint32 9 | Prop2 uint32 10 | Prop3 uint32 11 | Prop4 uint32 12 | Prop5 uint32 13 | Prop6 uint32 14 | Prop7 uint32 15 | Prop8 uint32 16 | Prop9 uint32 17 | Prop10 uint32 18 | Buffer uint32 19 | } 20 | 21 | type RVGathering struct { 22 | IDMyself uint32 // the ID of the gathering 23 | IDOwner uint32 // the PID of wh oowns the gathering 24 | IDHost uint32 // the PID of the host 25 | MinParticipants uint16 // always 0 26 | MaxParticipants uint16 // always 4 27 | ParticipationPolicy uint32 // unknown, always 1 28 | PolicyArgument uint32 // unknown 29 | Flags uint32 // unknown 30 | State uint32 // always 0, 2, or 6 31 | DescriptionCount uint32 // unused, always a 1 byte null string 32 | DescriptionString byte // unused, always a 1 byte null string 33 | HarmonixGathering HmxGathering // Harmonix-specific additions to the structure 34 | } 35 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "math/rand" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/go-chi/chi/v5" 15 | "github.com/joho/godotenv" 16 | "github.com/natefinch/lumberjack" 17 | "go.mongodb.org/mongo-driver/bson" 18 | "go.mongodb.org/mongo-driver/mongo" 19 | "go.mongodb.org/mongo-driver/mongo/options" 20 | "go.mongodb.org/mongo-driver/mongo/readpref" 21 | 22 | database "rb3server/database" 23 | "rb3server/restapi" 24 | "rb3server/servers" 25 | ) 26 | 27 | func main() { 28 | 29 | envFile := flag.String("env", ".env", "specify the .env file to load") 30 | flag.Parse() 31 | 32 | err := godotenv.Load(*envFile) 33 | if err != nil { 34 | log.Println("Error loading .env file, using environment variables instead") 35 | } 36 | 37 | // if the user has set a log path, log there, otherwise log to stdout 38 | logPath := os.Getenv("LOGPATH") 39 | 40 | if logPath != "" { 41 | log.SetOutput(&lumberjack.Logger{ 42 | Filename: logPath, 43 | MaxSize: 10, // Max size in MB before rotation 44 | MaxBackups: 3, // Max number of old log files to retain 45 | MaxAge: 28, // Max number of days to retain old log files 46 | Compress: true, // Compress/zip old log files 47 | }) 48 | } 49 | 50 | ticketVerifierEndpoint := os.Getenv("TICKETVERIFIERENDPOINT") 51 | 52 | if ticketVerifierEndpoint == "" { 53 | log.Println("Ticket verification is disabled, GoCentral will have no real authentication! Please do not use this server in a production environment.") 54 | } 55 | 56 | uri := os.Getenv("MONGOCONNECTIONSTRING") 57 | 58 | if uri == "" { 59 | log.Fatalln("GoCentral relies on MongoDB. You must set a MongoDB connection string to use GoCentral") 60 | } 61 | 62 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 63 | defer cancel() 64 | 65 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) 66 | 67 | if err != nil { 68 | log.Fatalln("Could not connect to MongoDB: ", err) 69 | } 70 | 71 | defer func() { 72 | if err = client.Disconnect(ctx); err != nil { 73 | log.Fatalln("Could not connect to MongoDB: ", err) 74 | } 75 | }() 76 | 77 | // Ping the primary 78 | if err := client.Ping(ctx, readpref.Primary()); err != nil { 79 | log.Fatalln("Could not ping MongoDB: ", err) 80 | } 81 | 82 | log.Println("Successfully established connection to MongoDB") 83 | 84 | database.GocentralDatabase = client.Database("gocentral") 85 | 86 | configCollection := database.GocentralDatabase.Collection("config") 87 | 88 | // get config from DB 89 | err = configCollection.FindOne(nil, bson.M{}).Decode(&servers.Config) 90 | if err != nil { 91 | log.Println("Could not get config from MongoDB database, creating default config: ", err) 92 | _, err = configCollection.InsertOne(nil, bson.D{ 93 | {Key: "last_pid", Value: 500}, 94 | {Key: "last_band_id", Value: 0}, 95 | {Key: "last_character_id", Value: 0}, 96 | {Key: "last_setlist_id", Value: 0}, 97 | {Key: "profanity_list", Value: []string{}}, 98 | {Key: "battle_limit", Value: 5}, 99 | {Key: "last_machine_id", Value: 1000000000}, 100 | }) 101 | 102 | servers.Config.LastPID = 500 103 | servers.Config.LastCharacterID = 0 104 | servers.Config.LastBandID = 0 105 | servers.Config.LastSetlistID = 0 106 | servers.Config.ProfanityList = []string{} 107 | servers.Config.BattleLimit = 1 108 | 109 | if err != nil { 110 | log.Fatalln("Could not create default config! GoCentral cannot proceed: ", err) 111 | } 112 | } 113 | 114 | // seed randomness with current time 115 | rand.Seed(time.Now().UnixNano()) 116 | 117 | go servers.StartAuthServer() 118 | go servers.StartSecureServer() 119 | 120 | // Start HTTP server using Chi 121 | enableRESTAPI := os.Getenv("ENABLERESTAPI") 122 | 123 | httpServer := &http.Server{} 124 | 125 | if enableRESTAPI == "1" { 126 | r := chi.NewRouter() 127 | 128 | // used to check if the server is up 129 | r.Get("/health", restapi.HealthHandler) 130 | 131 | // some basic stats about how many chars/bands/scores/etc are in the DB 132 | // does not include any user-specific information 133 | r.Get("/stats", restapi.StatsHandler) 134 | 135 | // used to get the current MOTD 136 | r.Get("/motd", restapi.MotdHandler) 137 | 138 | r.Get("/song_list", restapi.SongListHandler) 139 | 140 | r.Get("/leaderboards", restapi.LeaderboardHandler) 141 | 142 | httpPort := os.Getenv("HTTPPORT") 143 | 144 | if httpPort == "" { 145 | log.Printf("REST API enabled but HTTP port, not set, please set an HTTP port using the HTTPPORT environment variable!") 146 | return 147 | } 148 | 149 | httpServer = &http.Server{ 150 | Addr: ":" + httpPort, 151 | Handler: r, 152 | } 153 | 154 | go func() { 155 | if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { 156 | log.Fatalf("Could not listen on :%v: %v\n", httpPort, err) 157 | } 158 | }() 159 | log.Println("GoCentral REST API HTTP server started on:" + httpPort) 160 | } 161 | 162 | log.Printf("Starting housekeeping tasks...\n") 163 | 164 | ticker := time.NewTicker(1 * time.Minute) 165 | defer ticker.Stop() 166 | 167 | quit := make(chan struct{}) 168 | 169 | // automatically run some housekeeping tasks 170 | go func() { 171 | for { 172 | select { 173 | case <-ticker.C: 174 | database.CleanupDuplicateScores() 175 | database.PruneOldSessions() 176 | database.CleanupInvalidScores() 177 | database.DeleteExpiredBattles() 178 | case <-quit: 179 | return 180 | } 181 | } 182 | }() 183 | 184 | sig := make(chan os.Signal) 185 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) 186 | s := <-sig 187 | log.Printf("Signal (%s) received, stopping\n", s) 188 | 189 | if enableRESTAPI == "true" { 190 | ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) 191 | defer cancel() 192 | if err := httpServer.Shutdown(ctx); err != nil { 193 | log.Fatalf("HTTP server Shutdown: %v", err) 194 | } 195 | } 196 | close(quit) 197 | } 198 | -------------------------------------------------------------------------------- /servers/customfind.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/quazal" 9 | "time" 10 | 11 | "github.com/ihatecompvir/nex-go" 12 | nexproto "github.com/ihatecompvir/nex-protocols-go" 13 | "go.mongodb.org/mongo-driver/bson" 14 | ) 15 | 16 | func CustomFind(err error, client *nex.Client, callID uint32, data []byte) { 17 | 18 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.CustomMatchmakingProtocolID) 19 | 20 | if !res { 21 | return 22 | } 23 | 24 | log.Printf("Checking for available gatherings for %s...\n", client.Username) 25 | 26 | gatheringCollection := database.GocentralDatabase.Collection("gatherings") 27 | usersCollection := database.GocentralDatabase.Collection("users") 28 | 29 | // attempt to get a random gathering and deserialize it 30 | // any gatherings that havent been updated in 5 minutes are ignored 31 | // this should prevent endless loops of trying to join old/stale gatherings that are still in the DB 32 | // but any UI state change or playing a song will update the gathering 33 | cur, err := gatheringCollection.Aggregate(context.TODO(), []bson.M{ 34 | {"$match": bson.D{ 35 | // don't find our own gathering 36 | { 37 | Key: "creator", 38 | Value: bson.D{{Key: "$ne", Value: client.Username}}, 39 | }, 40 | // only look for gatherings updated in the last 5 minutes 41 | { 42 | Key: "last_updated", 43 | Value: bson.D{{Key: "$gt", Value: (time.Now().Unix()) - (5 * 60)}}, 44 | }, 45 | // don't look for gatherings in the "in song" state 46 | { 47 | Key: "state", 48 | Value: bson.D{{Key: "$ne", Value: 2}}, 49 | }, 50 | // don't look for gatherings in the "on song select" state 51 | { 52 | Key: "state", 53 | Value: bson.D{{Key: "$ne", Value: 6}}, 54 | }, 55 | // only look for public gatherings 56 | { 57 | Key: "public", 58 | Value: bson.D{{Key: "$eq", Value: 1}}, 59 | }, 60 | // only look for gatherings created by the current console type 61 | // with an additional match so RPCN can join real PS3 h/w gatherings 62 | { 63 | Key: "console_type", 64 | Value: bson.D{{ 65 | Key: "$in", 66 | Value: func() []int { 67 | switch client.Platform() { 68 | case 1, 3: 69 | return []int{1, 3} 70 | default: 71 | return []int{client.Platform()} 72 | } 73 | }(), 74 | }}, 75 | }, 76 | }}, 77 | {"$sample": bson.M{"size": 10}}, 78 | }) 79 | if err != nil { 80 | log.Printf("Could not get a random gathering: %s\n", err) 81 | SendErrorCode(SecureServer, client, nexproto.CustomMatchmakingProtocolID, callID, quazal.OperationError) 82 | return 83 | } 84 | var gatherings = make([]models.Gathering, 0) 85 | for cur.Next(nil) { 86 | var g models.Gathering 87 | err = cur.Decode(&g) 88 | if err != nil { 89 | log.Printf("Error decoding gathering: %+v\n", err) 90 | SendErrorCode(SecureServer, client, nexproto.CustomMatchmakingProtocolID, callID, quazal.OperationError) 91 | return 92 | } 93 | gatherings = append(gatherings, g) 94 | } 95 | 96 | rmcResponseStream := nex.NewStream() 97 | 98 | // if there are no availble gatherings, tell the client to check again. 99 | // otherwise, pass the available gathering to the client 100 | if len(gatherings) == 0 { 101 | log.Println("There are no active gatherings. Tell client to keep checking") 102 | rmcResponseStream.WriteUInt32LE(0) 103 | } else { 104 | log.Printf("Found %d gatherings - telling client to attempt joining", len(gatherings)) 105 | rmcResponseStream.WriteUInt32LE(uint32(len(gatherings))) 106 | for _, gathering := range gatherings { 107 | var user models.User 108 | 109 | if err = usersCollection.FindOne(nil, bson.M{"username": gathering.Creator}).Decode(&user); err != nil { 110 | log.Printf("Could not find creator %s of gathering: %+v\n", gathering.Creator, err) 111 | SendErrorCode(SecureServer, client, nexproto.CustomMatchmakingProtocolID, callID, quazal.OperationError) 112 | return 113 | } 114 | rmcResponseStream.WriteBufferString("HarmonixGathering") 115 | rmcResponseStream.WriteUInt32LE(uint32(len(gathering.Contents) + 4)) 116 | rmcResponseStream.WriteUInt32LE(uint32(len(gathering.Contents))) 117 | rmcResponseStream.Grow(int64(len(gathering.Contents))) 118 | rmcResponseStream.WriteBytesNext(gathering.Contents[0:4]) 119 | rmcResponseStream.WriteUInt32LE(user.PID) 120 | rmcResponseStream.WriteUInt32LE(user.PID) 121 | rmcResponseStream.WriteBytesNext(gathering.Contents[12:]) 122 | } 123 | } 124 | 125 | rmcResponseBody := rmcResponseStream.Bytes() 126 | 127 | rmcResponse := nex.NewRMCResponse(nexproto.CustomMatchmakingProtocolID, callID) 128 | rmcResponse.SetSuccess(nexproto.RegisterGathering, rmcResponseBody) 129 | 130 | rmcResponseBytes := rmcResponse.Bytes() 131 | 132 | responsePacket, _ := nex.NewPacketV0(client, nil) 133 | 134 | responsePacket.SetVersion(0) 135 | responsePacket.SetSource(0x31) 136 | responsePacket.SetDestination(0x3F) 137 | responsePacket.SetType(nex.DataPacket) 138 | 139 | responsePacket.SetPayload(rmcResponseBytes) 140 | 141 | responsePacket.AddFlag(nex.FlagNeedsAck) 142 | responsePacket.AddFlag(nex.FlagReliable) 143 | 144 | SecureServer.Send(responsePacket) 145 | 146 | } 147 | -------------------------------------------------------------------------------- /servers/deleteaccount.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/quazal" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | nexproto "github.com/ihatecompvir/nex-protocols-go" 12 | "go.mongodb.org/mongo-driver/bson" 13 | ) 14 | 15 | func DeleteAccount(err error, client *nex.Client, callID uint32, pid uint32) { 16 | 17 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.AccountManagementProtocolID) 18 | 19 | if !res { 20 | return 21 | } 22 | 23 | usersCollection := database.GocentralDatabase.Collection("users") 24 | 25 | // get the user 26 | var user models.User 27 | if err = usersCollection.FindOne(context.TODO(), 28 | bson.M{"pid": pid}).Decode(&user); err != nil { 29 | log.Printf("Could not find user with PID %d: %+v\n", pid, err) 30 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.InvalidPID) 31 | return 32 | } 33 | 34 | // make sure the machine ID who created the user matches the one trying to delete it 35 | if user.CreatedByMachineID != client.MachineID() { 36 | log.Printf("Client with machine ID %d is trying to delete account with PID %d, but it was created by machine ID %d\n", client.MachineID(), pid, user.CreatedByMachineID) 37 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.AccessDenied) 38 | return 39 | } 40 | 41 | // delete the user 42 | if _, err = usersCollection.DeleteOne(nil, bson.M{"pid": pid}); err != nil { 43 | log.Printf("Could not delete user with PID %d: %+v\n", pid, err) 44 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.OperationError) 45 | return 46 | } 47 | 48 | // just respond with nothing for now 49 | rmcResponseStream := nex.NewStream() 50 | 51 | rmcResponseStream.WriteUInt32LE(0) 52 | 53 | rmcResponseBody := rmcResponseStream.Bytes() 54 | 55 | rmcResponse := nex.NewRMCResponse(nexproto.AccountManagementProtocolID, callID) 56 | rmcResponse.SetSuccess(nexproto.DeleteAccount, rmcResponseBody) 57 | 58 | responsePacket, _ := nex.NewPacketV0(client, nil) 59 | 60 | responsePacket.SetVersion(0) 61 | responsePacket.SetSource(0x31) 62 | responsePacket.SetDestination(0x3F) 63 | responsePacket.SetType(nex.DataPacket) 64 | 65 | responsePacket.AddFlag(nex.FlagNeedsAck) 66 | responsePacket.AddFlag(nex.FlagReliable) 67 | 68 | SecureServer.Send(responsePacket) 69 | } 70 | -------------------------------------------------------------------------------- /servers/findbynamelike.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "rb3server/database" 8 | "rb3server/models" 9 | "rb3server/quazal" 10 | "regexp" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | nexproto "github.com/ihatecompvir/nex-protocols-go" 14 | "go.mongodb.org/mongo-driver/bson" 15 | ) 16 | 17 | func FindByNameLike(err error, client *nex.Client, callID uint32, uiGroups uint32, name string) { 18 | users := database.GocentralDatabase.Collection("users") 19 | var user models.User 20 | 21 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.AccountManagementProtocolID) 22 | 23 | if !res { 24 | return 25 | } 26 | 27 | log.Printf("Finding user by name like %s\n", name) 28 | 29 | // lookup the user by name 30 | if err = users.FindOne(context.TODO(), bson.M{"username": name}).Decode(&user); err != nil { 31 | var rgx = regexp.MustCompile(`\(([^()]*)\)`) 32 | res := rgx.FindStringSubmatch(name) 33 | 34 | if len(res) != 0 { 35 | machines := database.GocentralDatabase.Collection("machines") 36 | var machine models.Machine 37 | 38 | if err = machines.FindOne(context.TODO(), bson.M{"wii_friend_code": res[1]}).Decode(&machine); err != nil { 39 | log.Println("Could not find machine with friend code " + fmt.Sprint(res[1]) + " in database") 40 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.UnknownError) 41 | return 42 | } 43 | 44 | user.Username = "Master User (" + machine.WiiFriendCode + ")" 45 | user.PID = uint32(machine.MachineID) 46 | } else { 47 | log.Println("Could not find user or machine with name " + fmt.Sprint(name) + " in database") 48 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.UnknownError) 49 | return 50 | } 51 | } 52 | 53 | rmcResponseStream := nex.NewStream() 54 | 55 | rmcResponseStream.WriteUInt32LE(1) 56 | 57 | rmcResponseStream.WriteUInt32LE(user.PID) 58 | rmcResponseStream.WriteBufferString(user.Username) 59 | 60 | rmcResponseBody := rmcResponseStream.Bytes() 61 | 62 | rmcResponse := nex.NewRMCResponse(nexproto.AccountManagementProtocolID, callID) 63 | rmcResponse.SetSuccess(nexproto.FindByNameLike, rmcResponseBody) 64 | 65 | rmcResponseBytes := rmcResponse.Bytes() 66 | 67 | responsePacket, _ := nex.NewPacketV0(client, nil) 68 | 69 | responsePacket.SetVersion(0) 70 | responsePacket.SetSource(0x31) 71 | responsePacket.SetDestination(0x3F) 72 | responsePacket.SetType(nex.DataPacket) 73 | 74 | responsePacket.SetPayload(rmcResponseBytes) 75 | 76 | responsePacket.AddFlag(nex.FlagNeedsAck) 77 | responsePacket.AddFlag(nex.FlagReliable) 78 | 79 | SecureServer.Send(responsePacket) 80 | } 81 | -------------------------------------------------------------------------------- /servers/findbysingleid.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "rb3server/database" 7 | "rb3server/models" 8 | 9 | "github.com/ihatecompvir/nex-go" 10 | nexproto "github.com/ihatecompvir/nex-protocols-go" 11 | "go.mongodb.org/mongo-driver/bson" 12 | ) 13 | 14 | func FindBySingleID(err error, client *nex.Client, callID uint32, gatheringID uint32) { 15 | users := database.GocentralDatabase.Collection("users") 16 | gatheringCollection := database.GocentralDatabase.Collection("gatherings") 17 | var user models.User 18 | var gathering models.Gathering 19 | 20 | res, _ := ValidateClientPID(SecureServer, client, callID, nexproto.MatchmakingProtocolID) 21 | 22 | if !res { 23 | return 24 | } 25 | 26 | rmcResponseStream := nex.NewStream() 27 | 28 | if err = gatheringCollection.FindOne(nil, bson.M{"gathering_id": gatheringID}).Decode(&gathering); err != nil { 29 | log.Printf("Could not find gatheringID %s of gathering: %+v\n", gatheringID, err) 30 | rmcResponseStream.WriteUInt8(0) 31 | return 32 | } else { 33 | 34 | if err = users.FindOne(nil, bson.M{"username": gathering.Creator}).Decode(&user); err != nil { 35 | log.Println("Could not find user with username " + fmt.Sprint(gathering.Creator) + " in database") 36 | rmcResponseStream.WriteUInt8(0) 37 | return 38 | } else { 39 | rmcResponseStream.WriteUInt8(1) 40 | 41 | rmcResponseStream.WriteBufferString("HarmonixGathering") 42 | rmcResponseStream.WriteUInt32LE(uint32(len(gathering.Contents) + 4)) 43 | rmcResponseStream.WriteUInt32LE(uint32(len(gathering.Contents))) 44 | rmcResponseStream.Grow(int64(len(gathering.Contents))) 45 | rmcResponseStream.WriteBytesNext(gathering.Contents[0:4]) 46 | rmcResponseStream.WriteUInt32LE(user.PID) 47 | rmcResponseStream.WriteUInt32LE(user.PID) 48 | rmcResponseStream.WriteBytesNext(gathering.Contents[12:]) 49 | } 50 | } 51 | 52 | rmcResponseBody := rmcResponseStream.Bytes() 53 | 54 | rmcResponse := nex.NewRMCResponse(nexproto.MatchmakingProtocolID, callID) 55 | rmcResponse.SetSuccess(nexproto.FindBySingleID, rmcResponseBody) 56 | 57 | rmcResponseBytes := rmcResponse.Bytes() 58 | 59 | responsePacket, _ := nex.NewPacketV0(client, nil) 60 | 61 | responsePacket.SetVersion(0) 62 | responsePacket.SetSource(0x31) 63 | responsePacket.SetDestination(0x3F) 64 | responsePacket.SetType(nex.DataPacket) 65 | 66 | responsePacket.SetPayload(rmcResponseBytes) 67 | 68 | responsePacket.AddFlag(nex.FlagNeedsAck) 69 | responsePacket.AddFlag(nex.FlagReliable) 70 | 71 | SecureServer.Send(responsePacket) 72 | } 73 | -------------------------------------------------------------------------------- /servers/getconsoleusernames.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/quazal" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | nexproto "github.com/ihatecompvir/nex-protocols-go" 12 | "go.mongodb.org/mongo-driver/bson" 13 | ) 14 | 15 | func GetConsoleUsernames(err error, client *nex.Client, callID uint32, friendCode string) { 16 | 17 | res, _ := ValidateClientPID(SecureServer, client, callID, nexproto.NintendoManagementProtocolID) 18 | 19 | if !res { 20 | return 21 | } 22 | 23 | log.Printf("Getting console usernames for machine with friend code %v\n", friendCode) 24 | 25 | machinesCollection := database.GocentralDatabase.Collection("machines") 26 | 27 | // look up the machine with the associated friend code in the database 28 | var machine models.Machine 29 | 30 | _ = machinesCollection.FindOne(context.TODO(), bson.M{"wii_friend_code": friendCode}).Decode(&machine) 31 | 32 | var users []models.User 33 | 34 | // if the machine ID is 0, it doesn't exist 35 | if machine.MachineID == 0 { 36 | // TODO: fake machine ID we can use for Wii Friends that don't have RB3 37 | log.Printf("Machine with friend code %v does not exist\n", friendCode) 38 | } else { 39 | // add the master user to the usernames list 40 | var masterUser models.User 41 | masterUser.Username = "Master User (" + friendCode + ")" 42 | masterUser.PID = uint32(machine.MachineID) 43 | users = append(users, masterUser) 44 | 45 | // now that we have the machine ID, we can look up all associated users 46 | usersCollection := database.GocentralDatabase.Collection("users") 47 | 48 | cur, err := usersCollection.Find(context.TODO(), bson.M{"created_by_machine_id": machine.MachineID}) 49 | 50 | if err != nil { 51 | log.Printf("Could not find users for machine %v: %v\n", machine.MachineID, err) 52 | SendErrorCode(SecureServer, client, nexproto.NintendoManagementProtocolID, callID, quazal.UnknownError) 53 | return 54 | } 55 | 56 | // iterate through the users and add them to the list 57 | for cur.Next(context.Background()) { 58 | var user models.User 59 | err := cur.Decode(&user) 60 | if err != nil { 61 | log.Printf("Could not decode user: %v\n", err) 62 | SendErrorCode(SecureServer, client, nexproto.NintendoManagementProtocolID, callID, quazal.UnknownError) 63 | return 64 | } 65 | // limit the amount of users reported to 4 (including the Master User) 66 | if len(users) <= 4 { 67 | users = append(users, user) 68 | } 69 | } 70 | } 71 | 72 | // create a stream to hold the response 73 | rmcResponseStream := nex.NewStream() 74 | 75 | rmcResponseStream.WriteUInt32LE(uint32(len(users))) 76 | 77 | for _, user := range users { 78 | rmcResponseStream.WriteBufferString(user.Username) 79 | } 80 | 81 | rmcResponseBody := rmcResponseStream.Bytes() 82 | 83 | rmcResponse := nex.NewRMCResponse(nexproto.NintendoManagementProtocolID, callID) 84 | rmcResponse.SetSuccess(nexproto.GetConsoleUsernames, rmcResponseBody) 85 | 86 | rmcResponseBytes := rmcResponse.Bytes() 87 | 88 | responsePacket, _ := nex.NewPacketV0(client, nil) 89 | 90 | responsePacket.SetVersion(0) 91 | responsePacket.SetSource(0x31) 92 | responsePacket.SetDestination(0x3F) 93 | responsePacket.SetType(nex.DataPacket) 94 | 95 | responsePacket.SetPayload(rmcResponseBytes) 96 | 97 | responsePacket.AddFlag(nex.FlagNeedsAck) 98 | responsePacket.AddFlag(nex.FlagReliable) 99 | 100 | SecureServer.Send(responsePacket) 101 | 102 | } 103 | -------------------------------------------------------------------------------- /servers/getmessageheaders.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/ihatecompvir/nex-go" 7 | nexproto "github.com/ihatecompvir/nex-protocols-go" 8 | ) 9 | 10 | func GetMessageHeaders(err error, client *nex.Client, callID uint32, pid uint32, gatheringID uint32, rangeOffset uint32, rangeSize uint32) { 11 | 12 | res, _ := ValidateClientPID(SecureServer, client, callID, nexproto.MessagingProtocolID) 13 | 14 | if !res { 15 | return 16 | } 17 | 18 | log.Printf("Getting message headers for PID %v\n", pid) 19 | rmcResponseStream := nex.NewStream() 20 | rmcResponseStream.WriteUInt32LE(0) 21 | rmcResponseStream.WriteUInt32LE(0) 22 | 23 | rmcResponseBody := rmcResponseStream.Bytes() 24 | 25 | rmcResponse := nex.NewRMCResponse(nexproto.MessagingProtocolID, callID) 26 | rmcResponse.SetSuccess(nexproto.GetMessageHeaders, rmcResponseBody) 27 | 28 | rmcResponseBytes := rmcResponse.Bytes() 29 | 30 | responsePacket, _ := nex.NewPacketV0(client, nil) 31 | 32 | responsePacket.SetVersion(0) 33 | responsePacket.SetSource(0x31) 34 | responsePacket.SetDestination(0x3F) 35 | responsePacket.SetType(nex.DataPacket) 36 | 37 | responsePacket.SetPayload(rmcResponseBytes) 38 | 39 | responsePacket.AddFlag(nex.FlagNeedsAck) 40 | responsePacket.AddFlag(nex.FlagReliable) 41 | 42 | SecureServer.Send(responsePacket) 43 | } 44 | -------------------------------------------------------------------------------- /servers/getstatus.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "github.com/ihatecompvir/nex-go" 5 | nexproto "github.com/ihatecompvir/nex-protocols-go" 6 | ) 7 | 8 | func GetStatus(err error, client *nex.Client, callID uint32, pid uint32) { 9 | /* 10 | res, _ := ValidateClientPID(SecureServer, client, callID, nexproto.AccountManagementProtocolID) 11 | 12 | if !res { 13 | return 14 | } 15 | 16 | var status string 17 | 18 | log.Printf("Getting status for PID %d\n", pid) 19 | 20 | machines := database.GocentralDatabase.Collection("machines") 21 | var machine models.Machine 22 | 23 | if err = machines.FindOne(context.TODO(), bson.M{"machine_id": pid}).Decode(&machine); err != nil { 24 | log.Printf("Could not find machine with PID %d in database\n", pid) 25 | status = "Offline" 26 | } else { 27 | status = machine.Status 28 | } 29 | 30 | log.Printf("Responding %s\n", status) 31 | */ 32 | 33 | // stubbed GetStatus for now until friends can properly work 34 | 35 | var status = "Offline" 36 | 37 | rmcResponseStream := nex.NewStream() 38 | 39 | rmcResponseStream.WriteBufferString(status) 40 | 41 | rmcResponseBody := rmcResponseStream.Bytes() 42 | 43 | rmcResponse := nex.NewRMCResponse(nexproto.AccountManagementProtocolID, callID) 44 | rmcResponse.SetSuccess(nexproto.GetStatus, rmcResponseBody) 45 | 46 | rmcResponseBytes := rmcResponse.Bytes() 47 | 48 | responsePacket, _ := nex.NewPacketV0(client, nil) 49 | 50 | responsePacket.SetVersion(0) 51 | responsePacket.SetSource(0x31) 52 | responsePacket.SetDestination(0x3F) 53 | responsePacket.SetType(nex.DataPacket) 54 | 55 | responsePacket.SetPayload(rmcResponseBytes) 56 | 57 | responsePacket.AddFlag(nex.FlagNeedsAck) 58 | responsePacket.AddFlag(nex.FlagReliable) 59 | 60 | SecureServer.Send(responsePacket) 61 | 62 | } 63 | -------------------------------------------------------------------------------- /servers/jsonrequest.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "rb3server/protocols/jsonproto" 5 | "rb3server/quazal" 6 | 7 | "github.com/ihatecompvir/nex-go" 8 | nexproto "github.com/ihatecompvir/nex-protocols-go" 9 | ) 10 | 11 | var jsonMgr = jsonproto.NewServicesManager() 12 | 13 | func JSONRequest(err error, client *nex.Client, callID uint32, rawJson string) { 14 | 15 | validationRes, _ := ValidateClientPID(SecureServer, client, callID, nexproto.JsonProtocolID) 16 | 17 | if !validationRes { 18 | return 19 | } 20 | 21 | // the JSON server will handle the request depending on what needs to be returned 22 | res, err := jsonMgr.Handle(rawJson, client) 23 | if err != nil { 24 | SendErrorCode(SecureServer, client, nexproto.JsonProtocolID, callID, quazal.UnknownError) 25 | return 26 | } 27 | 28 | rmcResponseStream := nex.NewStream() 29 | rmcResponseStream.WriteBufferString(res) 30 | 31 | rmcResponseBody := rmcResponseStream.Bytes() 32 | 33 | // Build response packet 34 | rmcResponse := nex.NewRMCResponse(nexproto.JsonProtocolID, callID) 35 | rmcResponse.SetSuccess(nexproto.JsonRequest, rmcResponseBody) 36 | 37 | rmcResponseBytes := rmcResponse.Bytes() 38 | 39 | responsePacket, _ := nex.NewPacketV0(client, nil) 40 | 41 | responsePacket.SetVersion(0) 42 | responsePacket.SetSource(0x31) 43 | responsePacket.SetDestination(0x3F) 44 | responsePacket.SetType(nex.DataPacket) 45 | 46 | responsePacket.SetPayload(rmcResponseBytes) 47 | 48 | responsePacket.AddFlag(nex.FlagNeedsAck) 49 | responsePacket.AddFlag(nex.FlagReliable) 50 | 51 | SecureServer.Send(responsePacket) 52 | } 53 | 54 | func JSONRequest2(err error, client *nex.Client, callID uint32, rawJson string) { 55 | 56 | // we don't need to actually respond with any JSON here, the official servers just sent an empty response 57 | // this method was exclusively for telemetry 58 | _, _ = jsonMgr.Handle(rawJson, client) 59 | 60 | rmcResponseStream := nex.NewStream() 61 | 62 | rmcResponseBody := rmcResponseStream.Bytes() 63 | 64 | // Build response packet 65 | rmcResponse := nex.NewRMCResponse(nexproto.JsonProtocolID, callID) 66 | rmcResponse.SetSuccess(nexproto.JsonRequest2, rmcResponseBody) 67 | 68 | // Even though this is a JSON-style method, it returns an empty body unlike JSONRequest 69 | rmcResponseBytes := rmcResponse.Bytes() 70 | 71 | responsePacket, _ := nex.NewPacketV0(client, nil) 72 | 73 | responsePacket.SetVersion(0) 74 | responsePacket.SetSource(0x31) 75 | responsePacket.SetDestination(0x3F) 76 | responsePacket.SetType(nex.DataPacket) 77 | 78 | responsePacket.SetPayload(rmcResponseBytes) 79 | 80 | responsePacket.AddFlag(nex.FlagNeedsAck) 81 | responsePacket.AddFlag(nex.FlagReliable) 82 | 83 | SecureServer.Send(responsePacket) 84 | } 85 | -------------------------------------------------------------------------------- /servers/launchsession.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/ihatecompvir/nex-go" 7 | nexproto "github.com/ihatecompvir/nex-protocols-go" 8 | ) 9 | 10 | func LaunchSession(err error, client *nex.Client, callID uint32, gatheringID uint32) { 11 | 12 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.MatchmakingProtocolID) 13 | 14 | if !res { 15 | return 16 | } 17 | 18 | log.Printf("Launching session for %s...\n", client.Username) 19 | 20 | rmcResponseStream := nex.NewStream() 21 | rmcResponseStream.Grow(4) 22 | 23 | rmcResponseStream.WriteUInt8(1) 24 | 25 | rmcResponseBody := rmcResponseStream.Bytes() 26 | 27 | rmcResponse := nex.NewRMCResponse(nexproto.MatchmakingProtocolID, callID) 28 | rmcResponse.SetSuccess(nexproto.LaunchSession, rmcResponseBody) 29 | 30 | rmcResponseBytes := rmcResponse.Bytes() 31 | 32 | responsePacket, _ := nex.NewPacketV0(client, nil) 33 | 34 | responsePacket.SetVersion(0) 35 | responsePacket.SetSource(0x31) 36 | responsePacket.SetDestination(0x3F) 37 | responsePacket.SetType(nex.DataPacket) 38 | 39 | responsePacket.SetPayload(rmcResponseBytes) 40 | 41 | responsePacket.AddFlag(nex.FlagNeedsAck) 42 | responsePacket.AddFlag(nex.FlagReliable) 43 | 44 | SecureServer.Send(responsePacket) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /servers/lookuporcreateaccount.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "rb3server/database" 8 | "rb3server/models" 9 | "rb3server/quazal" 10 | "rb3server/utils" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | nexproto "github.com/ihatecompvir/nex-protocols-go" 14 | "go.mongodb.org/mongo-driver/bson" 15 | ) 16 | 17 | // also handles Xbox 360 account switching 18 | func LookupOrCreateAccount(err error, client *nex.Client, callID uint32, username string, key string, groups uint32, email string) { 19 | 20 | // don't allow users who have not logged in as a master user to create/lookup accounts 21 | res, _ := ValidateClientPID(SecureServer, client, callID, nexproto.AccountManagementProtocolID) 22 | 23 | if !res { 24 | return 25 | } 26 | 27 | rmcResponseStream := nex.NewStream() 28 | 29 | users := database.GocentralDatabase.Collection("users") 30 | configCollection := database.GocentralDatabase.Collection("config") 31 | machinesCollection := database.GocentralDatabase.Collection("machines") 32 | 33 | var user models.User 34 | 35 | var config models.Config 36 | err = configCollection.FindOne(context.TODO(), bson.M{}).Decode(&config) 37 | if err != nil { 38 | log.Printf("Could not get config %v\n", err) 39 | } 40 | 41 | if result := users.FindOne(context.TODO(), bson.M{"username": username}).Decode(&user); result != nil { 42 | log.Printf("%s has never connected before - create DB entry\n", username) 43 | 44 | guid, err := generateGUID() 45 | 46 | if client.Platform() == 2 { 47 | _, err = users.InsertOne(context.TODO(), bson.D{ 48 | {Key: "username", Value: username}, 49 | {Key: "pid", Value: config.LastPID + 1}, 50 | {Key: "console_type", Value: client.Platform()}, 51 | {Key: "guid", Value: guid}, 52 | {Key: "created_by_machine_id", Value: client.MachineID()}, 53 | }) 54 | } else { 55 | _, err = users.InsertOne(context.TODO(), bson.D{ 56 | {Key: "username", Value: username}, 57 | {Key: "pid", Value: config.LastPID + 1}, 58 | {Key: "console_type", Value: client.Platform()}, 59 | {Key: "guid", Value: guid}, 60 | }) 61 | } 62 | 63 | if err != nil { 64 | log.Printf("Could not create user %s: %s\n", username, err) 65 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.OperationError) 66 | return 67 | } 68 | 69 | _, err = configCollection.UpdateOne( 70 | context.TODO(), 71 | bson.M{}, 72 | bson.D{ 73 | {"$set", bson.D{{"last_pid", config.LastPID + 1}}}, 74 | }, 75 | ) 76 | if err != nil { 77 | log.Println("Could not update config in database: ", err) 78 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.OperationError) 79 | return 80 | } 81 | 82 | Config.LastPID = config.LastPID + 1 83 | Config.LastMachineID = config.LastMachineID 84 | Config.LastBandID = config.LastBandID 85 | Config.LastSetlistID = config.LastSetlistID 86 | Config.LastCharacterID = config.LastCharacterID 87 | 88 | // make sure we actually set the server-assigned PID to the new one when it is created 89 | client.SetPlayerID(user.PID) 90 | 91 | if err = users.FindOne(context.TODO(), bson.M{"username": username}).Decode(&user); err != nil { 92 | 93 | if err != nil { 94 | log.Printf("Could not find newly created user: %s\n", err) 95 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.OperationError) 96 | return 97 | } 98 | } 99 | } 100 | 101 | log.Printf("%s requesting to lookup or create an account\n", username) 102 | 103 | client.Username = username 104 | 105 | var stationURL string = "prudp:/address=" + client.Address().IP.String() + ";port=" + fmt.Sprint(client.Address().Port) + ";PID=" + fmt.Sprint(user.PID) + ";sid=15;type=3;RVCID=" + fmt.Sprint(client.ConnectionID()) 106 | 107 | client.SetExternalStationURL(stationURL) 108 | client.SetPlayerID(user.PID) 109 | utils.GetClientStoreSingleton().AddClient(client.Address().String()) 110 | utils.GetClientStoreSingleton().PushPID(client.Address().String(), client.PlayerID()) 111 | 112 | if client.Platform() == 2 { 113 | // update station URL of the machine that created the user 114 | result, err := machinesCollection.UpdateOne( 115 | context.TODO(), 116 | bson.M{"machine_id": client.MachineID()}, 117 | bson.D{ 118 | {"$set", bson.D{{"station_url", stationURL}}}, 119 | }, 120 | ) 121 | 122 | if err != nil { 123 | log.Printf("Could not update station URLs for machine ID %v: %s\n", client.MachineID(), err) 124 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.OperationError) 125 | return 126 | } 127 | 128 | log.Printf("Updated %v station URL for machine ID %v \n", result.ModifiedCount, client.MachineID()) 129 | } 130 | 131 | rmcResponseStream.WriteUInt32LE(user.PID) 132 | 133 | rmcResponseBody := rmcResponseStream.Bytes() 134 | 135 | rmcResponse := nex.NewRMCResponse(nexproto.AccountManagementProtocolID, callID) 136 | rmcResponse.SetSuccess(nexproto.LookupOrCreateAccount, rmcResponseBody) 137 | 138 | rmcResponseBytes := rmcResponse.Bytes() 139 | 140 | responsePacket, _ := nex.NewPacketV0(client, nil) 141 | 142 | responsePacket.SetVersion(0) 143 | responsePacket.SetSource(0x31) 144 | responsePacket.SetDestination(0x3F) 145 | responsePacket.SetType(nex.DataPacket) 146 | 147 | responsePacket.SetPayload(rmcResponseBytes) 148 | 149 | responsePacket.AddFlag(nex.FlagNeedsAck) 150 | responsePacket.AddFlag(nex.FlagReliable) 151 | 152 | SecureServer.Send(responsePacket) 153 | } 154 | -------------------------------------------------------------------------------- /servers/participate.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "github.com/ihatecompvir/nex-go" 5 | nexproto "github.com/ihatecompvir/nex-protocols-go" 6 | ) 7 | 8 | func Participate(err error, client *nex.Client, callID uint32, gatheringID uint32) { 9 | 10 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.MatchmakingProtocolID) 11 | 12 | if !res { 13 | return 14 | } 15 | 16 | rmcResponseStream := nex.NewStream() 17 | 18 | // i am not 100% sure what this method is for exactly 19 | rmcResponseStream.WriteUInt32LE(1) // response code 20 | 21 | rmcResponseBody := rmcResponseStream.Bytes() 22 | 23 | rmcResponse := nex.NewRMCResponse(nexproto.MatchmakingProtocolID, callID) 24 | rmcResponse.SetSuccess(nexproto.Participate, rmcResponseBody) 25 | 26 | rmcResponseBytes := rmcResponse.Bytes() 27 | 28 | responsePacket, _ := nex.NewPacketV0(client, nil) 29 | 30 | responsePacket.SetVersion(0) 31 | responsePacket.SetSource(0x31) 32 | responsePacket.SetDestination(0x3F) 33 | responsePacket.SetType(nex.DataPacket) 34 | 35 | responsePacket.SetPayload(rmcResponseBytes) 36 | 37 | responsePacket.AddFlag(nex.FlagNeedsAck) 38 | responsePacket.AddFlag(nex.FlagReliable) 39 | 40 | SecureServer.Send(responsePacket) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /servers/registergathering.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "rb3server/database" 7 | 8 | "time" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | nexproto "github.com/ihatecompvir/nex-protocols-go" 12 | "go.mongodb.org/mongo-driver/bson" 13 | ) 14 | 15 | func RegisterGathering(err error, client *nex.Client, callID uint32, gathering []byte) { 16 | 17 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.MatchmakingProtocolID) 18 | 19 | if !res { 20 | return 21 | } 22 | 23 | log.Println("Registering gathering...") 24 | 25 | // delete old gatherings, and create a new gathering 26 | 27 | gatherings := database.GocentralDatabase.Collection("gatherings") 28 | 29 | gatheringID := rand.Intn(250000-500) + 500 30 | 31 | // Attempt to clear stale gatherings that may exist 32 | // If there are stale gatherings registered, other clients will try to connect to sessions that don't exist anymore 33 | deleteResult, deleteError := gatherings.DeleteMany(nil, bson.D{ 34 | {Key: "creator", Value: client.Username}, 35 | }) 36 | 37 | if deleteError != nil { 38 | log.Println("Could not clear stale gatherings") 39 | } 40 | 41 | if deleteResult.DeletedCount != 0 { 42 | log.Printf("Successfully cleared %v stale gatherings for %s...\n", deleteResult.DeletedCount, client.Username) 43 | } 44 | 45 | // Create a new gathering 46 | _, err = gatherings.InsertOne(nil, bson.D{ 47 | {Key: "gathering_id", Value: gatheringID}, 48 | {Key: "contents", Value: gathering}, 49 | {Key: "creator", Value: client.Username}, 50 | {Key: "last_updated", Value: time.Now().Unix()}, 51 | {Key: "state", Value: 0}, 52 | {Key: "public", Value: 0}, 53 | {Key: "console_type", Value: client.Platform()}, 54 | }) 55 | 56 | if err != nil { 57 | log.Printf("Failed to create gathering: %+v\n", err) 58 | } 59 | 60 | rmcResponseStream := nex.NewStream() 61 | 62 | rmcResponseStream.WriteUInt32LE(uint32(gatheringID)) // client expects the new gathering ID in the response 63 | 64 | rmcResponseBody := rmcResponseStream.Bytes() 65 | 66 | rmcResponse := nex.NewRMCResponse(nexproto.MatchmakingProtocolID, callID) 67 | rmcResponse.SetSuccess(nexproto.RegisterGathering, rmcResponseBody) 68 | 69 | rmcResponseBytes := rmcResponse.Bytes() 70 | 71 | responsePacket, _ := nex.NewPacketV0(client, nil) 72 | 73 | responsePacket.SetVersion(0) 74 | responsePacket.SetSource(0x31) 75 | responsePacket.SetDestination(0x3F) 76 | responsePacket.SetType(nex.DataPacket) 77 | 78 | responsePacket.SetPayload(rmcResponseBytes) 79 | 80 | responsePacket.AddFlag(nex.FlagNeedsAck) 81 | responsePacket.AddFlag(nex.FlagReliable) 82 | 83 | SecureServer.Send(responsePacket) 84 | 85 | } 86 | -------------------------------------------------------------------------------- /servers/requestprobeinitiation.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "log" 5 | "rb3server/quazal" 6 | 7 | "github.com/ihatecompvir/nex-go" 8 | nexproto "github.com/ihatecompvir/nex-protocols-go" 9 | ) 10 | 11 | func RequestProbeInitiation(err error, client *nex.Client, callID uint32, stationURLs []string) { 12 | 13 | // check that client is not nil 14 | if client == nil { 15 | log.Println("Client is nil, cannot perform NAT probe") 16 | return 17 | } 18 | 19 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.NATTraversalProtocolID) 20 | 21 | if !res { 22 | return 23 | } 24 | 25 | log.Printf("Client wants to perform NAT traversal probes to %v servers...\n", len(stationURLs)) 26 | 27 | // make sure we aren't trying to probe more than 8 station URLs 28 | // RB3 is limited to 8 player lobbies, but I believe the game can probe both the internal and external station URLs of each player 29 | // so 8 should be a sufficient cap 30 | if len(stationURLs) > 8 { 31 | log.Println("Client is attempting to probe more than 8 servers, rejecting call") 32 | SendErrorCode(SecureServer, client, nexproto.NATTraversalProtocolID, callID, quazal.InvalidArgument) 33 | return 34 | } 35 | 36 | rmcResponseStream := nex.NewStream() 37 | 38 | rmcResponseBody := rmcResponseStream.Bytes() 39 | 40 | rmcResponse := nex.NewRMCResponse(nexproto.NATTraversalProtocolID, callID) 41 | rmcResponse.SetSuccess(nexproto.RequestProbeInitiation, rmcResponseBody) 42 | 43 | responsePacket, _ := nex.NewPacketV0(client, nil) 44 | 45 | responsePacket.SetVersion(0) 46 | responsePacket.SetSource(0x31) 47 | responsePacket.SetDestination(0x3F) 48 | responsePacket.SetType(nex.DataPacket) 49 | 50 | responsePacket.AddFlag(nex.FlagNeedsAck) 51 | responsePacket.AddFlag(nex.FlagReliable) 52 | 53 | SecureServer.Send(responsePacket) 54 | 55 | rmcMessage := nex.RMCRequest{} 56 | rmcMessage.SetProtocolID(nexproto.NATTraversalProtocolID) 57 | rmcMessage.SetCallID(callID) 58 | rmcMessage.SetMethodID(nexproto.InitiateProbe) 59 | rmcRequestStream := nex.NewStreamOut(SecureServer) 60 | rmcRequestStream.WriteBufferString(client.ExternalStationURL()) 61 | rmcRequestBody := rmcRequestStream.Bytes() 62 | rmcMessage.SetParameters(rmcRequestBody) 63 | rmcMessageBytes := rmcMessage.Bytes() 64 | 65 | // loop through every station URL in the probe request and send InitiateProbe to them 66 | // This should make all targets respond to NAT probes from the joining client 67 | for _, target := range stationURLs { 68 | 69 | // sanity check on station URL length 70 | if len(target) > 256 { 71 | log.Println("Station URL is too long, skipping probe") 72 | continue 73 | } 74 | 75 | targetUrl := nex.NewStationURL(target) 76 | 77 | if targetUrl == nil { 78 | log.Println("Could not parse station URL, skipping probe") 79 | continue 80 | } 81 | 82 | log.Println("Sending NAT probe to " + target) 83 | targetClient := SecureServer.FindClientFromIPAddress(targetUrl.Address() + ":" + targetUrl.Port()) 84 | if targetClient != nil { 85 | var messagePacket nex.PacketInterface 86 | 87 | messagePacket, _ = nex.NewPacketV0(targetClient, nil) 88 | 89 | log.Println("Found active client " + targetClient.ExternalStationURL() + " with RVCID " + targetUrl.RVCID() + " and username " + targetClient.Username + " and IP address " + targetClient.Address().IP.String()) 90 | messagePacket.SetVersion(0) 91 | 92 | messagePacket.SetSource(0x31) 93 | messagePacket.SetDestination(0x3F) 94 | messagePacket.SetType(nex.DataPacket) 95 | 96 | messagePacket.SetPayload(rmcMessageBytes) 97 | messagePacket.AddFlag(nex.FlagNeedsAck) 98 | messagePacket.AddFlag(nex.FlagReliable) 99 | 100 | SecureServer.Send(messagePacket) 101 | } else { 102 | log.Printf("Could not find active client with IP %v, skipping probe\n", targetUrl.Address()+":"+targetUrl.Port()) 103 | continue 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /servers/requestticket.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/md5" 6 | "log" 7 | "rb3server/quazal" 8 | 9 | "github.com/ihatecompvir/nex-go" 10 | nexproto "github.com/ihatecompvir/nex-protocols-go" 11 | ) 12 | 13 | func RequestTicket(err error, client *nex.Client, callID uint32, userPID uint32, serverPID uint32) { 14 | 15 | if userPID != client.PlayerID() { 16 | log.Printf("Requested ticket for PID %v does not match server-assigned PID %v\n", userPID, client.PlayerID()) 17 | SendErrorCode(AuthServer, client, nexproto.AuthenticationProtocolID, callID, quazal.InvalidPID) // invalid PID error 18 | return 19 | } 20 | 21 | log.Printf("PID %v requesting ticket...\n", userPID) 22 | 23 | encryptedTicket, kerberosKey := generateKerberosTicket(userPID, uint32(serverPID), 16, client.WiiFC) 24 | mac := hmac.New(md5.New, kerberosKey) 25 | mac.Write(encryptedTicket) 26 | calculatedHmac := mac.Sum(nil) 27 | 28 | // Build the response body 29 | rmcResponseStream := nex.NewStream() 30 | rmcResponseStream.Grow(int64(4 + 4 + len(encryptedTicket) + 0x10)) 31 | 32 | rmcResponseStream.WriteUInt32LE(0x10001) // success 33 | rmcResponseStream.WriteBuffer(append(encryptedTicket[:], calculatedHmac[:]...)) 34 | 35 | rmcResponseBody := rmcResponseStream.Bytes() 36 | 37 | // Build response packet 38 | rmcResponse := nex.NewRMCResponse(nexproto.AuthenticationProtocolID, callID) 39 | rmcResponse.SetSuccess(nexproto.AuthenticationMethodRequestTicket, rmcResponseBody) 40 | 41 | rmcResponseBytes := rmcResponse.Bytes() 42 | 43 | responsePacket, _ := nex.NewPacketV0(client, nil) 44 | 45 | responsePacket.SetVersion(0) 46 | responsePacket.SetSource(0x31) 47 | responsePacket.SetDestination(0x3F) 48 | responsePacket.SetType(nex.DataPacket) 49 | 50 | responsePacket.SetPayload(rmcResponseBytes) 51 | 52 | responsePacket.AddFlag(nex.FlagNeedsAck) 53 | responsePacket.AddFlag(nex.FlagReliable) 54 | 55 | AuthServer.Send(responsePacket) 56 | } 57 | -------------------------------------------------------------------------------- /servers/requesturls.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/quazal" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | nexproto "github.com/ihatecompvir/nex-protocols-go" 12 | "go.mongodb.org/mongo-driver/bson" 13 | ) 14 | 15 | func RequestURLs(err error, client *nex.Client, callID uint32, stationCID uint32, stationPID uint32) { 16 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.SecureProtocolID) 17 | 18 | if !res { 19 | return 20 | } 21 | 22 | rmcResponseStream := nex.NewStream() 23 | 24 | log.Printf("Requesting station URL for %v\n", stationPID) 25 | 26 | users := database.GocentralDatabase.Collection("users") 27 | 28 | var user models.User 29 | 30 | if err = users.FindOne(nil, bson.M{"pid": stationPID}).Decode(&user); err != nil { 31 | log.Println("Could not find user with PID " + fmt.Sprint(stationPID) + " in database") 32 | SendErrorCode(SecureServer, client, nexproto.SecureProtocolID, callID, quazal.InvalidPID) 33 | return 34 | } 35 | 36 | // check if the user was created by a machine or not 37 | if user.CreatedByMachineID == 0 { 38 | if user.IntStationURL != "" { 39 | rmcResponseStream.WriteUInt8(1) // response code 40 | rmcResponseStream.WriteUInt32LE(2) // the number of station urls present 41 | rmcResponseStream.WriteBufferString(user.StationURL) // WAN station URL 42 | rmcResponseStream.WriteBufferString(user.IntStationURL) // LAN station URL used for connecting to other players on the same LAN 43 | } else { 44 | rmcResponseStream.WriteUInt8(1) // response code 45 | rmcResponseStream.WriteUInt32LE(1) // the number of station urls present 46 | rmcResponseStream.WriteBufferString(user.StationURL) // WAN station URL 47 | } 48 | } else { 49 | machines := database.GocentralDatabase.Collection("machines") 50 | 51 | var machine models.Machine 52 | 53 | if err = machines.FindOne(nil, bson.M{"machine_id": user.CreatedByMachineID}).Decode(&machine); err != nil { 54 | log.Println("Could not find machine with ID " + fmt.Sprint(user.CreatedByMachineID) + " in database") 55 | SendErrorCode(SecureServer, client, nexproto.SecureProtocolID, callID, quazal.OperationError) 56 | return 57 | } 58 | 59 | if machine.IntStationURL != "" && machine.StationURL != "" { 60 | rmcResponseStream.WriteUInt8(1) 61 | rmcResponseStream.WriteUInt32LE(2) 62 | rmcResponseStream.WriteBufferString(machine.StationURL) 63 | rmcResponseStream.WriteBufferString(machine.IntStationURL) 64 | } else if machine.StationURL != "" { 65 | rmcResponseStream.WriteUInt8(1) 66 | rmcResponseStream.WriteUInt32LE(1) 67 | rmcResponseStream.WriteBufferString(machine.StationURL) 68 | } 69 | } 70 | 71 | rmcResponseBody := rmcResponseStream.Bytes() 72 | 73 | rmcResponse := nex.NewRMCResponse(nexproto.SecureProtocolID, callID) 74 | rmcResponse.SetSuccess(nexproto.SecureMethodRequestURLs, rmcResponseBody) 75 | 76 | rmcResponseBytes := rmcResponse.Bytes() 77 | 78 | responsePacket, _ := nex.NewPacketV0(client, nil) 79 | 80 | responsePacket.SetVersion(0) 81 | responsePacket.SetSource(0x31) 82 | responsePacket.SetDestination(0x3F) 83 | responsePacket.SetType(nex.DataPacket) 84 | 85 | responsePacket.SetPayload(rmcResponseBytes) 86 | 87 | responsePacket.AddFlag(nex.FlagNeedsAck) 88 | responsePacket.AddFlag(nex.FlagReliable) 89 | 90 | SecureServer.Send(responsePacket) 91 | } 92 | -------------------------------------------------------------------------------- /servers/savebinarydata.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | "rb3server/quazal" 9 | 10 | "github.com/ihatecompvir/nex-go" 11 | nexproto "github.com/ihatecompvir/nex-protocols-go" 12 | ) 13 | 14 | func SaveBinaryData(err error, client *nex.Client, callID uint32, metadata string, data []byte) { 15 | 16 | res, _ := ValidateClientPID(SecureServer, client, callID, nexproto.RBBinaryDataProtocolID) 17 | 18 | if !res { 19 | return 20 | } 21 | 22 | var metadataMap map[string]interface{} 23 | err = json.Unmarshal([]byte(metadata), &metadataMap) 24 | 25 | if err != nil { 26 | log.Println("Error parsing metadata: ", err) 27 | SendErrorCode(SecureServer, client, nexproto.RBBinaryDataProtocolID, callID, quazal.OperationError) 28 | return 29 | } 30 | 31 | // get the type 32 | dataType, ok := metadataMap["type"].(string) 33 | 34 | if !ok { 35 | log.Println("Error parsing type from requested metadata") 36 | SendErrorCode(SecureServer, client, nexproto.RBBinaryDataProtocolID, callID, quazal.OperationError) 37 | return 38 | } 39 | 40 | // switch on the type 41 | switch dataType { 42 | case "setlist_art": 43 | // get the setlist guid 44 | setlistGUID, ok := metadataMap["setlist_guid"].(string) 45 | 46 | if !ok { 47 | log.Println("Error parsing setlist_guid from requested metadata") 48 | SendErrorCode(SecureServer, client, nexproto.RBBinaryDataProtocolID, callID, quazal.OperationError) 49 | return 50 | } 51 | 52 | // get the revision 53 | revisionFloat, ok := metadataMap["revision"].(float64) 54 | 55 | if !ok { 56 | log.Println("Error parsing revision from requested metadata") 57 | SendErrorCode(SecureServer, client, nexproto.RBBinaryDataProtocolID, callID, quazal.OperationError) 58 | return 59 | } 60 | 61 | // convert float64 to int64 62 | revision := int64(revisionFloat) 63 | 64 | os.WriteFile(fmt.Sprintf("./binary_data/setlist_art/%s/%d.dxt", setlistGUID, revision), data, 0644) 65 | 66 | case "battle_art": 67 | // get the revision 68 | revisionFloat, ok := metadataMap["revision"].(float64) 69 | 70 | if !ok { 71 | log.Println("Error parsing revision from requested metadata") 72 | SendErrorCode(SecureServer, client, nexproto.RBBinaryDataProtocolID, callID, quazal.OperationError) 73 | return 74 | } 75 | 76 | // convert float64 to int64 77 | revision := int64(revisionFloat) 78 | 79 | // battle_art can optionally have battle_id; try to get it, but dont fail if it cant be found 80 | battleID, _ := metadataMap["battle_id"].(float64) 81 | 82 | os.WriteFile(fmt.Sprintf("./binary_data/battle_art/%d/%d.dxt", int64(battleID), revision), data, 0644) 83 | 84 | case "band_logo": 85 | // get the band id 86 | bandIDFloat, ok := metadataMap["band_id"].(float64) 87 | 88 | if !ok { 89 | log.Println("Error parsing band_id from requested metadata") 90 | SendErrorCode(SecureServer, client, nexproto.RBBinaryDataProtocolID, callID, quazal.OperationError) 91 | return 92 | } 93 | 94 | // convert float64 to int64 95 | bandID := int64(bandIDFloat) 96 | 97 | // get the revision 98 | revisionFloat, ok := metadataMap["revision"].(float64) 99 | 100 | if !ok { 101 | log.Println("Error parsing revision from requested metadata") 102 | SendErrorCode(SecureServer, client, nexproto.RBBinaryDataProtocolID, callID, quazal.OperationError) 103 | return 104 | } 105 | 106 | // convert float64 to int64 107 | revision := int64(revisionFloat) 108 | 109 | os.WriteFile(fmt.Sprintf("./binary_data/band_logo/%d/%d.dxt", bandID, revision), data, 0644) 110 | 111 | default: 112 | log.Printf("Unsupported type %s in requested metadata", dataType) 113 | SendErrorCode(SecureServer, client, nexproto.RBBinaryDataProtocolID, callID, quazal.OperationError) 114 | return 115 | } 116 | 117 | rmcResponseStream := nex.NewStream() 118 | 119 | rmcResponseStream.WriteBufferString("{}") // the game doesn't really care what we send here so just send empty json 120 | rmcResponseStream.WriteUInt8(0) 121 | 122 | rmcResponseBody := rmcResponseStream.Bytes() 123 | 124 | rmcResponse := nex.NewRMCResponse(nexproto.RBBinaryDataProtocolID, callID) 125 | rmcResponse.SetSuccess(nexproto.SaveBinaryData, rmcResponseBody) 126 | 127 | responsePacket, _ := nex.NewPacketV0(client, nil) 128 | 129 | responsePacket.SetVersion(0) 130 | responsePacket.SetSource(0x31) 131 | responsePacket.SetDestination(0x3F) 132 | responsePacket.SetType(nex.DataPacket) 133 | 134 | responsePacket.AddFlag(nex.FlagNeedsAck) 135 | responsePacket.AddFlag(nex.FlagReliable) 136 | 137 | SecureServer.Send(responsePacket) 138 | 139 | } 140 | -------------------------------------------------------------------------------- /servers/setstate.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/quazal" 9 | 10 | "time" 11 | 12 | "github.com/ihatecompvir/nex-go" 13 | nexproto "github.com/ihatecompvir/nex-protocols-go" 14 | "go.mongodb.org/mongo-driver/bson" 15 | ) 16 | 17 | func SetState(err error, client *nex.Client, callID uint32, gatheringID uint32, state uint32) { 18 | 19 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.MatchmakingProtocolID) 20 | 21 | if !res { 22 | return 23 | } 24 | 25 | log.Printf("Setting state to %v for gathering %v...\n", state, gatheringID) 26 | 27 | rmcResponseStream := nex.NewStream() 28 | 29 | gatherings := database.GocentralDatabase.Collection("gatherings") 30 | var gathering models.Gathering 31 | err = gatherings.FindOne(nil, bson.M{"gathering_id": gatheringID}).Decode(&gathering) 32 | 33 | if err != nil { 34 | log.Printf("Could not find gathering %v to set the state on: %v\n", gatheringID, err) 35 | SendErrorCode(SecureServer, client, nexproto.MatchmakingProtocolID, callID, quazal.OperationError) 36 | return 37 | } else { 38 | // Update the gathering.Contents, State, and LastUpdated fields 39 | gathering.Contents[0x1C] = (byte)(state>>(8*0)) & 0xff 40 | gathering.Contents[0x1D] = (byte)(state>>(8*1)) & 0xff 41 | gathering.Contents[0x1E] = (byte)(state>>(8*2)) & 0xff 42 | gathering.Contents[0x1F] = (byte)(state>>(8*3)) & 0xff 43 | 44 | update := bson.M{ 45 | "$set": bson.M{ 46 | "contents": gathering.Contents, 47 | "state": state, 48 | "last_updated": time.Now().Unix(), 49 | }, 50 | } 51 | 52 | _, err = gatherings.UpdateOne(context.TODO(), bson.M{"gathering_id": gatheringID}, update) 53 | if err != nil { 54 | log.Printf("Could not set state for gathering %v: %v\n", gatheringID, err) 55 | SendErrorCode(SecureServer, client, nexproto.MatchmakingProtocolID, callID, quazal.OperationError) 56 | return 57 | } else { 58 | rmcResponseStream.WriteUInt8(1) 59 | } 60 | } 61 | 62 | rmcResponseBody := rmcResponseStream.Bytes() 63 | 64 | rmcResponse := nex.NewRMCResponse(nexproto.MatchmakingProtocolID, callID) 65 | rmcResponse.SetSuccess(nexproto.SetState, rmcResponseBody) 66 | 67 | rmcResponseBytes := rmcResponse.Bytes() 68 | 69 | responsePacket, _ := nex.NewPacketV0(client, nil) 70 | 71 | responsePacket.SetVersion(0) 72 | responsePacket.SetSource(0x31) 73 | responsePacket.SetDestination(0x3F) 74 | responsePacket.SetType(nex.DataPacket) 75 | 76 | responsePacket.SetPayload(rmcResponseBytes) 77 | 78 | responsePacket.AddFlag(nex.FlagNeedsAck) 79 | responsePacket.AddFlag(nex.FlagReliable) 80 | 81 | SecureServer.Send(responsePacket) 82 | 83 | } 84 | -------------------------------------------------------------------------------- /servers/setstatus.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/database" 7 | "rb3server/quazal" 8 | 9 | "github.com/ihatecompvir/nex-go" 10 | nexproto "github.com/ihatecompvir/nex-protocols-go" 11 | "go.mongodb.org/mongo-driver/bson" 12 | ) 13 | 14 | func SetStatus(err error, client *nex.Client, callID uint32, status string) { 15 | 16 | res, _ := ValidateClientPID(SecureServer, client, callID, nexproto.AccountManagementProtocolID) 17 | 18 | if !res { 19 | return 20 | } 21 | 22 | machinesCollection := database.GocentralDatabase.Collection("machines") 23 | _, err = machinesCollection.UpdateOne( 24 | context.TODO(), 25 | bson.M{"machine_id": client.PlayerID()}, 26 | bson.D{ 27 | {"$set", bson.D{{"status", status}}}, 28 | }, 29 | ) 30 | 31 | if err != nil { 32 | log.Printf("Could not update status for machine %s: %s\n", client.Username, err) 33 | SendErrorCode(SecureServer, client, nexproto.AccountManagementProtocolID, callID, quazal.OperationError) 34 | return 35 | } 36 | 37 | rmcResponseStream := nex.NewStream() 38 | 39 | rmcResponseBody := rmcResponseStream.Bytes() 40 | 41 | rmcResponse := nex.NewRMCResponse(nexproto.AccountManagementProtocolID, callID) 42 | rmcResponse.SetSuccess(nexproto.SetStatus, rmcResponseBody) 43 | 44 | responsePacket, _ := nex.NewPacketV0(client, nil) 45 | 46 | responsePacket.SetVersion(0) 47 | responsePacket.SetSource(0x31) 48 | responsePacket.SetDestination(0x3F) 49 | responsePacket.SetType(nex.DataPacket) 50 | 51 | responsePacket.AddFlag(nex.FlagNeedsAck) 52 | responsePacket.AddFlag(nex.FlagReliable) 53 | 54 | SecureServer.Send(responsePacket) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /servers/terminategathering.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "log" 5 | "rb3server/database" 6 | "rb3server/quazal" 7 | 8 | "github.com/ihatecompvir/nex-go" 9 | nexproto "github.com/ihatecompvir/nex-protocols-go" 10 | "go.mongodb.org/mongo-driver/bson" 11 | ) 12 | 13 | func TerminateGathering(err error, client *nex.Client, callID uint32, gatheringID uint32) { 14 | 15 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.MatchmakingProtocolID) 16 | 17 | if !res { 18 | return 19 | } 20 | 21 | log.Printf("Terminating gathering ID %v for %s...\n", gatheringID, client.Username) 22 | 23 | gatherings := database.GocentralDatabase.Collection("gatherings") 24 | 25 | // remove the gathering from the DB so other players won't attempt to connect to it later 26 | result, err := gatherings.DeleteOne( 27 | nil, 28 | bson.M{"gathering_id": gatheringID}, 29 | ) 30 | 31 | if err != nil { 32 | log.Printf("Could not terminate gathering: %s\n", err) 33 | SendErrorCode(SecureServer, client, nexproto.MatchmakingProtocolID, callID, quazal.OperationError) 34 | return 35 | } 36 | 37 | log.Printf("Terminated %v gathering\n", result.DeletedCount) 38 | 39 | rmcResponseStream := nex.NewStream() 40 | 41 | rmcResponseStream.WriteUInt8(1) 42 | 43 | rmcResponseBody := rmcResponseStream.Bytes() 44 | 45 | rmcResponse := nex.NewRMCResponse(nexproto.MatchmakingProtocolID, callID) 46 | rmcResponse.SetSuccess(nexproto.TerminateGathering, rmcResponseBody) 47 | 48 | rmcResponseBytes := rmcResponse.Bytes() 49 | 50 | responsePacket, _ := nex.NewPacketV0(client, nil) 51 | 52 | responsePacket.SetVersion(0) 53 | responsePacket.SetSource(0x31) 54 | responsePacket.SetDestination(0x3F) 55 | responsePacket.SetType(nex.DataPacket) 56 | 57 | responsePacket.SetPayload(rmcResponseBytes) 58 | 59 | responsePacket.AddFlag(nex.FlagNeedsAck) 60 | responsePacket.AddFlag(nex.FlagReliable) 61 | 62 | SecureServer.Send(responsePacket) 63 | 64 | } 65 | -------------------------------------------------------------------------------- /servers/unparticipate.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "github.com/ihatecompvir/nex-go" 5 | nexproto "github.com/ihatecompvir/nex-protocols-go" 6 | ) 7 | 8 | func CancelParticipation(err error, client *nex.Client, callID uint32, gatheringID uint32) { 9 | 10 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.MatchmakingProtocolID) 11 | 12 | if !res { 13 | return 14 | } 15 | 16 | rmcResponseStream := nex.NewStream() 17 | 18 | // i am not 100% sure what this method is for, but it is the inverse of participate 19 | rmcResponseStream.WriteUInt8(1) 20 | 21 | rmcResponseBody := rmcResponseStream.Bytes() 22 | 23 | rmcResponse := nex.NewRMCResponse(nexproto.MatchmakingProtocolID, callID) 24 | rmcResponse.SetSuccess(nexproto.CancelParticipation, rmcResponseBody) 25 | 26 | rmcResponseBytes := rmcResponse.Bytes() 27 | 28 | responsePacket, _ := nex.NewPacketV0(client, nil) 29 | 30 | responsePacket.SetVersion(0) 31 | responsePacket.SetSource(0x31) 32 | responsePacket.SetDestination(0x3F) 33 | responsePacket.SetType(nex.DataPacket) 34 | 35 | responsePacket.SetPayload(rmcResponseBytes) 36 | 37 | responsePacket.AddFlag(nex.FlagNeedsAck) 38 | responsePacket.AddFlag(nex.FlagReliable) 39 | 40 | SecureServer.Send(responsePacket) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /servers/updategathering.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "rb3server/database" 7 | "rb3server/models" 8 | "rb3server/quazal" 9 | 10 | serialization "rb3server/serialization/gathering" 11 | "time" 12 | 13 | "github.com/ihatecompvir/nex-go" 14 | nexproto "github.com/ihatecompvir/nex-protocols-go" 15 | "go.mongodb.org/mongo-driver/bson" 16 | 17 | db "rb3server/database" 18 | ) 19 | 20 | func UpdateGathering(err error, client *nex.Client, callID uint32, gathering []byte, gatheringID uint32) { 21 | 22 | var deserializer serialization.GatheringDeserializer 23 | 24 | res, _ := ValidateNonMasterClientPID(SecureServer, client, callID, nexproto.MatchmakingProtocolID) 25 | 26 | if !res { 27 | return 28 | } 29 | 30 | g, err := deserializer.Deserialize(gathering) 31 | if err != nil { 32 | log.Printf("Could not deserialize the gathering!") 33 | SendErrorCode(SecureServer, client, nexproto.MatchmakingProtocolID, callID, quazal.OperationError) 34 | return 35 | } 36 | 37 | log.Printf("Updating gathering ID %v for %s\n", gatheringID, client.Username) 38 | 39 | gatherings := database.GocentralDatabase.Collection("gatherings") 40 | 41 | // get and deserialize the gathering from the DB 42 | var dbGathering models.Gathering 43 | 44 | err = gatherings.FindOne(context.TODO(), bson.M{"gathering_id": gatheringID}).Decode(&dbGathering) 45 | 46 | if err != nil { 47 | log.Println("Could not find gathering ID for " + client.Username) 48 | SendErrorCode(SecureServer, client, nexproto.MatchmakingProtocolID, callID, quazal.OperationError) 49 | return 50 | } 51 | 52 | // make sure the client's PID matches the creator of the gathering 53 | if dbGathering.Creator != db.GetUsernameForPID(int(client.PlayerID())) { 54 | // check if the creator of the gathering was created by the machine's master user 55 | machineID := db.GetMachineIDFromUsername(dbGathering.Creator) 56 | 57 | if machineID != client.MachineID() || machineID == 0 { 58 | log.Printf("Client %s is not the creator of gathering %v\n", client.Username, gatheringID) 59 | SendErrorCode(SecureServer, client, nexproto.MatchmakingProtocolID, callID, quazal.NotAuthenticated) 60 | return 61 | } 62 | } 63 | 64 | // the client sends the entire gathering again, so update it in the DB 65 | 66 | result, err := gatherings.UpdateOne( 67 | context.TODO(), 68 | bson.M{"gathering_id": gatheringID}, 69 | bson.D{ 70 | {"$set", bson.D{{"contents", gathering}}}, 71 | {"$set", bson.D{{"public", g.HarmonixGathering.Public}}}, 72 | {"$set", bson.D{{"last_updated", time.Now().Unix()}}}, 73 | {"$set", bson.D{{"creator", client.Username}}}, 74 | }, 75 | ) 76 | 77 | if err != nil { 78 | log.Println("Could not update gathering for " + client.Username) 79 | SendErrorCode(SecureServer, client, nexproto.MatchmakingProtocolID, callID, quazal.OperationError) 80 | return 81 | } 82 | 83 | log.Printf("Updated %v gatherings\n", result.ModifiedCount) 84 | 85 | rmcResponseStream := nex.NewStream() 86 | 87 | rmcResponseStream.WriteUInt8(1) 88 | 89 | rmcResponseBody := rmcResponseStream.Bytes() 90 | 91 | rmcResponse := nex.NewRMCResponse(nexproto.MatchmakingProtocolID, callID) 92 | rmcResponse.SetSuccess(nexproto.UpdateGathering, rmcResponseBody) 93 | 94 | rmcResponseBytes := rmcResponse.Bytes() 95 | 96 | responsePacket, _ := nex.NewPacketV0(client, nil) 97 | 98 | responsePacket.SetVersion(0) 99 | responsePacket.SetSource(0x31) 100 | responsePacket.SetDestination(0x3F) 101 | responsePacket.SetType(nex.DataPacket) 102 | 103 | responsePacket.SetPayload(rmcResponseBytes) 104 | 105 | responsePacket.AddFlag(nex.FlagNeedsAck) 106 | responsePacket.AddFlag(nex.FlagReliable) 107 | 108 | SecureServer.Send(responsePacket) 109 | 110 | } 111 | -------------------------------------------------------------------------------- /servers/validation.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "log" 5 | "rb3server/database" 6 | "rb3server/quazal" 7 | "rb3server/utils" 8 | 9 | "github.com/ihatecompvir/nex-go" 10 | nexproto "github.com/ihatecompvir/nex-protocols-go" 11 | ) 12 | 13 | // ValidateClientPID checks if the client has a valid, non-Master PID 14 | func ValidateNonMasterClientPID(server *nex.Server, client *nex.Client, callID uint32, protocolId int) (bool, error) { 15 | // Check that the claimed PID has logged in 16 | hasLoggedIn, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), client.PlayerID()) 17 | if err != nil { 18 | log.Println("Error checking PID validity: ", err) 19 | SendErrorCode(server, client, nexproto.MatchmakingProtocolID, callID, quazal.OperationError) 20 | return false, err 21 | } 22 | 23 | if !hasLoggedIn || client.PlayerID() == 0 || database.IsPIDAMasterUser(int(client.PlayerID())) { 24 | log.Println("Client is attempting to perform a privileged action without a valid server-assigned PID, rejecting call") 25 | SendErrorCode(server, client, nexproto.MatchmakingProtocolID, callID, quazal.NotAuthenticated) 26 | return false, nil 27 | } 28 | 29 | return true, nil 30 | } 31 | 32 | // ValidateClientPID checks if the client has a valid PID, Master User PIDs allowed 33 | func ValidateClientPID(server *nex.Server, client *nex.Client, callID uint32, protocolId int) (bool, error) { 34 | // Check that the claimed PID has logged in 35 | hasLoggedIn, err := utils.GetClientStoreSingleton().IsValidPID(client.Address().String(), client.PlayerID()) 36 | if err != nil { 37 | log.Println("Error checking PID validity: ", err) 38 | SendErrorCode(server, client, nexproto.MatchmakingProtocolID, callID, quazal.OperationError) 39 | return false, err 40 | } 41 | 42 | if !hasLoggedIn || client.PlayerID() == 0 { 43 | log.Println("Client is attempting to perform a privileged action without a valid server-assigned PID, rejecting call") 44 | SendErrorCode(server, client, nexproto.MatchmakingProtocolID, callID, quazal.NotAuthenticated) 45 | return false, nil 46 | } 47 | 48 | return true, nil 49 | } 50 | -------------------------------------------------------------------------------- /utils/clientInfo.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | type ClientInfo struct { 9 | IP string 10 | PIDStack []uint32 11 | mu sync.Mutex 12 | } 13 | 14 | type ClientStore struct { 15 | clients map[string]*ClientInfo 16 | mu sync.RWMutex 17 | } 18 | 19 | var ( 20 | clientStoreInstance *ClientStore 21 | once sync.Once 22 | ) 23 | 24 | func GetClientStoreSingleton() *ClientStore { 25 | once.Do(func() { 26 | clientStoreInstance = &ClientStore{ 27 | clients: make(map[string]*ClientInfo), 28 | } 29 | }) 30 | return clientStoreInstance 31 | } 32 | 33 | // adds a new client to the store 34 | func (cs *ClientStore) AddClient(ip string) { 35 | cs.mu.Lock() 36 | defer cs.mu.Unlock() 37 | if _, exists := cs.clients[ip]; !exists { 38 | cs.clients[ip] = &ClientInfo{ 39 | IP: ip, 40 | PIDStack: make([]uint32, 0, 8), // Limit stack size to 8 41 | } 42 | } 43 | } 44 | 45 | // pushes a PID to the client's stack of PIDs 46 | func (cs *ClientStore) PushPID(ip string, pid uint32) error { 47 | cs.mu.RLock() 48 | client, exists := cs.clients[ip] 49 | cs.mu.RUnlock() 50 | if !exists { 51 | return errors.New("client not found") 52 | } 53 | 54 | client.mu.Lock() 55 | defer client.mu.Unlock() 56 | if len(client.PIDStack) >= 8 { 57 | return errors.New("PID stack is full") 58 | } 59 | client.PIDStack = append(client.PIDStack, pid) 60 | return nil 61 | } 62 | 63 | // checks if a PID is valid for a client (i.e. have they logged in or called NintendoCreateAccount to switch to it, for multiple profile support) 64 | func (cs *ClientStore) IsValidPID(ip string, pid uint32) (bool, error) { 65 | cs.mu.RLock() 66 | client, exists := cs.clients[ip] 67 | cs.mu.RUnlock() 68 | if !exists { 69 | return false, errors.New("client not found") 70 | } 71 | 72 | client.mu.Lock() 73 | defer client.mu.Unlock() 74 | for _, storedPID := range client.PIDStack { 75 | if storedPID == pid { 76 | return true, nil 77 | } 78 | } 79 | return false, nil 80 | } 81 | 82 | // removes a client from the store 83 | func (cs *ClientStore) RemoveClient(ip string) { 84 | cs.mu.Lock() 85 | defer cs.mu.Unlock() 86 | delete(cs.clients, ip) 87 | } 88 | --------------------------------------------------------------------------------