Loading CHANGELOG.md +3 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,9 @@ All notable changes to this project are documented below. The format is based on [keep a changelog](http://keepachangelog.com/) and this project uses [semantic versioning](http://semver.org/). ### [Unreleased] ### Added - Advanced Matchmaking with custom filters and user properties. ### Changed - Script runtime RPC and HTTP hook errors now return more detail when verbose logging is enabled. Loading install/docker/docker-compose.yml +1 −1 Original line number Diff line number Diff line version: '3' services: cockroachdb: image: cockroachdb/cockroach:v1.0.3 image: cockroachdb/cockroach:v1.0.6 command: start --insecure --store=attrs=ssd,path=/var/lib/cockroach/ restart: always volumes: Loading server/api.proto +55 −2 Original line number Diff line number Diff line Loading @@ -887,6 +887,47 @@ message TopicPresence { repeated UserPresence leaves = 3; } /** * PropertyPair is a core domain type respresenting a single user property */ message PropertyPair { /// Set of string user property message StringSet { repeated string values = 1; } string key = 1; oneof value { StringSet stringSet = 2; bool boolValue = 3; int64 intValue = 4; } } /** * MatchmakeFilter is a core domain type respresenting a filter to use for matchmaking. */ message MatchmakeFilter { /// String term filters message TermFilter { repeated string terms = 1; bool matchAllTerms = 2; } /// Numeric range filter message RangeFilter { int64 lower_bound = 1; // inclusive lower_bound int64 upper_bound = 2; // inclusive upper_bound } string name = 1; oneof value { TermFilter term = 2; RangeFilter range = 3; bool check = 4; } } /** * TMatchmakeAdd is used to add the current user to the matchmaking pool. * Loading @@ -894,7 +935,11 @@ message TopicPresence { */ message TMatchmakeAdd { /// Match user with other users looking for a match with the the following number of users. int64 requiredCount = 1; int64 required_count = 1; /// List of filters that need to match. repeated MatchmakeFilter filters = 2; // "AND" /// List of properties for the current user. repeated PropertyPair properties = 3; } /** Loading @@ -915,12 +960,20 @@ message TMatchmakeRemove { * MatchmakeMatched is the core domain type representing a found match via matchmaking. */ message MatchmakeMatched { /// Matched user presence and properties message UserProperty { bytes user_id = 1; repeated PropertyPair properties = 2; repeated MatchmakeFilter filters = 3; } /// Matchmaking ticket. Use this to invalidate ticket cache on the client. bytes ticket = 1; /// Matchmaking token. Use this to accept the match. This is a onetime token which is only valid for 15seconds. /// Matchmaking token. Use this to accept the match. This is a onetime token which is only valid for a limited time. bytes token = 2; repeated UserPresence presences = 3; UserPresence self = 4; repeated UserProperty properties = 5; } /** Loading server/matchmaker.go +194 −23 Original line number Diff line number Diff line Loading @@ -16,17 +16,62 @@ package server import ( "errors" "github.com/satori/go.uuid" "sync" "github.com/satori/go.uuid" ) type Matchmaker interface { Add(sessionID uuid.UUID, userID uuid.UUID, meta PresenceMeta, requiredCount int64) (uuid.UUID, map[MatchmakerKey]*MatchmakerProfile) Add(sessionID uuid.UUID, userID uuid.UUID, requestProfile *MatchmakerProfile) (uuid.UUID, map[MatchmakerKey]*MatchmakerProfile, []*MatchmakerAcceptedProperty) Remove(sessionID uuid.UUID, userID uuid.UUID, ticket uuid.UUID) error RemoveAll(sessionID uuid.UUID) UpdateAll(sessionID uuid.UUID, meta PresenceMeta) } type Filter int const ( BOOL Filter = iota RANGE TERM ) type MatchmakerFilter interface { Type() Filter } type MatchmakerTermFilter struct { Terms []string AllTerms bool // set to False for Any Term } func (*MatchmakerTermFilter) Type() Filter { return TERM } type MatchmakerRangeFilter struct { LowerBound int64 UpperBound int64 } func (*MatchmakerRangeFilter) Type() Filter { return RANGE } type MatchmakerBoolFilter struct { Value bool } func (*MatchmakerBoolFilter) Type() Filter { return BOOL } type MatchmakerAcceptedProperty struct { UserID uuid.UUID Properties map[string]interface{} Filters map[string]MatchmakerFilter } type MatchmakerKey struct { ID PresenceID UserID uuid.UUID Loading @@ -35,7 +80,9 @@ type MatchmakerKey struct { type MatchmakerProfile struct { Meta PresenceMeta RequiredCount int64 RequiredCount int Properties map[string]interface{} Filters map[string]MatchmakerFilter } type MatchmakerService struct { Loading @@ -51,36 +98,160 @@ func NewMatchmakerService(name string) *MatchmakerService { } } func (m *MatchmakerService) Add(sessionID uuid.UUID, userID uuid.UUID, meta PresenceMeta, requiredCount int64) (uuid.UUID, map[MatchmakerKey]*MatchmakerProfile) { func (m *MatchmakerService) Add(sessionID uuid.UUID, userID uuid.UUID, incomingProfile *MatchmakerProfile) (uuid.UUID, map[MatchmakerKey]*MatchmakerProfile, []*MatchmakerAcceptedProperty) { ticket := uuid.NewV4() selected := make(map[MatchmakerKey]*MatchmakerProfile, requiredCount-1) qmk := MatchmakerKey{ID: PresenceID{SessionID: sessionID, Node: m.name}, UserID: userID, Ticket: ticket} qmp := &MatchmakerProfile{Meta: meta, RequiredCount: requiredCount} candidates := make(map[MatchmakerKey]*MatchmakerProfile, incomingProfile.RequiredCount-1) requestKey := MatchmakerKey{ID: PresenceID{SessionID: sessionID, Node: m.name}, UserID: userID, Ticket: ticket} m.Lock() for mk, mp := range m.values { if mk.ID.SessionID != sessionID && mk.UserID != userID && mp.RequiredCount == requiredCount { selected[mk] = mp if int64(len(selected)) == requiredCount-1 { break defer m.Unlock() // find list of suitable candidates for key, profile := range m.values { // if queued users match the current user, then skip if key.ID.SessionID == sessionID || key.UserID == userID { continue } // compatible with the request's filter if !m.checkFilter(incomingProfile, profile) { continue } // compatible with the profile's filter if !m.checkFilter(profile, incomingProfile) { continue } candidates[key] = profile } if int64(len(selected)) == requiredCount-1 { for mk, _ := range selected { // cross match all previously selected profiles // to see if they are compatible with each other as well matches := m.crossmatchCandidates(candidates, incomingProfile.RequiredCount-1) // not enough profiles, bail out early if len(matches) < int(incomingProfile.RequiredCount-1) { m.values[requestKey] = incomingProfile return ticket, nil, nil } // remove the matched profiles from the queue for mk, _ := range matches { delete(m.values, mk) } selected[qmk] = qmp } else { m.values[qmk] = qmp // add the incoming profile to the final list matches[requestKey] = incomingProfile return ticket, matches, m.calculateAcceptedProperties(matches) } m.Unlock() if int64(len(selected)) != requiredCount { return ticket, nil } else { return ticket, selected func (m *MatchmakerService) crossmatchCandidates(candidates map[MatchmakerKey]*MatchmakerProfile, requiredCount int) map[MatchmakerKey]*MatchmakerProfile { if requiredCount == 0 { return map[MatchmakerKey]*MatchmakerProfile{} } if requiredCount > len(candidates) { return nil } keys := make([]MatchmakerKey, 0) values := make([]*MatchmakerProfile, 0) for key, value := range candidates { keys = append(keys, key) values = append(values, value) } for i := 0; i < len(keys); i++ { s := values[i] tempCandidates := make(map[MatchmakerKey]*MatchmakerProfile, 0) for j := i + 1; j < len(keys); j++ { p := values[j] if m.checkFilter(s, p) && m.checkFilter(p, s) { tempCandidates[keys[j]] = p } } findCandidateResult := m.crossmatchCandidates(tempCandidates, requiredCount-1) if findCandidateResult != nil { findCandidateResult[keys[i]] = s return findCandidateResult } } return nil } func (m *MatchmakerService) checkFilter(requestProfile, queuedProfile *MatchmakerProfile) bool { if queuedProfile.RequiredCount != requestProfile.RequiredCount { return false } for filterName, filter := range requestProfile.Filters { propertyValue := queuedProfile.Properties[filterName] if propertyValue == nil { return false } if filter.Type() == TERM { termFilter := filter.(*MatchmakerTermFilter) propertyTermList, ok := propertyValue.([]string) if !ok { return false } matchingTerms := m.intersection(termFilter.Terms, propertyTermList) if len(matchingTerms) == 0 { return false } if termFilter.AllTerms && len(matchingTerms) != len(termFilter.Terms) { return false } } else if filter.Type() == RANGE { rangeFilter := filter.(*MatchmakerRangeFilter) propertyInt, ok := propertyValue.(int64) if !ok || propertyInt < rangeFilter.LowerBound || propertyInt > rangeFilter.UpperBound { return false } } else if filter.Type() == BOOL { boolFilter := filter.(*MatchmakerBoolFilter) propertyBool, ok := propertyValue.(bool) if !ok || boolFilter.Value != propertyBool { return false } } } return true } func (m *MatchmakerService) calculateAcceptedProperties(matched map[MatchmakerKey]*MatchmakerProfile) []*MatchmakerAcceptedProperty { props := make([]*MatchmakerAcceptedProperty, 0) for key, profile := range matched { prop := &MatchmakerAcceptedProperty{ UserID: key.UserID, Properties: profile.Properties, Filters: profile.Filters, } props = append(props, prop) } return props } func (m *MatchmakerService) intersection(a, b []string) []string { o := make([]string, 0) for i := range a { for j := range b { if a[i] == b[j] { o = append(o, a[i]) break } } } return o } func (m *MatchmakerService) Remove(sessionID uuid.UUID, userID uuid.UUID, ticket uuid.UUID) error { Loading server/pipeline_matchmake.go +93 −5 Original line number Diff line number Diff line Loading @@ -15,20 +15,52 @@ package server import ( "time" "github.com/dgrijalva/jwt-go" "github.com/satori/go.uuid" "go.uber.org/zap" "time" ) func (p *pipeline) matchmakeAdd(logger *zap.Logger, session *session, envelope *Envelope) { requiredCount := envelope.GetMatchmakeAdd().RequiredCount matchmakeAdd := envelope.GetMatchmakeAdd() requiredCount := matchmakeAdd.RequiredCount if requiredCount < 2 { session.Send(ErrorMessageBadInput(envelope.CollationId, "Required count must be >= 2")) return } ticket, selected := p.matchmaker.Add(session.id, session.userID, PresenceMeta{Handle: session.handle.Load()}, requiredCount) properties := make(map[string]interface{}, 0) for _, pair := range matchmakeAdd.Properties { switch v := pair.Value.(type) { case *PropertyPair_BoolValue: properties[pair.Key] = v.BoolValue case *PropertyPair_IntValue: properties[pair.Key] = v.IntValue case *PropertyPair_StringSet_: properties[pair.Key] = uniqueList(v.StringSet.Values) } } filters := make(map[string]MatchmakerFilter) for _, filter := range matchmakeAdd.Filters { switch v := filter.Value.(type) { case *MatchmakeFilter_Check: filters[filter.Name] = &MatchmakerBoolFilter{v.Check} case *MatchmakeFilter_Range: filters[filter.Name] = &MatchmakerRangeFilter{v.Range.LowerBound, v.Range.UpperBound} case *MatchmakeFilter_Term: filters[filter.Name] = &MatchmakerTermFilter{uniqueList(v.Term.Terms), v.Term.MatchAllTerms} } } matchmakerProfile := &MatchmakerProfile{ Meta: PresenceMeta{Handle: session.handle.Load()}, RequiredCount: int(requiredCount), Properties: properties, Filters: filters, } ticket, selected, props := p.matchmaker.Add(session.id, session.userID, matchmakerProfile) session.Send(&Envelope{CollationId: envelope.CollationId, Payload: &Envelope_MatchmakeTicket{MatchmakeTicket: &TMatchmakeTicket{ Ticket: ticket.Bytes(), Loading @@ -55,10 +87,51 @@ func (p *pipeline) matchmakeAdd(logger *zap.Logger, session *session, envelope * } idx++ } protoProps := make([]*MatchmakeMatched_UserProperty, 0) for _, prop := range props { protoProp := &MatchmakeMatched_UserProperty{ UserId: prop.UserID.Bytes(), Properties: make([]*PropertyPair, 0), Filters: make([]*MatchmakeFilter, 0), } protoProps = append(protoProps, protoProp) for userPropertyKey, userPropertyValue := range prop.Properties { pair := &PropertyPair{Key: userPropertyKey} protoProp.Properties = append(protoProp.Properties, pair) switch v := userPropertyValue.(type) { case int64: pair.Value = &PropertyPair_IntValue{v} case bool: pair.Value = &PropertyPair_BoolValue{v} case []string: pair.Value = &PropertyPair_StringSet_{&PropertyPair_StringSet{v}} } } for userFilterKey, userFilterValue := range prop.Filters { filter := &MatchmakeFilter{Name: userFilterKey} protoProp.Filters = append(protoProp.Filters, filter) switch userFilterValue.Type() { case TERM: f := userFilterValue.(*MatchmakerTermFilter) filter.Value = &MatchmakeFilter_Term{&MatchmakeFilter_TermFilter{f.Terms, f.AllTerms}} case RANGE: f := userFilterValue.(*MatchmakerRangeFilter) filter.Value = &MatchmakeFilter_Range{&MatchmakeFilter_RangeFilter{f.LowerBound, f.UpperBound}} case BOOL: f := userFilterValue.(*MatchmakerBoolFilter) filter.Value = &MatchmakeFilter_Check{f.Value} } } } outgoing := &Envelope{Payload: &Envelope_MatchmakeMatched{MatchmakeMatched: &MatchmakeMatched{ // Ticket: ..., // Set individually below for each recipient. Token: []byte(signedToken), Presences: ps, Properties: protoProps, // Self: ..., // Set individually below for each recipient. }}} for mk, mp := range selected { Loading @@ -76,6 +149,7 @@ func (p *pipeline) matchmakeAdd(logger *zap.Logger, session *session, envelope * SessionId: mk.ID.SessionID.Bytes(), Handle: mp.Meta.Handle, } p.messageRouter.Send(logger, to, outgoing) } } Loading @@ -96,3 +170,17 @@ func (p *pipeline) matchmakeRemove(logger *zap.Logger, session *session, envelop session.Send(&Envelope{CollationId: envelope.CollationId}) } func uniqueList(values []string) []string { m := make(map[string]struct{}) set := make([]string, 0) for _, v := range values { if _, ok := m[v]; !ok { m[v] = struct{}{} set = append(set, v) } } return set } Loading
CHANGELOG.md +3 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,9 @@ All notable changes to this project are documented below. The format is based on [keep a changelog](http://keepachangelog.com/) and this project uses [semantic versioning](http://semver.org/). ### [Unreleased] ### Added - Advanced Matchmaking with custom filters and user properties. ### Changed - Script runtime RPC and HTTP hook errors now return more detail when verbose logging is enabled. Loading
install/docker/docker-compose.yml +1 −1 Original line number Diff line number Diff line version: '3' services: cockroachdb: image: cockroachdb/cockroach:v1.0.3 image: cockroachdb/cockroach:v1.0.6 command: start --insecure --store=attrs=ssd,path=/var/lib/cockroach/ restart: always volumes: Loading
server/api.proto +55 −2 Original line number Diff line number Diff line Loading @@ -887,6 +887,47 @@ message TopicPresence { repeated UserPresence leaves = 3; } /** * PropertyPair is a core domain type respresenting a single user property */ message PropertyPair { /// Set of string user property message StringSet { repeated string values = 1; } string key = 1; oneof value { StringSet stringSet = 2; bool boolValue = 3; int64 intValue = 4; } } /** * MatchmakeFilter is a core domain type respresenting a filter to use for matchmaking. */ message MatchmakeFilter { /// String term filters message TermFilter { repeated string terms = 1; bool matchAllTerms = 2; } /// Numeric range filter message RangeFilter { int64 lower_bound = 1; // inclusive lower_bound int64 upper_bound = 2; // inclusive upper_bound } string name = 1; oneof value { TermFilter term = 2; RangeFilter range = 3; bool check = 4; } } /** * TMatchmakeAdd is used to add the current user to the matchmaking pool. * Loading @@ -894,7 +935,11 @@ message TopicPresence { */ message TMatchmakeAdd { /// Match user with other users looking for a match with the the following number of users. int64 requiredCount = 1; int64 required_count = 1; /// List of filters that need to match. repeated MatchmakeFilter filters = 2; // "AND" /// List of properties for the current user. repeated PropertyPair properties = 3; } /** Loading @@ -915,12 +960,20 @@ message TMatchmakeRemove { * MatchmakeMatched is the core domain type representing a found match via matchmaking. */ message MatchmakeMatched { /// Matched user presence and properties message UserProperty { bytes user_id = 1; repeated PropertyPair properties = 2; repeated MatchmakeFilter filters = 3; } /// Matchmaking ticket. Use this to invalidate ticket cache on the client. bytes ticket = 1; /// Matchmaking token. Use this to accept the match. This is a onetime token which is only valid for 15seconds. /// Matchmaking token. Use this to accept the match. This is a onetime token which is only valid for a limited time. bytes token = 2; repeated UserPresence presences = 3; UserPresence self = 4; repeated UserProperty properties = 5; } /** Loading
server/matchmaker.go +194 −23 Original line number Diff line number Diff line Loading @@ -16,17 +16,62 @@ package server import ( "errors" "github.com/satori/go.uuid" "sync" "github.com/satori/go.uuid" ) type Matchmaker interface { Add(sessionID uuid.UUID, userID uuid.UUID, meta PresenceMeta, requiredCount int64) (uuid.UUID, map[MatchmakerKey]*MatchmakerProfile) Add(sessionID uuid.UUID, userID uuid.UUID, requestProfile *MatchmakerProfile) (uuid.UUID, map[MatchmakerKey]*MatchmakerProfile, []*MatchmakerAcceptedProperty) Remove(sessionID uuid.UUID, userID uuid.UUID, ticket uuid.UUID) error RemoveAll(sessionID uuid.UUID) UpdateAll(sessionID uuid.UUID, meta PresenceMeta) } type Filter int const ( BOOL Filter = iota RANGE TERM ) type MatchmakerFilter interface { Type() Filter } type MatchmakerTermFilter struct { Terms []string AllTerms bool // set to False for Any Term } func (*MatchmakerTermFilter) Type() Filter { return TERM } type MatchmakerRangeFilter struct { LowerBound int64 UpperBound int64 } func (*MatchmakerRangeFilter) Type() Filter { return RANGE } type MatchmakerBoolFilter struct { Value bool } func (*MatchmakerBoolFilter) Type() Filter { return BOOL } type MatchmakerAcceptedProperty struct { UserID uuid.UUID Properties map[string]interface{} Filters map[string]MatchmakerFilter } type MatchmakerKey struct { ID PresenceID UserID uuid.UUID Loading @@ -35,7 +80,9 @@ type MatchmakerKey struct { type MatchmakerProfile struct { Meta PresenceMeta RequiredCount int64 RequiredCount int Properties map[string]interface{} Filters map[string]MatchmakerFilter } type MatchmakerService struct { Loading @@ -51,36 +98,160 @@ func NewMatchmakerService(name string) *MatchmakerService { } } func (m *MatchmakerService) Add(sessionID uuid.UUID, userID uuid.UUID, meta PresenceMeta, requiredCount int64) (uuid.UUID, map[MatchmakerKey]*MatchmakerProfile) { func (m *MatchmakerService) Add(sessionID uuid.UUID, userID uuid.UUID, incomingProfile *MatchmakerProfile) (uuid.UUID, map[MatchmakerKey]*MatchmakerProfile, []*MatchmakerAcceptedProperty) { ticket := uuid.NewV4() selected := make(map[MatchmakerKey]*MatchmakerProfile, requiredCount-1) qmk := MatchmakerKey{ID: PresenceID{SessionID: sessionID, Node: m.name}, UserID: userID, Ticket: ticket} qmp := &MatchmakerProfile{Meta: meta, RequiredCount: requiredCount} candidates := make(map[MatchmakerKey]*MatchmakerProfile, incomingProfile.RequiredCount-1) requestKey := MatchmakerKey{ID: PresenceID{SessionID: sessionID, Node: m.name}, UserID: userID, Ticket: ticket} m.Lock() for mk, mp := range m.values { if mk.ID.SessionID != sessionID && mk.UserID != userID && mp.RequiredCount == requiredCount { selected[mk] = mp if int64(len(selected)) == requiredCount-1 { break defer m.Unlock() // find list of suitable candidates for key, profile := range m.values { // if queued users match the current user, then skip if key.ID.SessionID == sessionID || key.UserID == userID { continue } // compatible with the request's filter if !m.checkFilter(incomingProfile, profile) { continue } // compatible with the profile's filter if !m.checkFilter(profile, incomingProfile) { continue } candidates[key] = profile } if int64(len(selected)) == requiredCount-1 { for mk, _ := range selected { // cross match all previously selected profiles // to see if they are compatible with each other as well matches := m.crossmatchCandidates(candidates, incomingProfile.RequiredCount-1) // not enough profiles, bail out early if len(matches) < int(incomingProfile.RequiredCount-1) { m.values[requestKey] = incomingProfile return ticket, nil, nil } // remove the matched profiles from the queue for mk, _ := range matches { delete(m.values, mk) } selected[qmk] = qmp } else { m.values[qmk] = qmp // add the incoming profile to the final list matches[requestKey] = incomingProfile return ticket, matches, m.calculateAcceptedProperties(matches) } m.Unlock() if int64(len(selected)) != requiredCount { return ticket, nil } else { return ticket, selected func (m *MatchmakerService) crossmatchCandidates(candidates map[MatchmakerKey]*MatchmakerProfile, requiredCount int) map[MatchmakerKey]*MatchmakerProfile { if requiredCount == 0 { return map[MatchmakerKey]*MatchmakerProfile{} } if requiredCount > len(candidates) { return nil } keys := make([]MatchmakerKey, 0) values := make([]*MatchmakerProfile, 0) for key, value := range candidates { keys = append(keys, key) values = append(values, value) } for i := 0; i < len(keys); i++ { s := values[i] tempCandidates := make(map[MatchmakerKey]*MatchmakerProfile, 0) for j := i + 1; j < len(keys); j++ { p := values[j] if m.checkFilter(s, p) && m.checkFilter(p, s) { tempCandidates[keys[j]] = p } } findCandidateResult := m.crossmatchCandidates(tempCandidates, requiredCount-1) if findCandidateResult != nil { findCandidateResult[keys[i]] = s return findCandidateResult } } return nil } func (m *MatchmakerService) checkFilter(requestProfile, queuedProfile *MatchmakerProfile) bool { if queuedProfile.RequiredCount != requestProfile.RequiredCount { return false } for filterName, filter := range requestProfile.Filters { propertyValue := queuedProfile.Properties[filterName] if propertyValue == nil { return false } if filter.Type() == TERM { termFilter := filter.(*MatchmakerTermFilter) propertyTermList, ok := propertyValue.([]string) if !ok { return false } matchingTerms := m.intersection(termFilter.Terms, propertyTermList) if len(matchingTerms) == 0 { return false } if termFilter.AllTerms && len(matchingTerms) != len(termFilter.Terms) { return false } } else if filter.Type() == RANGE { rangeFilter := filter.(*MatchmakerRangeFilter) propertyInt, ok := propertyValue.(int64) if !ok || propertyInt < rangeFilter.LowerBound || propertyInt > rangeFilter.UpperBound { return false } } else if filter.Type() == BOOL { boolFilter := filter.(*MatchmakerBoolFilter) propertyBool, ok := propertyValue.(bool) if !ok || boolFilter.Value != propertyBool { return false } } } return true } func (m *MatchmakerService) calculateAcceptedProperties(matched map[MatchmakerKey]*MatchmakerProfile) []*MatchmakerAcceptedProperty { props := make([]*MatchmakerAcceptedProperty, 0) for key, profile := range matched { prop := &MatchmakerAcceptedProperty{ UserID: key.UserID, Properties: profile.Properties, Filters: profile.Filters, } props = append(props, prop) } return props } func (m *MatchmakerService) intersection(a, b []string) []string { o := make([]string, 0) for i := range a { for j := range b { if a[i] == b[j] { o = append(o, a[i]) break } } } return o } func (m *MatchmakerService) Remove(sessionID uuid.UUID, userID uuid.UUID, ticket uuid.UUID) error { Loading
server/pipeline_matchmake.go +93 −5 Original line number Diff line number Diff line Loading @@ -15,20 +15,52 @@ package server import ( "time" "github.com/dgrijalva/jwt-go" "github.com/satori/go.uuid" "go.uber.org/zap" "time" ) func (p *pipeline) matchmakeAdd(logger *zap.Logger, session *session, envelope *Envelope) { requiredCount := envelope.GetMatchmakeAdd().RequiredCount matchmakeAdd := envelope.GetMatchmakeAdd() requiredCount := matchmakeAdd.RequiredCount if requiredCount < 2 { session.Send(ErrorMessageBadInput(envelope.CollationId, "Required count must be >= 2")) return } ticket, selected := p.matchmaker.Add(session.id, session.userID, PresenceMeta{Handle: session.handle.Load()}, requiredCount) properties := make(map[string]interface{}, 0) for _, pair := range matchmakeAdd.Properties { switch v := pair.Value.(type) { case *PropertyPair_BoolValue: properties[pair.Key] = v.BoolValue case *PropertyPair_IntValue: properties[pair.Key] = v.IntValue case *PropertyPair_StringSet_: properties[pair.Key] = uniqueList(v.StringSet.Values) } } filters := make(map[string]MatchmakerFilter) for _, filter := range matchmakeAdd.Filters { switch v := filter.Value.(type) { case *MatchmakeFilter_Check: filters[filter.Name] = &MatchmakerBoolFilter{v.Check} case *MatchmakeFilter_Range: filters[filter.Name] = &MatchmakerRangeFilter{v.Range.LowerBound, v.Range.UpperBound} case *MatchmakeFilter_Term: filters[filter.Name] = &MatchmakerTermFilter{uniqueList(v.Term.Terms), v.Term.MatchAllTerms} } } matchmakerProfile := &MatchmakerProfile{ Meta: PresenceMeta{Handle: session.handle.Load()}, RequiredCount: int(requiredCount), Properties: properties, Filters: filters, } ticket, selected, props := p.matchmaker.Add(session.id, session.userID, matchmakerProfile) session.Send(&Envelope{CollationId: envelope.CollationId, Payload: &Envelope_MatchmakeTicket{MatchmakeTicket: &TMatchmakeTicket{ Ticket: ticket.Bytes(), Loading @@ -55,10 +87,51 @@ func (p *pipeline) matchmakeAdd(logger *zap.Logger, session *session, envelope * } idx++ } protoProps := make([]*MatchmakeMatched_UserProperty, 0) for _, prop := range props { protoProp := &MatchmakeMatched_UserProperty{ UserId: prop.UserID.Bytes(), Properties: make([]*PropertyPair, 0), Filters: make([]*MatchmakeFilter, 0), } protoProps = append(protoProps, protoProp) for userPropertyKey, userPropertyValue := range prop.Properties { pair := &PropertyPair{Key: userPropertyKey} protoProp.Properties = append(protoProp.Properties, pair) switch v := userPropertyValue.(type) { case int64: pair.Value = &PropertyPair_IntValue{v} case bool: pair.Value = &PropertyPair_BoolValue{v} case []string: pair.Value = &PropertyPair_StringSet_{&PropertyPair_StringSet{v}} } } for userFilterKey, userFilterValue := range prop.Filters { filter := &MatchmakeFilter{Name: userFilterKey} protoProp.Filters = append(protoProp.Filters, filter) switch userFilterValue.Type() { case TERM: f := userFilterValue.(*MatchmakerTermFilter) filter.Value = &MatchmakeFilter_Term{&MatchmakeFilter_TermFilter{f.Terms, f.AllTerms}} case RANGE: f := userFilterValue.(*MatchmakerRangeFilter) filter.Value = &MatchmakeFilter_Range{&MatchmakeFilter_RangeFilter{f.LowerBound, f.UpperBound}} case BOOL: f := userFilterValue.(*MatchmakerBoolFilter) filter.Value = &MatchmakeFilter_Check{f.Value} } } } outgoing := &Envelope{Payload: &Envelope_MatchmakeMatched{MatchmakeMatched: &MatchmakeMatched{ // Ticket: ..., // Set individually below for each recipient. Token: []byte(signedToken), Presences: ps, Properties: protoProps, // Self: ..., // Set individually below for each recipient. }}} for mk, mp := range selected { Loading @@ -76,6 +149,7 @@ func (p *pipeline) matchmakeAdd(logger *zap.Logger, session *session, envelope * SessionId: mk.ID.SessionID.Bytes(), Handle: mp.Meta.Handle, } p.messageRouter.Send(logger, to, outgoing) } } Loading @@ -96,3 +170,17 @@ func (p *pipeline) matchmakeRemove(logger *zap.Logger, session *session, envelop session.Send(&Envelope{CollationId: envelope.CollationId}) } func uniqueList(values []string) []string { m := make(map[string]struct{}) set := make([]string, 0) for _, v := range values { if _, ok := m[v]; !ok { m[v] = struct{}{} set = append(set, v) } } return set }