diff --git a/api/api.pb.go b/api/api.pb.go index 33ff6cfb2e8cd53e69335c7ca3bfd55eb1490d65..f63520ac96e75eba20ee5488f933d69e22502140 100644 --- a/api/api.pb.go +++ b/api/api.pb.go @@ -38,8 +38,11 @@ It has these top-level messages: Groups ImportFacebookFriendsRequest LinkFacebookRequest + ListMatchesRequest ListNotificationsRequest ListStorageObjectsRequest + Match + MatchList Notification NotificationList ReadStorageObjectId @@ -1240,6 +1243,59 @@ func (m *LinkFacebookRequest) GetImport() *google_protobuf3.BoolValue { return nil } +type ListMatchesRequest struct { + // Limit the number of returned matches. + Limit *google_protobuf3.Int32Value `protobuf:"bytes,1,opt,name=limit" json:"limit,omitempty"` + // Authoritative or relayed matches. + Authoritative *google_protobuf3.BoolValue `protobuf:"bytes,2,opt,name=authoritative" json:"authoritative,omitempty"` + // Label filter. + Label *google_protobuf3.StringValue `protobuf:"bytes,3,opt,name=label" json:"label,omitempty"` + // Minimum user count. + MinSize *google_protobuf3.Int32Value `protobuf:"bytes,4,opt,name=min_size,json=minSize" json:"min_size,omitempty"` + // Maximum user count. + MaxSize *google_protobuf3.Int32Value `protobuf:"bytes,5,opt,name=max_size,json=maxSize" json:"max_size,omitempty"` +} + +func (m *ListMatchesRequest) Reset() { *m = ListMatchesRequest{} } +func (m *ListMatchesRequest) String() string { return proto.CompactTextString(m) } +func (*ListMatchesRequest) ProtoMessage() {} +func (*ListMatchesRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{30} } + +func (m *ListMatchesRequest) GetLimit() *google_protobuf3.Int32Value { + if m != nil { + return m.Limit + } + return nil +} + +func (m *ListMatchesRequest) GetAuthoritative() *google_protobuf3.BoolValue { + if m != nil { + return m.Authoritative + } + return nil +} + +func (m *ListMatchesRequest) GetLabel() *google_protobuf3.StringValue { + if m != nil { + return m.Label + } + return nil +} + +func (m *ListMatchesRequest) GetMinSize() *google_protobuf3.Int32Value { + if m != nil { + return m.MinSize + } + return nil +} + +func (m *ListMatchesRequest) GetMaxSize() *google_protobuf3.Int32Value { + if m != nil { + return m.MaxSize + } + return nil +} + // Get a list of unexpired notifications. type ListNotificationsRequest struct { // The number of notifications to get. Between 1 and 100. @@ -1251,7 +1307,7 @@ type ListNotificationsRequest struct { func (m *ListNotificationsRequest) Reset() { *m = ListNotificationsRequest{} } func (m *ListNotificationsRequest) String() string { return proto.CompactTextString(m) } func (*ListNotificationsRequest) ProtoMessage() {} -func (*ListNotificationsRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{30} } +func (*ListNotificationsRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{31} } func (m *ListNotificationsRequest) GetLimit() *google_protobuf3.Int32Value { if m != nil { @@ -1282,7 +1338,7 @@ type ListStorageObjectsRequest struct { func (m *ListStorageObjectsRequest) Reset() { *m = ListStorageObjectsRequest{} } func (m *ListStorageObjectsRequest) String() string { return proto.CompactTextString(m) } func (*ListStorageObjectsRequest) ProtoMessage() {} -func (*ListStorageObjectsRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{31} } +func (*ListStorageObjectsRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{32} } func (m *ListStorageObjectsRequest) GetUserId() string { if m != nil { @@ -1312,6 +1368,69 @@ func (m *ListStorageObjectsRequest) GetCursor() string { return "" } +// Represents a realtime match. +type Match struct { + // The ID of the match, can be used to join. + MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` + // True if it's an server-managed authoritative match, false otherwise. + Authoritative bool `protobuf:"varint,2,opt,name=authoritative" json:"authoritative,omitempty"` + // Match label, if any. + Label *google_protobuf3.StringValue `protobuf:"bytes,3,opt,name=label" json:"label,omitempty"` + // Current number of users in the match. + Size int32 `protobuf:"varint,4,opt,name=size" json:"size,omitempty"` +} + +func (m *Match) Reset() { *m = Match{} } +func (m *Match) String() string { return proto.CompactTextString(m) } +func (*Match) ProtoMessage() {} +func (*Match) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{33} } + +func (m *Match) GetMatchId() string { + if m != nil { + return m.MatchId + } + return "" +} + +func (m *Match) GetAuthoritative() bool { + if m != nil { + return m.Authoritative + } + return false +} + +func (m *Match) GetLabel() *google_protobuf3.StringValue { + if m != nil { + return m.Label + } + return nil +} + +func (m *Match) GetSize() int32 { + if m != nil { + return m.Size + } + return 0 +} + +// A list of realtime matches. +type MatchList struct { + // A number of matches corresponding to a list operation. + Matches []*Match `protobuf:"bytes,1,rep,name=matches" json:"matches,omitempty"` +} + +func (m *MatchList) Reset() { *m = MatchList{} } +func (m *MatchList) String() string { return proto.CompactTextString(m) } +func (*MatchList) ProtoMessage() {} +func (*MatchList) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{34} } + +func (m *MatchList) GetMatches() []*Match { + if m != nil { + return m.Matches + } + return nil +} + // A notification in the server. type Notification struct { // ID of the Notification. @@ -1333,7 +1452,7 @@ type Notification struct { func (m *Notification) Reset() { *m = Notification{} } func (m *Notification) String() string { return proto.CompactTextString(m) } func (*Notification) ProtoMessage() {} -func (*Notification) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{32} } +func (*Notification) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{35} } func (m *Notification) GetId() string { if m != nil { @@ -1395,7 +1514,7 @@ type NotificationList struct { func (m *NotificationList) Reset() { *m = NotificationList{} } func (m *NotificationList) String() string { return proto.CompactTextString(m) } func (*NotificationList) ProtoMessage() {} -func (*NotificationList) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{33} } +func (*NotificationList) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{36} } func (m *NotificationList) GetNotifications() []*Notification { if m != nil { @@ -1424,7 +1543,7 @@ type ReadStorageObjectId struct { func (m *ReadStorageObjectId) Reset() { *m = ReadStorageObjectId{} } func (m *ReadStorageObjectId) String() string { return proto.CompactTextString(m) } func (*ReadStorageObjectId) ProtoMessage() {} -func (*ReadStorageObjectId) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{34} } +func (*ReadStorageObjectId) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{37} } func (m *ReadStorageObjectId) GetCollection() string { if m != nil { @@ -1456,7 +1575,7 @@ type ReadStorageObjectsRequest struct { func (m *ReadStorageObjectsRequest) Reset() { *m = ReadStorageObjectsRequest{} } func (m *ReadStorageObjectsRequest) String() string { return proto.CompactTextString(m) } func (*ReadStorageObjectsRequest) ProtoMessage() {} -func (*ReadStorageObjectsRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{35} } +func (*ReadStorageObjectsRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{38} } func (m *ReadStorageObjectsRequest) GetObjectIds() []*ReadStorageObjectId { if m != nil { @@ -1478,7 +1597,7 @@ type Rpc struct { func (m *Rpc) Reset() { *m = Rpc{} } func (m *Rpc) String() string { return proto.CompactTextString(m) } func (*Rpc) ProtoMessage() {} -func (*Rpc) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{36} } +func (*Rpc) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{39} } func (m *Rpc) GetId() string { if m != nil { @@ -1512,7 +1631,7 @@ type Session struct { func (m *Session) Reset() { *m = Session{} } func (m *Session) String() string { return proto.CompactTextString(m) } func (*Session) ProtoMessage() {} -func (*Session) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{37} } +func (*Session) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{40} } func (m *Session) GetToken() string { if m != nil { @@ -1553,7 +1672,7 @@ type StorageObject struct { func (m *StorageObject) Reset() { *m = StorageObject{} } func (m *StorageObject) String() string { return proto.CompactTextString(m) } func (*StorageObject) ProtoMessage() {} -func (*StorageObject) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{38} } +func (*StorageObject) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{41} } func (m *StorageObject) GetCollection() string { if m != nil { @@ -1631,7 +1750,7 @@ type StorageObjectAck struct { func (m *StorageObjectAck) Reset() { *m = StorageObjectAck{} } func (m *StorageObjectAck) String() string { return proto.CompactTextString(m) } func (*StorageObjectAck) ProtoMessage() {} -func (*StorageObjectAck) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{39} } +func (*StorageObjectAck) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{42} } func (m *StorageObjectAck) GetCollection() string { if m != nil { @@ -1663,7 +1782,7 @@ type StorageObjectAcks struct { func (m *StorageObjectAcks) Reset() { *m = StorageObjectAcks{} } func (m *StorageObjectAcks) String() string { return proto.CompactTextString(m) } func (*StorageObjectAcks) ProtoMessage() {} -func (*StorageObjectAcks) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{40} } +func (*StorageObjectAcks) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{43} } func (m *StorageObjectAcks) GetAcks() []*StorageObjectAck { if m != nil { @@ -1681,7 +1800,7 @@ type StorageObjects struct { func (m *StorageObjects) Reset() { *m = StorageObjects{} } func (m *StorageObjects) String() string { return proto.CompactTextString(m) } func (*StorageObjects) ProtoMessage() {} -func (*StorageObjects) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{41} } +func (*StorageObjects) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{44} } func (m *StorageObjects) GetObjects() []*StorageObject { if m != nil { @@ -1701,7 +1820,7 @@ type StorageObjectList struct { func (m *StorageObjectList) Reset() { *m = StorageObjectList{} } func (m *StorageObjectList) String() string { return proto.CompactTextString(m) } func (*StorageObjectList) ProtoMessage() {} -func (*StorageObjectList) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{42} } +func (*StorageObjectList) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{45} } func (m *StorageObjectList) GetObjects() []*StorageObject { if m != nil { @@ -1736,7 +1855,7 @@ type UpdateAccountRequest struct { func (m *UpdateAccountRequest) Reset() { *m = UpdateAccountRequest{} } func (m *UpdateAccountRequest) String() string { return proto.CompactTextString(m) } func (*UpdateAccountRequest) ProtoMessage() {} -func (*UpdateAccountRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{43} } +func (*UpdateAccountRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{46} } func (m *UpdateAccountRequest) GetUsername() *google_protobuf3.StringValue { if m != nil { @@ -1819,7 +1938,7 @@ type User struct { func (m *User) Reset() { *m = User{} } func (m *User) String() string { return proto.CompactTextString(m) } func (*User) ProtoMessage() {} -func (*User) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{44} } +func (*User) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{47} } func (m *User) GetId() string { if m != nil { @@ -1942,7 +2061,7 @@ type Users struct { func (m *Users) Reset() { *m = Users{} } func (m *Users) String() string { return proto.CompactTextString(m) } func (*Users) ProtoMessage() {} -func (*Users) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{45} } +func (*Users) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{48} } func (m *Users) GetUsers() []*User { if m != nil { @@ -1970,7 +2089,7 @@ type WriteStorageObject struct { func (m *WriteStorageObject) Reset() { *m = WriteStorageObject{} } func (m *WriteStorageObject) String() string { return proto.CompactTextString(m) } func (*WriteStorageObject) ProtoMessage() {} -func (*WriteStorageObject) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{46} } +func (*WriteStorageObject) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{49} } func (m *WriteStorageObject) GetCollection() string { if m != nil { @@ -2023,7 +2142,7 @@ type WriteStorageObjectsRequest struct { func (m *WriteStorageObjectsRequest) Reset() { *m = WriteStorageObjectsRequest{} } func (m *WriteStorageObjectsRequest) String() string { return proto.CompactTextString(m) } func (*WriteStorageObjectsRequest) ProtoMessage() {} -func (*WriteStorageObjectsRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{47} } +func (*WriteStorageObjectsRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{50} } func (m *WriteStorageObjectsRequest) GetObjects() []*WriteStorageObject { if m != nil { @@ -2064,8 +2183,11 @@ func init() { proto.RegisterType((*Groups)(nil), "nakama.api.Groups") proto.RegisterType((*ImportFacebookFriendsRequest)(nil), "nakama.api.ImportFacebookFriendsRequest") proto.RegisterType((*LinkFacebookRequest)(nil), "nakama.api.LinkFacebookRequest") + proto.RegisterType((*ListMatchesRequest)(nil), "nakama.api.ListMatchesRequest") proto.RegisterType((*ListNotificationsRequest)(nil), "nakama.api.ListNotificationsRequest") proto.RegisterType((*ListStorageObjectsRequest)(nil), "nakama.api.ListStorageObjectsRequest") + proto.RegisterType((*Match)(nil), "nakama.api.Match") + proto.RegisterType((*MatchList)(nil), "nakama.api.MatchList") proto.RegisterType((*Notification)(nil), "nakama.api.Notification") proto.RegisterType((*NotificationList)(nil), "nakama.api.NotificationList") proto.RegisterType((*ReadStorageObjectId)(nil), "nakama.api.ReadStorageObjectId") @@ -2149,6 +2271,8 @@ type NakamaClient interface { LinkSteam(ctx context.Context, in *AccountSteam, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) // List all friends for the current user. ListFriends(ctx context.Context, in *google_protobuf1.Empty, opts ...grpc.CallOption) (*Friends, error) + // Fetch list of running matches. + ListMatches(ctx context.Context, in *ListMatchesRequest, opts ...grpc.CallOption) (*MatchList, error) // Fetch list of notifications. ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*NotificationList, error) // List collections of storage objects. @@ -2410,6 +2534,15 @@ func (c *nakamaClient) ListFriends(ctx context.Context, in *google_protobuf1.Emp return out, nil } +func (c *nakamaClient) ListMatches(ctx context.Context, in *ListMatchesRequest, opts ...grpc.CallOption) (*MatchList, error) { + out := new(MatchList) + err := grpc.Invoke(ctx, "/nakama.api.Nakama/ListMatches", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *nakamaClient) ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*NotificationList, error) { out := new(NotificationList) err := grpc.Invoke(ctx, "/nakama.api.Nakama/ListNotifications", in, out, c.cc, opts...) @@ -2580,6 +2713,8 @@ type NakamaServer interface { LinkSteam(context.Context, *AccountSteam) (*google_protobuf1.Empty, error) // List all friends for the current user. ListFriends(context.Context, *google_protobuf1.Empty) (*Friends, error) + // Fetch list of running matches. + ListMatches(context.Context, *ListMatchesRequest) (*MatchList, error) // Fetch list of notifications. ListNotifications(context.Context, *ListNotificationsRequest) (*NotificationList, error) // List collections of storage objects. @@ -3062,6 +3197,24 @@ func _Nakama_ListFriends_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _Nakama_ListMatches_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListMatchesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NakamaServer).ListMatches(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/nakama.api.Nakama/ListMatches", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NakamaServer).ListMatches(ctx, req.(*ListMatchesRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Nakama_ListNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListNotificationsRequest) if err := dec(in); err != nil { @@ -3400,6 +3553,10 @@ var _Nakama_serviceDesc = grpc.ServiceDesc{ MethodName: "ListFriends", Handler: _Nakama_ListFriends_Handler, }, + { + MethodName: "ListMatches", + Handler: _Nakama_ListMatches_Handler, + }, { MethodName: "ListNotifications", Handler: _Nakama_ListNotifications_Handler, @@ -3460,205 +3617,215 @@ var _Nakama_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("api/api.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 3188 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x5a, 0x4b, 0x73, 0xdc, 0xc6, - 0xb5, 0xbe, 0x98, 0x07, 0x87, 0x73, 0x86, 0x8f, 0x61, 0x53, 0x92, 0x87, 0x43, 0x3d, 0x28, 0x88, - 0xd6, 0x83, 0xd7, 0xe6, 0xe8, 0x52, 0xf7, 0x96, 0x5d, 0xf2, 0x4d, 0x2c, 0x8a, 0x1c, 0x31, 0x13, - 0xc9, 0x43, 0x16, 0x86, 0x94, 0x12, 0x55, 0xd9, 0x63, 0x10, 0x68, 0x0d, 0x61, 0x62, 0x00, 0x18, - 0xc0, 0x50, 0x61, 0x5c, 0x2a, 0xa7, 0xbc, 0x49, 0x2a, 0x8b, 0x2c, 0x9c, 0x65, 0xaa, 0x92, 0x94, - 0xb3, 0x49, 0x65, 0x95, 0xdf, 0x91, 0x4d, 0xaa, 0x92, 0x8d, 0x2b, 0xab, 0x2c, 0xb2, 0xf4, 0x3f, - 0xc8, 0x26, 0xd5, 0x0f, 0x00, 0x8d, 0x17, 0x9f, 0x96, 0x57, 0x9c, 0x6e, 0x9c, 0xee, 0xf3, 0x75, - 0xf7, 0x39, 0xdf, 0x39, 0x7d, 0x9a, 0x30, 0xa9, 0x3a, 0x46, 0x4b, 0x75, 0x8c, 0x65, 0xc7, 0xb5, - 0x7d, 0x1b, 0x81, 0xa5, 0xee, 0xab, 0x43, 0x75, 0x59, 0x75, 0x8c, 0xe6, 0xe5, 0x81, 0x6d, 0x0f, - 0x4c, 0xdc, 0xa2, 0x12, 0x96, 0x65, 0xfb, 0xaa, 0x6f, 0xd8, 0x96, 0xc7, 0x24, 0x9b, 0xf3, 0xfc, - 0x2b, 0x6d, 0xed, 0x8e, 0x5e, 0xb4, 0xf0, 0xd0, 0xf1, 0x0f, 0xf9, 0xc7, 0x6b, 0xc9, 0x8f, 0xbe, - 0x31, 0xc4, 0x9e, 0xaf, 0x0e, 0x1d, 0x2e, 0x70, 0x35, 0x29, 0xf0, 0xd2, 0x55, 0x1d, 0x07, 0xbb, - 0xc1, 0xec, 0x6f, 0xd1, 0x3f, 0xda, 0xdb, 0x03, 0x6c, 0xbd, 0xed, 0xbd, 0x54, 0x07, 0x03, 0xec, - 0xb6, 0x6c, 0x87, 0xea, 0x4f, 0x63, 0x91, 0xbf, 0x91, 0xa0, 0xb2, 0xaa, 0x69, 0xf6, 0xc8, 0xf2, - 0xd1, 0x22, 0x94, 0x46, 0x1e, 0x76, 0x1b, 0xd2, 0x82, 0x74, 0xbb, 0xb6, 0x52, 0x5f, 0x8e, 0x16, - 0xb4, 0xbc, 0xe3, 0x61, 0x57, 0xa1, 0x5f, 0xd1, 0x25, 0x18, 0x7b, 0xa9, 0x9a, 0x26, 0xf6, 0x1b, - 0x85, 0x05, 0xe9, 0x76, 0x55, 0xe1, 0x2d, 0x74, 0x01, 0xca, 0x78, 0xa8, 0x1a, 0x66, 0xa3, 0x48, - 0xbb, 0x59, 0x03, 0xdd, 0x83, 0x8a, 0x8e, 0x0f, 0x0c, 0x0d, 0x7b, 0x8d, 0xd2, 0x42, 0xf1, 0x76, - 0x6d, 0x65, 0x4e, 0x9c, 0x96, 0x6b, 0x5e, 0xa7, 0x12, 0x4a, 0x20, 0x89, 0xe6, 0xa1, 0xaa, 0x8d, - 0x3c, 0xdf, 0x1e, 0xf6, 0x0d, 0xbd, 0x51, 0xa6, 0xd3, 0x8d, 0xb3, 0x8e, 0x8e, 0x8e, 0xde, 0x83, - 0xda, 0x01, 0x76, 0x8d, 0x17, 0x87, 0x7d, 0xb2, 0x33, 0x8d, 0x31, 0x0a, 0xb6, 0xb9, 0xcc, 0x76, - 0x65, 0x39, 0xd8, 0x95, 0xe5, 0xed, 0x60, 0xdb, 0x14, 0x60, 0xe2, 0xa4, 0x43, 0xbe, 0x06, 0x93, - 0x5c, 0xe7, 0x1a, 0x9d, 0x0f, 0x4d, 0x41, 0xc1, 0xd0, 0xe9, 0x8a, 0xab, 0x4a, 0xc1, 0xd0, 0x05, - 0x01, 0x06, 0x2a, 0x25, 0xf0, 0x00, 0x26, 0xb8, 0x40, 0x9b, 0x2e, 0x30, 0x5c, 0xb6, 0x24, 0x2e, - 0xbb, 0x09, 0xe3, 0x8e, 0xea, 0x79, 0x2f, 0x6d, 0x57, 0xe7, 0xdb, 0x14, 0xb6, 0xe5, 0x5b, 0x30, - 0xcd, 0x67, 0x78, 0xa4, 0x6a, 0x78, 0xd7, 0xb6, 0xf7, 0xc9, 0x24, 0xbe, 0xbd, 0x8f, 0xad, 0x60, - 0x12, 0xda, 0x90, 0xff, 0x26, 0xc1, 0x0c, 0x97, 0xdc, 0x50, 0x87, 0x78, 0x0d, 0x5b, 0x3e, 0x76, - 0xc9, 0xe6, 0x38, 0xa6, 0x7a, 0x88, 0xdd, 0x7e, 0x88, 0x6b, 0x9c, 0x75, 0x74, 0x74, 0xf2, 0x71, - 0x77, 0x64, 0xe9, 0x26, 0x26, 0x1f, 0xb9, 0x62, 0xd6, 0xd1, 0xd1, 0xd1, 0x7f, 0xc3, 0x4c, 0x68, - 0x4c, 0x7d, 0x0f, 0x6b, 0xb6, 0xa5, 0x7b, 0xf4, 0xb4, 0x8a, 0x4a, 0x3d, 0xfc, 0xd0, 0x63, 0xfd, - 0x08, 0x41, 0xc9, 0x53, 0x4d, 0xbf, 0x51, 0xa2, 0x93, 0xd0, 0xdf, 0xe8, 0x32, 0x54, 0x3d, 0x63, - 0x60, 0xa9, 0xfe, 0xc8, 0xc5, 0xfc, 0x5c, 0xa2, 0x0e, 0xb4, 0x08, 0x53, 0xce, 0x68, 0xd7, 0x34, - 0xb4, 0xfe, 0x3e, 0x3e, 0xec, 0x8f, 0x5c, 0x93, 0x9e, 0x4d, 0x55, 0x99, 0x60, 0xbd, 0x8f, 0xf1, - 0xe1, 0x8e, 0x6b, 0xca, 0x6f, 0x86, 0x1b, 0xbc, 0x41, 0x4f, 0x2c, 0x67, 0xed, 0x8b, 0xe1, 0x36, - 0xf7, 0x7c, 0xac, 0x0e, 0x73, 0xa4, 0xd6, 0x60, 0x66, 0x55, 0xd7, 0x1f, 0xb9, 0x06, 0xb6, 0x74, - 0x4f, 0xc1, 0x9f, 0x8e, 0xb0, 0xe7, 0xa3, 0x3a, 0x14, 0x0d, 0xdd, 0x6b, 0x48, 0x0b, 0xc5, 0xdb, - 0x55, 0x85, 0xfc, 0x24, 0xb8, 0x89, 0xe9, 0x5a, 0xea, 0x10, 0x7b, 0x8d, 0x02, 0xed, 0x8f, 0x3a, - 0xe4, 0xdf, 0x4b, 0x30, 0xb7, 0x3a, 0xf2, 0xf7, 0xb0, 0xe5, 0x1b, 0x9a, 0xea, 0x63, 0x66, 0x19, - 0xc1, 0x6c, 0xf7, 0xa0, 0xa2, 0x32, 0x20, 0xdc, 0x2f, 0xb2, 0x0c, 0x98, 0x0f, 0x09, 0x24, 0xd1, - 0x0a, 0x8c, 0x69, 0x2e, 0x56, 0x7d, 0x4c, 0xcf, 0x20, 0xcb, 0x3c, 0x1f, 0xda, 0xb6, 0xf9, 0x54, - 0x35, 0x47, 0x58, 0xe1, 0x92, 0xc4, 0x64, 0x02, 0x4c, 0xdc, 0x85, 0xc2, 0x76, 0x0a, 0x22, 0x77, - 0x98, 0xd3, 0x40, 0x0c, 0x7c, 0xec, 0x75, 0x41, 0xfc, 0xad, 0x04, 0x0d, 0x11, 0x22, 0xf5, 0x8e, - 0x00, 0xe1, 0x4a, 0x12, 0x61, 0x23, 0x03, 0x21, 0x1b, 0xf1, 0xda, 0x00, 0x7e, 0x2d, 0xc1, 0xbc, - 0x08, 0x30, 0x70, 0xbe, 0x00, 0xe3, 0xff, 0x25, 0x31, 0xce, 0x67, 0x60, 0x0c, 0x07, 0xbd, 0x2e, - 0x98, 0x64, 0x3e, 0x63, 0xe8, 0xd8, 0x2e, 0xf3, 0xbc, 0x63, 0xe6, 0x63, 0x92, 0xf2, 0x1f, 0x25, - 0xb8, 0x22, 0x2e, 0x2d, 0x62, 0x8b, 0x60, 0x71, 0xef, 0x24, 0x17, 0x77, 0x25, 0x63, 0x71, 0xc2, - 0xb0, 0xef, 0xcc, 0x92, 0x19, 0x09, 0x9c, 0xca, 0x92, 0xf9, 0x90, 0xef, 0xcc, 0x92, 0x29, 0x01, - 0x9d, 0xca, 0x92, 0xd9, 0x88, 0xd7, 0x06, 0xb0, 0x0d, 0xb3, 0x0f, 0x4d, 0x5b, 0xdb, 0x3f, 0x27, - 0xef, 0x1d, 0x40, 0x75, 0x6d, 0x4f, 0xb5, 0x2c, 0x6c, 0x76, 0xf4, 0x64, 0x98, 0x23, 0xf4, 0xef, - 0x1f, 0x3a, 0x0c, 0x71, 0x59, 0xa1, 0xbf, 0xe5, 0x36, 0x94, 0xb6, 0x0f, 0x1d, 0xc2, 0xd8, 0xf5, - 0xed, 0x1f, 0x6f, 0xb5, 0xfb, 0x3b, 0xdd, 0xde, 0x56, 0x7b, 0xad, 0xf3, 0xa8, 0xd3, 0x5e, 0xaf, - 0xff, 0x17, 0x1a, 0x87, 0x92, 0xb2, 0xb9, 0xf9, 0x41, 0x5d, 0x42, 0x08, 0xa6, 0xd6, 0x3b, 0x4a, - 0x7b, 0x6d, 0xbb, 0xff, 0x41, 0xbb, 0xd7, 0x5b, 0xdd, 0x68, 0xd7, 0x0b, 0xa8, 0x0a, 0xe5, 0x0d, - 0x65, 0x73, 0x67, 0xab, 0x5e, 0x94, 0x7f, 0x59, 0x80, 0xd9, 0x35, 0xba, 0xca, 0x0d, 0xd7, 0x1e, - 0x39, 0x21, 0xfe, 0x07, 0x30, 0x36, 0xa0, 0x1d, 0x74, 0x09, 0xb5, 0x95, 0xdb, 0xe2, 0xce, 0x66, - 0x0c, 0x58, 0xee, 0xe2, 0x97, 0xb4, 0x43, 0xe1, 0xe3, 0x9a, 0x7f, 0x96, 0x60, 0x3c, 0xe8, 0x24, - 0x2b, 0xa0, 0xbb, 0xc7, 0xd6, 0x44, 0x7f, 0xa3, 0x05, 0xa8, 0xe9, 0xd8, 0xd3, 0x5c, 0x83, 0xe6, - 0x43, 0x3c, 0x40, 0x8a, 0x5d, 0x68, 0x0e, 0xc6, 0x4d, 0xd5, 0x1a, 0xf4, 0x7d, 0x75, 0xc0, 0xf7, - 0xbd, 0x42, 0xda, 0xdb, 0xea, 0x80, 0x1c, 0xc9, 0x10, 0xfb, 0xaa, 0xae, 0xfa, 0x2a, 0x8f, 0x8a, - 0x61, 0x1b, 0x5d, 0x01, 0x50, 0x0f, 0x54, 0x5f, 0x75, 0x69, 0xdc, 0xe3, 0xa1, 0x91, 0xf5, 0xec, - 0xb8, 0x26, 0x6a, 0x40, 0xc5, 0x71, 0x8d, 0x03, 0x62, 0x02, 0x24, 0x26, 0x8e, 0x2b, 0x41, 0x53, - 0x7e, 0x04, 0x17, 0xd6, 0xb1, 0x89, 0x7d, 0x7c, 0xce, 0xc3, 0x5c, 0x86, 0x26, 0x9b, 0xa7, 0x6b, - 0xfb, 0xc6, 0x0b, 0x62, 0xb7, 0x24, 0xc9, 0xcb, 0x9d, 0x4d, 0xd6, 0xe0, 0x22, 0x93, 0xef, 0xf9, - 0xb6, 0xab, 0x0e, 0xf0, 0xe6, 0xee, 0x27, 0x58, 0xf3, 0x3b, 0x3a, 0xba, 0x0a, 0xa0, 0xd9, 0xa6, - 0x89, 0x35, 0xba, 0x43, 0x6c, 0xf3, 0x84, 0x1e, 0x32, 0xd5, 0x3e, 0x3e, 0xe4, 0x5b, 0x47, 0x7e, - 0x92, 0xc5, 0x1d, 0x60, 0xd7, 0x23, 0xe2, 0x7c, 0xc7, 0x78, 0x53, 0xfe, 0x08, 0xe6, 0x33, 0x94, - 0x84, 0xa8, 0xde, 0x87, 0xaa, 0xcd, 0xd5, 0x06, 0x67, 0x7e, 0x5d, 0x3c, 0xf3, 0x4c, 0x80, 0x4a, - 0x34, 0x46, 0xfe, 0x83, 0x04, 0x63, 0x6c, 0xdf, 0x4e, 0x98, 0xbb, 0x5e, 0x80, 0xb2, 0xe7, 0x07, - 0x8e, 0x58, 0x56, 0x58, 0x43, 0xfe, 0x10, 0xca, 0x3d, 0xf2, 0x03, 0x5d, 0x84, 0x99, 0xde, 0xf6, - 0xea, 0x76, 0xd2, 0xb2, 0x01, 0xc6, 0x1e, 0x29, 0x9d, 0x76, 0x77, 0xbd, 0x2e, 0xa1, 0x69, 0xa8, - 0x75, 0xba, 0x4f, 0x3b, 0xdb, 0xed, 0x7e, 0xaf, 0xdd, 0xdd, 0xae, 0x17, 0xd0, 0x2c, 0x4c, 0xf3, - 0x0e, 0xa5, 0xbd, 0xd6, 0xee, 0x3c, 0x6d, 0xaf, 0xd7, 0x8b, 0xa8, 0x06, 0x95, 0x87, 0x4f, 0x36, - 0xd7, 0x1e, 0xb7, 0xd7, 0xeb, 0x25, 0xf9, 0x1d, 0xa8, 0xf0, 0xc3, 0x45, 0x6f, 0x41, 0xe5, 0x05, - 0xfb, 0xc9, 0xd7, 0x8b, 0x44, 0xa0, 0x4c, 0x4a, 0x09, 0x44, 0x64, 0x1d, 0xa6, 0x37, 0xb0, 0x4f, - 0xe0, 0x9f, 0xd5, 0x2c, 0xd0, 0x75, 0x98, 0x78, 0xc1, 0x43, 0x56, 0xdf, 0xa0, 0xd9, 0x1e, 0x11, - 0xa8, 0x05, 0x7d, 0x64, 0x13, 0xbf, 0x29, 0x40, 0x99, 0x79, 0x4c, 0x92, 0x03, 0xae, 0x00, 0x50, - 0x36, 0xb2, 0xdd, 0x28, 0x9b, 0xac, 0xf2, 0x9e, 0x8e, 0x1e, 0x3a, 0x58, 0x31, 0xdf, 0xc1, 0x4a, - 0x47, 0x3b, 0x58, 0x39, 0xdf, 0xc1, 0xc6, 0x8e, 0x74, 0xb0, 0xca, 0x11, 0x0e, 0x36, 0x1e, 0x73, - 0x30, 0x72, 0xe4, 0x8c, 0xae, 0xab, 0xec, 0xc8, 0x19, 0x25, 0xbf, 0x07, 0x35, 0x46, 0xb4, 0xec, - 0x12, 0x01, 0xc7, 0x5f, 0x22, 0x98, 0x38, 0xe9, 0x20, 0x83, 0x47, 0x8e, 0x1e, 0x0e, 0xae, 0x1d, - 0x3f, 0x98, 0x89, 0xd3, 0x1b, 0xc8, 0x3d, 0x18, 0x63, 0x2c, 0x86, 0xee, 0x24, 0xf8, 0x6e, 0x46, - 0xb4, 0x85, 0x18, 0xb1, 0xc9, 0x3f, 0x97, 0xe0, 0x72, 0x87, 0xc6, 0xfa, 0x20, 0x01, 0x49, 0xd0, - 0xc5, 0x19, 0x93, 0x97, 0xbb, 0x50, 0x76, 0xb1, 0xc7, 0xaf, 0x72, 0x47, 0x07, 0x26, 0x26, 0x28, - 0xff, 0x4c, 0x82, 0xd9, 0x27, 0x86, 0xb5, 0xff, 0xed, 0x65, 0x4f, 0xa7, 0xce, 0x76, 0x7e, 0x02, - 0x8d, 0x27, 0x86, 0xe7, 0x67, 0x12, 0xdd, 0xff, 0x40, 0xd9, 0x34, 0x86, 0x46, 0x04, 0x22, 0x39, - 0x5d, 0xc7, 0xf2, 0xef, 0xad, 0xf0, 0x15, 0x51, 0x49, 0x74, 0x07, 0xea, 0x9a, 0xaa, 0xed, 0x61, - 0x75, 0xd7, 0xc4, 0x7d, 0x6d, 0xe4, 0x7a, 0xb6, 0xcb, 0x6d, 0x7d, 0x3a, 0xec, 0x5f, 0xa3, 0xdd, - 0xf2, 0xef, 0x24, 0x98, 0x23, 0xaa, 0xb3, 0xe9, 0xec, 0x0d, 0xa8, 0x10, 0xc7, 0x8b, 0xae, 0x65, - 0x63, 0xa4, 0x99, 0xa2, 0xd4, 0x42, 0x8a, 0x52, 0x43, 0xd0, 0xc5, 0x13, 0x83, 0xbe, 0x04, 0x63, - 0x1c, 0x2a, 0x73, 0x31, 0xde, 0x92, 0xff, 0x29, 0xc1, 0x84, 0xb8, 0x31, 0x29, 0x9f, 0x6e, 0x40, - 0xc5, 0x1b, 0x51, 0xdc, 0x1c, 0x48, 0xd0, 0x24, 0x5f, 0x34, 0xdb, 0xf2, 0xb1, 0xe5, 0x07, 0x34, - 0xce, 0x9b, 0xc4, 0xd1, 0x35, 0x5b, 0xc7, 0x54, 0x55, 0x59, 0xa1, 0xbf, 0xc9, 0x45, 0xd3, 0xc3, - 0x96, 0xce, 0x96, 0xcb, 0xaf, 0xe8, 0xac, 0x83, 0x5d, 0xd1, 0x45, 0xef, 0x1a, 0x3b, 0x95, 0x77, - 0x5d, 0x05, 0x70, 0x48, 0xfc, 0xf0, 0x28, 0x94, 0x0a, 0xf5, 0x66, 0xa1, 0x47, 0x7e, 0x05, 0x75, - 0x71, 0x85, 0xe4, 0x3c, 0xd0, 0xf7, 0x61, 0xd2, 0x12, 0xcd, 0x81, 0x7b, 0x54, 0x2c, 0x37, 0x13, - 0x07, 0x29, 0x71, 0xf1, 0xd3, 0xd8, 0xc0, 0xc7, 0x30, 0xab, 0x60, 0x55, 0x3f, 0x7f, 0xd8, 0x14, - 0xcc, 0xa5, 0x28, 0x9a, 0x8b, 0xfc, 0x1c, 0xe6, 0x52, 0x1a, 0x42, 0x23, 0xfb, 0x5e, 0x3a, 0x66, - 0x5e, 0x13, 0x57, 0x99, 0x81, 0x4d, 0x8c, 0x98, 0x3f, 0x84, 0xa2, 0xe2, 0x68, 0x59, 0x56, 0xe1, - 0xa8, 0x87, 0xa6, 0xad, 0x06, 0x34, 0x1f, 0x34, 0x09, 0x5d, 0xef, 0xf9, 0xbe, 0x43, 0xae, 0xf4, - 0x81, 0x59, 0x90, 0xf6, 0x63, 0x7c, 0x28, 0xff, 0x3f, 0x54, 0x7a, 0xd8, 0x23, 0x81, 0x3e, 0xfb, - 0x76, 0x4e, 0x6c, 0x64, 0xa4, 0x3b, 0x7d, 0xf6, 0x85, 0x17, 0x23, 0x46, 0xba, 0xb3, 0x4d, 0xaf, - 0xee, 0x5f, 0x17, 0x60, 0x32, 0x06, 0xf4, 0x5b, 0xdc, 0x42, 0x82, 0xe7, 0x80, 0xb8, 0x0b, 0xf7, - 0x0e, 0xd6, 0x10, 0x13, 0x95, 0x72, 0x2c, 0x51, 0x41, 0xb7, 0x60, 0xda, 0xc1, 0xee, 0xd0, 0xa0, - 0xab, 0xe9, 0xbb, 0x58, 0xd5, 0xa9, 0xd1, 0x96, 0x95, 0xa9, 0xa8, 0x9b, 0xec, 0x2c, 0x31, 0x14, - 0x41, 0xf0, 0xa5, 0x6b, 0xf8, 0x98, 0x9a, 0x68, 0x59, 0x11, 0x26, 0x78, 0x46, 0xba, 0x93, 0x4e, - 0x30, 0x7e, 0x9e, 0x10, 0x53, 0x3d, 0x55, 0x88, 0xf9, 0x08, 0xea, 0xb1, 0x9d, 0x5d, 0xd5, 0xf6, - 0xbf, 0xd5, 0xb4, 0xae, 0x0d, 0x33, 0xc9, 0xf9, 0x3d, 0x74, 0x17, 0x4a, 0xaa, 0xb6, 0x1f, 0xd8, - 0xe4, 0x65, 0xd1, 0x26, 0x93, 0xc2, 0x0a, 0x95, 0x94, 0xdb, 0x30, 0x15, 0xb7, 0x71, 0x72, 0xfd, - 0x63, 0xa6, 0x1a, 0x4c, 0x33, 0x97, 0x3b, 0x8d, 0x12, 0x48, 0xca, 0x1f, 0x27, 0xd0, 0x50, 0x42, - 0x38, 0xcb, 0x4c, 0x02, 0xa9, 0x16, 0x62, 0xa4, 0xfa, 0xef, 0x02, 0x5c, 0xd8, 0xa1, 0xdb, 0xcb, - 0xe3, 0x58, 0xe0, 0x8c, 0xef, 0x0a, 0x97, 0x34, 0x16, 0x70, 0x2e, 0xa7, 0x8e, 0xa8, 0xe7, 0xbb, - 0x86, 0x35, 0x60, 0xe4, 0x1d, 0xdd, 0xf2, 0xdf, 0x87, 0x09, 0xdd, 0xf0, 0x1c, 0x53, 0x3d, 0xec, - 0xd3, 0xd1, 0x85, 0x13, 0x8c, 0xae, 0xf1, 0x11, 0x5d, 0x95, 0x1a, 0x88, 0x98, 0x0f, 0x15, 0x4f, - 0x30, 0x5c, 0xc8, 0x96, 0xde, 0x11, 0x72, 0xb0, 0xd2, 0x09, 0x86, 0x86, 0x19, 0xda, 0xbb, 0x30, - 0x6e, 0xda, 0x8c, 0x34, 0xa9, 0x0b, 0x1d, 0xbb, 0xe0, 0x40, 0x9a, 0x8c, 0x24, 0x96, 0xfc, 0x53, - 0xdb, 0x0a, 0xe2, 0xc1, 0x31, 0x23, 0x03, 0x69, 0xf9, 0xcb, 0x12, 0x94, 0x48, 0x0e, 0x9c, 0x22, - 0x2d, 0xf1, 0x8a, 0x5c, 0x48, 0x54, 0x51, 0xae, 0x27, 0xf6, 0xb7, 0xc8, 0x13, 0x51, 0x61, 0x07, - 0xe3, 0x19, 0x65, 0x29, 0x99, 0x51, 0x1e, 0x9d, 0xa7, 0x86, 0xbb, 0xc0, 0xf3, 0xd4, 0x70, 0x9d, - 0x4d, 0x61, 0x9d, 0x2c, 0x4b, 0x0d, 0xdb, 0xb1, 0xfc, 0x76, 0x3c, 0x91, 0xdf, 0x5e, 0x83, 0x9a, - 0x90, 0xa8, 0x53, 0x87, 0xaf, 0x2a, 0x10, 0xe5, 0xe9, 0x84, 0x4c, 0xd9, 0x7e, 0x91, 0xcf, 0xc0, - 0x46, 0xb3, 0x8e, 0x8e, 0x8e, 0x6e, 0xc0, 0xe4, 0x40, 0x1d, 0x62, 0x8d, 0x16, 0x6f, 0x88, 0x40, - 0x8d, 0x55, 0x5e, 0xa3, 0xce, 0x0e, 0xa5, 0x72, 0xcf, 0xc7, 0x2a, 0x2d, 0xaa, 0x4f, 0xf0, 0xd8, - 0x4f, 0xda, 0x1d, 0x9d, 0x58, 0xbe, 0x6d, 0x99, 0x86, 0x85, 0x1b, 0x93, 0x34, 0xde, 0xf2, 0x16, - 0xd9, 0x23, 0xac, 0x0f, 0x70, 0x9f, 0x25, 0x76, 0x53, 0x94, 0xe8, 0xaa, 0xa4, 0x67, 0x2d, 0x2b, - 0x8b, 0x9e, 0x3e, 0x0f, 0xc5, 0xd5, 0x4f, 0x45, 0x71, 0x2d, 0x28, 0xd3, 0x7b, 0x11, 0xba, 0x09, - 0x65, 0x72, 0xe8, 0x81, 0x9b, 0xa7, 0x2f, 0x7e, 0xec, 0xb3, 0xfc, 0x17, 0x09, 0x10, 0xe5, 0xe5, - 0xf3, 0xc6, 0x9c, 0x30, 0xb4, 0x14, 0x73, 0x42, 0x4b, 0xe9, 0xd8, 0xd0, 0x52, 0x3e, 0x71, 0x68, - 0x19, 0xcb, 0x0c, 0x2d, 0xf2, 0x53, 0x68, 0xa6, 0xd7, 0xe2, 0x45, 0xac, 0x94, 0xe0, 0xbe, 0xab, - 0xe2, 0xa6, 0xa4, 0x07, 0x86, 0x04, 0xb8, 0x64, 0xc1, 0x45, 0xfe, 0x65, 0x2b, 0x8e, 0xed, 0x16, - 0xdc, 0xe8, 0x6d, 0x6f, 0x2a, 0xab, 0x1b, 0xed, 0xfe, 0x56, 0x5b, 0xf9, 0xa0, 0xd3, 0xeb, 0x75, - 0x36, 0xbb, 0x7d, 0xa5, 0xbd, 0xba, 0x9e, 0xb8, 0x2a, 0xd7, 0xa0, 0xd2, 0xdd, 0xa4, 0x1f, 0xea, - 0x12, 0x9a, 0x02, 0xd8, 0x7c, 0xd6, 0x6d, 0x2b, 0xac, 0x5d, 0x20, 0x77, 0xe7, 0xad, 0x9d, 0x87, - 0x4f, 0x3a, 0x6b, 0xac, 0xa3, 0xb8, 0xa4, 0xc2, 0xa5, 0x94, 0x3e, 0x16, 0x3c, 0x6f, 0xc3, 0x62, - 0x86, 0xc2, 0x67, 0x4a, 0x27, 0x75, 0x39, 0x9f, 0x80, 0xf1, 0xee, 0x26, 0xfb, 0xc2, 0xae, 0xe7, - 0x4c, 0x25, 0xeb, 0x28, 0xac, 0xfc, 0x75, 0x01, 0xc6, 0xba, 0x74, 0xf5, 0xe8, 0x19, 0x40, 0xf4, - 0x58, 0x80, 0xe2, 0x05, 0xd0, 0xe4, 0x23, 0x42, 0xf3, 0x52, 0xca, 0x10, 0xdb, 0x43, 0xc7, 0x3f, - 0x94, 0xd1, 0x17, 0x7f, 0xff, 0xd7, 0xaf, 0x0b, 0x13, 0x32, 0xb4, 0x0e, 0x56, 0x5a, 0xec, 0xa2, - 0x8e, 0xbe, 0x90, 0x00, 0xa5, 0x1f, 0x10, 0xd0, 0x9b, 0x31, 0x0d, 0x79, 0x0f, 0x0c, 0xcd, 0xd9, - 0x58, 0x64, 0x62, 0x09, 0x95, 0x7c, 0x97, 0xaa, 0x59, 0x92, 0xaf, 0x11, 0x35, 0xfc, 0xb2, 0xd4, - 0x52, 0x85, 0x39, 0x5a, 0xec, 0x3d, 0xec, 0x7e, 0x78, 0x93, 0x4a, 0x82, 0xe0, 0xcf, 0x57, 0xb9, - 0x20, 0x62, 0x4f, 0x08, 0x67, 0x05, 0xc1, 0x1e, 0xec, 0x22, 0x10, 0x9f, 0xc3, 0x4c, 0xea, 0x0d, - 0x00, 0x2d, 0xe6, 0x41, 0x10, 0x9f, 0x08, 0xb2, 0x11, 0xb4, 0x28, 0x82, 0x3b, 0xf2, 0xd5, 0x5c, - 0x04, 0xf4, 0xb9, 0x2d, 0x02, 0xf0, 0x0b, 0x09, 0x2e, 0x64, 0x15, 0xf9, 0xd1, 0xad, 0x3c, 0x10, - 0x89, 0x8b, 0x6c, 0x36, 0x8e, 0x15, 0x8a, 0xe3, 0x2d, 0xf9, 0x7a, 0x2e, 0x8e, 0x80, 0xa9, 0x23, - 0x28, 0xbf, 0x92, 0xe0, 0x52, 0x76, 0x51, 0x1e, 0xdd, 0xc9, 0x03, 0x93, 0x2a, 0xdc, 0x67, 0xc3, - 0xf9, 0x5f, 0x0a, 0x67, 0x59, 0xbe, 0x91, 0x0b, 0x27, 0x22, 0xfe, 0x7c, 0x0b, 0xe1, 0xef, 0x6f, - 0xb9, 0x16, 0x12, 0x2b, 0xcd, 0x9f, 0xd5, 0x42, 0x98, 0x17, 0xe5, 0x5a, 0x08, 0x7b, 0xdc, 0xcb, - 0xb5, 0x10, 0xb1, 0xf4, 0x7e, 0x56, 0x0b, 0xa1, 0x31, 0x2e, 0x02, 0xa0, 0xc2, 0x84, 0x58, 0x3c, - 0x47, 0xb1, 0xdb, 0x53, 0x46, 0x59, 0x3d, 0x97, 0x09, 0x1a, 0x54, 0x33, 0x92, 0xeb, 0x11, 0x13, - 0xb4, 0x76, 0xc9, 0x78, 0xf4, 0x23, 0xa8, 0x09, 0xe5, 0xea, 0xb8, 0x86, 0x8c, 0x3a, 0x76, 0x13, - 0xa5, 0x0a, 0x3f, 0x9e, 0x7c, 0x81, 0xce, 0x3e, 0x25, 0x57, 0xc9, 0xec, 0xb4, 0x0a, 0x74, 0x5f, - 0x5a, 0x42, 0x1f, 0xc2, 0x64, 0xac, 0x5a, 0x8c, 0x16, 0xd2, 0xf5, 0xd2, 0xd3, 0x11, 0xd9, 0x92, - 0x48, 0x64, 0x36, 0xcc, 0x66, 0x14, 0x91, 0xd1, 0xcd, 0xb4, 0x92, 0xac, 0xe2, 0xcb, 0x71, 0x3b, - 0xb5, 0x44, 0x77, 0x4a, 0xbc, 0x78, 0x23, 0x33, 0xa8, 0x7e, 0x27, 0x2e, 0x02, 0xb7, 0x8e, 0x29, - 0x03, 0x1f, 0xab, 0x72, 0x96, 0xaa, 0x9c, 0x5c, 0xaa, 0x11, 0x95, 0x1e, 0x1b, 0x8a, 0xba, 0x00, - 0x1b, 0xd8, 0x0f, 0xfe, 0xdb, 0x21, 0x67, 0x68, 0xdc, 0xcc, 0xb8, 0x70, 0x30, 0x1f, 0xaa, 0x09, - 0x66, 0x86, 0x9e, 0xc0, 0x78, 0x50, 0x9f, 0x45, 0xb1, 0x72, 0x57, 0xa2, 0x6a, 0xdb, 0x9c, 0x49, - 0x66, 0x25, 0x9e, 0x5c, 0xa7, 0x13, 0x02, 0x1a, 0x27, 0x13, 0xd2, 0xda, 0x74, 0x0f, 0x6a, 0x3f, - 0xc0, 0xaa, 0xe9, 0xef, 0x69, 0x7b, 0x58, 0xdb, 0xcf, 0x85, 0x97, 0xb7, 0x62, 0x6e, 0x30, 0x68, - 0xa2, 0xb5, 0x27, 0xcc, 0xf2, 0x39, 0x5c, 0xcc, 0xac, 0x1b, 0xa2, 0xd8, 0xe3, 0xca, 0x51, 0xa5, - 0xc5, 0x5c, 0x85, 0x8b, 0x54, 0xe1, 0x55, 0x79, 0x56, 0xb0, 0xff, 0x34, 0x0b, 0x6a, 0x00, 0x4f, - 0x0c, 0x6b, 0x9f, 0x87, 0xc4, 0xfc, 0xb7, 0xf3, 0x5c, 0x35, 0x32, 0x55, 0x73, 0x59, 0x7e, 0x43, - 0x74, 0x70, 0xd3, 0xb0, 0xf6, 0x83, 0x08, 0x28, 0x2d, 0x05, 0x4a, 0x78, 0xc8, 0xcb, 0x7f, 0xfd, - 0x3e, 0x83, 0x12, 0x1e, 0xe1, 0xa4, 0x25, 0xf4, 0x31, 0x54, 0x89, 0x12, 0x16, 0xd3, 0x72, 0xdf, - 0xaf, 0x73, 0x55, 0x5c, 0xa7, 0x2a, 0xe6, 0xe5, 0x4b, 0x29, 0x15, 0x2c, 0x84, 0x49, 0x4b, 0xc8, - 0x83, 0x09, 0xb1, 0xb4, 0x1a, 0x27, 0x8e, 0x8c, 0xa2, 0x6b, 0xae, 0xae, 0x25, 0xaa, 0x6b, 0x51, - 0x9e, 0x4b, 0xe9, 0x4a, 0x1f, 0x90, 0x0d, 0x53, 0x64, 0x6a, 0x21, 0x3a, 0x1d, 0xfd, 0x34, 0x9c, - 0xab, 0xf4, 0x26, 0x55, 0xba, 0x20, 0xcf, 0xa7, 0x94, 0x0a, 0xc1, 0x28, 0x3a, 0x2c, 0x1e, 0x7d, - 0xf2, 0x1f, 0x78, 0xcf, 0x70, 0x58, 0x3c, 0xd8, 0x44, 0x87, 0xc5, 0xc2, 0x4b, 0xee, 0x13, 0xed, - 0x19, 0x0e, 0x8b, 0x45, 0x13, 0x69, 0x09, 0x75, 0xa1, 0xf6, 0xc4, 0xf0, 0xfc, 0xc0, 0x9f, 0x4e, - 0xc4, 0x26, 0x5c, 0x38, 0xe0, 0x5e, 0x24, 0x72, 0xef, 0xa7, 0x30, 0x93, 0xaa, 0x6a, 0xc7, 0x03, - 0x63, 0x5e, 0xd1, 0xbb, 0x79, 0x39, 0xaf, 0xcc, 0x49, 0x46, 0x04, 0xec, 0x8b, 0xd2, 0xec, 0xfb, - 0x95, 0x04, 0x28, 0x5d, 0xce, 0x8e, 0x27, 0x04, 0xb9, 0xe5, 0xee, 0xe6, 0x95, 0xdc, 0x8a, 0x0a, - 0x55, 0xfb, 0x88, 0xaa, 0x7d, 0x80, 0x1a, 0x02, 0x03, 0xb7, 0x3e, 0x8b, 0xae, 0x56, 0xaf, 0x9e, - 0x2f, 0x22, 0x39, 0xef, 0x5b, 0xeb, 0x33, 0x5e, 0xd7, 0x7b, 0x85, 0x6c, 0x40, 0xe9, 0x6a, 0x68, - 0x1c, 0x63, 0x6e, 0xb5, 0xb4, 0xd9, 0xcc, 0xc5, 0xe8, 0xc9, 0x97, 0x28, 0xc0, 0xba, 0x2c, 0x86, - 0x08, 0x72, 0xb0, 0xcf, 0xa1, 0xa2, 0x38, 0xda, 0xa3, 0x91, 0xa5, 0xa1, 0xe9, 0x98, 0x16, 0x47, - 0x6b, 0x26, 0x3b, 0xe4, 0xb7, 0xe9, 0x24, 0xb7, 0xe4, 0x09, 0x32, 0x89, 0xeb, 0x68, 0xad, 0xcf, - 0x0c, 0xfd, 0xd5, 0xfd, 0xa0, 0x7a, 0xfa, 0x9c, 0xd0, 0xb1, 0xf0, 0x01, 0x0d, 0x60, 0x62, 0x87, - 0xdc, 0xac, 0xcf, 0xc1, 0x87, 0x01, 0xed, 0xc6, 0x7c, 0x7b, 0x64, 0x25, 0x18, 0x31, 0x54, 0x74, - 0x76, 0x4e, 0x3c, 0x4a, 0x51, 0xc4, 0x8a, 0x3a, 0xd4, 0x98, 0xa2, 0xb3, 0xf2, 0xe2, 0x0d, 0xaa, - 0xe6, 0x8a, 0xdc, 0xc8, 0x50, 0x13, 0x32, 0xe3, 0x10, 0xa6, 0x98, 0x96, 0x90, 0x1b, 0x8f, 0x7a, - 0x5e, 0x3a, 0x1d, 0x45, 0x71, 0x5d, 0x21, 0x33, 0x52, 0x22, 0xae, 0x33, 0x75, 0xe7, 0x67, 0xc5, - 0xdb, 0x54, 0xa5, 0x2c, 0x5f, 0xc9, 0x50, 0x19, 0xe7, 0xc5, 0xf0, 0xc8, 0xce, 0xce, 0x8c, 0x47, - 0x1d, 0x59, 0xc4, 0x8d, 0xe1, 0x91, 0x9d, 0x95, 0x1d, 0x8f, 0x3a, 0xb2, 0x90, 0x1f, 0x55, 0x98, - 0x8c, 0xd5, 0x4c, 0xe3, 0xa9, 0x6a, 0x56, 0x39, 0x35, 0x57, 0x1f, 0xf7, 0xd4, 0xa6, 0x98, 0x7c, - 0x11, 0x15, 0x3e, 0xcc, 0x66, 0x94, 0x41, 0xe2, 0xe9, 0x6a, 0x7e, 0x9d, 0xe4, 0x08, 0x02, 0x5b, - 0xd5, 0xf6, 0xbd, 0xb8, 0xd6, 0x88, 0x1f, 0x1e, 0xfe, 0xa6, 0xf0, 0xe5, 0xea, 0x3f, 0x24, 0x34, - 0x82, 0x49, 0x56, 0x57, 0x58, 0x58, 0xdd, 0xea, 0x2c, 0x1c, 0xac, 0xc8, 0x7d, 0xb8, 0xbe, 0xbd, - 0x87, 0x17, 0x82, 0xce, 0x91, 0xbf, 0x67, 0xbb, 0xde, 0xc2, 0xcd, 0x85, 0x35, 0xdb, 0xf2, 0x5d, - 0x63, 0x77, 0xe4, 0xdb, 0xae, 0x87, 0x16, 0xf7, 0x7c, 0xdf, 0xf1, 0xee, 0xb7, 0x5a, 0x03, 0xc3, - 0xdf, 0x1b, 0xed, 0x2e, 0x6b, 0xf6, 0xb0, 0xb5, 0x87, 0x5d, 0xdb, 0xd0, 0x4c, 0x75, 0xd7, 0x6b, - 0x31, 0x40, 0xcd, 0x0b, 0x7b, 0xd8, 0x34, 0xed, 0x07, 0xd1, 0x07, 0x22, 0xb7, 0x52, 0x5c, 0x59, - 0xbe, 0xbb, 0x24, 0x49, 0x2b, 0x75, 0xd5, 0x71, 0x4c, 0xce, 0xe1, 0xad, 0x4f, 0x3c, 0xdb, 0xba, - 0x9f, 0xea, 0x71, 0xef, 0xc3, 0x3c, 0x07, 0xe2, 0x61, 0xf7, 0x00, 0xbb, 0x0b, 0xba, 0xad, 0x8d, - 0x86, 0xd8, 0x62, 0xff, 0xfc, 0x8b, 0xe6, 0x03, 0x18, 0x71, 0x15, 0x2d, 0xdd, 0xd6, 0x3c, 0x98, - 0xd3, 0xec, 0xe1, 0xb2, 0xf0, 0x21, 0xda, 0xa5, 0x87, 0x55, 0x36, 0xe9, 0xaa, 0x63, 0x6c, 0x49, - 0xcf, 0x8b, 0xaa, 0x63, 0x7c, 0x55, 0x28, 0x75, 0x1f, 0x6f, 0x3d, 0xfc, 0x53, 0x81, 0x17, 0x59, - 0x76, 0xc7, 0xe8, 0xd9, 0xdd, 0xfb, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe3, 0x44, 0xd4, 0x4a, - 0x1d, 0x2d, 0x00, 0x00, + // 3354 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x5a, 0xcd, 0x73, 0x1b, 0xc7, + 0x95, 0xdf, 0x01, 0x08, 0x02, 0x78, 0xe0, 0x07, 0xd8, 0x94, 0x64, 0x10, 0xd4, 0x07, 0x35, 0xa2, + 0xf5, 0x41, 0xdb, 0x84, 0x16, 0xda, 0x5d, 0xbb, 0xe4, 0xdd, 0xb5, 0x28, 0x12, 0xe2, 0x62, 0x25, + 0x81, 0xac, 0x01, 0x29, 0x25, 0xaa, 0xb2, 0xe1, 0xe6, 0x4c, 0x0b, 0x1c, 0x73, 0x30, 0x33, 0x9e, + 0x19, 0x50, 0xa6, 0x5d, 0x2a, 0xa7, 0x7c, 0x49, 0x2a, 0x87, 0x54, 0xca, 0x39, 0xa6, 0x2a, 0x49, + 0x39, 0x17, 0x57, 0xaa, 0x52, 0x95, 0xbf, 0x23, 0xc7, 0xe4, 0xe2, 0xca, 0x29, 0x87, 0x1c, 0xfd, + 0x1f, 0xe4, 0x92, 0xea, 0x8f, 0x19, 0xcc, 0x27, 0x3f, 0x25, 0x9f, 0x88, 0xee, 0x79, 0xdd, 0xef, + 0xd7, 0xaf, 0x5f, 0xff, 0x5e, 0xf7, 0x7b, 0x84, 0x49, 0x6c, 0xeb, 0x0d, 0x6c, 0xeb, 0xcb, 0xb6, + 0x63, 0x79, 0x16, 0x02, 0x13, 0xef, 0xe1, 0x01, 0x5e, 0xc6, 0xb6, 0x5e, 0xbf, 0xd8, 0xb7, 0xac, + 0xbe, 0x41, 0x1a, 0x4c, 0xc2, 0x34, 0x2d, 0x0f, 0x7b, 0xba, 0x65, 0xba, 0x5c, 0xb2, 0x3e, 0x2f, + 0xbe, 0xb2, 0xd6, 0xce, 0xf0, 0x79, 0x83, 0x0c, 0x6c, 0xef, 0x40, 0x7c, 0xbc, 0x12, 0xff, 0xe8, + 0xe9, 0x03, 0xe2, 0x7a, 0x78, 0x60, 0x0b, 0x81, 0xcb, 0x71, 0x81, 0x17, 0x0e, 0xb6, 0x6d, 0xe2, + 0xf8, 0xb3, 0xbf, 0xcd, 0xfe, 0xa8, 0xef, 0xf4, 0x89, 0xf9, 0x8e, 0xfb, 0x02, 0xf7, 0xfb, 0xc4, + 0x69, 0x58, 0x36, 0xd3, 0x9f, 0xc4, 0x22, 0x7f, 0x2f, 0x41, 0x71, 0x45, 0x55, 0xad, 0xa1, 0xe9, + 0xa1, 0x45, 0x18, 0x1b, 0xba, 0xc4, 0xa9, 0x49, 0x0b, 0xd2, 0xcd, 0x4a, 0xb3, 0xba, 0x3c, 0x5a, + 0xd0, 0xf2, 0xb6, 0x4b, 0x1c, 0x85, 0x7d, 0x45, 0x17, 0x60, 0xfc, 0x05, 0x36, 0x0c, 0xe2, 0xd5, + 0x72, 0x0b, 0xd2, 0xcd, 0xb2, 0x22, 0x5a, 0xe8, 0x1c, 0x14, 0xc8, 0x00, 0xeb, 0x46, 0x2d, 0xcf, + 0xba, 0x79, 0x03, 0xdd, 0x81, 0xa2, 0x46, 0xf6, 0x75, 0x95, 0xb8, 0xb5, 0xb1, 0x85, 0xfc, 0xcd, + 0x4a, 0x73, 0x2e, 0x3c, 0xad, 0xd0, 0xbc, 0xc6, 0x24, 0x14, 0x5f, 0x12, 0xcd, 0x43, 0x59, 0x1d, + 0xba, 0x9e, 0x35, 0xe8, 0xe9, 0x5a, 0xad, 0xc0, 0xa6, 0x2b, 0xf1, 0x8e, 0xb6, 0x86, 0xde, 0x87, + 0xca, 0x3e, 0x71, 0xf4, 0xe7, 0x07, 0x3d, 0x6a, 0x99, 0xda, 0x38, 0x03, 0x5b, 0x5f, 0xe6, 0x56, + 0x59, 0xf6, 0xad, 0xb2, 0xbc, 0xe5, 0x9b, 0x4d, 0x01, 0x2e, 0x4e, 0x3b, 0xe4, 0x2b, 0x30, 0x29, + 0x74, 0xae, 0xb2, 0xf9, 0xd0, 0x14, 0xe4, 0x74, 0x8d, 0xad, 0xb8, 0xac, 0xe4, 0x74, 0x2d, 0x24, + 0xc0, 0x41, 0x25, 0x04, 0xee, 0xc1, 0x84, 0x10, 0x68, 0xb1, 0x05, 0x06, 0xcb, 0x96, 0xc2, 0xcb, + 0xae, 0x43, 0xc9, 0xc6, 0xae, 0xfb, 0xc2, 0x72, 0x34, 0x61, 0xa6, 0xa0, 0x2d, 0xdf, 0x80, 0x69, + 0x31, 0xc3, 0x03, 0xac, 0x92, 0x1d, 0xcb, 0xda, 0xa3, 0x93, 0x78, 0xd6, 0x1e, 0x31, 0xfd, 0x49, + 0x58, 0x43, 0xfe, 0x8b, 0x04, 0x33, 0x42, 0x72, 0x1d, 0x0f, 0xc8, 0x2a, 0x31, 0x3d, 0xe2, 0x50, + 0xe3, 0xd8, 0x06, 0x3e, 0x20, 0x4e, 0x2f, 0xc0, 0x55, 0xe2, 0x1d, 0x6d, 0x8d, 0x7e, 0xdc, 0x19, + 0x9a, 0x9a, 0x41, 0xe8, 0x47, 0xa1, 0x98, 0x77, 0xb4, 0x35, 0xf4, 0x16, 0xcc, 0x04, 0xce, 0xd4, + 0x73, 0x89, 0x6a, 0x99, 0x9a, 0xcb, 0x76, 0x2b, 0xaf, 0x54, 0x83, 0x0f, 0x5d, 0xde, 0x8f, 0x10, + 0x8c, 0xb9, 0xd8, 0xf0, 0x6a, 0x63, 0x6c, 0x12, 0xf6, 0x1b, 0x5d, 0x84, 0xb2, 0xab, 0xf7, 0x4d, + 0xec, 0x0d, 0x1d, 0x22, 0xf6, 0x65, 0xd4, 0x81, 0x16, 0x61, 0xca, 0x1e, 0xee, 0x18, 0xba, 0xda, + 0xdb, 0x23, 0x07, 0xbd, 0xa1, 0x63, 0xb0, 0xbd, 0x29, 0x2b, 0x13, 0xbc, 0xf7, 0x21, 0x39, 0xd8, + 0x76, 0x0c, 0xf9, 0xcd, 0xc0, 0xc0, 0xeb, 0x6c, 0xc7, 0x32, 0xd6, 0xbe, 0x18, 0x98, 0xb9, 0xeb, + 0x11, 0x3c, 0xc8, 0x90, 0x5a, 0x85, 0x99, 0x15, 0x4d, 0x7b, 0xe0, 0xe8, 0xc4, 0xd4, 0x5c, 0x85, + 0x7c, 0x3a, 0x24, 0xae, 0x87, 0xaa, 0x90, 0xd7, 0x35, 0xb7, 0x26, 0x2d, 0xe4, 0x6f, 0x96, 0x15, + 0xfa, 0x93, 0xe2, 0xa6, 0xae, 0x6b, 0xe2, 0x01, 0x71, 0x6b, 0x39, 0xd6, 0x3f, 0xea, 0x90, 0x7f, + 0x27, 0xc1, 0xdc, 0xca, 0xd0, 0xdb, 0x25, 0xa6, 0xa7, 0xab, 0xd8, 0x23, 0xdc, 0x33, 0xfc, 0xd9, + 0xee, 0x40, 0x11, 0x73, 0x20, 0xe2, 0x5c, 0xa4, 0x39, 0xb0, 0x18, 0xe2, 0x4b, 0xa2, 0x26, 0x8c, + 0xab, 0x0e, 0xc1, 0x1e, 0x61, 0x7b, 0x90, 0xe6, 0x9e, 0xf7, 0x2d, 0xcb, 0x78, 0x82, 0x8d, 0x21, + 0x51, 0x84, 0x24, 0x75, 0x19, 0x1f, 0x93, 0x38, 0x42, 0x41, 0x3b, 0x01, 0x51, 0x1c, 0x98, 0x93, + 0x40, 0xf4, 0xcf, 0xd8, 0xeb, 0x82, 0xf8, 0x1b, 0x09, 0x6a, 0x61, 0x88, 0xec, 0x74, 0xf8, 0x08, + 0x9b, 0x71, 0x84, 0xb5, 0x14, 0x84, 0x7c, 0xc4, 0x6b, 0x03, 0xf8, 0x9d, 0x04, 0xf3, 0x61, 0x80, + 0xfe, 0xe1, 0xf3, 0x31, 0xfe, 0x67, 0x1c, 0xe3, 0x7c, 0x0a, 0xc6, 0x60, 0xd0, 0xeb, 0x82, 0x49, + 0xe7, 0xd3, 0x07, 0xb6, 0xe5, 0xf0, 0x93, 0x77, 0xc4, 0x7c, 0x5c, 0x52, 0xfe, 0x56, 0x82, 0x4b, + 0xe1, 0xa5, 0x8d, 0xd8, 0xc2, 0x5f, 0xdc, 0xbb, 0xf1, 0xc5, 0x5d, 0x4a, 0x59, 0x5c, 0x68, 0xd8, + 0x0f, 0xe6, 0xc9, 0x9c, 0x04, 0x4e, 0xe4, 0xc9, 0x62, 0xc8, 0x0f, 0xe6, 0xc9, 0x8c, 0x80, 0x4e, + 0xe4, 0xc9, 0x7c, 0xc4, 0x6b, 0x03, 0xd8, 0x82, 0xd9, 0xfb, 0x86, 0xa5, 0xee, 0x9d, 0x91, 0xf7, + 0xf6, 0xa1, 0xbc, 0xba, 0x8b, 0x4d, 0x93, 0x18, 0x6d, 0x2d, 0x1e, 0xe6, 0x28, 0xfd, 0x7b, 0x07, + 0x36, 0x47, 0x5c, 0x50, 0xd8, 0x6f, 0xb9, 0x05, 0x63, 0x5b, 0x07, 0x36, 0x65, 0xec, 0xea, 0xd6, + 0x8f, 0x37, 0x5b, 0xbd, 0xed, 0x4e, 0x77, 0xb3, 0xb5, 0xda, 0x7e, 0xd0, 0x6e, 0xad, 0x55, 0xff, + 0x0d, 0x95, 0x60, 0x4c, 0xd9, 0xd8, 0x78, 0x5c, 0x95, 0x10, 0x82, 0xa9, 0xb5, 0xb6, 0xd2, 0x5a, + 0xdd, 0xea, 0x3d, 0x6e, 0x75, 0xbb, 0x2b, 0xeb, 0xad, 0x6a, 0x0e, 0x95, 0xa1, 0xb0, 0xae, 0x6c, + 0x6c, 0x6f, 0x56, 0xf3, 0xf2, 0xcf, 0x73, 0x30, 0xbb, 0xca, 0x56, 0xb9, 0xee, 0x58, 0x43, 0x3b, + 0xc0, 0x7f, 0x0f, 0xc6, 0xfb, 0xac, 0x83, 0x2d, 0xa1, 0xd2, 0xbc, 0x19, 0xb6, 0x6c, 0xca, 0x80, + 0xe5, 0x0e, 0x79, 0xc1, 0x3a, 0x14, 0x31, 0xae, 0xfe, 0x27, 0x09, 0x4a, 0x7e, 0x27, 0x5d, 0x01, + 0xb3, 0x1e, 0x5f, 0x13, 0xfb, 0x8d, 0x16, 0xa0, 0xa2, 0x11, 0x57, 0x75, 0x74, 0x76, 0x1f, 0x12, + 0x01, 0x32, 0xdc, 0x85, 0xe6, 0xa0, 0x64, 0x60, 0xb3, 0xdf, 0xf3, 0x70, 0x5f, 0xd8, 0xbd, 0x48, + 0xdb, 0x5b, 0xb8, 0x4f, 0xb7, 0x64, 0x40, 0x3c, 0xac, 0x61, 0x0f, 0x8b, 0xa8, 0x18, 0xb4, 0xd1, + 0x25, 0x00, 0xbc, 0x8f, 0x3d, 0xec, 0xb0, 0xb8, 0x27, 0x42, 0x23, 0xef, 0xd9, 0x76, 0x0c, 0x54, + 0x83, 0xa2, 0xed, 0xe8, 0xfb, 0xd4, 0x05, 0x68, 0x4c, 0x2c, 0x29, 0x7e, 0x53, 0x7e, 0x00, 0xe7, + 0xd6, 0x88, 0x41, 0x3c, 0x72, 0xc6, 0xcd, 0x5c, 0x86, 0x3a, 0x9f, 0xa7, 0x63, 0x79, 0xfa, 0x73, + 0xea, 0xb7, 0xf4, 0x92, 0x97, 0x39, 0x9b, 0xac, 0xc2, 0x79, 0x2e, 0xdf, 0xf5, 0x2c, 0x07, 0xf7, + 0xc9, 0xc6, 0xce, 0x27, 0x44, 0xf5, 0xda, 0x1a, 0xba, 0x0c, 0xa0, 0x5a, 0x86, 0x41, 0x54, 0x66, + 0x21, 0x6e, 0xbc, 0x50, 0x0f, 0x9d, 0x6a, 0x8f, 0x1c, 0x08, 0xd3, 0xd1, 0x9f, 0x74, 0x71, 0xfb, + 0xc4, 0x71, 0xa9, 0xb8, 0xb0, 0x98, 0x68, 0xca, 0x1f, 0xc1, 0x7c, 0x8a, 0x92, 0x00, 0xd5, 0x07, + 0x50, 0xb6, 0x84, 0x5a, 0x7f, 0xcf, 0xaf, 0x86, 0xf7, 0x3c, 0x15, 0xa0, 0x32, 0x1a, 0x23, 0xff, + 0x5e, 0x82, 0x71, 0x6e, 0xb7, 0x63, 0xde, 0x5d, 0xcf, 0x41, 0xc1, 0xf5, 0xfc, 0x83, 0x58, 0x50, + 0x78, 0x43, 0xfe, 0x10, 0x0a, 0x5d, 0xfa, 0x03, 0x9d, 0x87, 0x99, 0xee, 0xd6, 0xca, 0x56, 0xdc, + 0xb3, 0x01, 0xc6, 0x1f, 0x28, 0xed, 0x56, 0x67, 0xad, 0x2a, 0xa1, 0x69, 0xa8, 0xb4, 0x3b, 0x4f, + 0xda, 0x5b, 0xad, 0x5e, 0xb7, 0xd5, 0xd9, 0xaa, 0xe6, 0xd0, 0x2c, 0x4c, 0x8b, 0x0e, 0xa5, 0xb5, + 0xda, 0x6a, 0x3f, 0x69, 0xad, 0x55, 0xf3, 0xa8, 0x02, 0xc5, 0xfb, 0x8f, 0x36, 0x56, 0x1f, 0xb6, + 0xd6, 0xaa, 0x63, 0xf2, 0xbb, 0x50, 0x14, 0x9b, 0x8b, 0xde, 0x86, 0xe2, 0x73, 0xfe, 0x53, 0xac, + 0x17, 0x85, 0x81, 0x72, 0x29, 0xc5, 0x17, 0x91, 0x35, 0x98, 0x5e, 0x27, 0x1e, 0x85, 0x7f, 0x5a, + 0xb7, 0x40, 0x57, 0x61, 0xe2, 0xb9, 0x08, 0x59, 0x3d, 0x9d, 0xdd, 0xf6, 0xa8, 0x40, 0xc5, 0xef, + 0xa3, 0x46, 0xfc, 0x3e, 0x07, 0x05, 0x7e, 0x62, 0xe2, 0x1c, 0x70, 0x09, 0x80, 0xb1, 0x91, 0xe5, + 0x8c, 0x6e, 0x93, 0x65, 0xd1, 0xd3, 0xd6, 0x82, 0x03, 0x96, 0xcf, 0x3e, 0x60, 0x63, 0x87, 0x1f, + 0xb0, 0x42, 0xf6, 0x01, 0x1b, 0x3f, 0xf4, 0x80, 0x15, 0x0f, 0x39, 0x60, 0xa5, 0xc8, 0x01, 0xa3, + 0x5b, 0xce, 0xe9, 0xba, 0xcc, 0xb7, 0x9c, 0x53, 0xf2, 0xfb, 0x50, 0xe1, 0x44, 0xcb, 0x1f, 0x11, + 0x70, 0xf4, 0x23, 0x82, 0x8b, 0xd3, 0x0e, 0x3a, 0x78, 0x68, 0x6b, 0xc1, 0xe0, 0xca, 0xd1, 0x83, + 0xb9, 0x38, 0x7b, 0x81, 0xdc, 0x81, 0x71, 0xce, 0x62, 0xe8, 0x56, 0x8c, 0xef, 0x66, 0xc2, 0xbe, + 0x10, 0x21, 0x36, 0xf9, 0xa7, 0x12, 0x5c, 0x6c, 0xb3, 0x58, 0xef, 0x5f, 0x40, 0x62, 0x74, 0x71, + 0xca, 0xcb, 0xcb, 0x6d, 0x28, 0x38, 0xc4, 0x15, 0x4f, 0xb9, 0xc3, 0x03, 0x13, 0x17, 0x94, 0x7f, + 0x22, 0xc1, 0xec, 0x23, 0xdd, 0xdc, 0x7b, 0x75, 0xb7, 0xa7, 0x13, 0xdf, 0x76, 0xfe, 0x98, 0x03, + 0xf4, 0x48, 0x77, 0xbd, 0xc7, 0xd8, 0x53, 0x77, 0x49, 0x60, 0x82, 0x7f, 0x87, 0x82, 0xa1, 0x0f, + 0xf4, 0x91, 0xfe, 0xf8, 0x4c, 0x6d, 0xd3, 0xbb, 0xd3, 0x14, 0x8b, 0x61, 0x92, 0xe8, 0x1e, 0x4c, + 0xe2, 0xa1, 0xb7, 0x6b, 0x39, 0x3a, 0x7d, 0x14, 0xef, 0x1f, 0x27, 0x3e, 0x47, 0x07, 0xa0, 0x26, + 0x14, 0x0c, 0xbc, 0x43, 0xf8, 0xa3, 0xb7, 0xd2, 0xbc, 0x98, 0x18, 0xd9, 0xf5, 0x1c, 0xdd, 0xec, + 0xfb, 0x5a, 0xa9, 0x28, 0xfa, 0x2f, 0x28, 0x0d, 0x74, 0xb3, 0xe7, 0xea, 0x9f, 0x13, 0xb1, 0xea, + 0x43, 0xb1, 0x16, 0x07, 0xba, 0xd9, 0xd5, 0x3f, 0x27, 0x6c, 0x1c, 0xfe, 0x8c, 0x8f, 0x2b, 0x1c, + 0x67, 0x1c, 0xfe, 0x8c, 0x8e, 0x93, 0x3f, 0x83, 0x1a, 0x35, 0x57, 0x6a, 0x60, 0x38, 0x85, 0xd1, + 0x6e, 0x41, 0x55, 0xc5, 0xea, 0x2e, 0xc1, 0x3b, 0x06, 0xe9, 0xa9, 0x43, 0xc7, 0xb5, 0x1c, 0xc1, + 0x0d, 0xd3, 0x41, 0xff, 0x2a, 0xeb, 0x96, 0x7f, 0x2b, 0xc1, 0x1c, 0x55, 0x9d, 0x4e, 0xff, 0x6f, + 0x40, 0x91, 0x12, 0xd5, 0xe8, 0x19, 0x3b, 0x4e, 0x9b, 0x89, 0x10, 0x94, 0x4b, 0x84, 0xa0, 0x00, + 0x74, 0xfe, 0xd8, 0xa0, 0x2f, 0xc0, 0xb8, 0x80, 0xca, 0x29, 0x49, 0xb4, 0xe4, 0x5f, 0x4a, 0x50, + 0x60, 0x7e, 0x44, 0x79, 0x69, 0x40, 0x7f, 0x8c, 0xe0, 0x14, 0x59, 0xbb, 0x4d, 0x63, 0x4b, 0x8a, + 0x9b, 0x94, 0x5e, 0x85, 0x2b, 0xd0, 0x47, 0xb6, 0xef, 0x06, 0x05, 0x85, 0xfd, 0x96, 0xdf, 0x83, + 0x32, 0x43, 0x44, 0x0d, 0x87, 0xde, 0x02, 0x8e, 0x82, 0xa4, 0x92, 0x04, 0x93, 0x53, 0x7c, 0x09, + 0xf9, 0xef, 0x12, 0x4c, 0x84, 0x77, 0x39, 0x41, 0xe8, 0x35, 0x28, 0xba, 0x43, 0xb6, 0x09, 0xc2, + 0xaa, 0x7e, 0x93, 0x7e, 0x51, 0x2d, 0xd3, 0x23, 0xa6, 0xe7, 0xc7, 0x70, 0xd1, 0xa4, 0x10, 0x55, + 0x4b, 0x0b, 0x20, 0xd2, 0xdf, 0x68, 0x1e, 0xca, 0x2e, 0x31, 0x35, 0xbe, 0x77, 0x22, 0x3f, 0xc3, + 0x3b, 0x78, 0x7e, 0x26, 0x4c, 0xad, 0xe3, 0x27, 0xa2, 0xd6, 0xcb, 0x00, 0x36, 0xbd, 0x3c, 0xb8, + 0x0c, 0x4a, 0x91, 0xd9, 0x39, 0xd4, 0x23, 0xbf, 0x84, 0x6a, 0x78, 0x85, 0xcc, 0x46, 0xff, 0x0b, + 0x93, 0x66, 0xd8, 0xb7, 0x85, 0xa5, 0x22, 0x17, 0xf3, 0xf0, 0x20, 0x25, 0x2a, 0x7e, 0x12, 0x87, + 0xfe, 0x18, 0x66, 0x15, 0x82, 0xb5, 0xb3, 0xdf, 0x99, 0x42, 0xbe, 0x9f, 0x0f, 0xfb, 0xbe, 0xfc, + 0x0c, 0xe6, 0x12, 0x1a, 0x82, 0x13, 0xf3, 0x3f, 0xc9, 0x0b, 0xd3, 0x95, 0xf0, 0x2a, 0x53, 0xb0, + 0x85, 0xaf, 0x4b, 0xff, 0x0f, 0x79, 0xc5, 0x56, 0xd3, 0xbc, 0xc2, 0xc6, 0x07, 0x86, 0x85, 0xfd, + 0x18, 0xef, 0x37, 0xe9, 0x99, 0xd8, 0xf5, 0x3c, 0xbb, 0x47, 0xc1, 0x0b, 0xb7, 0xa0, 0xed, 0x87, + 0xe4, 0x40, 0xfe, 0x6f, 0x28, 0x76, 0x89, 0x4b, 0x6f, 0x79, 0xe9, 0xa9, 0x19, 0xea, 0x23, 0x43, + 0xcd, 0xee, 0xf1, 0x2f, 0x22, 0x13, 0x35, 0xd4, 0xec, 0x2d, 0x96, 0xb7, 0xf9, 0x2e, 0x07, 0x93, + 0x11, 0xa0, 0xaf, 0xd0, 0x84, 0x14, 0xcf, 0x3e, 0x3d, 0x64, 0xe2, 0xa8, 0xf3, 0x46, 0xf8, 0x96, + 0x5a, 0x88, 0xdc, 0x52, 0xd1, 0x0d, 0x98, 0xb6, 0x89, 0x33, 0xd0, 0xd9, 0x6a, 0x7a, 0x0e, 0xc1, + 0x1a, 0x73, 0xda, 0x82, 0x32, 0x35, 0xea, 0xa6, 0x96, 0xa5, 0x8e, 0x12, 0x12, 0x7c, 0xe1, 0xe8, + 0x1e, 0x61, 0x2e, 0x5a, 0x50, 0x42, 0x13, 0x3c, 0xa5, 0xdd, 0xf1, 0x43, 0x50, 0x3a, 0xcb, 0xfd, + 0xa2, 0x7c, 0xa2, 0xfb, 0xc5, 0x47, 0x50, 0x8d, 0x58, 0x76, 0x45, 0xdd, 0x7b, 0xa5, 0x77, 0xfa, + 0x16, 0xcc, 0xc4, 0xe7, 0x77, 0xd1, 0x6d, 0x18, 0xc3, 0xea, 0x9e, 0xef, 0x93, 0x17, 0xc3, 0x3e, + 0x19, 0x17, 0x56, 0x98, 0xa4, 0xdc, 0x82, 0xa9, 0xa8, 0x8f, 0xd3, 0xb7, 0x3f, 0x77, 0x55, 0x7f, + 0x9a, 0xb9, 0xcc, 0x69, 0x14, 0x5f, 0x52, 0xfe, 0x38, 0x86, 0x86, 0x11, 0xc2, 0x69, 0x66, 0x0a, + 0x45, 0x88, 0x5c, 0x24, 0x42, 0xfc, 0x33, 0x07, 0xe7, 0xb6, 0x99, 0x79, 0xc5, 0x25, 0xc6, 0x3f, + 0x8c, 0xef, 0x85, 0x5e, 0xe8, 0xd2, 0x31, 0x28, 0x7f, 0x94, 0xe2, 0xf9, 0x00, 0x26, 0x34, 0xdd, + 0xb5, 0x0d, 0x7c, 0xd0, 0x63, 0xa3, 0x73, 0xc7, 0x18, 0x5d, 0x11, 0x23, 0x3a, 0x98, 0x39, 0x48, + 0xf8, 0x32, 0x7c, 0x9c, 0x78, 0x13, 0xba, 0x2a, 0xbf, 0x1b, 0xba, 0x80, 0x8f, 0x1d, 0x63, 0x68, + 0x70, 0x3d, 0x7f, 0x0f, 0x4a, 0x86, 0xc5, 0x49, 0x53, 0xdc, 0x3f, 0x8e, 0x58, 0xb0, 0x2f, 0x4d, + 0x47, 0x52, 0x4f, 0xfe, 0xdc, 0x32, 0xfd, 0x78, 0x70, 0xc4, 0x48, 0x5f, 0x5a, 0xfe, 0x7a, 0x0c, + 0xc6, 0xe8, 0x03, 0x28, 0x41, 0x5a, 0xe1, 0xfc, 0x48, 0x2e, 0x96, 0x42, 0xbb, 0x1a, 0xb3, 0x6f, + 0x5e, 0xbc, 0x42, 0x42, 0x16, 0x8c, 0x3e, 0x27, 0xc6, 0xe2, 0xcf, 0x89, 0xc3, 0x1f, 0x29, 0x81, + 0x15, 0xc4, 0x23, 0x25, 0x58, 0x67, 0x3d, 0xb4, 0x4e, 0xfe, 0x44, 0x09, 0xda, 0x91, 0xc7, 0x4d, + 0x29, 0xf6, 0xb8, 0xb9, 0x02, 0x95, 0xd0, 0x2b, 0x8d, 0x1d, 0xf8, 0xb2, 0x02, 0xa3, 0x47, 0x1a, + 0x25, 0x53, 0x6e, 0x2f, 0xfa, 0x19, 0xf8, 0x68, 0xde, 0xd1, 0xd6, 0xd0, 0x35, 0x98, 0xec, 0xe3, + 0x01, 0x51, 0x59, 0xe6, 0x8e, 0x0a, 0x54, 0x78, 0xda, 0x7d, 0xd4, 0xd9, 0x66, 0x54, 0xee, 0x7a, + 0x04, 0xb3, 0x8a, 0xca, 0x84, 0x88, 0xfd, 0xb4, 0xdd, 0xd6, 0xa8, 0xe7, 0x5b, 0xa6, 0xa1, 0x9b, + 0xa4, 0x36, 0xc9, 0xe2, 0xad, 0x68, 0x51, 0x1b, 0x11, 0xad, 0x4f, 0x7a, 0xfc, 0x56, 0x3f, 0xc5, + 0x88, 0xae, 0x4c, 0x7b, 0x56, 0xd3, 0x9e, 0x50, 0xd3, 0x67, 0xa1, 0xb8, 0xea, 0x89, 0x28, 0xae, + 0x01, 0x05, 0xf6, 0x28, 0x46, 0xd7, 0xa1, 0x40, 0x37, 0xdd, 0x3f, 0xe6, 0xc9, 0x57, 0x3f, 0xff, + 0x2c, 0xff, 0x59, 0x02, 0xc4, 0x78, 0xf9, 0xac, 0x31, 0x27, 0x08, 0x2d, 0xf9, 0x8c, 0xd0, 0x32, + 0x76, 0x64, 0x68, 0x29, 0x1c, 0x3b, 0xb4, 0x8c, 0xa7, 0x86, 0x16, 0xf9, 0x09, 0xd4, 0x93, 0x6b, + 0x71, 0x47, 0xac, 0x14, 0xe3, 0xbe, 0xcb, 0x61, 0xa3, 0x24, 0x07, 0x06, 0x04, 0xb8, 0x64, 0xc2, + 0x79, 0xf1, 0x65, 0x33, 0x8a, 0xed, 0x06, 0x5c, 0xeb, 0x6e, 0x6d, 0x28, 0x2b, 0xeb, 0xad, 0xde, + 0x66, 0x4b, 0x79, 0xdc, 0xee, 0x76, 0xdb, 0x1b, 0x9d, 0x9e, 0xd2, 0x5a, 0x59, 0x8b, 0xe5, 0x49, + 0x2a, 0x50, 0xec, 0x6c, 0xb0, 0x0f, 0x55, 0x09, 0x4d, 0x01, 0x6c, 0x3c, 0xed, 0xb4, 0x14, 0xde, + 0xce, 0xa1, 0x69, 0xa8, 0x6c, 0x6e, 0xdf, 0x7f, 0xd4, 0x5e, 0xe5, 0x1d, 0xf9, 0x25, 0x0c, 0x17, + 0x12, 0xfa, 0x78, 0xf0, 0xbc, 0x09, 0x8b, 0x29, 0x0a, 0x9f, 0x2a, 0xed, 0x44, 0x66, 0x66, 0x02, + 0x4a, 0x9d, 0x0d, 0xfe, 0x85, 0xe7, 0x66, 0xb8, 0x4a, 0xde, 0x91, 0x6b, 0x7e, 0x7b, 0x15, 0xc6, + 0x3b, 0x6c, 0xf5, 0xe8, 0x29, 0xc0, 0xa8, 0x52, 0x84, 0xa2, 0xd9, 0xef, 0x78, 0x05, 0xa9, 0x7e, + 0x21, 0xe1, 0x88, 0xad, 0x81, 0xed, 0x1d, 0xc8, 0xe8, 0xab, 0xbf, 0xfe, 0xe3, 0x57, 0xb9, 0x09, + 0x19, 0x1a, 0xfb, 0xcd, 0x06, 0xcf, 0xd2, 0xa0, 0xaf, 0x24, 0x40, 0xc9, 0xea, 0x11, 0x7a, 0x33, + 0xa2, 0x21, 0xab, 0xba, 0x54, 0x9f, 0x8d, 0x44, 0x26, 0x7e, 0xa1, 0x92, 0x6f, 0x33, 0x35, 0x4b, + 0xf2, 0x15, 0xaa, 0x46, 0xbc, 0x94, 0x1b, 0x38, 0x34, 0x47, 0x83, 0x17, 0x43, 0xef, 0x06, 0xcf, + 0xe8, 0x38, 0x08, 0x51, 0xbb, 0xcc, 0x04, 0x11, 0xa9, 0x1f, 0x9d, 0x16, 0x04, 0xaf, 0xd6, 0x8e, + 0x40, 0x7c, 0x09, 0x33, 0x89, 0x02, 0x10, 0x5a, 0xcc, 0x82, 0x10, 0xae, 0x0f, 0xa5, 0x23, 0x68, + 0x30, 0x04, 0xb7, 0xe4, 0xcb, 0x99, 0x08, 0x58, 0xad, 0x75, 0x04, 0xe0, 0x67, 0x12, 0x9c, 0x4b, + 0xab, 0xf0, 0xa0, 0x1b, 0x59, 0x20, 0x62, 0x59, 0x8c, 0x74, 0x1c, 0x4d, 0x86, 0xe3, 0x6d, 0xf9, + 0x6a, 0x26, 0x0e, 0x9f, 0xa9, 0x47, 0x50, 0x7e, 0x21, 0xc1, 0x85, 0xf4, 0x8a, 0x0c, 0xba, 0x95, + 0x05, 0x26, 0x51, 0xb5, 0x49, 0x87, 0xf3, 0x1f, 0x0c, 0xce, 0xb2, 0x7c, 0x2d, 0x13, 0xce, 0x88, + 0xf8, 0xb3, 0x3d, 0x44, 0x14, 0x5f, 0x33, 0x3d, 0x24, 0x52, 0x97, 0x39, 0xad, 0x87, 0xf0, 0x53, + 0x94, 0xe9, 0x21, 0xbc, 0xb2, 0x9b, 0xe9, 0x21, 0xe1, 0xba, 0xcb, 0x69, 0x3d, 0x84, 0xc5, 0xb8, + 0x11, 0x00, 0x0c, 0x13, 0xe1, 0xca, 0x09, 0x8a, 0xbc, 0x9e, 0x52, 0x6a, 0x2a, 0x99, 0x4c, 0x50, + 0x63, 0x9a, 0x91, 0x5c, 0x1d, 0x31, 0x41, 0x63, 0x87, 0x8e, 0x47, 0x3f, 0x82, 0x4a, 0xa8, 0x56, + 0x11, 0xd5, 0x90, 0x52, 0xc4, 0xa8, 0xa3, 0x44, 0xd6, 0xcf, 0x95, 0xcf, 0xb1, 0xd9, 0xa7, 0xe4, + 0x32, 0x9d, 0x9d, 0xa5, 0x00, 0xef, 0x4a, 0x4b, 0xe8, 0x43, 0x98, 0x8c, 0x94, 0x0a, 0xd0, 0x42, + 0x32, 0x59, 0x7e, 0x32, 0x22, 0x5b, 0x0a, 0x13, 0x99, 0x05, 0xb3, 0x29, 0x15, 0x04, 0x74, 0x3d, + 0xa9, 0x24, 0x2d, 0x93, 0x74, 0x94, 0xa5, 0x96, 0x98, 0xa5, 0xc2, 0x0f, 0x6f, 0x64, 0xf8, 0xa5, + 0x8f, 0xd8, 0x43, 0xe0, 0xc6, 0x11, 0x35, 0x80, 0x23, 0x55, 0xce, 0x32, 0x95, 0x93, 0x4b, 0x15, + 0xaa, 0xd2, 0xe5, 0x43, 0x51, 0x07, 0x60, 0x9d, 0x78, 0xfe, 0xbf, 0xba, 0x64, 0x0c, 0x8d, 0xba, + 0x99, 0x10, 0xf6, 0xe7, 0x43, 0x95, 0x90, 0x9b, 0xa1, 0x47, 0x50, 0xf2, 0x93, 0xf3, 0x28, 0x92, + 0xeb, 0x8c, 0xa5, 0xec, 0xeb, 0x33, 0xf1, 0x5b, 0x89, 0x2b, 0x57, 0xd9, 0x84, 0x80, 0x4a, 0x74, + 0x42, 0x56, 0x98, 0xe8, 0x42, 0xe5, 0xff, 0x08, 0x36, 0xbc, 0x5d, 0x75, 0x97, 0xa8, 0x7b, 0x99, + 0xf0, 0xb2, 0x56, 0x2c, 0x1c, 0x06, 0x4d, 0x34, 0x76, 0x43, 0xb3, 0x7c, 0x09, 0xe7, 0x53, 0x93, + 0xc6, 0x28, 0x52, 0x59, 0x3b, 0x2c, 0xaf, 0x9c, 0xa9, 0x70, 0x91, 0x29, 0xbc, 0x2c, 0xcf, 0x86, + 0xfc, 0x3f, 0xc9, 0x82, 0x2a, 0xc0, 0x23, 0xdd, 0xdc, 0x13, 0x21, 0x31, 0xfb, 0x1f, 0x27, 0x32, + 0xd5, 0xc8, 0x4c, 0xcd, 0x45, 0xf9, 0x8d, 0xf0, 0x01, 0x37, 0x74, 0x73, 0xcf, 0x8f, 0x80, 0xd2, + 0x92, 0xaf, 0x44, 0x84, 0xbc, 0xec, 0x7f, 0x7d, 0x38, 0x85, 0x12, 0x11, 0xe1, 0xa4, 0x25, 0xf4, + 0x31, 0x94, 0xa9, 0x12, 0x1e, 0xd3, 0x32, 0xff, 0x79, 0x21, 0x53, 0xc5, 0x55, 0xa6, 0x62, 0x5e, + 0xbe, 0x90, 0x50, 0xc1, 0x43, 0x98, 0xb4, 0x84, 0x5c, 0x98, 0x08, 0xe7, 0xd5, 0xa3, 0xc4, 0x91, + 0x92, 0x71, 0xcf, 0xd4, 0xb5, 0xc4, 0x74, 0x2d, 0xca, 0x73, 0x09, 0x5d, 0xc9, 0x0d, 0xb2, 0x60, + 0x8a, 0x4e, 0x1d, 0x8a, 0x4e, 0x87, 0xff, 0x5f, 0x40, 0xa6, 0xd2, 0xeb, 0x4c, 0xe9, 0x82, 0x3c, + 0x9f, 0x50, 0x1a, 0x0a, 0x46, 0xa3, 0xcd, 0x12, 0xd1, 0x27, 0xbb, 0xba, 0x7f, 0x8a, 0xcd, 0x12, + 0xc1, 0x66, 0xb4, 0x59, 0x3c, 0xbc, 0x64, 0xd6, 0xe7, 0x4f, 0xb1, 0x59, 0x3c, 0x9a, 0x48, 0x4b, + 0xa8, 0x03, 0x95, 0x47, 0xba, 0xeb, 0xf9, 0xe7, 0xe9, 0x58, 0x6c, 0x22, 0x84, 0x7d, 0xee, 0x45, + 0x61, 0xee, 0x7d, 0xca, 0xe7, 0x13, 0x15, 0x0d, 0x74, 0x39, 0xba, 0xf7, 0xf1, 0x52, 0x47, 0xfd, + 0x7c, 0x22, 0x09, 0x4c, 0x85, 0xe4, 0x19, 0x36, 0x73, 0x05, 0xb1, 0xb0, 0xc1, 0x92, 0xc2, 0xe8, + 0x53, 0x98, 0x49, 0xe4, 0xfe, 0xa3, 0x11, 0x37, 0xab, 0x34, 0x50, 0xbf, 0x98, 0x95, 0x3f, 0x65, + 0xba, 0x04, 0xad, 0xa3, 0x24, 0xad, 0x7f, 0x23, 0xf1, 0xf2, 0x4c, 0x8c, 0xd5, 0xdf, 0x8c, 0x2b, + 0x4d, 0xe7, 0xf4, 0x4b, 0x99, 0xa9, 0x1a, 0xa6, 0xf6, 0x01, 0x53, 0x7b, 0x0f, 0xd5, 0x42, 0xd4, + 0xde, 0xf8, 0x62, 0xf4, 0x66, 0x7b, 0xf9, 0x6c, 0x11, 0xc9, 0x59, 0xdf, 0x1a, 0x5f, 0x88, 0x84, + 0xe1, 0x4b, 0x64, 0x01, 0x4a, 0xa6, 0x59, 0xa3, 0x18, 0x33, 0xd3, 0xb0, 0xf5, 0x7a, 0x26, 0x46, + 0x57, 0xbe, 0xc0, 0x00, 0x56, 0xe5, 0x70, 0xec, 0xa1, 0x1e, 0xf3, 0x0c, 0x8a, 0x8a, 0xad, 0x3e, + 0x18, 0x9a, 0x2a, 0x9a, 0x8e, 0x68, 0xb1, 0xd5, 0x7a, 0xbc, 0x43, 0x7e, 0x87, 0x4d, 0x72, 0x43, + 0x9e, 0xa0, 0x93, 0x38, 0xb6, 0xda, 0xf8, 0x42, 0xd7, 0x5e, 0xde, 0xf5, 0xd3, 0xb2, 0xcf, 0x28, + 0xcf, 0x87, 0x3e, 0xa0, 0x3e, 0x4c, 0x6c, 0xd3, 0x27, 0xfb, 0x19, 0x88, 0xd6, 0xe7, 0xf3, 0x08, + 0x69, 0x0c, 0xcd, 0x18, 0xd5, 0x06, 0x8a, 0x4e, 0x4f, 0xb6, 0x87, 0x29, 0x1a, 0xd1, 0xad, 0x06, + 0x15, 0xae, 0xe8, 0xb4, 0x84, 0x7b, 0x8d, 0xa9, 0xb9, 0x24, 0xd7, 0x52, 0xd4, 0x04, 0x94, 0x3b, + 0x80, 0x29, 0xae, 0x25, 0x20, 0xdd, 0xc3, 0x8a, 0x96, 0x27, 0xe3, 0x3e, 0xa1, 0x2b, 0xa0, 0x5c, + 0xc6, 0xf0, 0x55, 0xae, 0xee, 0xec, 0x74, 0x7b, 0x93, 0xa9, 0x94, 0xe5, 0x4b, 0x29, 0x2a, 0xa3, + 0x84, 0x1b, 0x6c, 0xd9, 0xe9, 0x29, 0xf7, 0xb0, 0x2d, 0x1b, 0x91, 0x6e, 0xb0, 0x65, 0xa7, 0xa5, + 0xdd, 0xc3, 0xb6, 0x2c, 0x20, 0x5e, 0x0c, 0x93, 0x91, 0x64, 0x6c, 0xf4, 0x0e, 0x9c, 0x96, 0xa7, + 0xcd, 0xd4, 0x27, 0x4e, 0x6a, 0x3d, 0x7c, 0xab, 0xa3, 0x2a, 0x3c, 0x98, 0x4d, 0xc9, 0xaf, 0x44, + 0xef, 0xc1, 0xd9, 0x09, 0x98, 0x43, 0x08, 0x6c, 0x45, 0xdd, 0x73, 0xa3, 0x5a, 0x47, 0xfc, 0x70, + 0xff, 0xd7, 0xb9, 0xaf, 0x57, 0xfe, 0x26, 0xa1, 0x21, 0x4c, 0xf2, 0x84, 0xc5, 0xc2, 0xca, 0x66, + 0x7b, 0x61, 0xbf, 0x29, 0xf7, 0xe0, 0xea, 0xd6, 0x2e, 0x59, 0xf0, 0x3b, 0x59, 0xc5, 0xd1, 0x5d, + 0xb8, 0xbe, 0xb0, 0x6a, 0x99, 0x9e, 0xa3, 0xef, 0x0c, 0x3d, 0xcb, 0x71, 0xd1, 0xe2, 0xae, 0xe7, + 0xd9, 0xee, 0xdd, 0x46, 0xa3, 0xaf, 0x7b, 0xbb, 0xc3, 0x9d, 0x65, 0xd5, 0x1a, 0x34, 0x76, 0x89, + 0x63, 0xe9, 0xaa, 0x81, 0x77, 0xdc, 0x06, 0x07, 0x54, 0x3f, 0xb7, 0x4b, 0x0c, 0xc3, 0xba, 0x37, + 0xfa, 0x40, 0xe5, 0x9a, 0xf9, 0xe6, 0xf2, 0xed, 0x25, 0x49, 0x6a, 0x56, 0xb1, 0x6d, 0x1b, 0x82, + 0xc3, 0x1b, 0x9f, 0xb8, 0x96, 0x79, 0x37, 0xd1, 0xe3, 0xdc, 0x85, 0x79, 0x01, 0xc4, 0x25, 0xce, + 0x3e, 0x71, 0x16, 0x34, 0x4b, 0x1d, 0x0e, 0x88, 0xc9, 0xff, 0xa5, 0x1c, 0xcd, 0xfb, 0x30, 0xa2, + 0x2a, 0x1a, 0x9a, 0xa5, 0xba, 0x30, 0xa7, 0x5a, 0x83, 0xe5, 0xd0, 0x87, 0x91, 0x95, 0xee, 0x97, + 0xf9, 0xa4, 0x2b, 0xb6, 0xbe, 0x29, 0x3d, 0xcb, 0x63, 0x5b, 0xff, 0x26, 0x37, 0xd6, 0x79, 0xb8, + 0x79, 0xff, 0x0f, 0x39, 0x91, 0xbd, 0xd9, 0x19, 0x67, 0x7b, 0x77, 0xe7, 0x5f, 0x01, 0x00, 0x00, + 0xff, 0xff, 0xda, 0xc5, 0xa7, 0xce, 0x73, 0x2f, 0x00, 0x00, } diff --git a/api/api.pb.gw.go b/api/api.pb.gw.go index dade3ab8211ed0da5cf5cb58d868883ad8ec4d3d..5de5234e81c61730f746d9242fa94c330cffa558 100644 --- a/api/api.pb.gw.go +++ b/api/api.pb.gw.go @@ -470,6 +470,23 @@ func request_Nakama_ListFriends_0(ctx context.Context, marshaler runtime.Marshal } +var ( + filter_Nakama_ListMatches_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_Nakama_ListMatches_0(ctx context.Context, marshaler runtime.Marshaler, client NakamaClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListMatchesRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_Nakama_ListMatches_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.ListMatches(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + var ( filter_Nakama_ListNotifications_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) @@ -1557,6 +1574,35 @@ func RegisterNakamaHandlerClient(ctx context.Context, mux *runtime.ServeMux, cli }) + mux.Handle("GET", pattern_Nakama_ListMatches_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + if cn, ok := w.(http.CloseNotifier); ok { + go func(done <-chan struct{}, closed <-chan bool) { + select { + case <-done: + case <-closed: + cancel() + } + }(ctx.Done(), cn.CloseNotify()) + } + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Nakama_ListMatches_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Nakama_ListMatches_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Nakama_ListNotifications_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -2046,6 +2092,8 @@ var ( pattern_Nakama_ListFriends_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v2", "friend"}, "")) + pattern_Nakama_ListMatches_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v2", "match"}, "")) + pattern_Nakama_ListNotifications_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v2", "notification"}, "")) pattern_Nakama_ListStorageObjects_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v2", "storage", "collection"}, "")) @@ -2128,6 +2176,8 @@ var ( forward_Nakama_ListFriends_0 = runtime.ForwardResponseMessage + forward_Nakama_ListMatches_0 = runtime.ForwardResponseMessage + forward_Nakama_ListNotifications_0 = runtime.ForwardResponseMessage forward_Nakama_ListStorageObjects_0 = runtime.ForwardResponseMessage diff --git a/api/api.proto b/api/api.proto index 9b7cdfbdc7801fcd4d75a3e33ea393ef319aa069..865e78475afbbd8165df4b8287f66c009c8af34e 100644 --- a/api/api.proto +++ b/api/api.proto @@ -260,6 +260,11 @@ service Nakama { option (google.api.http).get = "/v2/friend"; } + // Fetch list of running matches. + rpc ListMatches (ListMatchesRequest) returns (MatchList) { + option (google.api.http).get = "/v2/match"; + } + // Fetch list of notifications. rpc ListNotifications (ListNotificationsRequest) returns (NotificationList) { option (google.api.http).get = "/v2/notification"; @@ -685,6 +690,19 @@ message LinkFacebookRequest { google.protobuf.BoolValue import = 4; } +message ListMatchesRequest { + // Limit the number of returned matches. + google.protobuf.Int32Value limit = 1; + // Authoritative or relayed matches. + google.protobuf.BoolValue authoritative = 2; + // Label filter. + google.protobuf.StringValue label = 3; + // Minimum user count. + google.protobuf.Int32Value min_size = 4; + // Maximum user count. + google.protobuf.Int32Value max_size = 5; +} + // Get a list of unexpired notifications. message ListNotificationsRequest { // The number of notifications to get. Between 1 and 100. @@ -705,6 +723,24 @@ message ListStorageObjectsRequest { string cursor = 4; // value from StorageObjectList.cursor. } +// Represents a realtime match. +message Match { + // The ID of the match, can be used to join. + string match_id = 1; + // True if it's an server-managed authoritative match, false otherwise. + bool authoritative = 2; + // Match label, if any. + google.protobuf.StringValue label = 3; + // Current number of users in the match. + int32 size = 4; +} + +// A list of realtime matches. +message MatchList { + // A number of matches corresponding to a list operation. + repeated Match matches = 1; +} + // A notification in the server. message Notification { // ID of the Notification. diff --git a/api/api.swagger.json b/api/api.swagger.json index b92ae526b7e52bf80cf1f32ca08c9089f7f64e6d..e016944fa603620653f31787607d8a2830d0a351 100644 --- a/api/api.swagger.json +++ b/api/api.swagger.json @@ -763,6 +763,64 @@ ] } }, + "/v2/match": { + "get": { + "summary": "Fetch list of running matches.", + "operationId": "ListMatches", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/apiMatchList" + } + } + }, + "parameters": [ + { + "name": "limit", + "description": "Limit the number of returned matches.", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "authoritative", + "description": "Authoritative or relayed matches.", + "in": "query", + "required": false, + "type": "boolean", + "format": "boolean" + }, + { + "name": "label", + "description": "Label filter.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "min_size", + "description": "Minimum user count.", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "max_size", + "description": "Maximum user count.", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ], + "tags": [ + "Nakama" + ] + } + }, "/v2/notification": { "get": { "summary": "Fetch list of notifications.", @@ -1379,6 +1437,43 @@ }, "description": "A collection of zero or more groups." }, + "apiMatch": { + "type": "object", + "properties": { + "match_id": { + "type": "string", + "description": "The ID of the match, can be used to join." + }, + "authoritative": { + "type": "boolean", + "format": "boolean", + "description": "True if it's an server-managed authoritative match, false otherwise." + }, + "label": { + "type": "string", + "description": "Match label, if any." + }, + "size": { + "type": "integer", + "format": "int32", + "description": "Current number of users in the match." + } + }, + "description": "Represents a realtime match." + }, + "apiMatchList": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/definitions/apiMatch" + }, + "description": "A number of matches corresponding to a list operation." + } + }, + "description": "A list of realtime matches." + }, "apiNotification": { "type": "object", "properties": { diff --git a/data/modules/clientrpc.lua b/data/modules/clientrpc.lua index d7458966ff4876310f1bf7bb10c41000221adad2..e1c6c37f5f1749f90050edef989334bda60ab7ed 100644 --- a/data/modules/clientrpc.lua +++ b/data/modules/clientrpc.lua @@ -63,3 +63,9 @@ local function send_stream_data(context, payload) nk.stream_send(stream, tostring(payload)) end nk.register_rpc(send_stream_data, "clientrpc.send_stream_data") + +local function create_authoritative_match(_context, _payload) + local match_id = nk.match_create("match", {}) + return nk.json_encode({ match_id = match_id }) +end +nk.register_rpc(create_authoritative_match, "clientrpc.create_authoritative_match") diff --git a/data/modules/debug_utils.lua b/data/modules/debug_utils.lua new file mode 100644 index 0000000000000000000000000000000000000000..66e42ddcbfcd6f958559a4baaf97dbb74023beae --- /dev/null +++ b/data/modules/debug_utils.lua @@ -0,0 +1,45 @@ +--[[ + Copyright 2018 The Nakama Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--]] + +local function print_r(arr, indentLevel) + if type(arr) ~= "table" then + return tostring(arr) + end + + local str = "" + local indentStr = "#" + + if(indentLevel == nil) then + return print_r(arr, 0) + end + + for i = 0, indentLevel do + indentStr = indentStr.."\t" + end + + for index,Value in pairs(arr) do + if type(Value) == "table" then + str = str..indentStr..index..": \n"..print_r(Value, (indentLevel + 1)) + else + str = str..indentStr..index..": "..tostring(Value).."\n" + end + end + return str +end + +return { + print_r = print_r +} diff --git a/data/modules/match.lua b/data/modules/match.lua new file mode 100644 index 0000000000000000000000000000000000000000..b02832834a16ad77a4352eaed8107eb19df3fb36 --- /dev/null +++ b/data/modules/match.lua @@ -0,0 +1,211 @@ +--[[ + Copyright 2018 The Nakama Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--]] + +local du = require("debug_utils") + +--[[ +Called when a match is created as a result of nk.match_create(). + +Context represents information about the match and server, for information purposes. Format: +{ + Env = {}, -- key-value data set in the runtime.env server configuration. + ExecutionMode = "Match", + MatchId = "client-friendly match ID, can be shared with clients and used in match join operations", + MatchNode = "name of the Nakama node hosting this match" +} + +Params is the optional arbitrary second argument passed to `nk.match_create()`, or `nil` if none was used. + +Expected return these values (all required) in order: +1. The initial in-memory state of the match. May be any non-nil Lua term, or nil to end the match. +2. Tick rate representing the desired number of match loop calls per second. Must be between 1 and 30, inclusive. +3. A string label that can be used to filter matches in listing operations. Must be between 0 and 256 characters long. +--]] +local function match_init(context, params) + local state = { + debug = (params and params.debug) or false + } + if state.debug then + print("match init context:\n" .. du.print_r(context) .. "match init params:\n" .. du.print_r(params)) + end + local tick_rate = 1 + local label = "skill=100-150" + + return state, tick_rate, label +end + +--[[ +Called when a user attempts to join the match using the client's match join operation. + +Context represents information about the match and server, for information purposes. Format: +{ + Env = {}, -- key-value data set in the runtime.env server configuration. + ExecutionMode = "Match", + MatchId = "client-friendly match ID, can be shared with clients and used in match join operations", + MatchNode = "name of the Nakama node hosting this match", + MatchLabel = "the label string returned from match_init", + MatchTickrate = 1 -- the tick rate returned by match_init +} + +Dispatcher exposes useful functions to the match. Format: +{ + broadcast_message = function(op_code, data, presences, sender), + -- numeric message op code + -- a data payload string, or nil + -- list of presences (a subset of match participants) to use as message targets, or nil to send to the whole match + -- a presence to tag on the message as the 'sender', or nil + match_kick = function(presences) + -- a list of presences to remove from the match +} + +Tick is the current match tick number, starts at 0 and increments after every match_loop call. Does not increment with +calls to match_join_attempt or match_leave. + +State is the current in-memory match state, may be any Lua term except nil. + +Presence is the user attempting to join the match. Format: +{ + UserId: "user unique ID", + SessionId: "session ID of the user's current connection", + Username: "user's unique username", + Node: "name of the Nakama node the user is connected to" +} + +Expected return these values (all required) in order: +1. An (optionally) updated state. May be any non-nil Lua term, or nil to end the match. +2. Boolean true if the join attempt should be allowed, false otherwise. +--]] +local function match_join_attempt(context, dispatcher, tick, state, presence) + if state.debug then + print("match join attempt:\n" .. du.print_r(presence)) + end + return state, true +end + +--[[ +Called when one or more users have left the match for any reason, including connection loss. + +Context represents information about the match and server, for information purposes. Format: +{ + Env = {}, -- key-value data set in the runtime.env server configuration. + ExecutionMode = "Match", + MatchId = "client-friendly match ID, can be shared with clients and used in match join operations", + MatchNode = "name of the Nakama node hosting this match", + MatchLabel = "the label string returned from match_init", + MatchTickrate = 1 -- the tick rate returned by match_init +} + +Dispatcher exposes useful functions to the match. Format: +{ + broadcast_message = function(op_code, data, presences, sender), + -- numeric message op code + -- a data payload string, or nil + -- list of presences (a subset of match participants) to use as message targets, or nil to send to the whole match + -- a presence to tag on the message as the 'sender', or nil + match_kick = function(presences) + -- a list of presences to remove from the match +} + +Tick is the current match tick number, starts at 0 and increments after every match_loop call. Does not increment with +calls to match_join_attempt or match_leave. + +State is the current in-memory match state, may be any Lua term except nil. + +Presences is a list of users that have left the match. Format: +{ + { + UserId: "user unique ID", + SessionId: "session ID of the user's current connection", + Username: "user's unique username", + Node: "name of the Nakama node the user is connected to" + }, + ... +} + +Expected return these values (all required) in order: +1. An (optionally) updated state. May be any non-nil Lua term, or nil to end the match. +--]] +local function match_leave(context, dispatcher, tick, state, presences) + if state.debug then + print("match leave:\n" .. du.print_r(presences)) + end + return state +end + +--[[ +Called on an interval based on the tick rate returned by match_init. + +Context represents information about the match and server, for information purposes. Format: +{ + Env = {}, -- key-value data set in the runtime.env server configuration. + ExecutionMode = "Match", + MatchId = "client-friendly match ID, can be shared with clients and used in match join operations", + MatchNode = "name of the Nakama node hosting this match", + MatchLabel = "the label string returned from match_init", + MatchTickrate = 1 -- the tick rate returned by match_init +} + +Dispatcher exposes useful functions to the match. Format: +{ + broadcast_message = function(op_code, data, presences, sender), + -- numeric message op code + -- a data payload string, or nil + -- list of presences (a subset of match participants) to use as message targets, or nil to send to the whole match + -- a presence to tag on the message as the 'sender', or nil + match_kick = function(presences) + -- a list of presences to remove from the match +} + +Tick is the current match tick number, starts at 0 and increments after every match_loop call. Does not increment with +calls to match_join_attempt or match_leave. + +State is the current in-memory match state, may be any Lua term except nil. + +Messages is a list of data messages received from users between the previous and current ticks. Format: +{ + { + Sender = { + UserId: "user unique ID", + SessionId: "session ID of the user's current connection", + Username: "user's unique username", + Node: "name of the Nakama node the user is connected to" + }, + OpCode = 1, -- numeric op code set by the sender. + Data = "any string data set by the sender" -- may be nil. + }, + ... +} + +Expected return these values (all required) in order: +1. An (optionally) updated state. May be any non-nil Lua term, or nil to end the match. +--]] +local function match_loop(context, dispatcher, tick, state, messages) + if state.debug then + print("match " .. context.MatchId .. " tick " .. tick) + print("match " .. context.MatchId .. " messages:\n" .. du.print_r(messages)) + end + if tick < 180 then + return state + end +end + +-- Match modules must return a table with these functions defined. All functions are required. +return { + match_init = match_init, + match_join_attempt = match_join_attempt, + match_leave = match_leave, + match_loop = match_loop +} diff --git a/data/modules/match_init.lua b/data/modules/match_init.lua new file mode 100644 index 0000000000000000000000000000000000000000..400486798a565ba40fee74049fd2681e3c12609a --- /dev/null +++ b/data/modules/match_init.lua @@ -0,0 +1,17 @@ +--[[ + Copyright 2018 The Nakama Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--]] + +--require("nakama").match_create("match", {debug = true}) diff --git a/main.go b/main.go index 326cf9dca2034f640eb2a8c9d516540948faafc6..b23c857d9c932d05e3b7f1df52e08eb7e6de6648 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ import ( "github.com/heroiclabs/nakama/social" _ "github.com/lib/pq" "go.uber.org/zap" + "sync" ) var ( @@ -74,28 +75,40 @@ func main() { jsonLogger, multiLogger := server.SetupLogging(config) multiLogger.Info("Nakama starting") - multiLogger.Info("Node", zap.String("name", config.GetName()), zap.String("version", semver), zap.String("runtime", runtime.Version())) + multiLogger.Info("Node", zap.String("name", config.GetName()), zap.String("version", semver), zap.String("runtime", runtime.Version()), zap.Int("cpu", runtime.NumCPU())) multiLogger.Info("Data directory", zap.String("path", config.GetDataDir())) multiLogger.Info("Database connections", zap.Strings("dsns", config.GetDatabase().Addresses)) db, dbVersion := dbConnect(multiLogger, config) multiLogger.Info("Database information", zap.String("version", dbVersion)) - // Check migration status and log if the schema has diverged. + // Check migration status and fail fast if the schema has diverged. migrate.StartupCheck(multiLogger, db) + // Access to social provider integrations. socialClient := social.NewClient(5 * time.Second) + // Used to govern once-per-server-start executions in all Lua runtime instances, across both pooled and match VMs. + once := &sync.Once{} // Start up server components. - registry := server.NewSessionRegistry() - tracker := server.StartLocalTracker(jsonLogger, registry, jsonpbMarshaler, config.GetName()) - router := server.NewLocalMessageRouter(registry, tracker, jsonpbMarshaler) - runtimePool, err := server.NewRuntimePool(jsonLogger, multiLogger, db, config, socialClient, registry, tracker, router) + sessionRegistry := server.NewSessionRegistry() + tracker := server.StartLocalTracker(jsonLogger, sessionRegistry, jsonpbMarshaler, config.GetName()) + router := server.NewLocalMessageRouter(sessionRegistry, tracker, jsonpbMarshaler) + stdLibs, modules, err := server.LoadRuntimeModules(jsonLogger, multiLogger, config) + if err != nil { + multiLogger.Fatal("Failed reading runtime modules", zap.Error(err)) + } + matchRegistry := server.NewLocalMatchRegistry(jsonLogger, db, config, socialClient, sessionRegistry, tracker, router, stdLibs, once, config.GetName()) + tracker.SetMatchLeaveListener(matchRegistry.Leave) + // Separate module evaluation/validation from module loading. + // We need the match registry to be available to wire all functions exposed to the runtime, which in turn needs the modules at least cached first. + regRPC, err := server.ValidateRuntimeModules(jsonLogger, multiLogger, db, config, socialClient, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, once) if err != nil { multiLogger.Fatal("Failed initializing runtime modules", zap.Error(err)) } - pipeline := server.NewPipeline(config, db, registry, tracker, router, runtimePool) - apiServer := server.StartApiServer(jsonLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, registry, tracker, router, pipeline, runtimePool) + runtimePool := server.NewRuntimePool(jsonLogger, multiLogger, db, config, socialClient, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, regRPC, once) + pipeline := server.NewPipeline(config, db, sessionRegistry, matchRegistry, tracker, router, runtimePool) + apiServer := server.StartApiServer(jsonLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, sessionRegistry, matchRegistry, tracker, router, pipeline, runtimePool) // Respect OS stop signals. c := make(chan os.Signal, 2) @@ -109,8 +122,9 @@ func main() { // Gracefully stop server components. apiServer.Stop() + matchRegistry.Stop() tracker.Stop() - registry.Stop() + sessionRegistry.Stop() os.Exit(0) } diff --git a/migrate/migrate-packr.go b/migrate/migrate-packr.go index faa91fd320f21e2219ace8f792982b413a363b4b..78312be840714e92d2182712e01dd610f9d95f49 100644 --- a/migrate/migrate-packr.go +++ b/migrate/migrate-packr.go @@ -9,5 +9,5 @@ import "github.com/gobuffalo/packr" // Go binary. You can use the "packr clean" command to clean up this, // and any other packr generated files. func init() { - packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"H4sIAAAAAAAA/6RX23ObzhV+119xxg+19Cu62EmmmbjNDJZwQqMgV6AkftKs4Ai2hl2yu0hRO/3fO8tFAqxL3PJkw7ffuX17ztHwjw78AWOe7gQNIwW3o5v34EUIDnkmCQEzUxEXsgM5bkp9ZBIDyFiAAlSEYKbEj7D6YsA3FJJyBreDEXQ14Kr8dNW70xQ7nkFCdsC4gkwiqIhKWNMYAX/5mCqgDHyepDElzEfYUhXldkqWgeZ4Kjn4ShHKgIDP0x3wdR0IRJVOR0qlH4bD7XY7ILmzAy7CYVzA5HBqjy3Htfq3g1F5YMFilBIE/syowABWOyBpGlOfrGKEmGyBCyChQAxAce3wVlBFWWiA5Gu1JQI1TUClEnSVqUa+KveobAA4A8LgynTBdq/g3nRt19Ak323v82zhwXdzPjcdz7ZcmM1hPHMmtmfPHBdmD2A6T/DFdiYGIFURCsBfqdARcAFUZxKDPG0uYsOFNS9ckin6dE19iAkLMxIihHyDglEWQooioVJXVAJhgaaJaUIVUfmrF3FpQ8NOp9+HPyc0FEQhLNLOeG6ZngWeeT+1wH4AZ+aB9cN2PVdrQEjodgAAHuf2V3P+BF+sJ+jSoGd08tc0gNqzWNiTw3+ayVlMp0aO1GSMJFh8+2bOx5/Neffm9n0PdM5cb27ajlfYXFbg5TPuYOHY/1hYLbqAyjQmu2VBWdHdvnvXK76TDVFELDMR180dvvf7ufjkh+FQcR7LAUW1ztUXqSQervz07V9yoE78UpGw5bd2GybWg7mYenCN7Lqgjbmfp7+Jzs1qkzgIB3DlEgYPgjCfSp8bMDav8rOKJvgvzvDs2UdS6MGjCUJ34cKfYEwYCUivIElQkYAoUpD83Z059/uC7N3993+uW+nckjhGVQF/+xgmhMZ7YN1lKMtW4FIi5ZaLUiz3T55l7k+NP1vjL9CNkYUq6lbIHvwV3tyORqOyXmvi44rz52WuuKZ86pZCzsMYl6Uuz+BIgj4yhUJjT+OkQpJUdGdwfiYVTy7bxSDEpc8zlidbKx5eJHpU5aQG/vg3GPVa2fcFEoVLrRudVftTja5kqEM+vmTI0uASQx1yhGGDgq53ZxnqkCKMQ6AvLrXu4yXbUbIG5BRbp3fXudTYlgFuqI8n25t++zCbW/Ynp3ibH6JBD+bWgzW3nLFVdsgcr2+oN5vMPsDMgYk1tTwLxqY7NidWq1U2tPGyR2oJNfroq4LSijkWkuSZ8PW1MEAqotCAlEuqO9WxUPfo/yXYF2wBSkVZ3hb/n/ztnSrSss9RM4VVWGXy9hrao7Shhd6T9IiVXCjgQg9KzkDwrTxyLQ4kv3clmvGecjQvw+Huu1/N6VRbOXI3tMtrQZEF3VHPAMo2VGH3Zv9n0L3tGbCKuf+MQfdNz4AAY9Tv3/YMIMKP6AaD7rtekchyntY10SrRJakxrvQQKibdEbWVOjYaHcp0xwZUpX4UuEahNzhdAr3NFFWQEc/iACKyQVghMphY7hhWmYIx958FJ350LeFnhmIHhJF4J1HofU2vrH6MG71hMZ6F0eDIetJeUBqLRz2mJQ1Obx7VJT3F2Spztvon+vvJ2pqRbbTPmUJ2QDcH8dlR7PMA627sBdV2q/i33wcHQ6LoBmFD4gwlEIEgd1JhAgIlig0GRQioN8lDyDpgA04+mnkxnRb3q6BLUEoSojwytdpt/vLkuiROqbggx7ugz+MYfV1hAwSSwIBn3BlVRXun61uluDrerGUx5du11AKqP+fReQ1q6FdUflP+rKue5nrVNqTjPqeTytBNVYj8wLH1Q/+ywtcwFQcubzKv18SxbeYky5n2XeuPtSZWU80z7hrd0XYm1o9jg5gGS5245eGw7ihLGvzSA26v0b2RQo4HtLbR78OUSgXEF1zKYlgOzlk+ZrHypm25bbB1F+7O2amZyE2es3Phzt01f5BO+JZ1JvPZ4+FqN6/13fGv9e59ArLfj859L5bCMwh51/lvAAAA//8SHZVZmBEAAA==\"") - } + packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"H4sIAAAAAAAA/7RX33PithZ+5684k4cbvNcBwm7m7mxud8YBZ9ddYlJsdjd9YYR9sNXYkisJCO30f+/IP8AGEtJOy1MiffrOD30657j7pgVvYMCzjaBRrKDfu3wPfozgkkeSErCWKuZCtiDHjWiATGIISxaiABUjWBkJYqx2TPiKQlLOoN/pQVsDzsqtM+NaU2z4ElKyAcYVLCWCiqmEBU0Q8CnATAFlEPA0SyhhAcKaqji3U7J0NMdDycHnilAGBAKebYAv6kAgqnQ6Vir70O2u1+sOyZ3tcBF1kwImuyNnYLuefdHv9MoDU5aglCDw1yUVGMJ8AyTLEhqQeYKQkDVwASQSiCEorh1eC6ooi0yQfKHWRKCmCalUgs6XqpGvyj0qGwDOgDA4szxwvDO4sTzHMzXJN8f/PJ768M2aTCzXd2wPxhMYjN2h4ztj14PxLVjuA3xx3KEJSFWMAvApEzoCLoDqTGKYp81DbLiw4IVLMsOALmgACWHRkkQIEV+hYJRFkKFIqdQ3KoGwUNMkNKWKqHzpIC5tqNtqXVzAf1MaCaIQpllrMLEt3wbfuhnZ4NyCO/bB/u54vqc1ICS0WwAA9xPnzpo8wBf7Ado0NMxWvkxDqP2mU2e4+08zudPRyMyRmoyRFIu9r9Zk8NmatC/77w3QOfP8ieW4fmFzVoFnj7iBqev8NLX36EIqs4RsZgVlRde/ujKKfbIiiojZUiR1c7v9i4tcfPJDt6s4T2SHolrk6otVmnTnQfbufzlQJ36mSLTnt3YbhvatNR35cI7svKBNeJCnv4nOzWqT2Ik6cOYRBreCsIDKgJswsM7ys4qm+Btn+OLZe1LowacpQnvqwX9gQBgJiVGQpKhISBQpSH70xu7N9kK27v7+x/leOtckSVBVwFcfw5TQZAusuwzltRW4jEi55qIUy82Db1vbU4PP9uALtBNkkYrbFdKA/8Pbfq/XK+9rQQKcc/44yxXXlE/dUsR5lOCs1OULOJJigEyh0NjncVIhSSu6F3DBUiqenraLYYSzgC9ZnmyteDhIdK/KSQ388QfoGXvZDwQShTOtG51V51ONrmSoQz4eMiyz8BRDHXKEYYWCLjbHGQ7iqYOPBRRSqet4yXaCrAFusrWM69apwjYLcUUDfLa86dXb8cR2PrnFan6IhgZM7Ft7YrsDe1sh9erYhaE9sn0bBpY3sIb2XoVsSOKwNGrlNMrnX4pFC+VYJJIvRaBfgwlSEYUmZFxSXaCORbhFG4ULrw70gClEqSjLK+HfSNnWj8KNbVqaWasiKfO1VcsWpYvmVE9EuplKLhRwoVsiZyD4WnaOvIAdy+vU34zzOU/z1O+euXdnjUbayk7SDZ8XgiIL2z3DBMpWVGH7cvtn2O4bJswTHjxi2H5rmBBignr9nWECEUFMVxi2r4wik2XrrOtg72pOyYtxpftN0dTaVeu8db7f2R8g4MGj4CSIz/UEQpKNRKGnJz1ABgmu9LzD+DKKYR0ja9SrmEgY2t4AUh6ivhI9qFAW4lPnUMblAzEbDJY3MOFfeaqwP8rU5pN6PmY0fH5AqR71AdmeNpbzXzBQjRKR98+9Us+ZwqJnNLvzC8050Jnd1xzsvxAXI6LoCmFFkiVKIAJBbqTCFARKFKt8QtWeoh4oi5B0QCYc/DTddDQqHlzBkaKUJMLytdXvr1HeT7WrUzKVigtyvAYGPEkw0PdlgkASmvCIG7O6n39QPjtDL1Z7rZjq94qmUOmnbLf6ksrDr9XBqvzsqxt82zf2dJCGV/pRxvpbrbDCc2UWF6cTd1RLld3L6gpz5LHmrj/E8FUUBfLUxOM7d7bnW3f3/s8H3xq1qv0MrFEe6xJpiEPLrlSd4w7t73uq252b6bBn5TFdFGY0fNJK2QrzUIbbqvaIGy3v+ofZkK9ZazgZ3+/U3lT69fHdenl6BrIdGF7aL4ajFxDyuvVnAAAA///9ex2coBAAAA==\"") +} diff --git a/migrate/sql/20180103142001_initial_schema.sql b/migrate/sql/20180103142001_initial_schema.sql index 40fbee2135ac824f34d8db784a91b40beddb9add..f1387f1305e6eebdb3cce9418b8b629e17efa867 100644 --- a/migrate/sql/20180103142001_initial_schema.sql +++ b/migrate/sql/20180103142001_initial_schema.sql @@ -38,8 +38,8 @@ CREATE TABLE IF NOT EXISTS users ( edge_count INT DEFAULT 0 CHECK (edge_count >= 0) NOT NULL, create_time BIGINT CHECK (create_time > 0) NOT NULL, update_time BIGINT CHECK (update_time > 0) NOT NULL, - verify_time BIGINT CHECK (verify_time >= 0) DEFAULT 0 NOT NULL, - disable_time BIGINT CHECK (disable_time >= 0) DEFAULT 0 NOT NULL + verify_time BIGINT DEFAULT 0 CHECK (verify_time >= 0) NOT NULL, + disable_time BIGINT DEFAULT 0 CHECK (disable_time >= 0) NOT NULL ); CREATE TABLE IF NOT EXISTS user_device ( @@ -65,7 +65,7 @@ CREATE TABLE IF NOT EXISTS user_edge ( ); CREATE TABLE IF NOT EXISTS notification ( - -- FIXME: cockroach's analyser is not clever enough when create_time has DESC mode on the index. + -- FIXME: cockroach's analyser is not clever enough when create_time has DESC mode on the index. PRIMARY KEY (user_id, create_time ASC, id), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, diff --git a/rtapi/realtime.pb.go b/rtapi/realtime.pb.go index b6ebffa336aee6ff4cdda99cef47dda5164d97db..3dccbf177462ffac3639c3556762fb348c71447a 100644 --- a/rtapi/realtime.pb.go +++ b/rtapi/realtime.pb.go @@ -28,6 +28,7 @@ package rtapi import proto "github.com/golang/protobuf/proto" import fmt "fmt" import math "math" +import google_protobuf "github.com/golang/protobuf/ptypes/wrappers" import nakama_api "github.com/heroiclabs/nakama/api" // Reference imports to suppress errors if they are not otherwise used. @@ -55,10 +56,12 @@ const ( Error_BAD_INPUT Error_Code = 3 // The match id was not found. Error_MATCH_NOT_FOUND Error_Code = 4 + // The match join was rejected. + Error_MATCH_JOIN_REJECTED Error_Code = 5 // The runtime function does not exist on the server. - Error_RUNTIME_FUNCTION_NOT_FOUND Error_Code = 5 + Error_RUNTIME_FUNCTION_NOT_FOUND Error_Code = 6 // The runtime function executed with an error. - Error_RUNTIME_FUNCTION_EXCEPTION Error_Code = 6 + Error_RUNTIME_FUNCTION_EXCEPTION Error_Code = 7 ) var Error_Code_name = map[int32]string{ @@ -67,8 +70,9 @@ var Error_Code_name = map[int32]string{ 2: "MISSING_PAYLOAD", 3: "BAD_INPUT", 4: "MATCH_NOT_FOUND", - 5: "RUNTIME_FUNCTION_NOT_FOUND", - 6: "RUNTIME_FUNCTION_EXCEPTION", + 5: "MATCH_JOIN_REJECTED", + 6: "RUNTIME_FUNCTION_NOT_FOUND", + 7: "RUNTIME_FUNCTION_EXCEPTION", } var Error_Code_value = map[string]int32{ "RUNTIME_EXCEPTION": 0, @@ -76,8 +80,9 @@ var Error_Code_value = map[string]int32{ "MISSING_PAYLOAD": 2, "BAD_INPUT": 3, "MATCH_NOT_FOUND": 4, - "RUNTIME_FUNCTION_NOT_FOUND": 5, - "RUNTIME_FUNCTION_EXCEPTION": 6, + "MATCH_JOIN_REJECTED": 5, + "RUNTIME_FUNCTION_NOT_FOUND": 6, + "RUNTIME_FUNCTION_EXCEPTION": 7, } func (x Error_Code) String() string { @@ -563,9 +568,18 @@ func (m *Error) GetContext() map[string]string { // A realtime match. type Match struct { - MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` - Presences []*StreamPresence `protobuf:"bytes,2,rep,name=presences" json:"presences,omitempty"` - Self *StreamPresence `protobuf:"bytes,3,opt,name=self" json:"self,omitempty"` + // The match unique ID. + MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` + // True if it's an server-managed authoritative match, false otherwise. + Authoritative bool `protobuf:"varint,2,opt,name=authoritative" json:"authoritative,omitempty"` + // Match label, if any. + Label *google_protobuf.StringValue `protobuf:"bytes,3,opt,name=label" json:"label,omitempty"` + // The number of users currently in the match. + Size int32 `protobuf:"varint,4,opt,name=size" json:"size,omitempty"` + // The users currently in the match. + Presences []*StreamPresence `protobuf:"bytes,5,rep,name=presences" json:"presences,omitempty"` + // A reference to the current user's presence in the match. + Self *StreamPresence `protobuf:"bytes,6,opt,name=self" json:"self,omitempty"` } func (m *Match) Reset() { *m = Match{} } @@ -580,6 +594,27 @@ func (m *Match) GetMatchId() string { return "" } +func (m *Match) GetAuthoritative() bool { + if m != nil { + return m.Authoritative + } + return false +} + +func (m *Match) GetLabel() *google_protobuf.StringValue { + if m != nil { + return m.Label + } + return nil +} + +func (m *Match) GetSize() int32 { + if m != nil { + return m.Size + } + return 0 +} + func (m *Match) GetPresences() []*StreamPresence { if m != nil { return m.Presences @@ -605,10 +640,14 @@ func (*MatchCreate) Descriptor() ([]byte, []int) { return fileDescriptor0, []int // Realtime match data received from the server. type MatchData struct { - MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` + // The match unique ID. + MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` + // A reference to the user presence that sent this data, if any. Presence *StreamPresence `protobuf:"bytes,2,opt,name=presence" json:"presence,omitempty"` - OpCode int64 `protobuf:"varint,3,opt,name=op_code,json=opCode" json:"op_code,omitempty"` - Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` + // Op code value. + OpCode int64 `protobuf:"varint,3,opt,name=op_code,json=opCode" json:"op_code,omitempty"` + // Data payload, if any. + Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` } func (m *MatchData) Reset() { *m = MatchData{} } @@ -646,9 +685,13 @@ func (m *MatchData) GetData() []byte { // Send realtime match data to the server. type MatchDataSend struct { - MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` - OpCode int64 `protobuf:"varint,2,opt,name=op_code,json=opCode" json:"op_code,omitempty"` - Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + // The match unique ID. + MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` + // Op code value. + OpCode int64 `protobuf:"varint,2,opt,name=op_code,json=opCode" json:"op_code,omitempty"` + // Data payload, if any. + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + // List of presences in the match to deliver to, if filtering is required. Otherwise deliver to everyone in the match. Presences []*StreamPresence `protobuf:"bytes,4,rep,name=presences" json:"presences,omitempty"` } @@ -801,6 +844,7 @@ func _MatchJoin_OneofSizer(msg proto.Message) (n int) { // Leave a realtime match. type MatchLeave struct { + // The match unique ID. MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` } @@ -818,9 +862,12 @@ func (m *MatchLeave) GetMatchId() string { // A set of joins and leaves on a particular realtime match. type MatchPresenceEvent struct { - MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` - Joins []*StreamPresence `protobuf:"bytes,2,rep,name=joins" json:"joins,omitempty"` - Leaves []*StreamPresence `protobuf:"bytes,3,rep,name=leaves" json:"leaves,omitempty"` + // The match unique ID. + MatchId string `protobuf:"bytes,1,opt,name=match_id,json=matchId" json:"match_id,omitempty"` + // User presences that have just joined the match. + Joins []*StreamPresence `protobuf:"bytes,2,rep,name=joins" json:"joins,omitempty"` + // User presences that have just left the match. + Leaves []*StreamPresence `protobuf:"bytes,3,rep,name=leaves" json:"leaves,omitempty"` } func (m *MatchPresenceEvent) Reset() { *m = MatchPresenceEvent{} } @@ -1059,71 +1106,78 @@ func init() { func init() { proto.RegisterFile("rtapi/realtime.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 1056 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x56, 0x4d, 0x6f, 0xdb, 0x46, - 0x10, 0x15, 0x25, 0x91, 0x16, 0x47, 0x56, 0xec, 0xae, 0x3f, 0xc2, 0x3a, 0xad, 0x6b, 0x30, 0x05, - 0x1a, 0xf4, 0x20, 0x03, 0x09, 0x8a, 0x16, 0x09, 0x12, 0xd4, 0x92, 0xe5, 0x50, 0x6d, 0x4c, 0x09, - 0x6b, 0x1b, 0x6d, 0x7d, 0x21, 0xd6, 0xe4, 0x26, 0x66, 0x2c, 0x7e, 0x80, 0xbb, 0x32, 0xea, 0xbf, - 0xd0, 0x53, 0x4f, 0x3d, 0xb6, 0x40, 0x8f, 0x3d, 0xe5, 0xd4, 0x3f, 0xd4, 0x3f, 0x52, 0xec, 0x92, - 0x14, 0x29, 0x59, 0x92, 0x93, 0x4b, 0x4e, 0xe2, 0xec, 0xbe, 0x37, 0x33, 0x7a, 0x3b, 0xfb, 0x48, - 0xd8, 0x4c, 0x38, 0x89, 0xfd, 0xfd, 0x84, 0x92, 0x11, 0xf7, 0x03, 0xda, 0x8e, 0x93, 0x88, 0x47, - 0x68, 0x2d, 0x24, 0x57, 0x24, 0x20, 0xed, 0x7c, 0x79, 0xe7, 0xeb, 0x37, 0x3e, 0xbf, 0x1c, 0x5f, - 0xb4, 0xdd, 0x28, 0xd8, 0xbf, 0xa4, 0x49, 0xe4, 0xbb, 0x23, 0x72, 0xc1, 0xf6, 0x53, 0xd8, 0xbe, - 0xc8, 0x40, 0x62, 0x3f, 0x25, 0x9b, 0xef, 0x34, 0x68, 0xf4, 0xc2, 0x6b, 0x3a, 0x8a, 0x62, 0x8a, - 0xd6, 0xa1, 0xe6, 0xfa, 0x9e, 0xa1, 0xec, 0x29, 0x8f, 0x74, 0x2c, 0x1e, 0x51, 0x1b, 0x54, 0x9a, - 0x24, 0x51, 0x62, 0x54, 0xf7, 0x94, 0x47, 0xcd, 0xc7, 0xdb, 0xed, 0x99, 0x5a, 0xed, 0x9e, 0xd8, - 0xb5, 0x2a, 0x38, 0x85, 0x09, 0x7c, 0x40, 0xb8, 0x7b, 0x69, 0xd4, 0x16, 0xe0, 0x8f, 0xc5, 0xae, - 0xc0, 0x4b, 0x18, 0x3a, 0x80, 0x55, 0xf9, 0xe0, 0xb8, 0x09, 0x25, 0x9c, 0x1a, 0x75, 0x49, 0xfb, - 0x6c, 0x3e, 0xad, 0x2b, 0x31, 0x56, 0x05, 0x37, 0x83, 0x22, 0x44, 0xcf, 0x00, 0xd2, 0x14, 0x1e, - 0xe1, 0xc4, 0x50, 0x65, 0x82, 0x9d, 0xf9, 0x09, 0x0e, 0x09, 0x27, 0x56, 0x05, 0xeb, 0x41, 0x1e, - 0x20, 0x0b, 0xd6, 0x0a, 0xb2, 0xc3, 0x68, 0xe8, 0x19, 0x9a, 0xcc, 0xb0, 0xbb, 0x38, 0xc3, 0x09, - 0x0d, 0x3d, 0xab, 0x82, 0x5b, 0x41, 0x79, 0xa1, 0x68, 0xe3, 0x6d, 0xe4, 0x87, 0xc6, 0xca, 0xb2, - 0x36, 0x7e, 0x88, 0xfc, 0x70, 0xd2, 0x86, 0x08, 0xd0, 0x0b, 0x48, 0xff, 0x92, 0x33, 0xa2, 0xe4, - 0x9a, 0x1a, 0x0d, 0xc9, 0x7e, 0x30, 0x9f, 0xfd, 0x4a, 0x40, 0xac, 0x0a, 0x4e, 0xcb, 0xc9, 0x08, - 0xfd, 0x04, 0x9b, 0x29, 0x3f, 0x4e, 0x28, 0xa3, 0xa1, 0x4b, 0x1d, 0x7a, 0x4d, 0x43, 0x6e, 0xe8, - 0x32, 0xd1, 0xc3, 0xf9, 0x89, 0x86, 0x19, 0xb6, 0x27, 0xa0, 0x56, 0x05, 0xa3, 0xe0, 0xd6, 0x2a, - 0x3a, 0x82, 0x56, 0x18, 0x71, 0xff, 0xb5, 0xef, 0x12, 0xee, 0x47, 0x21, 0x33, 0x60, 0x81, 0x3a, - 0x76, 0x19, 0x25, 0xd4, 0x99, 0xa2, 0xa1, 0x87, 0x50, 0x4b, 0x62, 0xd7, 0x68, 0x4a, 0xf6, 0x5a, - 0xce, 0x16, 0x63, 0x88, 0x63, 0xd7, 0xaa, 0x60, 0xb1, 0x2b, 0x54, 0x60, 0x3c, 0xa1, 0x24, 0x48, - 0x8f, 0x72, 0x75, 0x81, 0x0a, 0x27, 0x12, 0x93, 0x9d, 0x25, 0xb0, 0x49, 0x84, 0xce, 0x61, 0x2b, - 0xe3, 0xcf, 0xc8, 0xd0, 0x92, 0x99, 0xbe, 0x5c, 0x90, 0x69, 0x56, 0x87, 0x0d, 0x76, 0x7b, 0xb9, - 0xa3, 0xc3, 0x4a, 0x40, 0x19, 0x23, 0x6f, 0xa8, 0xf9, 0x5f, 0x15, 0x54, 0x39, 0xf6, 0x08, 0x41, - 0xdd, 0x8d, 0x3c, 0x2a, 0x2f, 0x8c, 0x8a, 0xe5, 0x33, 0x32, 0x26, 0x40, 0x79, 0x67, 0x74, 0x9c, - 0x87, 0xe8, 0x39, 0xac, 0xb8, 0x51, 0xc8, 0xe9, 0xaf, 0xdc, 0xa8, 0xed, 0xd5, 0xe6, 0x9e, 0x8b, - 0x4c, 0xdb, 0xee, 0xa6, 0xa8, 0x5e, 0xc8, 0x93, 0x1b, 0x9c, 0x73, 0x76, 0x9e, 0xc2, 0x6a, 0x79, - 0x43, 0x5c, 0xd6, 0x2b, 0x7a, 0x93, 0x5f, 0xd6, 0x2b, 0x7a, 0x83, 0x36, 0x41, 0xbd, 0x26, 0xa3, - 0x71, 0x5e, 0x38, 0x0d, 0x9e, 0x56, 0xbf, 0x53, 0xcc, 0x77, 0x0a, 0xd4, 0xbb, 0xa2, 0xbb, 0x2d, - 0xf8, 0x04, 0x9f, 0xd9, 0xa7, 0xfd, 0xe3, 0x9e, 0xd3, 0xfb, 0xb9, 0xdb, 0x1b, 0x9e, 0xf6, 0x07, - 0xf6, 0x7a, 0x05, 0x19, 0xb0, 0x79, 0x66, 0xe3, 0x5e, 0x77, 0xf0, 0xd2, 0xee, 0x9f, 0xf7, 0x0e, - 0x9d, 0xe1, 0xc1, 0x2f, 0xaf, 0x06, 0x07, 0x87, 0xeb, 0x0a, 0xda, 0x80, 0xb5, 0xe3, 0xfe, 0xc9, - 0x49, 0xdf, 0x7e, 0x39, 0x59, 0xac, 0xa2, 0x16, 0xe8, 0x9d, 0x83, 0x43, 0xa7, 0x6f, 0x0f, 0xcf, - 0x4e, 0xd7, 0x6b, 0x12, 0x73, 0x70, 0xda, 0xb5, 0x1c, 0x7b, 0x70, 0xea, 0x1c, 0x0d, 0xce, 0xec, - 0xc3, 0xf5, 0x3a, 0xda, 0x85, 0x9d, 0xbc, 0xd2, 0xd1, 0x99, 0xdd, 0x15, 0x85, 0x4a, 0xfb, 0xea, - 0xdc, 0xfd, 0xa2, 0x25, 0xcd, 0xfc, 0x43, 0x01, 0x55, 0x8e, 0x29, 0xfa, 0x14, 0x1a, 0xe9, 0x70, - 0x4f, 0xac, 0x69, 0x45, 0xc6, 0x7d, 0x0f, 0x3d, 0x07, 0x3d, 0x3f, 0x6a, 0x66, 0x54, 0xa5, 0xa8, - 0x5f, 0xdc, 0x71, 0xca, 0xb8, 0x60, 0xa0, 0x27, 0x50, 0x67, 0x74, 0xf4, 0x3a, 0x33, 0xab, 0x3b, - 0x99, 0x12, 0x6c, 0xb6, 0xa0, 0x59, 0x72, 0x23, 0xf3, 0x77, 0x05, 0xf4, 0x89, 0x35, 0x2c, 0xeb, - 0xf5, 0x19, 0x34, 0xf2, 0xca, 0x99, 0x9b, 0xde, 0x59, 0x70, 0x42, 0x40, 0xf7, 0x61, 0x25, 0x8a, - 0x1d, 0x39, 0x6c, 0xa2, 0xd9, 0x1a, 0xd6, 0xa2, 0x58, 0x1e, 0x28, 0x82, 0xba, 0xbc, 0x2c, 0xc2, - 0x38, 0x57, 0xb1, 0x7c, 0x16, 0xd2, 0xb5, 0xa6, 0xdc, 0x6a, 0x59, 0x5b, 0xa5, 0xcc, 0xd5, 0xb9, - 0x99, 0x6b, 0x45, 0xe6, 0x69, 0xbd, 0xeb, 0x1f, 0xaa, 0xb7, 0x79, 0x94, 0x49, 0x25, 0x3d, 0xef, - 0xc1, 0x6c, 0x4f, 0x56, 0xa5, 0xe8, 0x6a, 0x1b, 0x54, 0x1e, 0x5d, 0xd1, 0x30, 0x1d, 0x65, 0xf1, - 0xbe, 0x90, 0x61, 0xa7, 0x0e, 0x55, 0xdf, 0x33, 0xbf, 0x02, 0x28, 0xac, 0x70, 0xc9, 0x9f, 0x33, - 0xff, 0x52, 0x00, 0xdd, 0xf6, 0xba, 0x65, 0x72, 0x7c, 0x03, 0xaa, 0x30, 0xf0, 0xf7, 0x9e, 0xa6, - 0x14, 0x8d, 0xbe, 0x05, 0x4d, 0x5a, 0x37, 0xcb, 0xae, 0xf6, 0x9d, 0xbc, 0x0c, 0x6e, 0x0e, 0xa0, - 0x35, 0x65, 0x9d, 0xe8, 0xc5, 0xac, 0xe3, 0x2a, 0x32, 0xa1, 0x51, 0xf6, 0xcc, 0x32, 0x63, 0xc6, - 0x69, 0xcd, 0x11, 0x68, 0x69, 0x29, 0x71, 0x80, 0x41, 0xc9, 0x9d, 0x82, 0xcc, 0x9d, 0xd8, 0xf8, - 0xe2, 0x2d, 0x75, 0x79, 0xee, 0x4e, 0x59, 0x88, 0x76, 0x01, 0x3c, 0xca, 0xdc, 0xc4, 0x8f, 0x79, - 0x94, 0xc8, 0x43, 0xd7, 0x71, 0x69, 0x45, 0x98, 0xcb, 0x88, 0x5c, 0xd0, 0x91, 0x9c, 0x34, 0x1d, - 0xa7, 0x81, 0xf9, 0x9b, 0x02, 0x50, 0xf8, 0x31, 0xda, 0x07, 0x2d, 0x35, 0x4f, 0x59, 0xb4, 0xf9, - 0xf8, 0xfe, 0x02, 0x19, 0x70, 0x06, 0x13, 0xba, 0x89, 0x97, 0x2e, 0x4d, 0xde, 0xf7, 0x4a, 0x64, - 0xf0, 0xa9, 0xe9, 0xd4, 0xb3, 0xb9, 0xff, 0x53, 0x81, 0x7b, 0xd3, 0x70, 0x31, 0xdd, 0x63, 0x46, - 0x93, 0xe2, 0xa0, 0x35, 0x11, 0xf6, 0x3d, 0xf4, 0x39, 0x00, 0xa3, 0x8c, 0xf9, 0x51, 0x28, 0xf6, - 0x52, 0x2d, 0xf4, 0x6c, 0xa5, 0xef, 0xa1, 0x1d, 0x68, 0x08, 0x60, 0x48, 0x02, 0x9a, 0x95, 0x98, - 0xc4, 0x68, 0x0f, 0x9a, 0x31, 0x4d, 0x98, 0xcf, 0xb8, 0xbc, 0xcb, 0x42, 0x8f, 0x06, 0x2e, 0x2f, - 0xa1, 0x6d, 0x21, 0x03, 0xe1, 0x63, 0x26, 0x3f, 0x47, 0x74, 0x9c, 0x45, 0xe6, 0xbf, 0x0a, 0x6c, - 0xcc, 0x79, 0xe7, 0x7c, 0xb8, 0x6c, 0x1f, 0x79, 0x4a, 0x3b, 0xdf, 0xc3, 0x96, 0x1b, 0x05, 0xed, - 0xe2, 0x63, 0x32, 0x23, 0x76, 0xee, 0xd9, 0xf2, 0x17, 0x67, 0xfc, 0xa1, 0x72, 0xae, 0xca, 0x6f, - 0xd4, 0xbf, 0xab, 0x75, 0xfb, 0xc7, 0x61, 0xe7, 0x9f, 0xaa, 0x96, 0x02, 0x2e, 0x34, 0xf9, 0xb9, - 0xf9, 0xe4, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0x97, 0x8b, 0x36, 0x13, 0xc3, 0x0a, 0x00, 0x00, + // 1153 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x56, 0x4d, 0x6f, 0x1a, 0x47, + 0x18, 0x66, 0x81, 0x05, 0xf6, 0xc5, 0xc4, 0x74, 0xfc, 0xb5, 0x25, 0xa9, 0x6b, 0x6d, 0x22, 0x35, + 0xea, 0x01, 0x24, 0x47, 0x55, 0xab, 0x44, 0x89, 0x6a, 0x60, 0x1d, 0x70, 0xe3, 0x05, 0x8d, 0x71, + 0x3f, 0x7c, 0x41, 0xc3, 0x32, 0xb6, 0x37, 0x66, 0x3f, 0xb4, 0x3b, 0xb8, 0x75, 0xcf, 0x3d, 0xf5, + 0xd4, 0x53, 0x8f, 0xad, 0xd4, 0x63, 0x4f, 0x3d, 0xf5, 0x3f, 0xf4, 0x3f, 0xf5, 0x50, 0xcd, 0xcc, + 0x2e, 0x5f, 0x06, 0x3b, 0xb9, 0xf4, 0xc4, 0xbc, 0x33, 0xcf, 0xf3, 0xce, 0xcb, 0x33, 0xef, 0x3c, + 0xb3, 0xb0, 0x19, 0x32, 0x12, 0x38, 0xb5, 0x90, 0x92, 0x11, 0x73, 0x5c, 0x5a, 0x0d, 0x42, 0x9f, + 0xf9, 0x68, 0xdd, 0x23, 0x57, 0xc4, 0x25, 0xd5, 0x64, 0xba, 0xb2, 0x7b, 0xe1, 0xfb, 0x17, 0x23, + 0x5a, 0x13, 0xcb, 0x83, 0xf1, 0x79, 0xed, 0xfb, 0x90, 0x04, 0x01, 0x0d, 0x23, 0x49, 0xa8, 0x7c, + 0x7a, 0xe1, 0xb0, 0xcb, 0xf1, 0xa0, 0x6a, 0xfb, 0x6e, 0xed, 0x92, 0x86, 0xbe, 0x63, 0x8f, 0xc8, + 0x20, 0xaa, 0xc9, 0x34, 0x35, 0xbe, 0x03, 0x09, 0x1c, 0x89, 0x35, 0xfe, 0xca, 0x41, 0xc1, 0xf4, + 0xae, 0xe9, 0xc8, 0x0f, 0x28, 0x2a, 0x43, 0xc6, 0x76, 0x86, 0xba, 0xb2, 0xa7, 0x3c, 0xd5, 0x30, + 0x1f, 0xa2, 0x2a, 0xa8, 0x34, 0x0c, 0xfd, 0x50, 0x4f, 0xef, 0x29, 0x4f, 0x8b, 0xfb, 0xdb, 0xd5, + 0x85, 0x5a, 0xaa, 0x26, 0x5f, 0x6d, 0xa5, 0xb0, 0x84, 0x71, 0xbc, 0x4b, 0x98, 0x7d, 0xa9, 0x67, + 0x56, 0xe0, 0x8f, 0xf9, 0x2a, 0xc7, 0x0b, 0x18, 0x3a, 0x80, 0x35, 0x31, 0xe8, 0xdb, 0x21, 0x25, + 0x8c, 0xea, 0x59, 0x41, 0x7b, 0xb4, 0x9c, 0xd6, 0x10, 0x98, 0x56, 0x0a, 0x17, 0xdd, 0x69, 0x88, + 0x5e, 0x00, 0xc8, 0x14, 0x43, 0xc2, 0x88, 0xae, 0x8a, 0x04, 0x95, 0xe5, 0x09, 0x9a, 0x84, 0x91, + 0x56, 0x0a, 0x6b, 0x6e, 0x12, 0xa0, 0x16, 0xac, 0x4f, 0xc9, 0xfd, 0x88, 0x7a, 0x43, 0x3d, 0x27, + 0x32, 0xec, 0xae, 0xce, 0x70, 0x42, 0xbd, 0x61, 0x2b, 0x85, 0x4b, 0xee, 0xec, 0xc4, 0xb4, 0x8c, + 0xb7, 0xbe, 0xe3, 0xe9, 0xf9, 0xbb, 0xca, 0x38, 0xf2, 0x1d, 0x6f, 0x52, 0x06, 0x0f, 0xd0, 0x2b, + 0x90, 0x7f, 0xa9, 0x3f, 0xa2, 0xe4, 0x9a, 0xea, 0x05, 0xc1, 0x7e, 0xb8, 0x9c, 0xfd, 0x86, 0x43, + 0x5a, 0x29, 0x2c, 0xb7, 0x13, 0x11, 0xfa, 0x06, 0x36, 0x25, 0x3f, 0x08, 0x69, 0x44, 0x3d, 0x9b, + 0xf6, 0xe9, 0x35, 0xf5, 0x98, 0xae, 0x89, 0x44, 0x8f, 0x97, 0x27, 0xea, 0xc6, 0x58, 0x93, 0x43, + 0x5b, 0x29, 0x8c, 0xdc, 0x5b, 0xb3, 0xe8, 0x10, 0x4a, 0x9e, 0xcf, 0x9c, 0x73, 0xc7, 0x26, 0xcc, + 0xf1, 0xbd, 0x48, 0x87, 0x15, 0xea, 0x58, 0xb3, 0x28, 0xae, 0xce, 0x1c, 0x0d, 0x3d, 0x86, 0x4c, + 0x18, 0xd8, 0x7a, 0x51, 0xb0, 0xd7, 0x13, 0x36, 0x6f, 0x43, 0x1c, 0xd8, 0xad, 0x14, 0xe6, 0xab, + 0x5c, 0x85, 0x88, 0x85, 0x94, 0xb8, 0xf2, 0x28, 0xd7, 0x56, 0xa8, 0x70, 0x22, 0x30, 0xf1, 0x59, + 0x42, 0x34, 0x89, 0xd0, 0x19, 0x6c, 0xc5, 0xfc, 0x05, 0x19, 0x4a, 0x22, 0xd3, 0x93, 0x15, 0x99, + 0x16, 0x75, 0xd8, 0x88, 0x6e, 0x4f, 0xd7, 0x35, 0xc8, 0xbb, 0x34, 0x8a, 0xc8, 0x05, 0x35, 0xfe, + 0x4d, 0x83, 0x2a, 0xda, 0x1e, 0x21, 0xc8, 0xda, 0xfe, 0x90, 0x8a, 0x0b, 0xa3, 0x62, 0x31, 0x46, + 0xfa, 0x04, 0x28, 0xee, 0x8c, 0x86, 0x93, 0x10, 0xbd, 0x84, 0xbc, 0xed, 0x7b, 0x8c, 0xfe, 0xc0, + 0xf4, 0xcc, 0x5e, 0x66, 0xe9, 0xb9, 0x88, 0xb4, 0xd5, 0x86, 0x44, 0x99, 0x1e, 0x0b, 0x6f, 0x70, + 0xc2, 0xa9, 0x3c, 0x87, 0xb5, 0xd9, 0x05, 0x7e, 0x59, 0xaf, 0xe8, 0x4d, 0x72, 0x59, 0xaf, 0xe8, + 0x0d, 0xda, 0x04, 0xf5, 0x9a, 0x8c, 0xc6, 0xc9, 0xc6, 0x32, 0x78, 0x9e, 0xfe, 0x42, 0x31, 0xfe, + 0x51, 0x20, 0xdb, 0xe0, 0xd5, 0x6d, 0xc1, 0x07, 0xf8, 0xd4, 0xea, 0xb5, 0x8f, 0xcd, 0xbe, 0xf9, + 0x6d, 0xc3, 0xec, 0xf6, 0xda, 0x1d, 0xab, 0x9c, 0x42, 0x3a, 0x6c, 0x9e, 0x5a, 0xd8, 0x6c, 0x74, + 0x5e, 0x5b, 0xed, 0x33, 0xb3, 0xd9, 0xef, 0x1e, 0x7c, 0xf7, 0xa6, 0x73, 0xd0, 0x2c, 0x2b, 0x68, + 0x03, 0xd6, 0x8f, 0xdb, 0x27, 0x27, 0x6d, 0xeb, 0xf5, 0x64, 0x32, 0x8d, 0x4a, 0xa0, 0xd5, 0x0f, + 0x9a, 0xfd, 0xb6, 0xd5, 0x3d, 0xed, 0x95, 0x33, 0x02, 0x73, 0xd0, 0x6b, 0xb4, 0xfa, 0x56, 0xa7, + 0xd7, 0x3f, 0xec, 0x9c, 0x5a, 0xcd, 0x72, 0x16, 0xed, 0xc0, 0x86, 0x9c, 0x3c, 0xea, 0xb4, 0xad, + 0x3e, 0x36, 0x8f, 0xcc, 0x46, 0xcf, 0x6c, 0x96, 0x55, 0xb4, 0x0b, 0x95, 0xa4, 0x84, 0xc3, 0x53, + 0xab, 0xc1, 0x2b, 0x98, 0x21, 0xe6, 0x96, 0xae, 0x4f, 0x6b, 0xcd, 0x1b, 0x3f, 0xa5, 0x41, 0x15, + 0xfd, 0x8b, 0x3e, 0x84, 0x82, 0xec, 0xfa, 0x89, 0x67, 0xe5, 0x45, 0xdc, 0x1e, 0xa2, 0x27, 0x50, + 0x22, 0x63, 0x76, 0xe9, 0x87, 0x0e, 0x23, 0xcc, 0xb9, 0x96, 0x92, 0x14, 0xf0, 0xfc, 0x24, 0xda, + 0x07, 0x75, 0x44, 0x06, 0x74, 0x14, 0xbb, 0xd5, 0xa3, 0xaa, 0x34, 0xd6, 0x6a, 0x62, 0xac, 0xbc, + 0x41, 0x1c, 0xef, 0xe2, 0x6b, 0xae, 0x23, 0x96, 0x50, 0x7e, 0xe6, 0x91, 0xf3, 0xa3, 0x74, 0x2a, + 0x15, 0x8b, 0x31, 0x7a, 0x09, 0x5a, 0xd2, 0x71, 0x91, 0xae, 0x8a, 0xb3, 0xfd, 0xf8, 0x9e, 0x66, + 0xc3, 0x53, 0x06, 0x7a, 0x06, 0xd9, 0x88, 0x8e, 0xce, 0x63, 0xe7, 0xb9, 0x97, 0x29, 0xc0, 0x46, + 0x09, 0x8a, 0x33, 0xa6, 0x68, 0xfc, 0xa2, 0x80, 0x36, 0x71, 0xa8, 0xbb, 0x94, 0x79, 0x01, 0x85, + 0x64, 0xe7, 0xd8, 0xd4, 0xef, 0xdd, 0x70, 0x42, 0x40, 0x3b, 0x90, 0xf7, 0x83, 0xbe, 0xe8, 0x79, + 0x2e, 0x59, 0x06, 0xe7, 0xfc, 0x40, 0xf4, 0x15, 0x82, 0xac, 0xb8, 0xb3, 0x5c, 0x95, 0x35, 0x2c, + 0xc6, 0xc6, 0xaf, 0x0a, 0x94, 0xe6, 0x4c, 0xf3, 0xae, 0xb2, 0x66, 0x32, 0xa7, 0x97, 0x66, 0xce, + 0x4c, 0x33, 0xcf, 0xeb, 0x9d, 0x7d, 0x5f, 0xbd, 0x8d, 0xc3, 0x58, 0x2a, 0x61, 0xbd, 0x0f, 0x17, + 0x6b, 0x6a, 0xa5, 0xa6, 0x55, 0x6d, 0x83, 0xca, 0xfc, 0x2b, 0xea, 0xc9, 0x1b, 0xc5, 0x9f, 0x2d, + 0x11, 0xd6, 0xb3, 0x90, 0x76, 0x86, 0xc6, 0x27, 0x00, 0x53, 0x47, 0xbe, 0xe3, 0xcf, 0x19, 0xbf, + 0x2b, 0x80, 0x6e, 0x5b, 0xee, 0x5d, 0x72, 0x7c, 0x06, 0x2a, 0x7f, 0x47, 0x22, 0x3d, 0xfd, 0x6e, + 0xff, 0x4e, 0xa2, 0xd1, 0xe7, 0x90, 0x13, 0x2f, 0x48, 0x14, 0x3b, 0xcc, 0xbd, 0xbc, 0x18, 0x6e, + 0x74, 0xa0, 0x34, 0xe7, 0xe0, 0xe8, 0xd5, 0xa2, 0xf1, 0x2b, 0x22, 0xa1, 0x3e, 0x6b, 0xdd, 0xb3, + 0x8c, 0x05, 0xc3, 0x37, 0x46, 0x90, 0x93, 0x5b, 0xf1, 0x03, 0x74, 0x67, 0x4c, 0xd2, 0x8d, 0x4d, + 0x32, 0x1a, 0x0f, 0xde, 0x52, 0x9b, 0x25, 0x26, 0x19, 0x87, 0x68, 0x17, 0x60, 0x48, 0x23, 0x3b, + 0x74, 0x02, 0xe6, 0x87, 0xe2, 0xd0, 0x35, 0x3c, 0x33, 0xc3, 0x3d, 0x4e, 0x5e, 0xd9, 0xac, 0xf4, + 0x38, 0x11, 0x18, 0x3f, 0x2b, 0x00, 0xd3, 0x67, 0x01, 0xd5, 0x20, 0x27, 0x3d, 0x5c, 0x6c, 0x5a, + 0xdc, 0xdf, 0x59, 0x21, 0x03, 0x8e, 0x61, 0x5c, 0x37, 0xfe, 0xf6, 0xd3, 0xf0, 0x5d, 0xaf, 0x44, + 0x0c, 0x9f, 0xeb, 0x4e, 0x2d, 0xee, 0xfb, 0xdf, 0x14, 0x78, 0x30, 0x0f, 0xe7, 0xdd, 0x3d, 0x8e, + 0x68, 0x38, 0x3d, 0xe8, 0x1c, 0x0f, 0xdb, 0x43, 0xf4, 0x11, 0x40, 0x44, 0xa3, 0xc8, 0xf1, 0x3d, + 0xbe, 0x26, 0xb5, 0xd0, 0xe2, 0x99, 0xf6, 0x10, 0x55, 0xa0, 0xc0, 0x81, 0x1e, 0x71, 0x69, 0xbc, + 0xc5, 0x24, 0x46, 0x7b, 0x50, 0xe4, 0xdf, 0x7c, 0x4e, 0xc4, 0xc4, 0x5d, 0xce, 0x0a, 0x83, 0x9b, + 0x9d, 0x42, 0xdb, 0x5c, 0x06, 0xc2, 0xc6, 0x91, 0xf8, 0x2a, 0xd2, 0x70, 0x1c, 0x19, 0x7f, 0x2b, + 0xb0, 0xb1, 0xe4, 0xe9, 0x7b, 0x7f, 0xd9, 0xfe, 0xe7, 0x2e, 0xad, 0x7f, 0x09, 0x5b, 0xb6, 0xef, + 0x56, 0xa7, 0xdf, 0xb4, 0x31, 0xb1, 0xfe, 0xc0, 0x12, 0xbf, 0x38, 0xe6, 0x77, 0x95, 0x33, 0x55, + 0x7c, 0x4a, 0xff, 0x91, 0xce, 0x5a, 0x5f, 0x75, 0xeb, 0x7f, 0xa6, 0x73, 0x12, 0x30, 0xc8, 0x09, + 0x6b, 0x7f, 0xf6, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x3c, 0xab, 0xb4, 0x6f, 0x6a, 0x0b, 0x00, + 0x00, } diff --git a/rtapi/realtime.proto b/rtapi/realtime.proto index cfc365fbcb845efbe9adba427e4c66e0ef3d470c..c058ceeb8f6f5ecd1bb2739715a2f02eac83d46d 100644 --- a/rtapi/realtime.proto +++ b/rtapi/realtime.proto @@ -19,6 +19,7 @@ syntax = "proto3"; package nakama.realtime; +import "google/protobuf/wrappers.proto"; import "github.com/heroiclabs/nakama/api/api.proto"; option go_package = "rtapi"; @@ -76,10 +77,12 @@ message Error { BAD_INPUT = 3; // The match id was not found. MATCH_NOT_FOUND = 4; + // The match join was rejected. + MATCH_JOIN_REJECTED = 5; // The runtime function does not exist on the server. - RUNTIME_FUNCTION_NOT_FOUND = 5; + RUNTIME_FUNCTION_NOT_FOUND = 6; // The runtime function executed with an error. - RUNTIME_FUNCTION_EXCEPTION = 6; + RUNTIME_FUNCTION_EXCEPTION = 7; } // The error code which should be one of "Error.Code" enums. @@ -92,9 +95,18 @@ message Error { // A realtime match. message Match { + // The match unique ID. string match_id = 1; - repeated StreamPresence presences = 2; - StreamPresence self = 3; + // True if it's an server-managed authoritative match, false otherwise. + bool authoritative = 2; + // Match label, if any. + google.protobuf.StringValue label = 3; + // The number of users currently in the match. + int32 size = 4; + // The users currently in the match. + repeated StreamPresence presences = 5; + // A reference to the current user's presence in the match. + StreamPresence self = 6; } // Create a new realtime match. @@ -102,37 +114,51 @@ message MatchCreate {} // Realtime match data received from the server. message MatchData { + // The match unique ID. string match_id = 1; + // A reference to the user presence that sent this data, if any. StreamPresence presence = 2; + // Op code value. int64 op_code = 3; + // Data payload, if any. bytes data = 4; } // Send realtime match data to the server. message MatchDataSend { + // The match unique ID. string match_id = 1; + // Op code value. int64 op_code = 2; + // Data payload, if any. bytes data = 3; + // List of presences in the match to deliver to, if filtering is required. Otherwise deliver to everyone in the match. repeated StreamPresence presences = 4; } // Join an existing realtime match. message MatchJoin { oneof id { + // The match unique ID. string match_id = 1; + // A matchmaking result token. string token = 2; } } // Leave a realtime match. message MatchLeave { + // The match unique ID. string match_id = 1; } // A set of joins and leaves on a particular realtime match. message MatchPresenceEvent { + // The match unique ID. string match_id = 1; + // User presences that have just joined the match. repeated StreamPresence joins = 2; + // User presences that have just left the match. repeated StreamPresence leaves = 3; } diff --git a/server/api.go b/server/api.go index 06df1c73ec41c2d8c47000720231426ba5f4802b..88f52e44e5fc4f90b19f31752d6fffd63454c8c9 100644 --- a/server/api.go +++ b/server/api.go @@ -51,29 +51,31 @@ type ApiServer struct { logger *zap.Logger db *sql.DB config Config - runtimePool *RuntimePool + socialClient *social.Client + matchRegistry MatchRegistry tracker Tracker router MessageRouter - socialClient *social.Client + runtimePool *RuntimePool grpcServer *grpc.Server grpcGatewayServer *http.Server } -func StartApiServer(logger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, registry *SessionRegistry, tracker Tracker, router MessageRouter, pipeline *pipeline, runtimePool *RuntimePool) *ApiServer { +func StartApiServer(logger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, pipeline *pipeline, runtimePool *RuntimePool) *ApiServer { grpcServer := grpc.NewServer( grpc.StatsHandler(ocgrpc.NewServerStatsHandler()), grpc.UnaryInterceptor(SecurityInterceptorFunc(logger, config)), ) s := &ApiServer{ - logger: logger, - db: db, - config: config, - runtimePool: runtimePool, - tracker: tracker, - router: router, - socialClient: socialClient, - grpcServer: grpcServer, + logger: logger, + db: db, + config: config, + socialClient: socialClient, + matchRegistry: matchRegistry, + tracker: tracker, + router: router, + runtimePool: runtimePool, + grpcServer: grpcServer, } // Register and start GRPC server. @@ -102,7 +104,7 @@ func StartApiServer(logger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Mars grpcGatewayRouter := mux.NewRouter() // Special case routes. Do NOT enable compression on WebSocket route, it results in "http: response.Write on hijacked connection" errors. grpcGatewayRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }).Methods("GET") - grpcGatewayRouter.HandleFunc("/ws", NewSocketWsAcceptor(logger, config, registry, tracker, jsonpbMarshaler, jsonpbUnmarshaler, pipeline.processRequest)) + grpcGatewayRouter.HandleFunc("/ws", NewSocketWsAcceptor(logger, config, sessionRegistry, tracker, jsonpbMarshaler, jsonpbUnmarshaler, pipeline)) // TODO restore when admin endpoints are available. // grpcGatewayRouter.HandleFunc("/metrics", zpages.RpczHandler) // grpcGatewayRouter.HandleFunc("/trace", zpages.TracezHandler) diff --git a/server/api_authenticate.go b/server/api_authenticate.go index d25720475d1fa3838414d4d7547a036fd9bd0f45..acb55d75d286f7e8c850933874dde1c8a1f01881 100644 --- a/server/api_authenticate.go +++ b/server/api_authenticate.go @@ -256,7 +256,7 @@ func (s *ApiServer) AuthenticateSteam(ctx context.Context, in *api.AuthenticateS } func generateToken(config Config, userID, username string) string { - exp := time.Now().UTC().Add(time.Duration(config.GetSession().TokenExpiryMs) * time.Millisecond).Unix() + exp := time.Now().UTC().Add(time.Duration(config.GetSession().TokenExpirySec) * time.Second).Unix() return generateTokenWithExpiry(config, userID, username, exp) } diff --git a/server/api_match.go b/server/api_match.go new file mode 100644 index 0000000000000000000000000000000000000000..e96467054508ce1f5c448cead497f445346a57db --- /dev/null +++ b/server/api_match.go @@ -0,0 +1,50 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "github.com/heroiclabs/nakama/api" + "golang.org/x/net/context" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (s *ApiServer) ListMatches(ctx context.Context, in *api.ListMatchesRequest) (*api.MatchList, error) { + limit := 1 + if in.GetLimit() != nil { + if in.GetLimit().Value < 1 || in.GetLimit().Value > 100 { + return nil, status.Error(codes.InvalidArgument, "Invalid limit - limit must be between 1 and 100.") + } + limit = int(in.GetLimit().Value) + } + + if in.Label != nil && (in.Authoritative != nil && !in.Authoritative.Value) { + return nil, status.Error(codes.InvalidArgument, "Label filtering is not supported for non-authoritative matches.") + } + + if in.MinSize != nil && in.MinSize.Value < 0 { + return nil, status.Error(codes.InvalidArgument, "Minimum size must be 0 or above.") + } + if in.MaxSize != nil && in.MaxSize.Value < 0 { + return nil, status.Error(codes.InvalidArgument, "Maximum size must be 0 or above.") + } + if in.MinSize != nil && in.MaxSize != nil && in.MinSize.Value > in.MaxSize.Value { + return nil, status.Error(codes.InvalidArgument, "Maximum size must be greater than or equal to minimum size when both are specified.") + } + + results := s.matchRegistry.ListMatches(limit, in.Authoritative, in.Label, in.MinSize, in.MaxSize) + + return &api.MatchList{Matches: results}, nil +} diff --git a/server/api_rpc.go b/server/api_rpc.go index 771464837e181782d26e4304d9cdaa1dc1987518..420722d0db8ac0099da3c622ea832892e12e54fd 100644 --- a/server/api_rpc.go +++ b/server/api_rpc.go @@ -57,7 +57,7 @@ func (s *ApiServer) RpcFunc(ctx context.Context, in *api.Rpc) (*api.Rpc, error) return nil, status.Error(codes.NotFound, "RPC function not found") } - result, fnErr := runtime.InvokeFunctionRPC(lf, uid, username, expiry, "", in.Payload) + result, fnErr, code := runtime.InvokeFunctionRPC(lf, uid, username, expiry, "", in.Payload) s.runtimePool.Put(runtime) if fnErr != nil { s.logger.Error("Runtime RPC function caused an error", zap.String("id", in.Id), zap.Error(fnErr)) @@ -72,9 +72,9 @@ func (s *ApiServer) RpcFunc(ctx context.Context, in *api.Rpc) (*api.Rpc, error) msg = msgParts[0] } } - return nil, status.Error(codes.Aborted, msg) + return nil, status.Error(code, msg) } else { - return nil, status.Error(codes.Aborted, fnErr.Error()) + return nil, status.Error(code, fnErr.Error()) } } diff --git a/server/config.go b/server/config.go index aa6ac7443ecc1c5e1cb066f19e7eb58118de7ed4..9e6b73e03ba8df5cfcf882d790d4dda190e5ba44 100644 --- a/server/config.go +++ b/server/config.go @@ -85,6 +85,14 @@ func ParseArgs(logger *zap.Logger, args []string) Config { logger.Fatal("Could not parse command line arguments", zap.Error(err)) } + // Fail fast on invalid values. + if l := len(mainConfig.Name); l < 1 || l > 16 { + logger.Fatal("Name must be 1-16 characters", zap.String("param", "name")) + } + if mainConfig.GetSocket().PingPeriodMs >= mainConfig.GetSocket().PongWaitMs { + logger.Fatal("Ping period value must be less than pong wait value", zap.Int("socket.ping_period_ms", mainConfig.GetSocket().PingPeriodMs), zap.Int("socket.pong_wait_ms", mainConfig.GetSocket().PongWaitMs)) + } + // If the runtime path is not overridden, set it to `datadir/modules`. if mainConfig.GetRuntime().Path == "" { mainConfig.GetRuntime().Path = filepath.Join(mainConfig.GetDataDir(), "modules") @@ -105,15 +113,15 @@ func ParseArgs(logger *zap.Logger, args []string) Config { } type config struct { - Name string `yaml:"name" json:"name" usage:"Nakama server’s node name - must be unique"` + Name string `yaml:"name" json:"name" usage:"Nakama server’s node name - must be unique."` Config string `yaml:"config" json:"config" usage:"The absolute file path to configuration YAML file."` Datadir string `yaml:"data_dir" json:"data_dir" usage:"An absolute path to a writeable folder where Nakama will store its data."` - Log *LogConfig `yaml:"log" json:"log" usage:"Log levels and output"` - Session *SessionConfig `yaml:"session" json:"session" usage:"Session authentication settings"` - Socket *SocketConfig `yaml:"socket" json:"socket" usage:"Socket configurations"` - Database *DatabaseConfig `yaml:"database" json:"database" usage:"Database connection settings"` - Social *SocialConfig `yaml:"social" json:"social" usage:"Properties for social providers"` - Runtime *RuntimeConfig `yaml:"runtime" json:"runtime" usage:"Script Runtime properties"` + Log *LogConfig `yaml:"log" json:"log" usage:"Log levels and output."` + Session *SessionConfig `yaml:"session" json:"session" usage:"Session authentication settings."` + Socket *SocketConfig `yaml:"socket" json:"socket" usage:"Socket configuration."` + Database *DatabaseConfig `yaml:"database" json:"database" usage:"Database connection settings."` + Social *SocialConfig `yaml:"social" json:"social" usage:"Properties for social provider integrations."` + Runtime *RuntimeConfig `yaml:"runtime" json:"runtime" usage:"Script Runtime properties."` } // NewConfig constructs a Config struct which represents server settings, and populates it with default values. @@ -167,13 +175,13 @@ func (c *config) GetRuntime() *RuntimeConfig { // LogConfig is configuration relevant to logging levels and output. type LogConfig struct { - // By default, log all messages with Warn and Error messages to a log file inside Data/Log/.log file. The content will be in JSON. + // By default, log all messages with Info and higher levels to a log file inside Data/Log/.log file. The content will be in JSON. // if --log.verbose is passed, log messages with Debug and higher levels. // if --log.stdout is passed, logs are only printed to stdout. - // In all cases, Error messages trigger the stacktrace to be dumped as well. + // In all cases, Error and Fatal messages trigger the stacktrace to be dumped as well. - Verbose bool `yaml:"verbose" json:"verbose" usage:"Turn verbose logging on"` - Stdout bool `yaml:"stdout" json:"stdout" usage:"Log to stdout instead of file"` + Verbose bool `yaml:"verbose" json:"verbose" usage:"Turn verbose logging on."` + Stdout bool `yaml:"stdout" json:"stdout" usage:"Log to stdout instead of file."` } // NewLogConfig creates a new LogConfig struct. @@ -186,15 +194,15 @@ func NewLogConfig() *LogConfig { // SessionConfig is configuration relevant to the session. type SessionConfig struct { - EncryptionKey string `yaml:"encryption_key" json:"encryption_key" usage:"The encryption key used to produce the client token."` - TokenExpiryMs int64 `yaml:"token_expiry_ms" json:"token_expiry_ms" usage:"Token expiry in milliseconds."` + EncryptionKey string `yaml:"encryption_key" json:"encryption_key" usage:"The encryption key used to produce the client token."` + TokenExpirySec int64 `yaml:"token_expiry_sec" json:"token_expiry_sec" usage:"Token expiry in seconds."` } // NewSessionConfig creates a new SessionConfig struct. func NewSessionConfig() *SessionConfig { return &SessionConfig{ - EncryptionKey: "defaultencryptionkey", - TokenExpiryMs: 60000, + EncryptionKey: "defaultencryptionkey", + TokenExpirySec: 60, } } @@ -204,8 +212,8 @@ type SocketConfig struct { Port int `yaml:"port" json:"port" usage:"The port for accepting connections from the client, listening on all interfaces."` MaxMessageSizeBytes int64 `yaml:"max_message_size_bytes" json:"max_message_size_bytes" usage:"Maximum amount of data in bytes allowed to be read from the client socket per message."` WriteWaitMs int `yaml:"write_wait_ms" json:"write_wait_ms" usage:"Time in milliseconds to wait for an ack from the client when writing data."` - PongWaitMs int `yaml:"pong_wait_ms" json:"pong_wait_ms" usage:"Time in milliseconds to wait for a pong message from the client after sending a ping."` - PingPeriodMs int `yaml:"ping_period_ms" json:"ping_period_ms" usage:"Time in milliseconds to wait between client ping messages. This value must be less than the pong_wait_ms."` + PongWaitMs int `yaml:"pong_wait_ms" json:"pong_wait_ms" usage:"Time in milliseconds to wait between pong messages received from the client."` + PingPeriodMs int `yaml:"ping_period_ms" json:"ping_period_ms" usage:"Time in milliseconds to wait between sending ping messages to the client. This value must be less than the pong_wait_ms."` OutgoingQueueSize int `yaml:"outgoing_queue_size" json:"outgoing_queue_size" usage:"The maximum number of messages waiting to be sent to the client. If this is exceeded the client is considered too slow and will disconnect."` SSLCertificate string `yaml:"ssl_certificate" json:"ssl_certificate" usage:"Path to certificate file if you want the server to use SSL directly. Must also supply ssl_private_key"` SSLPrivateKey string `yaml:"ssl_private_key" json:"ssl_private_key" usage:"Path to private key file if you want the server to use SSL directly. Must also supply ssl_certificate"` @@ -228,7 +236,7 @@ func NewSocketConfig() *SocketConfig { // DatabaseConfig is configuration relevant to the Database storage. type DatabaseConfig struct { - Addresses []string `yaml:"address" json:"address" usage:"List of CockroachDB servers (username:password@address:port/dbname)"` + Addresses []string `yaml:"address" json:"address" usage:"List of CockroachDB servers (username:password@address:port/dbname)."` ConnMaxLifetimeMs int `yaml:"conn_max_lifetime_ms" json:"conn_max_lifetime_ms" usage:"Time in milliseconds to reuse a database connection before the connection is killed and a new one is created."` MaxOpenConns int `yaml:"max_open_conns" json:"max_open_conns" usage:"Maximum number of allowed open connections to the database."` MaxIdleConns int `yaml:"max_idle_conns" json:"max_idle_conns" usage:"Maximum number of allowed open but unused connections to the database."` @@ -246,7 +254,7 @@ func NewDatabaseConfig() *DatabaseConfig { // SocialConfig is configuration relevant to the social authentication providers. type SocialConfig struct { - Steam *SocialConfigSteam `yaml:"steam" json:"steam" usage:"Steam configuration"` + Steam *SocialConfigSteam `yaml:"steam" json:"steam" usage:"Steam configuration."` } // SocialConfigSteam is configuration relevant to Steam @@ -268,8 +276,8 @@ func NewSocialConfig() *SocialConfig { // RuntimeConfig is configuration relevant to the Runtime Lua VM. type RuntimeConfig struct { Environment map[string]interface{} `yaml:"env" json:"env"` // Not supported in FlagOverrides. - Path string `yaml:"path" json:"path" usage:"Path of modules for the server to scan."` - HTTPKey string `yaml:"http_key" json:"http_key" usage:"Runtime HTTP Invocation key"` + Path string `yaml:"path" json:"path" usage:"Path for the server to scan for *.lua files."` + HTTPKey string `yaml:"http_key" json:"http_key" usage:"Runtime HTTP Invocation key."` } // NewRuntimeConfig creates a new RuntimeConfig struct. diff --git a/server/match_handler.go b/server/match_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..df97d77c2bb9490b15d53f4a4a0d14db5483e703 --- /dev/null +++ b/server/match_handler.go @@ -0,0 +1,723 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "database/sql" + "fmt" + "github.com/heroiclabs/nakama/rtapi" + "github.com/heroiclabs/nakama/social" + "github.com/pkg/errors" + "github.com/satori/go.uuid" + "github.com/yuin/gopher-lua" + "go.uber.org/zap" + "sync" + "time" +) + +const ( + InputQueueSize = 128 + CallQueueSize = 128 +) + +type MatchDataMessage struct { + UserID uuid.UUID + SessionID uuid.UUID + Username string + Node string + OpCode int64 + Data []byte +} + +type MatchHandler struct { + sync.Mutex + logger *zap.Logger + matchRegistry MatchRegistry + tracker Tracker + router MessageRouter + + // Identification not (directly) controlled by match init. + ID uuid.UUID + Node string + IDStr string + Stream PresenceStream + + // Internal state. + tick lua.LNumber + vm *lua.LState + initFn lua.LValue + joinAttemptFn lua.LValue + leaveFn lua.LValue + loopFn lua.LValue + ctx *lua.LTable + dispatcher *lua.LTable + + // Control elements. + inputCh chan *MatchDataMessage + ticker *time.Ticker + callCh chan func(*MatchHandler) + stopCh chan struct{} + stopped bool + + // Immutable configuration set by match init. + Label string + Rate int + + // Match state. + state lua.LValue +} + +func NewMatchHandler(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, once *sync.Once, id uuid.UUID, node string, name string, params interface{}) (*MatchHandler, error) { + // Set up the Lua VM that will handle this match. + vm := lua.NewState(lua.Options{ + CallStackSize: 1024, + RegistrySize: 1024, + SkipOpenLibs: true, + IncludeGoStackTrace: true, + }) + for name, lib := range stdLibs { + vm.Push(vm.NewFunction(lib)) + vm.Push(lua.LString(name)) + vm.Call(1, 0) + } + nakamaModule := NewNakamaModule(logger, db, config, socialClient, vm, sessionRegistry, matchRegistry, tracker, router, once, nil) + vm.PreloadModule("nakama", nakamaModule.Loader) + + // Create the context to be used throughout this match. + ctx := vm.CreateTable(6, 6) + ctx.RawSetString(__CTX_ENV, ConvertMap(vm, config.GetRuntime().Environment)) + ctx.RawSetString(__CTX_MODE, lua.LString(Match.String())) + ctx.RawSetString(__CTX_MATCH_ID, lua.LString(fmt.Sprintf("%v:%v", id.String(), node))) + ctx.RawSetString(__CTX_MATCH_NODE, lua.LString(node)) + + // Require the match module to load it (and its dependencies) and get its returned value. + req := vm.GetGlobal("require").(*lua.LFunction) + err := vm.GPCall(req.GFunction, lua.LString(name)) + if err != nil { + return nil, fmt.Errorf("error loading match module: %v", err.Error()) + } + + // Extract the expected function references. + var tab *lua.LTable + if t := vm.Get(-1); t.Type() != lua.LTTable { + return nil, errors.New("match module must return a table containing the match callback functions") + } else { + tab = t.(*lua.LTable) + } + initFn := tab.RawGet(lua.LString("match_init")) + if initFn.Type() != lua.LTFunction { + return nil, errors.New("match_init not found or not a function") + } + joinAttemptFn := tab.RawGet(lua.LString("match_join_attempt")) + if joinAttemptFn.Type() != lua.LTFunction { + return nil, errors.New("match_join_attempt not found or not a function") + } + leaveFn := tab.RawGet(lua.LString("match_leave")) + if leaveFn.Type() != lua.LTFunction { + return nil, errors.New("match_leave not found or not a function") + } + loopFn := tab.RawGet(lua.LString("match_loop")) + if loopFn.Type() != lua.LTFunction { + return nil, errors.New("match_loop not found or not a function") + } + + // Run the match_init sequence. + vm.Push(LSentinel) + vm.Push(initFn) + vm.Push(ctx) + if params == nil { + vm.Push(lua.LNil) + } else { + vm.Push(convertValue(vm, params)) + } + + err = vm.PCall(2, lua.MultRet, nil) + if err != nil { + return nil, fmt.Errorf("error running match_init: %v", err.Error()) + } + + // Extract desired label. + label := vm.Get(-1) + if label.Type() == LTSentinel { + return nil, errors.New("match_init returned unexpected third value, must be a label string") + } else if label.Type() != lua.LTString { + return nil, errors.New("match_init returned unexpected third value, must be a label string") + } + vm.Pop(1) + + labelStr := label.String() + if len(labelStr) > 256 { + return nil, errors.New("match_init returned invalid label, must be 256 bytes or less") + } + + // Extract desired tick rate. + rate := vm.Get(-1) + if rate.Type() == LTSentinel { + return nil, errors.New("match_init returned unexpected second value, must be a tick rate number") + } else if rate.Type() != lua.LTNumber { + return nil, errors.New("match_init returned unexpected second value, must be a tick rate number") + } + vm.Pop(1) + + rateInt := int(rate.(lua.LNumber)) + if rateInt > 30 || rateInt < 1 { + return nil, errors.New("match_init returned invalid tick rate, must be between 1 and 30") + } + + // Extract initial state. + state := vm.Get(-1) + if state.Type() == LTSentinel { + return nil, errors.New("match_init returned unexpected first value, must be a state") + } + vm.Pop(1) + + // Drop the sentinel value from the stack. + if sentinel := vm.Get(-1); sentinel.Type() != LTSentinel { + return nil, errors.New("match_init returned too many arguments, must be: state, tick rate number, label string") + } + vm.Pop(1) + + // Add context values only available after match_init completes. + ctx.RawSetString(__CTX_MATCH_LABEL, label) + ctx.RawSetString(__CTX_MATCH_TICK_RATE, rate) + + // Construct the match. + mh := &MatchHandler{ + logger: logger.With(zap.String("mid", id.String())), + matchRegistry: matchRegistry, + tracker: tracker, + router: router, + + ID: id, + Node: node, + IDStr: fmt.Sprintf("%v:%v", id.String(), node), + Stream: PresenceStream{ + Mode: StreamModeMatchAuthoritative, + Subject: id, + Label: node, + }, + + tick: lua.LNumber(0), + vm: vm, + initFn: initFn, + joinAttemptFn: joinAttemptFn, + leaveFn: leaveFn, + loopFn: loopFn, + ctx: ctx, + // Dispatcher below. + + inputCh: make(chan *MatchDataMessage, InputQueueSize), + // Ticker below. + callCh: make(chan func(mh *MatchHandler), CallQueueSize), + stopCh: make(chan struct{}), + stopped: false, + + Label: labelStr, + Rate: rateInt, + + state: state, + } + + // Set up the dispatcher that exposes control functions to the match loop. + mh.dispatcher = vm.SetFuncs(vm.CreateTable(2, 2), map[string]lua.LGFunction{ + "broadcast_message": mh.broadcastMessage, + "match_kick": mh.matchKick, + }) + + // Set up the ticker that governs the match loop. + mh.ticker = time.NewTicker(time.Second / time.Duration(mh.Rate)) + + // Continuously run queued actions until the match stops. + go func() { + for { + select { + case <-mh.stopCh: + // Match has been stopped. + return + case <-mh.ticker.C: + // Tick, queue a match loop invocation. + if !mh.QueueCall(loop) { + return + } + case call := <-mh.callCh: + // An invocation to one of the match functions. + call(mh) + } + } + }() + + mh.logger.Debug("Match started") + + return mh, nil +} + +// Used when an internal match process (or error) requires it to stop. +func (mh *MatchHandler) Stop() { + mh.Close() + mh.matchRegistry.RemoveMatch(mh.ID, mh.Stream) +} + +// Used when the match is closed externally. +func (mh *MatchHandler) Close() { + mh.Lock() + if mh.stopped { + mh.Unlock() + return + } + mh.stopped = true + mh.Unlock() + close(mh.stopCh) + mh.ticker.Stop() +} + +func (mh *MatchHandler) QueueCall(f func(*MatchHandler)) bool { + select { + case mh.callCh <- f: + return true + default: + // Match call queue is full, the handler isn't processing fast enough. + mh.logger.Warn("Match handler call processing too slow, closing match") + mh.Stop() + return false + } +} + +func (mh *MatchHandler) QueueData(m *MatchDataMessage) { + select { + case mh.inputCh <- m: + return + default: + // Match input queue is full, the handler isn't processing fast enough or there's too much incoming data. + mh.logger.Warn("Match handler data processing too slow, dropping data message") + return + } +} + +func loop(mh *MatchHandler) { + mh.Lock() + if mh.stopped { + mh.Unlock() + return + } + mh.Unlock() + + // Drain the input queue into a Lua table. + size := len(mh.inputCh) + input := mh.vm.CreateTable(size, size) + for i := 1; i <= size; i++ { + msg := <-mh.inputCh + + presence := mh.vm.CreateTable(4, 4) + presence.RawSetString("UserId", lua.LString(msg.UserID.String())) + presence.RawSetString("SessionId", lua.LString(msg.SessionID.String())) + presence.RawSetString("Username", lua.LString(msg.Username)) + presence.RawSetString("Node", lua.LString(msg.Node)) + + in := mh.vm.CreateTable(3, 3) + in.RawSetString("Sender", presence) + in.RawSetString("OpCode", lua.LNumber(msg.OpCode)) + if msg.Data != nil { + in.RawSetString("Data", lua.LString(msg.Data)) + } else { + in.RawSetString("Data", lua.LNil) + } + + input.RawSetInt(i, in) + } + + // Execute the match_loop call. + mh.vm.Push(LSentinel) + mh.vm.Push(mh.loopFn) + mh.vm.Push(mh.ctx) + mh.vm.Push(mh.dispatcher) + mh.vm.Push(mh.tick) + mh.vm.Push(mh.state) + mh.vm.Push(input) + + err := mh.vm.PCall(5, lua.MultRet, nil) + if err != nil { + mh.Stop() + mh.logger.Warn("Stopping match after error from match_loop execution", zap.Int("tick", int(mh.tick)), zap.Error(err)) + return + } + + // Extract the resulting state. + state := mh.vm.Get(-1) + if state.Type() == lua.LTNil || state.Type() == LTSentinel { + mh.logger.Debug("Match loop returned nil or no state, stopping match") + mh.Stop() + return + } + mh.vm.Pop(1) + // Check for and remove the sentinel value, will fail if there are any extra return values. + if sentinel := mh.vm.Get(-1); sentinel.Type() != LTSentinel { + mh.logger.Warn("Match loop returned too many values, stopping match") + mh.Stop() + return + } + mh.vm.Pop(1) + + mh.state = state + mh.tick++ +} + +func JoinAttempt(resultCh chan *MatchJoinResult, userID, sessionID uuid.UUID, username, node string) func(mh *MatchHandler) { + return func(mh *MatchHandler) { + mh.Lock() + if mh.stopped { + mh.Unlock() + resultCh <- &MatchJoinResult{Allow: false} + return + } + mh.Unlock() + + presence := mh.vm.CreateTable(4, 4) + presence.RawSetString("UserId", lua.LString(userID.String())) + presence.RawSetString("SessionId", lua.LString(sessionID.String())) + presence.RawSetString("Username", lua.LString(username)) + presence.RawSetString("Node", lua.LString(node)) + + // Execute the match_join_attempt call. + mh.vm.Push(LSentinel) + mh.vm.Push(mh.joinAttemptFn) + mh.vm.Push(mh.ctx) + mh.vm.Push(mh.dispatcher) + mh.vm.Push(mh.tick) + mh.vm.Push(mh.state) + mh.vm.Push(presence) + + err := mh.vm.PCall(5, lua.MultRet, nil) + if err != nil { + mh.Stop() + mh.logger.Warn("Stopping match after error from match_join_attempt execution", zap.Int("tick", int(mh.tick)), zap.Error(err)) + resultCh <- &MatchJoinResult{Allow: false} + return + } + + // Extract the join attempt response. + allow := mh.vm.Get(-1) + if allow.Type() == LTSentinel { + mh.logger.Warn("Match join attempt returned too few values, stopping match - expected: state, join result boolean") + mh.Stop() + resultCh <- &MatchJoinResult{Allow: false} + return + } else if allow.Type() != lua.LTBool { + mh.logger.Warn("Match join attempt returned non-boolean join result, stopping match") + mh.Stop() + resultCh <- &MatchJoinResult{Allow: false} + return + } + mh.vm.Pop(1) + // Extract the resulting state. + state := mh.vm.Get(-1) + if state.Type() == lua.LTNil || state.Type() == LTSentinel { + mh.logger.Debug("Match join attempt returned nil or no state, stopping match") + mh.Stop() + resultCh <- &MatchJoinResult{Allow: false} + return + } + mh.vm.Pop(1) + // Check for and remove the sentinel value, will fail if there are any extra return values. + if sentinel := mh.vm.Get(-1); sentinel.Type() != LTSentinel { + mh.logger.Warn("Match join attempt returned too many values, stopping match") + mh.Stop() + resultCh <- &MatchJoinResult{Allow: false} + return + } + mh.vm.Pop(1) + + mh.state = state + resultCh <- &MatchJoinResult{Allow: lua.LVAsBool(allow), Label: mh.Label} + } +} + +func Leave(leaves []*MatchPresence) func(mh *MatchHandler) { + return func(mh *MatchHandler) { + mh.Lock() + if mh.stopped { + mh.Unlock() + return + } + mh.Unlock() + + size := len(leaves) + presences := mh.vm.CreateTable(size, size) + for i, p := range leaves { + presence := mh.vm.CreateTable(4, 4) + presence.RawSetString("UserId", lua.LString(p.UserID.String())) + presence.RawSetString("SessionId", lua.LString(p.SessionID.String())) + presence.RawSetString("Username", lua.LString(p.Username)) + presence.RawSetString("Node", lua.LString(p.Node)) + + presences.RawSetInt(i+1, presence) + } + + // Execute the match_leave call. + mh.vm.Push(LSentinel) + mh.vm.Push(mh.leaveFn) + mh.vm.Push(mh.ctx) + mh.vm.Push(mh.dispatcher) + mh.vm.Push(mh.tick) + mh.vm.Push(mh.state) + mh.vm.Push(presences) + + err := mh.vm.PCall(5, lua.MultRet, nil) + if err != nil { + mh.Stop() + mh.logger.Warn("Stopping match after error from match_leave execution", zap.Int("tick", int(mh.tick)), zap.Error(err)) + return + } + + // Extract the resulting state. + state := mh.vm.Get(-1) + if state.Type() == lua.LTNil || state.Type() == LTSentinel { + mh.logger.Debug("Match leave returned nil or no state, stopping match") + mh.Stop() + return + } + mh.vm.Pop(1) + // Check for and remove the sentinel value, will fail if there are any extra return values. + if sentinel := mh.vm.Get(-1); sentinel.Type() != LTSentinel { + mh.logger.Warn("Match leave returned too many values, stopping match") + mh.Stop() + return + } + mh.vm.Pop(1) + + mh.state = state + } +} + +func (mh *MatchHandler) broadcastMessage(l *lua.LState) int { + opCode := l.CheckInt64(1) + + var dataBytes []byte + if data := l.Get(2); data.Type() != lua.LTNil { + if data.Type() != lua.LTString { + l.ArgError(2, "expects data to be a string or nil") + return 0 + } + dataBytes = []byte(data.(lua.LString)) + } + + filter := l.OptTable(3, nil) + var presenceIDs []*PresenceID + if filter != nil { + fl := filter.Len() + if fl == 0 { + return 0 + } + presenceIDs = make([]*PresenceID, 0, fl) + conversionError := false + filter.ForEach(func(_, p lua.LValue) { + pt, ok := p.(*lua.LTable) + if !ok { + conversionError = true + l.ArgError(1, "expects a valid set of presences") + return + } + + presenceID := &PresenceID{} + pt.ForEach(func(k, v lua.LValue) { + switch k.String() { + case "SessionId": + sid, err := uuid.FromString(v.String()) + if err != nil { + conversionError = true + l.ArgError(1, "expects each presence to have a valid SessionId") + return + } + presenceID.SessionID = sid + case "Node": + if v.Type() != lua.LTString { + conversionError = true + l.ArgError(1, "expects Node to be string") + return + } + presenceID.Node = v.String() + } + }) + if presenceID.SessionID == uuid.Nil || presenceID.Node == "" { + conversionError = true + l.ArgError(1, "expects each presence to have a valid UserId, SessionId, and Node") + return + } + if conversionError { + return + } + presenceIDs = append(presenceIDs, presenceID) + }) + if conversionError { + return 0 + } + } + + if presenceIDs != nil && len(presenceIDs) == 0 { + // Filter is empty, there are no requested message targets. + return 0 + } + + sender := l.OptTable(4, nil) + var presence *rtapi.StreamPresence + if sender != nil { + presence := &rtapi.StreamPresence{} + conversionError := false + sender.ForEach(func(k, v lua.LValue) { + switch k.String() { + case "UserId": + s := v.String() + _, err := uuid.FromString(s) + if err != nil { + conversionError = true + l.ArgError(4, "expects presence to have a valid UserId") + return + } + presence.UserId = s + case "SessionId": + s := v.String() + _, err := uuid.FromString(s) + if err != nil { + conversionError = true + l.ArgError(4, "expects presence to have a valid SessionId") + return + } + presence.SessionId = s + case "Username": + if v.Type() != lua.LTString { + conversionError = true + l.ArgError(4, "expects Username to be string") + return + } + presence.Username = v.String() + } + }) + if presence.UserId == "" || presence.SessionId == "" || presence.Username == "" { + l.ArgError(4, "expects presence to have a valid UserId, SessionId, and Username") + return 0 + } + if conversionError { + return 0 + } + } + + if presenceIDs != nil { + // Ensure specific presences actually exist to prevent sending bogus messages to arbitrary users. + actualPresenceIDs := mh.tracker.ListPresenceIDByStream(mh.Stream) + for i := 0; i < len(presenceIDs); i++ { + found := false + presenceID := presenceIDs[i] + for j := 0; j < len(actualPresenceIDs); j++ { + if actual := actualPresenceIDs[j]; presenceID.SessionID == actual.SessionID && presenceID.Node == actual.Node { + // If it matches, drop it. + actualPresenceIDs[j] = actualPresenceIDs[len(actualPresenceIDs)-1] + actualPresenceIDs = actualPresenceIDs[:len(actualPresenceIDs)-1] + found = true + break + } + } + if !found { + // If this presence wasn't in the filters, it's not needed. + presenceIDs[i] = presenceIDs[len(presenceIDs)-1] + presenceIDs = presenceIDs[:len(presenceIDs)-1] + i-- + } + } + if len(presenceIDs) == 0 { + // None of the target presenceIDs existed in the list of match members. + return 0 + } + } + + msg := &rtapi.Envelope{Message: &rtapi.Envelope_MatchData{MatchData: &rtapi.MatchData{ + MatchId: mh.IDStr, + Presence: presence, + OpCode: opCode, + Data: dataBytes, + }}} + + if presenceIDs == nil { + mh.router.SendToStream(mh.logger, mh.Stream, msg) + } else { + mh.router.SendToPresenceIDs(mh.logger, presenceIDs, msg) + } + + return 0 +} + +func (mh *MatchHandler) matchKick(l *lua.LState) int { + input := l.OptTable(1, nil) + if input == nil { + return 0 + } + size := input.Len() + if size == 0 { + return 0 + } + + presences := make([]*MatchPresence, 0, size) + conversionError := false + input.ForEach(func(_, p lua.LValue) { + pt, ok := p.(*lua.LTable) + if !ok { + conversionError = true + l.ArgError(1, "expects a valid set of presences") + return + } + + presence := &MatchPresence{} + pt.ForEach(func(k, v lua.LValue) { + switch k.String() { + case "UserId": + uid, err := uuid.FromString(v.String()) + if err != nil { + conversionError = true + l.ArgError(1, "expects each presence to have a valid UserId") + return + } + presence.UserID = uid + case "SessionId": + sid, err := uuid.FromString(v.String()) + if err != nil { + conversionError = true + l.ArgError(1, "expects each presence to have a valid SessionId") + return + } + presence.SessionID = sid + case "Node": + if v.Type() != lua.LTString { + conversionError = true + l.ArgError(1, "expects Node to be string") + return + } + presence.Node = v.String() + } + }) + if presence.UserID == uuid.Nil || presence.SessionID == uuid.Nil || presence.Node == "" { + conversionError = true + l.ArgError(1, "expects each presence to have a valid UserId, SessionId, and Node") + return + } + if conversionError { + return + } + presences = append(presences, presence) + }) + if conversionError { + return 0 + } + + mh.matchRegistry.Kick(mh.Stream, presences) + return 0 +} diff --git a/server/match_registry.go b/server/match_registry.go new file mode 100644 index 0000000000000000000000000000000000000000..f531cba1301039e0aa1d9b4c43bcdbee00491f62 --- /dev/null +++ b/server/match_registry.go @@ -0,0 +1,346 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "database/sql" + "fmt" + "github.com/golang/protobuf/ptypes/wrappers" + "github.com/heroiclabs/nakama/api" + "github.com/heroiclabs/nakama/social" + "github.com/satori/go.uuid" + "github.com/yuin/gopher-lua" + "go.uber.org/zap" + "sync" + "time" +) + +var ( + MatchFilterValue = uint8(0) + MatchFilterPtr = &MatchFilterValue + + MatchFilterAny = map[uint8]*uint8{StreamModeMatchRelayed: MatchFilterPtr, StreamModeMatchAuthoritative: MatchFilterPtr} + MatchFilterRelayed = map[uint8]*uint8{StreamModeMatchRelayed: MatchFilterPtr} + MatchFilterAuthoritative = map[uint8]*uint8{StreamModeMatchAuthoritative: MatchFilterPtr} +) + +type MatchPresence struct { + Node string + UserID uuid.UUID + SessionID uuid.UUID + Username string +} + +type MatchJoinResult struct { + Allow bool + Label string +} + +type MatchRegistry interface { + // Create and start a new match, given a Lua module name. + NewMatch(name string, params interface{}) (*MatchHandler, error) + // Return a match handler by ID, only from the local node. + GetMatch(id uuid.UUID) *MatchHandler + // Remove a tracked match and ensure all its presences are cleaned up. + // Does not ensure the match process itself is no longer running, that must be handled separately. + RemoveMatch(id uuid.UUID, stream PresenceStream) + // List (and optionally filter) currently running matches. + // This can list across both authoritative and relayed matches. + ListMatches(limit int, authoritative *wrappers.BoolValue, label *wrappers.StringValue, minSize *wrappers.Int32Value, maxSize *wrappers.Int32Value) []*api.Match + // Stop the match registry and close all matches it's tracking. + Stop() + + // Pass a user join attempt to a match handler. Returns if the match was found, if the join was accepted, and the match label. + Join(id uuid.UUID, node string, userID, sessionID uuid.UUID, username, fromNode string) (bool, bool, string) + // Notify a match handler that a user has left or disconnected. + // Expects that the caller has already determined the match is hosted on the current node. + Leave(id uuid.UUID, presences []*MatchPresence) + // Called by match handlers to request the removal fo a match participant. + Kick(stream PresenceStream, presences []*MatchPresence) + // Pass a data payload (usually from a user) to the appropriate match handler. + // Assumes that the data sender has already been validated as a match participant before this call. + SendData(id uuid.UUID, node string, userID, sessionID uuid.UUID, username, fromNode string, opCode int64, data []byte) +} + +type LocalMatchRegistry struct { + sync.RWMutex + logger *zap.Logger + db *sql.DB + config Config + socialClient *social.Client + sessionRegistry *SessionRegistry + tracker Tracker + router MessageRouter + stdLibs map[string]lua.LGFunction + modules *sync.Map + once *sync.Once + node string + matches map[uuid.UUID]*MatchHandler +} + +func NewLocalMatchRegistry(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, sessionRegistry *SessionRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, once *sync.Once, node string) MatchRegistry { + return &LocalMatchRegistry{ + logger: logger, + db: db, + config: config, + socialClient: socialClient, + sessionRegistry: sessionRegistry, + tracker: tracker, + router: router, + stdLibs: stdLibs, + once: once, + node: node, + matches: make(map[uuid.UUID]*MatchHandler), + } +} + +func (r *LocalMatchRegistry) NewMatch(name string, params interface{}) (*MatchHandler, error) { + id := uuid.NewV4() + match, err := NewMatchHandler(r.logger, r.db, r.config, r.socialClient, r.sessionRegistry, r, r.tracker, r.router, r.stdLibs, r.once, id, r.node, name, params) + if err != nil { + return nil, err + } + r.Lock() + r.matches[id] = match + r.Unlock() + return match, nil +} + +func (r *LocalMatchRegistry) GetMatch(id uuid.UUID) *MatchHandler { + var mh *MatchHandler + r.RLock() + mh = r.matches[id] + r.RUnlock() + return mh +} + +func (r *LocalMatchRegistry) RemoveMatch(id uuid.UUID, stream PresenceStream) { + r.Lock() + delete(r.matches, id) + r.Unlock() + r.tracker.UntrackByStream(stream) +} + +func (r *LocalMatchRegistry) ListMatches(limit int, authoritative *wrappers.BoolValue, label *wrappers.StringValue, minSize *wrappers.Int32Value, maxSize *wrappers.Int32Value) []*api.Match { + var modes map[uint8]*uint8 + if authoritative == nil { + modes = MatchFilterAny + } else if authoritative.Value { + modes = MatchFilterAuthoritative + } else { + modes = MatchFilterRelayed + } + + // Initial list of candidate matches. + matches := r.tracker.CountByStreamModeFilter(modes) + + // Results. + results := make([]*api.Match, 0, limit) + + // Track authoritative matches that have been checked already, if authoritative results are allowed. + var checked map[uuid.UUID]struct{} + if authoritative == nil || authoritative.Value { + checked = make(map[uuid.UUID]struct{}) + } + + // Maybe filter by label. + for stream, size := range matches { + if stream.Mode == StreamModeMatchRelayed { + if label != nil { + // Any label filter fails for relayed matches. + continue + } + if minSize != nil && minSize.Value > size { + // Too few users. + continue + } + if maxSize != nil && maxSize.Value < size { + // Too many users. + continue + } + + matchID := fmt.Sprintf("%v:", stream.Subject.String()) + results = append(results, &api.Match{ + MatchId: matchID, + Authoritative: false, + // No label. + Size: size, + }) + if len(results) == limit { + break + } + } else if stream.Mode == StreamModeMatchAuthoritative { + // Authoritative matches that have already been checked. + checked[stream.Subject] = struct{}{} + + if minSize != nil && minSize.Value > size { + // Too few users. + continue + } + if maxSize != nil && maxSize.Value < size { + // Too many users. + continue + } + + mh := r.GetMatch(stream.Subject) + if mh == nil || (label != nil && label.Value != mh.Label) { + continue + } + results = append(results, &api.Match{ + MatchId: mh.IDStr, + Authoritative: true, + Label: &wrappers.StringValue{Value: mh.Label}, + Size: size, + }) + if len(results) == limit { + break + } + } else { + r.logger.Warn("Ignoring unknown stream mode in match listing operation", zap.Uint8("mode", stream.Mode)) + } + } + + // Return results here if: + // 1. We have enough results. + // or + // 2. Not enough results, but we're not allowed to return potentially empty authoritative matches. + if len(results) == limit || ((authoritative != nil && !authoritative.Value) || (minSize != nil && minSize.Value > 0)) { + return results + } + + // Otherwise look for empty matches to help fulfil the request, but ensure no duplicates. + r.RLock() + for _, mh := range r.matches { + if _, ok := checked[mh.ID]; ok { + // Already checked and discarded this match for failing a filter, skip it. + continue + } + if label != nil && label.Value != mh.Label { + // Label mismatch. + continue + } + size := int32(r.tracker.CountByStream(mh.Stream)) + if minSize != nil && minSize.Value > size { + // Too few users. + continue + } + if maxSize != nil && maxSize.Value < size { + // Too many users. + continue + } + results = append(results, &api.Match{ + MatchId: mh.IDStr, + Authoritative: true, + Label: &wrappers.StringValue{Value: mh.Label}, + Size: size, + }) + if len(results) == limit { + break + } + } + r.RUnlock() + + return results +} + +func (r *LocalMatchRegistry) Stop() { + r.Lock() + for id, mh := range r.matches { + mh.Close() + delete(r.matches, id) + } + r.Unlock() +} + +func (r *LocalMatchRegistry) Join(id uuid.UUID, node string, userID, sessionID uuid.UUID, username, fromNode string) (bool, bool, string) { + if node != r.node { + return false, false, "" + } + + var mh *MatchHandler + var ok bool + r.RLock() + mh, ok = r.matches[id] + r.RUnlock() + if !ok { + return false, false, "" + } + + resultCh := make(chan *MatchJoinResult, 1) + if !mh.QueueCall(JoinAttempt(resultCh, userID, sessionID, username, fromNode)) { + // The match call queue was full, so will be closed and therefore can't be joined. + return true, false, "" + } + + // Set up a limit to how long the call will wait, default is 10 seconds. + ticker := time.NewTicker(time.Second * 10) + select { + case <-ticker.C: + ticker.Stop() + // The join attempt has timed out, join is assumed to be rejected. + return true, false, "" + case r := <-resultCh: + ticker.Stop() + // The join attempt has returned a result. + return true, r.Allow, r.Label + } +} + +func (r *LocalMatchRegistry) Leave(id uuid.UUID, presences []*MatchPresence) { + var mh *MatchHandler + var ok bool + r.RLock() + mh, ok = r.matches[id] + r.RUnlock() + if !ok { + return + } + + // Doesn't matter if the call queue was full here. If the match is being closed then leaves don't matter anyway. + mh.QueueCall(Leave(presences)) +} + +func (r *LocalMatchRegistry) Kick(stream PresenceStream, presences []*MatchPresence) { + for _, presence := range presences { + if presence.Node != r.node { + continue + } + r.tracker.Untrack(presence.SessionID, stream, presence.UserID) + } +} + +func (r *LocalMatchRegistry) SendData(id uuid.UUID, node string, userID, sessionID uuid.UUID, username, fromNode string, opCode int64, data []byte) { + if node != r.node { + return + } + + var mh *MatchHandler + var ok bool + r.RLock() + mh, ok = r.matches[id] + r.RUnlock() + if !ok { + return + } + + mh.QueueData(&MatchDataMessage{ + UserID: userID, + SessionID: sessionID, + Username: username, + Node: node, + OpCode: opCode, + Data: data, + }) +} diff --git a/server/message_router.go b/server/message_router.go index 80d291b092b3924a838f143dc9a6040e120a9ad0..dd8e42bce816d8efcad263076b068ea7483c8941 100644 --- a/server/message_router.go +++ b/server/message_router.go @@ -16,55 +16,56 @@ package server import ( "github.com/golang/protobuf/jsonpb" - "github.com/golang/protobuf/proto" + "github.com/heroiclabs/nakama/rtapi" "go.uber.org/zap" ) // MessageRouter is responsible for sending a message to a list of presences or to an entire stream. type MessageRouter interface { - SendToPresences(*zap.Logger, []Presence, proto.Message) - SendToStream(*zap.Logger, PresenceStream, proto.Message) + SendToPresenceIDs(*zap.Logger, []*PresenceID, *rtapi.Envelope) + SendToStream(*zap.Logger, PresenceStream, *rtapi.Envelope) } type LocalMessageRouter struct { jsonpbMarshaler *jsonpb.Marshaler - registry *SessionRegistry + sessionRegistry *SessionRegistry tracker Tracker } -func NewLocalMessageRouter(registry *SessionRegistry, tracker Tracker, jsonpbMarshaler *jsonpb.Marshaler) MessageRouter { +func NewLocalMessageRouter(sessionRegistry *SessionRegistry, tracker Tracker, jsonpbMarshaler *jsonpb.Marshaler) MessageRouter { return &LocalMessageRouter{ jsonpbMarshaler: jsonpbMarshaler, - registry: registry, + sessionRegistry: sessionRegistry, tracker: tracker, } } -func (r *LocalMessageRouter) SendToPresences(logger *zap.Logger, presences []Presence, msg proto.Message) { - if len(presences) == 0 { +func (r *LocalMessageRouter) SendToPresenceIDs(logger *zap.Logger, presenceIDs []*PresenceID, envelope *rtapi.Envelope) { + if len(presenceIDs) == 0 { return } - payload, err := r.jsonpbMarshaler.MarshalToString(msg) + payload, err := r.jsonpbMarshaler.MarshalToString(envelope) if err != nil { logger.Error("Could not marshall message to json", zap.Error(err)) return } payloadBytes := []byte(payload) - for _, presence := range presences { - session := r.registry.Get(presence.ID.SessionID) + for _, presenceID := range presenceIDs { + session := r.sessionRegistry.Get(presenceID.SessionID) if session == nil { - logger.Warn("No session to route to", zap.Any("sid", presence.ID.SessionID)) + if logger.Core().Enabled(zap.DebugLevel) { + logger.Debug("No session to route to", zap.Any("sid", presenceID.SessionID)) + } continue } - err := session.SendBytes(payloadBytes) - if err != nil { - logger.Error("Failed to route to", zap.Any("sid", presence.ID.SessionID), zap.Error(err)) + if err := session.SendBytes(payloadBytes); err != nil { + logger.Error("Failed to route to", zap.Any("sid", presenceID.SessionID), zap.Error(err)) } } } -func (r *LocalMessageRouter) SendToStream(logger *zap.Logger, stream PresenceStream, msg proto.Message) { - presences := r.tracker.ListByStream(stream) - r.SendToPresences(logger, presences, msg) +func (r *LocalMessageRouter) SendToStream(logger *zap.Logger, stream PresenceStream, envelope *rtapi.Envelope) { + presenceIDs := r.tracker.ListPresenceIDByStream(stream) + r.SendToPresenceIDs(logger, presenceIDs, envelope) } diff --git a/server/pipeline.go b/server/pipeline.go index 2a3ba25fdd67d6f67bb6b266c5e403d8c9a300e3..b6c8cefca816ac76e34b5eb3ae330a830c8dcb52 100644 --- a/server/pipeline.go +++ b/server/pipeline.go @@ -18,31 +18,38 @@ import ( "database/sql" "fmt" + "errors" "github.com/heroiclabs/nakama/rtapi" "go.uber.org/zap" ) +var ErrPipelineUnrecognizedPayload = errors.New("pipeline received unrecognized payload") + type pipeline struct { - config Config - db *sql.DB - registry *SessionRegistry - tracker Tracker - router MessageRouter - runtimePool *RuntimePool + config Config + db *sql.DB + sessionRegistry *SessionRegistry + matchRegistry MatchRegistry + tracker Tracker + router MessageRouter + runtimePool *RuntimePool + node string } -func NewPipeline(config Config, db *sql.DB, registry *SessionRegistry, tracker Tracker, router MessageRouter, runtimePool *RuntimePool) *pipeline { +func NewPipeline(config Config, db *sql.DB, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, runtimePool *RuntimePool) *pipeline { return &pipeline{ - config: config, - db: db, - registry: registry, - tracker: tracker, - router: router, - runtimePool: runtimePool, + config: config, + db: db, + sessionRegistry: sessionRegistry, + matchRegistry: matchRegistry, + tracker: tracker, + router: router, + runtimePool: runtimePool, + node: config.GetName(), } } -func (p *pipeline) processRequest(logger *zap.Logger, session session, envelope *rtapi.Envelope) { +func (p *pipeline) processRequest(logger *zap.Logger, session session, envelope *rtapi.Envelope) error { if logger.Core().Enabled(zap.DebugLevel) { logger.Debug(fmt.Sprintf("Received %T message", envelope.Message), zap.Any("message", envelope.Message)) } @@ -63,6 +70,9 @@ func (p *pipeline) processRequest(logger *zap.Logger, session session, envelope Code: int32(rtapi.Error_UNRECOGNIZED_PAYLOAD), Message: "Unrecognized payload", }}}) - return + // If we reached this point the envelope was valid but the contents are missing or unknown. + // Usually caused by a version mismatch, and should cause the session making this pipeline request to close. + return ErrPipelineUnrecognizedPayload } + return nil } diff --git a/server/pipeline_match.go b/server/pipeline_match.go index 8aeec417521e8e5593cb283635d95629a40b0ace..2cf59e4e37264fa39f7b5f5beb0b629f0c89e397 100644 --- a/server/pipeline_match.go +++ b/server/pipeline_match.go @@ -15,9 +15,12 @@ package server import ( + "fmt" + "github.com/golang/protobuf/ptypes/wrappers" "github.com/heroiclabs/nakama/rtapi" "github.com/satori/go.uuid" "go.uber.org/zap" + "strings" ) type matchDataFilter struct { @@ -30,10 +33,13 @@ func (p *pipeline) matchCreate(logger *zap.Logger, session session, envelope *rt username := session.Username() - p.tracker.Track(session.ID(), PresenceStream{Mode: StreamModeMatchRelayed, Subject: matchID}, session.UserID(), PresenceMeta{ + if success, _ := p.tracker.Track(session.ID(), PresenceStream{Mode: StreamModeMatchRelayed, Subject: matchID}, session.UserID(), PresenceMeta{ Username: username, Format: session.Format(), - }) + }, false); !success { + // Presence creation was rejected due to `allowIfFirstForSession` flag, session is gone so no need to reply. + return + } self := &rtapi.StreamPresence{ UserId: session.UserID().String(), @@ -42,7 +48,10 @@ func (p *pipeline) matchCreate(logger *zap.Logger, session session, envelope *rt } session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Match{Match: &rtapi.Match{ - MatchId: matchID.String(), + MatchId: fmt.Sprintf("%v:", matchID.String()), + Authoritative: false, + // No label. + Size: 1, Presences: []*rtapi.StreamPresence{self}, Self: self, }}}) @@ -52,12 +61,22 @@ func (p *pipeline) matchJoin(logger *zap.Logger, session session, envelope *rtap m := envelope.GetMatchJoin() var err error var matchID uuid.UUID + var node string var matchIDString string switch m.Id.(type) { case *rtapi.MatchJoin_MatchId: matchIDString = m.GetMatchId() - matchID, err = uuid.FromString(matchIDString) + // Validate the match ID. + matchIDComponents := strings.SplitN(matchIDString, ":", 2) + if len(matchIDComponents) != 2 { + session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ + Code: int32(rtapi.Error_BAD_INPUT), + Message: "Invalid match ID", + }}}) + return + } + matchID, err = uuid.FromString(matchIDComponents[0]) if err != nil { session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ Code: int32(rtapi.Error_BAD_INPUT), @@ -65,8 +84,9 @@ func (p *pipeline) matchJoin(logger *zap.Logger, session session, envelope *rtap }}}) return } + node = matchIDComponents[1] case *rtapi.MatchJoin_Token: - // TODO restore when matchmaking is available + // TODO Restore token-based join behaviour when matchmaking is available. session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ Code: int32(rtapi.Error_BAD_INPUT), Message: "Token-based match join not available", @@ -86,9 +106,16 @@ func (p *pipeline) matchJoin(logger *zap.Logger, session session, envelope *rtap return } - stream := PresenceStream{Mode: StreamModeMatchRelayed, Subject: matchID} + // Decide if it's an authoritative or relayed match. + mode := StreamModeMatchRelayed + if node != "" { + mode = StreamModeMatchAuthoritative + } + + stream := PresenceStream{Mode: mode, Subject: matchID, Label: node} - if !p.tracker.StreamExists(stream) { + if mode == StreamModeMatchRelayed && !p.tracker.StreamExists(stream) { + // Relayed matches must 'exist' by already having some members. session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ Code: int32(rtapi.Error_MATCH_NOT_FOUND), Message: "Match not found", @@ -96,13 +123,50 @@ func (p *pipeline) matchJoin(logger *zap.Logger, session session, envelope *rtap return } - username := session.Username() - - p.tracker.Track(session.ID(), stream, session.UserID(), PresenceMeta{ - Username: username, - Format: session.Format(), - }) + var label *wrappers.StringValue + meta := p.tracker.GetLocalBySessionIDStreamUserID(session.ID(), stream, session.UserID()) + if meta == nil { + username := session.Username() + found := true + allow := true + var l string + // The user is not yet part of the match, attempt to join. + if mode == StreamModeMatchAuthoritative { + // If it's an authoritative match, ask the match handler if it will allow the join. + found, allow, l = p.matchRegistry.Join(matchID, node, session.UserID(), session.ID(), username, p.node) + } + if !found { + // Match did not exist. + session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ + Code: int32(rtapi.Error_MATCH_NOT_FOUND), + Message: "Match join rejected", + }}}) + return + } + if !allow { + // Match exists, but rejected the join. + session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ + Code: int32(rtapi.Error_MATCH_JOIN_REJECTED), + Message: "Match join rejected", + }}}) + return + } + if mode == StreamModeMatchAuthoritative { + // If we've reached here, it was an accepted authoritative join. + label = &wrappers.StringValue{Value: l} + } + m := PresenceMeta{ + Username: username, + Format: session.Format(), + } + if success, _ := p.tracker.Track(session.ID(), stream, session.UserID(), m, false); !success { + // Presence creation was rejected due to `allowIfFirstForSession` flag, session is gone so no need to reply. + return + } + meta = &m + } + // Whether the user has just (successfully) joined the match or was already a member, return the match info anyway. ps := p.tracker.ListByStream(stream) presences := make([]*rtapi.StreamPresence, 0, len(ps)) for _, p := range ps { @@ -115,37 +179,47 @@ func (p *pipeline) matchJoin(logger *zap.Logger, session session, envelope *rtap self := &rtapi.StreamPresence{ UserId: session.UserID().String(), SessionId: session.ID().String(), - Username: username, + Username: meta.Username, } session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Match{Match: &rtapi.Match{ - MatchId: matchIDString, - Presences: presences, - Self: self, + MatchId: matchIDString, + Authoritative: mode == StreamModeMatchAuthoritative, + Label: label, + Size: int32(len(presences)), + Presences: presences, + Self: self, }}}) } func (p *pipeline) matchLeave(logger *zap.Logger, session session, envelope *rtapi.Envelope) { - matchIDString := envelope.GetMatchLeave().MatchId - matchID, err := uuid.FromString(matchIDString) - if err != nil { + // Validate the match ID. + matchIDComponents := strings.SplitN(envelope.GetMatchLeave().MatchId, ":", 2) + if len(matchIDComponents) != 2 { session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ Code: int32(rtapi.Error_BAD_INPUT), Message: "Invalid match ID", }}}) return } - - stream := PresenceStream{Mode: StreamModeMatchRelayed, Subject: matchID} - - if p.tracker.GetLocalBySessionIDStreamUserID(session.ID(), stream, session.UserID()) == nil { + matchID, err := uuid.FromString(matchIDComponents[0]) + if err != nil { session.Send(&rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ - Code: int32(rtapi.Error_MATCH_NOT_FOUND), - Message: "Match not found", + Code: int32(rtapi.Error_BAD_INPUT), + Message: "Invalid match ID", }}}) return } + // Decide if it's an authoritative or relayed match. + mode := StreamModeMatchRelayed + if matchIDComponents[1] != "" { + mode = StreamModeMatchAuthoritative + } + + // Check and drop the presence if possible, will always succeed. + stream := PresenceStream{Mode: mode, Subject: matchID, Label: matchIDComponents[1]} + p.tracker.Untrack(session.ID(), stream, session.UserID()) session.Send(&rtapi.Envelope{Cid: envelope.Cid}) @@ -153,12 +227,29 @@ func (p *pipeline) matchLeave(logger *zap.Logger, session session, envelope *rta func (p *pipeline) matchDataSend(logger *zap.Logger, session session, envelope *rtapi.Envelope) { incoming := envelope.GetMatchDataSend() - matchIDString := incoming.MatchId - matchID, err := uuid.FromString(matchIDString) + + // Validate the match ID. + matchIDComponents := strings.SplitN(incoming.MatchId, ":", 2) + if len(matchIDComponents) != 2 { + return + } + matchID, err := uuid.FromString(matchIDComponents[0]) if err != nil { return } + // If it's an authoritative match pass the data to the match handler. + if matchIDComponents[1] != "" { + if p.tracker.GetLocalBySessionIDStreamUserID(session.ID(), PresenceStream{Mode: StreamModeMatchAuthoritative, Subject: matchID, Label: matchIDComponents[1]}, session.UserID()) == nil { + // User is not part of the match. + return + } + + p.matchRegistry.SendData(matchID, matchIDComponents[1], session.UserID(), session.ID(), session.Username(), p.node, incoming.OpCode, incoming.Data) + return + } + + // Parse any filters. var filters []*matchDataFilter if len(incoming.Presences) != 0 { filters = make([]*matchDataFilter, len(incoming.Presences)) @@ -175,19 +266,20 @@ func (p *pipeline) matchDataSend(logger *zap.Logger, session session, envelope * } } + // If it was a relayed match, proceed with filter and data routing logic. stream := PresenceStream{Mode: StreamModeMatchRelayed, Subject: matchID} - ps := p.tracker.ListByStream(stream) - if len(ps) == 0 { + presenceIDs := p.tracker.ListPresenceIDByStream(stream) + if len(presenceIDs) == 0 { return } senderFound := false - for i := 0; i < len(ps); i++ { - p := ps[i] - if p.ID.SessionID == session.ID() && p.UserID == session.UserID() { + for i := 0; i < len(presenceIDs); i++ { + presenceID := presenceIDs[i] + if presenceID.SessionID == session.ID() { // Don't echo back to sender. - ps[i] = ps[len(ps)-1] - ps = ps[:len(ps)-1] + presenceIDs[i] = presenceIDs[len(presenceIDs)-1] + presenceIDs = presenceIDs[:len(presenceIDs)-1] senderFound = true if filters == nil { break @@ -198,7 +290,7 @@ func (p *pipeline) matchDataSend(logger *zap.Logger, session session, envelope * // Check if this presence is specified in the filters. filterFound := false for j := 0; j < len(filters); j++ { - if filter := filters[j]; p.ID.SessionID == filter.sessionID && p.UserID == filter.userID { + if filter := filters[j]; presenceID.SessionID == filter.sessionID { // If a filter matches, drop it. filters[j] = filters[len(filters)-1] filters = filters[:len(filters)-1] @@ -208,8 +300,8 @@ func (p *pipeline) matchDataSend(logger *zap.Logger, session session, envelope * } if !filterFound { // If this presence wasn't in the filters, it's not needed. - ps[i] = ps[len(ps)-1] - ps = ps[:len(ps)-1] + presenceIDs[i] = presenceIDs[len(presenceIDs)-1] + presenceIDs = presenceIDs[:len(presenceIDs)-1] i-- } } @@ -221,12 +313,12 @@ func (p *pipeline) matchDataSend(logger *zap.Logger, session session, envelope * } // Check if there are any recipients left. - if len(ps) == 0 { + if len(presenceIDs) == 0 { return } outgoing := &rtapi.Envelope{Message: &rtapi.Envelope_MatchData{MatchData: &rtapi.MatchData{ - MatchId: matchIDString, + MatchId: incoming.MatchId, Presence: &rtapi.StreamPresence{ UserId: session.UserID().String(), SessionId: session.ID().String(), @@ -236,5 +328,5 @@ func (p *pipeline) matchDataSend(logger *zap.Logger, session session, envelope * Data: incoming.Data, }}} - p.router.SendToPresences(logger, ps, outgoing) + p.router.SendToPresenceIDs(logger, presenceIDs, outgoing) } diff --git a/server/pipeline_rpc.go b/server/pipeline_rpc.go index 807a5766ed73e7b21c195dff1aa3cc65c6f3a26f..1b8c5b4b9fd3f8a98e8a79346b5112d7f3b0c05c 100644 --- a/server/pipeline_rpc.go +++ b/server/pipeline_rpc.go @@ -54,7 +54,7 @@ func (p *pipeline) rpc(logger *zap.Logger, session session, envelope *rtapi.Enve return } - result, fnErr := runtime.InvokeFunctionRPC(lf, session.UserID().String(), session.Username(), session.Expiry(), session.ID().String(), rpcMessage.Payload) + result, fnErr, _ := runtime.InvokeFunctionRPC(lf, session.UserID().String(), session.Username(), session.Expiry(), session.ID().String(), rpcMessage.Payload) p.runtimePool.Put(runtime) if fnErr != nil { logger.Error("Runtime RPC function caused an error", zap.String("id", rpcMessage.Id), zap.Error(fnErr)) diff --git a/server/runtime.go b/server/runtime.go index 54f65e67318764052d327239984b3a1a99ce2c53..eb1afcbc3cb129219ad6043a7acfec271cc2e055 100644 --- a/server/runtime.go +++ b/server/runtime.go @@ -15,29 +15,31 @@ package server import ( - "os" - "path/filepath" - "errors" - "strings" - "database/sql" "bytes" - "io/ioutil" "sync" "context" + "github.com/heroiclabs/nakama/social" "github.com/yuin/gopher-lua" "go.uber.org/zap" - "github.com/heroiclabs/nakama/social" + "google.golang.org/grpc/codes" ) -const ( - __nakamaReturnValue = "__nakama_return_flag__" -) +const LTSentinel = lua.LValueType(-1) + +type LSentinelType struct { + lua.LNilType +} + +func (s *LSentinelType) String() string { return "" } +func (s *LSentinelType) Type() lua.LValueType { return LTSentinel } + +var LSentinel = lua.LValue(&LSentinelType{}) type RuntimeModule struct { name string @@ -46,92 +48,26 @@ type RuntimeModule struct { } type RuntimePool struct { - once *sync.Once // Used to govern once-per-server-start executions. regRPC map[string]struct{} - stdLibs map[string]lua.LGFunction modules *sync.Map pool *sync.Pool } -func NewRuntimePool(logger *zap.Logger, multiLogger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, registry *SessionRegistry, tracker Tracker, router MessageRouter) (*RuntimePool, error) { - runtimeConfig := config.GetRuntime() - if err := os.MkdirAll(runtimeConfig.Path, os.ModePerm); err != nil { - return nil, err - } - - rp := &RuntimePool{ - once: &sync.Once{}, - regRPC: make(map[string]struct{}), - modules: new(sync.Map), - stdLibs: map[string]lua.LGFunction{ - lua.LoadLibName: lua.OpenPackage, - lua.BaseLibName: lua.OpenBase, - lua.TabLibName: lua.OpenTable, - lua.OsLibName: OpenOs, - lua.StringLibName: lua.OpenString, - lua.MathLibName: lua.OpenMath, - }, - } - - // Override before Package library is invoked. - lua.LuaLDir = runtimeConfig.Path - lua.LuaPathDefault = lua.LuaLDir + "/?.lua;" + lua.LuaLDir + "/?/init.lua" - os.Setenv(lua.LuaPath, lua.LuaPathDefault) - - logger.Info("Initialising modules", zap.String("path", lua.LuaLDir)) - modulePaths := make([]string, 0) - if err := filepath.Walk(lua.LuaLDir, func(path string, f os.FileInfo, err error) error { - if err != nil { - logger.Error("Could not read module", zap.Error(err)) - return err - } else if !f.IsDir() { - if strings.ToLower(filepath.Ext(path)) == ".lua" { - var content []byte - if content, err = ioutil.ReadFile(path); err != nil { - logger.Error("Could not read module", zap.String("path", path), zap.Error(err)) - return err +func NewRuntimePool(logger, multiLogger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, modules *sync.Map, regRPC map[string]struct{}, once *sync.Once) *RuntimePool { + return &RuntimePool{ + regRPC: regRPC, + modules: modules, + pool: &sync.Pool{ + New: func() interface{} { + r, err := newVM(logger, db, config, socialClient, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, once, nil) + if err != nil { + multiLogger.Fatal("Failed initializing runtime.", zap.Error(err)) } - relPath, _ := filepath.Rel(lua.LuaLDir, path) - name := strings.TrimSuffix(relPath, filepath.Ext(relPath)) - // Make paths Lua friendly. - name = strings.Replace(name, "/", ".", -1) - rp.modules.Store(path, &RuntimeModule{ - name: name, - path: path, - content: content, - }) - modulePaths = append(modulePaths, relPath) - } - } - return nil - }); err != nil { - logger.Error("Failed to list modules", zap.Error(err)) - return nil, err - } - - multiLogger.Info("Evaluating modules", zap.Int("count", len(modulePaths)), zap.Strings("modules", modulePaths)) - r, err := rp.newVM(logger, db, config, socialClient, registry, tracker, router, func(id string) { - rp.regRPC[id] = struct{}{} - logger.Info("Registered RPC function invocation", zap.String("id", id)) - }) - if err != nil { - return nil, err - } - multiLogger.Info("Modules loaded") - r.Stop() - - rp.pool = &sync.Pool{ - New: func() interface{} { - r, err := rp.newVM(logger, db, config, socialClient, registry, tracker, router, nil) - if err != nil { - multiLogger.Fatal("Failed initializing runtime.", zap.Error(err)) - } - // TODO find a way to run r.Stop() when the pool discards this runtime. - return r + // TODO find a way to run r.Stop() when the pool discards this runtime. + return r + }, }, } - - return rp, nil } func (rp *RuntimePool) HasRPC(id string) bool { @@ -147,7 +83,7 @@ func (rp *RuntimePool) Put(r *Runtime) { rp.pool.Put(r) } -func (rp *RuntimePool) newVM(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, registry *SessionRegistry, tracker Tracker, router MessageRouter, announceRPC func(string)) (*Runtime, error) { +func newVM(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, modules *sync.Map, once *sync.Once, announceRPC func(string)) (*Runtime, error) { // Initialize a one-off runtime to ensure startup code runs and modules are valid. vm := lua.NewState(lua.Options{ CallStackSize: 1024, @@ -155,12 +91,12 @@ func (rp *RuntimePool) newVM(logger *zap.Logger, db *sql.DB, config Config, soci SkipOpenLibs: true, IncludeGoStackTrace: true, }) - for name, lib := range rp.stdLibs { + for name, lib := range stdLibs { vm.Push(vm.NewFunction(lib)) vm.Push(lua.LString(name)) vm.Call(1, 0) } - nakamaModule := NewNakamaModule(logger, db, config, socialClient, vm, registry, tracker, router, rp.once, announceRPC) + nakamaModule := NewNakamaModule(logger, db, config, socialClient, vm, sessionRegistry, matchRegistry, tracker, router, once, announceRPC) vm.PreloadModule("nakama", nakamaModule.Loader) r := &Runtime{ logger: logger, @@ -168,13 +104,13 @@ func (rp *RuntimePool) newVM(logger *zap.Logger, db *sql.DB, config Config, soci luaEnv: ConvertMap(vm, config.GetRuntime().Environment), } - modules := make([]*RuntimeModule, 0) - rp.modules.Range(func(key interface{}, value interface{}) bool { - modules = append(modules, value.(*RuntimeModule)) + mods := make([]*RuntimeModule, 0) + modules.Range(func(key interface{}, value interface{}) bool { + mods = append(mods, value.(*RuntimeModule)) return true }) - return r, r.loadModules(modules) + return r, r.loadModules(mods) } type Runtime struct { @@ -261,7 +197,7 @@ func (r *Runtime) GetRuntimeCallback(e ExecutionMode, key string) *lua.LFunction return nil } -func (r *Runtime) InvokeFunctionRPC(fn *lua.LFunction, uid string, username string, sessionExpiry int64, sid string, payload string) (string, error) { +func (r *Runtime) InvokeFunctionRPC(fn *lua.LFunction, uid string, username string, sessionExpiry int64, sid string, payload string) (string, error, codes.Code) { l, _ := r.NewStateThread() defer l.Close() @@ -271,22 +207,22 @@ func (r *Runtime) InvokeFunctionRPC(fn *lua.LFunction, uid string, username stri lv = lua.LString(payload) } - retValue, err := r.invokeFunction(l, fn, ctx, lv) + retValue, err, code := r.invokeFunction(l, fn, ctx, lv) if err != nil { - return "", err + return "", err, code } if retValue == nil || retValue == lua.LNil { - return "", nil + return "", nil, 0 } else if retValue.Type() == lua.LTString { - return retValue.String(), nil + return retValue.String(), nil, 0 } - return "", errors.New("runtime function returned invalid data - only allowed one return value of type String/Byte") + return "", errors.New("runtime function returned invalid data - only allowed one return value of type String/Byte"), codes.Internal } -func (r *Runtime) invokeFunction(l *lua.LState, fn *lua.LFunction, ctx *lua.LTable, payload lua.LValue) (lua.LValue, error) { - l.Push(lua.LString(__nakamaReturnValue)) +func (r *Runtime) invokeFunction(l *lua.LState, fn *lua.LFunction, ctx *lua.LTable, payload lua.LValue) (lua.LValue, error, codes.Code) { + l.Push(LSentinel) l.Push(fn) nargs := 1 @@ -299,15 +235,53 @@ func (r *Runtime) invokeFunction(l *lua.LState, fn *lua.LFunction, ctx *lua.LTab err := l.PCall(nargs, lua.MultRet, nil) if err != nil { - return nil, err + // Unwind the stack up to and including our sentinel value, effectively discarding any other returned parameters. + for { + v := l.Get(-1) + l.Pop(1) + if v.Type() == LTSentinel { + break + } + } + + if apiError, ok := err.(*lua.ApiError); ok && apiError.Object.Type() == lua.LTTable { + t := apiError.Object.(*lua.LTable) + switch t.Len() { + case 0: + return nil, err, codes.Internal + case 1: + apiError.Object = t.RawGetInt(1) + return nil, err, codes.Internal + default: + // Ignore everything beyond the first 2 params, if there are more. + apiError.Object = t.RawGetInt(1) + code := codes.Internal + if c := t.RawGetInt(2); c.Type() == lua.LTNumber { + code = codes.Code(c.(lua.LNumber)) + } + return nil, err, code + } + } + + return nil, err, codes.Internal } retValue := l.Get(-1) - if retValue.Type() == lua.LTString && lua.LVAsString(retValue) == __nakamaReturnValue { - return nil, nil + l.Pop(1) + if retValue.Type() == LTSentinel { + return nil, nil, 0 + } + + // Unwind the stack up to and including our sentinel value, effectively discarding any other returned parameters. + for { + v := l.Get(-1) + l.Pop(1) + if v.Type() == LTSentinel { + break + } } - return retValue, nil + return retValue, nil, 0 } func (r *Runtime) Stop() { diff --git a/server/runtime_loadlib.go b/server/runtime_loadlib.go new file mode 100644 index 0000000000000000000000000000000000000000..d45c89995cdf463668d26b4d844913ee7daf17ea --- /dev/null +++ b/server/runtime_loadlib.go @@ -0,0 +1,131 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Yusuke Inuzuka +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package server + +import ( + "bytes" + "fmt" + "github.com/yuin/gopher-lua" + "os" + "path/filepath" + "strings" + "sync" +) + +const emptyLString lua.LString = lua.LString("") + +func loGetPath(env string, defpath string) string { + path := os.Getenv(env) + if len(path) == 0 { + path = defpath + } + path = strings.Replace(path, ";;", ";"+defpath+";", -1) + if os.PathSeparator != '/' { + dir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + panic(err) + } + path = strings.Replace(path, "!", dir, -1) + } + return path +} + +func OpenPackage(modules *sync.Map) func(L *lua.LState) int { + return func(L *lua.LState) int { + loLoaderCache := func(L *lua.LState) int { + name := L.CheckString(1) + m, ok := modules.Load(name) + if !ok { + L.Push(lua.LString(fmt.Sprintf("no cached module '%s'", name))) + return 1 + } + module, ok := m.(*RuntimeModule) + if !ok { + L.Push(lua.LString(fmt.Sprintf("invalid cached module '%s'", name))) + return 1 + } + fn, err := L.Load(bytes.NewReader(module.content), module.path) + if err != nil { + L.RaiseError(err.Error()) + } + L.Push(fn) + return 1 + } + + packagemod := L.RegisterModule(lua.LoadLibName, loFuncs) + + L.SetField(packagemod, "preload", L.NewTable()) + + loaders := L.CreateTable(2, 0) + L.RawSetInt(loaders, 1, L.NewFunction(loLoaderPreload)) + L.RawSetInt(loaders, 2, L.NewFunction(loLoaderCache)) + L.SetField(packagemod, "loaders", loaders) + L.SetField(L.Get(lua.RegistryIndex), "_LOADERS", loaders) + + loaded := L.NewTable() + L.SetField(packagemod, "loaded", loaded) + L.SetField(L.Get(lua.RegistryIndex), "_LOADED", loaded) + + L.SetField(packagemod, "path", lua.LString(loGetPath(lua.LuaPath, lua.LuaPathDefault))) + L.SetField(packagemod, "cpath", emptyLString) + + L.Push(packagemod) + return 1 + } +} + +var loFuncs = map[string]lua.LGFunction{ + "loadlib": loLoadLib, + "seeall": loSeeAll, +} + +func loLoaderPreload(L *lua.LState) int { + name := L.CheckString(1) + preload := L.GetField(L.GetField(L.Get(lua.EnvironIndex), "package"), "preload") + if _, ok := preload.(*lua.LTable); !ok { + L.RaiseError("package.preload must be a table") + } + lv := L.GetField(preload, name) + if lv == lua.LNil { + L.Push(lua.LString(fmt.Sprintf("no field package.preload['%s']", name))) + return 1 + } + L.Push(lv) + return 1 +} + +func loLoadLib(L *lua.LState) int { + L.RaiseError("loadlib is not supported") + return 0 +} + +func loSeeAll(L *lua.LState) int { + mod := L.CheckTable(1) + mt := L.GetMetatable(mod) + if mt == lua.LNil { + mt = L.CreateTable(0, 1) + L.SetMetatable(mod, mt) + } + L.SetField(mt, "__index", L.Get(lua.GlobalsIndex)) + return 0 +} diff --git a/server/runtime_lua_context.go b/server/runtime_lua_context.go index 22b1e2bab5ce2444b5dcb1b5e6a8711f9fe1650c..3a2ebce93903973d984b133c0a854cf0dabc52e9 100644 --- a/server/runtime_lua_context.go +++ b/server/runtime_lua_context.go @@ -24,12 +24,15 @@ type ExecutionMode int const ( RPC ExecutionMode = iota + Match ) func (e ExecutionMode) String() string { switch e { case RPC: return "rpc" + case Match: + return "match" } return "" @@ -42,10 +45,22 @@ const ( __CTX_USERNAME = "Username" __CTX_USER_SESSION_EXP = "UserSessionExp" __CTX_SESSION_ID = "SessionId" + __CTX_MATCH_ID = "MatchId" + __CTX_MATCH_NODE = "MatchNode" + __CTX_MATCH_LABEL = "MatchLabel" + __CTX_MATCH_TICK_RATE = "MatchTickRate" ) func NewLuaContext(l *lua.LState, env *lua.LTable, mode ExecutionMode, uid string, username string, sessionExpiry int64, sid string) *lua.LTable { - lt := l.NewTable() + size := 2 + if uid != "" { + size += 3 + if sid != "" { + size++ + } + } + + lt := l.CreateTable(size, size) lt.RawSetString(__CTX_ENV, env) lt.RawSetString(__CTX_MODE, lua.LString(mode.String())) @@ -53,16 +68,17 @@ func NewLuaContext(l *lua.LState, env *lua.LTable, mode ExecutionMode, uid strin lt.RawSetString(__CTX_USER_ID, lua.LString(uid)) lt.RawSetString(__CTX_USERNAME, lua.LString(username)) lt.RawSetString(__CTX_USER_SESSION_EXP, lua.LNumber(sessionExpiry)) - } - if sid != "" { - lt.RawSetString(__CTX_SESSION_ID, lua.LString(sid)) + if sid != "" { + lt.RawSetString(__CTX_SESSION_ID, lua.LString(sid)) + } } return lt } func ConvertMap(l *lua.LState, data map[string]interface{}) *lua.LTable { - lt := l.NewTable() + size := len(data) + lt := l.CreateTable(size, size) for k, v := range data { lt.RawSetString(k, convertValue(l, v)) @@ -108,7 +124,8 @@ func convertValue(l *lua.LState, val interface{}) lua.LValue { case map[string]interface{}: return ConvertMap(l, v) case []interface{}: - lt := l.NewTable() + size := len(val.([]interface{})) + lt := l.CreateTable(size, size) for k, v := range v { lt.RawSetInt(k+1, convertValue(l, v)) } diff --git a/server/runtime_module_cache.go b/server/runtime_module_cache.go new file mode 100644 index 0000000000000000000000000000000000000000..75c3b43f7107d2bdd2e459bb6ec430937b517b7d --- /dev/null +++ b/server/runtime_module_cache.go @@ -0,0 +1,101 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "database/sql" + "github.com/heroiclabs/nakama/social" + "github.com/yuin/gopher-lua" + "go.uber.org/zap" + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" +) + +func LoadRuntimeModules(logger, multiLogger *zap.Logger, config Config) (map[string]lua.LGFunction, *sync.Map, error) { + runtimeConfig := config.GetRuntime() + if err := os.MkdirAll(runtimeConfig.Path, os.ModePerm); err != nil { + return nil, nil, err + } + + modules := new(sync.Map) + + // Override before Package library is invoked. + lua.LuaLDir = runtimeConfig.Path + lua.LuaPathDefault = lua.LuaLDir + "/?.lua;" + lua.LuaLDir + "/?/init.lua" + os.Setenv(lua.LuaPath, lua.LuaPathDefault) + + logger.Info("Initialising modules", zap.String("path", lua.LuaLDir)) + modulePaths := make([]string, 0) + if err := filepath.Walk(lua.LuaLDir, func(path string, f os.FileInfo, err error) error { + if err != nil { + logger.Error("Could not read module", zap.Error(err)) + return err + } else if !f.IsDir() { + if strings.ToLower(filepath.Ext(path)) == ".lua" { + var content []byte + if content, err = ioutil.ReadFile(path); err != nil { + logger.Error("Could not read module", zap.String("path", path), zap.Error(err)) + return err + } + relPath, _ := filepath.Rel(lua.LuaLDir, path) + name := strings.TrimSuffix(relPath, filepath.Ext(relPath)) + // Make paths Lua friendly. + name = strings.Replace(name, "/", ".", -1) + modules.Store(name, &RuntimeModule{ + name: name, + path: path, + content: content, + }) + modulePaths = append(modulePaths, relPath) + } + } + return nil + }); err != nil { + logger.Error("Failed to list modules", zap.Error(err)) + return nil, nil, err + } + + stdLibs := map[string]lua.LGFunction{ + lua.LoadLibName: OpenPackage(modules), + lua.BaseLibName: lua.OpenBase, + lua.TabLibName: lua.OpenTable, + lua.OsLibName: OpenOs, + lua.StringLibName: lua.OpenString, + lua.MathLibName: lua.OpenMath, + } + + multiLogger.Info("Found modules", zap.Int("count", len(modulePaths)), zap.Strings("modules", modulePaths)) + + return stdLibs, modules, nil +} + +func ValidateRuntimeModules(logger, multiLogger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, modules *sync.Map, once *sync.Once) (map[string]struct{}, error) { + regRPC := make(map[string]struct{}) + multiLogger.Info("Evaluating modules") + r, err := newVM(logger, db, config, socialClient, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, once, func(id string) { + regRPC[id] = struct{}{} + logger.Info("Registered RPC function invocation", zap.String("id", id)) + }) + if err != nil { + return nil, err + } + multiLogger.Info("Modules loaded") + r.Stop() + + return regRPC, nil +} diff --git a/server/runtime_nakama_module.go b/server/runtime_nakama_module.go index d7e5d4259a8fb30b98be9914479be3b99af8f4b1..ba3be474187d81d7fb10ecb1101a3692b57c35a4 100644 --- a/server/runtime_nakama_module.go +++ b/server/runtime_nakama_module.go @@ -40,6 +40,7 @@ import ( "crypto/hmac" "crypto/sha256" "github.com/golang/protobuf/ptypes/timestamp" + "github.com/golang/protobuf/ptypes/wrappers" "github.com/gorhill/cronexpr" "github.com/heroiclabs/nakama/api" "github.com/heroiclabs/nakama/rtapi" @@ -57,32 +58,34 @@ type Callbacks struct { } type NakamaModule struct { - logger *zap.Logger - db *sql.DB - config Config - socialClient *social.Client - registry *SessionRegistry - tracker Tracker - router MessageRouter - once *sync.Once - announceRPC func(string) - client *http.Client + logger *zap.Logger + db *sql.DB + config Config + socialClient *social.Client + sessionRegistry *SessionRegistry + matchRegistry MatchRegistry + tracker Tracker + router MessageRouter + once *sync.Once + announceRPC func(string) + client *http.Client } -func NewNakamaModule(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, l *lua.LState, registry *SessionRegistry, tracker Tracker, router MessageRouter, once *sync.Once, announceRPC func(string)) *NakamaModule { +func NewNakamaModule(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, l *lua.LState, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, once *sync.Once, announceRPC func(string)) *NakamaModule { l.SetContext(context.WithValue(context.Background(), CALLBACKS, &Callbacks{ RPC: make(map[string]*lua.LFunction), })) return &NakamaModule{ - logger: logger, - db: db, - config: config, - socialClient: socialClient, - registry: registry, - tracker: tracker, - router: router, - once: once, - announceRPC: announceRPC, + logger: logger, + db: db, + config: config, + socialClient: socialClient, + sessionRegistry: sessionRegistry, + matchRegistry: matchRegistry, + tracker: tracker, + router: router, + once: once, + announceRPC: announceRPC, client: &http.Client{ Timeout: 5 * time.Second, }, @@ -90,7 +93,7 @@ func NewNakamaModule(logger *zap.Logger, db *sql.DB, config Config, socialClient } func (n *NakamaModule) Loader(l *lua.LState) int { - mod := l.SetFuncs(l.NewTable(), map[string]lua.LGFunction{ + functions := map[string]lua.LGFunction{ "sql_exec": n.sqlExec, "sql_query": n.sqlQuery, "uuid_v4": n.uuidV4, @@ -128,12 +131,15 @@ func (n *NakamaModule) Loader(l *lua.LState) int { "stream_count": n.streamCount, "stream_close": n.streamClose, "stream_send": n.streamSend, + "match_create": n.matchCreate, + "match_list": n.matchList, "register_rpc": n.registerRPC, "notification_send": n.notificationSend, "notifications_send": n.notificationsSend, "wallet_write": n.walletWrite, // "run_once": n.runOnce, - }) + } + mod := l.SetFuncs(l.CreateTable(len(functions), len(functions)), functions) l.Push(mod) return 1 @@ -219,9 +225,9 @@ func (n *NakamaModule) sqlQuery(l *lua.LState) int { return 0 } - rt := l.NewTable() + rt := l.CreateTable(len(resultRows), len(resultRows)) for i, r := range resultRows { - rowTable := l.NewTable() + rowTable := l.CreateTable(resultColumnCount, resultColumnCount) for j, col := range resultColumns { rowTable.RawSetString(col, convertValue(l, r[j])) } @@ -372,7 +378,13 @@ func (n *NakamaModule) base64Encode(l *lua.LState) int { return 0 } - output := base64.StdEncoding.EncodeToString([]byte(input)) + padding := l.OptBool(2, true) + + e := base64.StdEncoding + if !padding { + e = base64.RawStdEncoding + } + output := e.EncodeToString([]byte(input)) l.Push(lua.LString(output)) return 1 } @@ -384,7 +396,16 @@ func (n *NakamaModule) base64Decode(l *lua.LState) int { return 0 } - output, err := base64.RawStdEncoding.DecodeString(input) + padding := l.OptBool(2, false) + + if !padding { + // Pad string up to length multiple of 4 if needed to effectively make padding optional. + if maybePad := len(input) % 4; maybePad != 0 { + input += strings.Repeat("=", 4-maybePad) + } + } + + output, err := base64.StdEncoding.DecodeString(input) if err != nil { l.RaiseError("not a valid base64 string: %v", err.Error()) return 0 @@ -401,7 +422,13 @@ func (n *NakamaModule) base64URLEncode(l *lua.LState) int { return 0 } - output := base64.URLEncoding.EncodeToString([]byte(input)) + padding := l.OptBool(2, true) + + e := base64.URLEncoding + if !padding { + e = base64.RawURLEncoding + } + output := e.EncodeToString([]byte(input)) l.Push(lua.LString(output)) return 1 } @@ -413,7 +440,16 @@ func (n *NakamaModule) base64URLDecode(l *lua.LState) int { return 0 } - output, err := base64.RawURLEncoding.DecodeString(input) + padding := l.OptBool(2, false) + + if !padding { + // Pad string up to length multiple of 4 if needed to effectively make padding optional. + if maybePad := len(input) % 4; maybePad != 0 { + input += strings.Repeat("=", 4-maybePad) + } + } + + output, err := base64.URLEncoding.DecodeString(input) if err != nil { l.RaiseError("not a valid base64 url string: %v", err.Error()) return 0 @@ -920,7 +956,7 @@ func (n *NakamaModule) authenticateTokenGenerate(l *lua.LState) int { exp := l.OptInt64(3, 0) if exp == 0 { // If expiry is 0 or not set, use standard configured expiry. - exp = time.Now().UTC().Add(time.Duration(n.config.GetSession().TokenExpiryMs) * time.Millisecond).Unix() + exp = time.Now().UTC().Add(time.Duration(n.config.GetSession().TokenExpirySec) * time.Second).Unix() } token := generateTokenWithExpiry(n.config, userIDString, username, exp) @@ -1066,7 +1102,7 @@ func (n *NakamaModule) streamUserGet(l *lua.LState) int { if meta == nil { l.Push(lua.LNil) } else { - metaTable := l.NewTable() + metaTable := l.CreateTable(4, 4) metaTable.RawSetString("Hidden", lua.LBool(meta.Hidden)) metaTable.RawSetString("Persistence", lua.LBool(meta.Persistence)) metaTable.RawSetString("Username", lua.LString(meta.Username)) @@ -1158,20 +1194,24 @@ func (n *NakamaModule) streamUserJoin(l *lua.LState) int { persistence := l.OptBool(5, true) // Look up the session. - session := n.registry.Get(sessionID) + session := n.sessionRegistry.Get(sessionID) if session == nil { l.ArgError(2, "session id does not exist") return 0 } - alreadyTracked := n.tracker.Track(sessionID, stream, userID, PresenceMeta{ + success, newlyTracked := n.tracker.Track(sessionID, stream, userID, PresenceMeta{ Format: session.Format(), Hidden: hidden, Persistence: persistence, Username: session.Username(), - }) + }, false) + if !success { + l.RaiseError("tracker rejected new presence, session is closing") + return 0 + } - l.Push(lua.LBool(alreadyTracked)) + l.Push(lua.LBool(newlyTracked)) return 1 } @@ -1446,6 +1486,92 @@ func (n *NakamaModule) streamSend(l *lua.LState) int { return 0 } +func (n *NakamaModule) matchCreate(l *lua.LState) int { + // Parse the name of the Lua module that should handle the match. + name := l.CheckString(1) + if name == "" { + l.ArgError(1, "expects module name") + return 0 + } + + params := convertLuaValue(l.Get(2)) + + // Start the match. + mh, err := n.matchRegistry.NewMatch(name, params) + if err != nil { + l.RaiseError("error creating match: %v", err.Error()) + return 0 + } + + // Return the match ID in a form that can be directly sent to clients. + l.Push(lua.LString(mh.IDStr)) + return 1 +} + +func (n *NakamaModule) matchList(l *lua.LState) int { + // Parse limit. + limit := l.OptInt(1, 1) + + // Parse authoritative flag. + var authoritative *wrappers.BoolValue + if v := l.Get(2); v.Type() != lua.LTNil { + if v.Type() != lua.LTBool { + l.ArgError(2, "expects authoritative true/false or nil") + return 0 + } + authoritative = &wrappers.BoolValue{Value: lua.LVAsBool(v)} + } + + // Parse label filter. + var label *wrappers.StringValue + if v := l.Get(3); v.Type() != lua.LTNil { + if v.Type() != lua.LTString { + l.ArgError(3, "expects label string or nil") + return 0 + } + label = &wrappers.StringValue{Value: lua.LVAsString(v)} + } + + // Parse minimum size filter. + var minSize *wrappers.Int32Value + if v := l.Get(4); v.Type() != lua.LTNil { + if v.Type() != lua.LTNumber { + l.ArgError(4, "expects minimum size number or nil") + return 0 + } + minSize = &wrappers.Int32Value{Value: int32(lua.LVAsNumber(v))} + } + + // Parse maximum size filter. + var maxSize *wrappers.Int32Value + if v := l.Get(5); v.Type() != lua.LTNil { + if v.Type() != lua.LTNumber { + l.ArgError(5, "expects maximum size number or nil") + return 0 + } + maxSize = &wrappers.Int32Value{Value: int32(lua.LVAsNumber(v))} + } + + results := n.matchRegistry.ListMatches(limit, authoritative, label, minSize, maxSize) + + s := len(results) + matches := l.CreateTable(s, s) + for i, result := range results { + match := l.CreateTable(4, 4) + match.RawSetString("MatchId", lua.LString(result.MatchId)) + match.RawSetString("Authoritative", lua.LBool(result.Authoritative)) + if result.Label == nil { + match.RawSetString("Label", lua.LNil) + } else { + match.RawSetString("Label", lua.LString(result.Label.Value)) + } + match.RawSetString("Size", lua.LNumber(result.Size)) + matches.RawSetInt(i+1, match) + } + l.Push(matches) + return 1 +} + func (n *NakamaModule) registerRPC(l *lua.LState) int { fn := l.CheckFunction(1) id := l.CheckString(2) diff --git a/server/runtime_oslib.go b/server/runtime_oslib.go index 82d7972732e8adc40b7a91f4b34f2c7501e302fd..f0e34929f6119abf56f542136f8bb1b93f22eb65 100644 --- a/server/runtime_oslib.go +++ b/server/runtime_oslib.go @@ -87,7 +87,7 @@ func osDate(L *lua.LState) int { t = time.Unix(L.CheckInt64(2), 0) } if strings.HasPrefix(cfmt, "*t") { - ret := L.NewTable() + ret := L.CreateTable(9, 9) ret.RawSetString("year", lua.LNumber(t.Year())) ret.RawSetString("month", lua.LNumber(t.Month())) ret.RawSetString("day", lua.LNumber(t.Day())) diff --git a/server/session_registry.go b/server/session_registry.go index ec8ca7d3c9b90792471a8ca31760eb64fbc479bc..f67eb1e94b041beea21b68445155cf5ec21d3169 100644 --- a/server/session_registry.go +++ b/server/session_registry.go @@ -25,8 +25,8 @@ import ( type SessionFormat uint8 const ( - SessionFormatProtobuf SessionFormat = iota - SessionFormatJson + SessionFormatJson SessionFormat = iota + SessionFormatProtobuf ) type session interface { @@ -38,7 +38,7 @@ type session interface { SetUsername(string) Expiry() int64 - Consume(func(logger *zap.Logger, session session, envelope *rtapi.Envelope)) + Consume(func(logger *zap.Logger, session session, envelope *rtapi.Envelope) error) Format() SessionFormat Send(envelope *rtapi.Envelope) error diff --git a/server/session_ws.go b/server/session_ws.go index 81c49037f36da02cf913b4f94b598e957cfb931c..1f3598c669ce566a1a0cd5b41f08b28820e33976 100644 --- a/server/session_ws.go +++ b/server/session_ws.go @@ -27,9 +27,10 @@ import ( "github.com/satori/go.uuid" "go.uber.org/atomic" "go.uber.org/zap" + "net" ) -var ErrQueueFull = errors.New("outgoing queue full") +var ErrSessionQueueFull = errors.New("session outgoing queue full") type sessionWS struct { sync.Mutex @@ -43,8 +44,8 @@ type sessionWS struct { jsonpbMarshaler *jsonpb.Marshaler jsonpbUnmarshaler *jsonpb.Unmarshaler - registry *SessionRegistry - tracker Tracker + sessionRegistry *SessionRegistry + tracker Tracker stopped bool conn *websocket.Conn @@ -53,7 +54,7 @@ type sessionWS struct { outgoingStopCh chan struct{} } -func NewSessionWS(logger *zap.Logger, config Config, userID uuid.UUID, username string, expiry int64, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, conn *websocket.Conn, registry *SessionRegistry, tracker Tracker) session { +func NewSessionWS(logger *zap.Logger, config Config, userID uuid.UUID, username string, expiry int64, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, conn *websocket.Conn, sessionRegistry *SessionRegistry, tracker Tracker) session { sessionID := uuid.NewV4() sessionLogger := logger.With(zap.String("uid", userID.String()), zap.String("sid", sessionID.String())) @@ -70,8 +71,8 @@ func NewSessionWS(logger *zap.Logger, config Config, userID uuid.UUID, username jsonpbMarshaler: jsonpbMarshaler, jsonpbUnmarshaler: jsonpbUnmarshaler, - registry: registry, - tracker: tracker, + sessionRegistry: sessionRegistry, + tracker: tracker, stopped: false, conn: conn, @@ -105,7 +106,7 @@ func (s *sessionWS) Expiry() int64 { return s.expiry } -func (s *sessionWS) Consume(processRequest func(logger *zap.Logger, session session, envelope *rtapi.Envelope)) { +func (s *sessionWS) Consume(processRequest func(logger *zap.Logger, session session, envelope *rtapi.Envelope) error) { defer s.cleanupClosedConnection() s.conn.SetReadLimit(s.config.GetSocket().MaxMessageSizeBytes) s.conn.SetReadDeadline(time.Now().Add(time.Duration(s.config.GetSocket().PongWaitMs) * time.Millisecond)) @@ -126,8 +127,12 @@ func (s *sessionWS) Consume(processRequest func(logger *zap.Logger, session sess for { _, data, err := s.conn.ReadMessage() if err != nil { + // Ignore "normal" WebSocket errors. if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { - s.logger.Warn("Error reading message from client", zap.Error(err)) + // Ignore underlying connection being shut down while read is waiting for data. + if e, ok := err.(*net.OpError); !ok || e.Err.Error() != "use of closed network connection" { + s.logger.Warn("Error reading message from client", zap.Error(err)) + } } break } @@ -136,15 +141,14 @@ func (s *sessionWS) Consume(processRequest func(logger *zap.Logger, session sess if err = s.jsonpbUnmarshaler.Unmarshal(bytes.NewReader(data), request); err != nil { // If the payload is malformed the client is incompatible or misbehaving, either way disconnect it now. s.logger.Warn("Received malformed payload", zap.String("data", string(data))) - s.Send(&rtapi.Envelope{Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ - Code: int32(rtapi.Error_UNRECOGNIZED_PAYLOAD), - Message: "Unrecognized payload", - }}}) break } else { // TODO Add session-global context here to cancel in-progress operations when the session is closed. requestLogger := s.logger.With(zap.String("cid", request.Cid)) - processRequest(requestLogger, s, request) + if err = processRequest(requestLogger, s, request); err != nil { + requestLogger.Warn("Received unrecognized payload", zap.String("data", string(data))) + break + } } } } @@ -162,12 +166,21 @@ func (s *sessionWS) processOutgoing() { return } case payload := <-s.outgoingCh: + s.Lock() + if s.stopped { + // The connection may have stopped between the payload being queued on the outgoing channel and reaching here. + // If that's the case then abort outgoing processing at this point and exit. + s.Unlock() + return + } // Process the outgoing message queue. s.conn.SetWriteDeadline(time.Now().Add(time.Duration(s.config.GetSocket().WriteWaitMs) * time.Millisecond)) if err := s.conn.WriteMessage(websocket.TextMessage, payload); err != nil { + s.Unlock() s.logger.Warn("Could not write message", zap.Error(err)) return } + s.Unlock() } } } @@ -229,9 +242,9 @@ func (s *sessionWS) SendBytes(payload []byte) error { // Terminate the connection immediately because the only alternative that doesn't block the server is // to start dropping messages, which might cause unexpected behaviour. s.Unlock() - s.logger.Warn("Could not write message, outgoing queue full") + s.logger.Warn("Could not write message, session outgoing queue full") s.cleanupClosedConnection() - return ErrQueueFull + return ErrSessionQueueFull } } @@ -244,10 +257,12 @@ func (s *sessionWS) cleanupClosedConnection() { s.stopped = true s.Unlock() - s.logger.Debug("Cleaning up closed client connection", zap.String("remoteAddress", s.conn.RemoteAddr().String())) + if s.logger.Core().Enabled(zap.DebugLevel) { + s.logger.Debug("Cleaning up closed client connection", zap.String("remoteAddress", s.conn.RemoteAddr().String())) + } // When connection close originates internally in the session, ensure cleanup of external resources and references. - s.registry.remove(s.id) + s.sessionRegistry.remove(s.id) s.tracker.UntrackAll(s.id) // Clean up internals. diff --git a/server/socket_ws.go b/server/socket_ws.go index f62a2fdd238e551a09deadceb223ab6c9596816b..9e02c201ad73ed0976977b7fcdb5f75e81db40f7 100644 --- a/server/socket_ws.go +++ b/server/socket_ws.go @@ -19,11 +19,10 @@ import ( "github.com/golang/protobuf/jsonpb" "github.com/gorilla/websocket" - "github.com/heroiclabs/nakama/rtapi" "go.uber.org/zap" ) -func NewSocketWsAcceptor(logger *zap.Logger, config Config, registry *SessionRegistry, tracker Tracker, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, processRequest func(*zap.Logger, session, *rtapi.Envelope)) func(http.ResponseWriter, *http.Request) { +func NewSocketWsAcceptor(logger *zap.Logger, config Config, sessionRegistry *SessionRegistry, tracker Tracker, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, pipeline *pipeline) func(http.ResponseWriter, *http.Request) { upgrader := &websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -53,15 +52,15 @@ func NewSocketWsAcceptor(logger *zap.Logger, config Config, registry *SessionReg } // Wrap the connection for application handling. - s := NewSessionWS(logger, config, userID, username, expiry, jsonpbMarshaler, jsonpbUnmarshaler, conn, registry, tracker) + s := NewSessionWS(logger, config, userID, username, expiry, jsonpbMarshaler, jsonpbUnmarshaler, conn, sessionRegistry, tracker) // Add to the session registry. - registry.add(s) + sessionRegistry.add(s) // Register initial presences for this session. - tracker.Track(s.ID(), PresenceStream{Mode: StreamModeNotifications, Subject: s.UserID()}, s.UserID(), PresenceMeta{Format: s.Format(), Username: s.Username()}) + tracker.Track(s.ID(), PresenceStream{Mode: StreamModeNotifications, Subject: s.UserID()}, s.UserID(), PresenceMeta{Format: s.Format(), Username: s.Username(), Hidden: true}, true) // Allow the server to begin processing incoming messages from this session. - s.Consume(processRequest) + s.Consume(pipeline.processRequest) } } diff --git a/server/tracker.go b/server/tracker.go index ba50e919fd92abb801161e1100432783fd462366..f1d2b6b019a5ba2d2f0a720556e67878518f514a 100644 --- a/server/tracker.go +++ b/server/tracker.go @@ -17,6 +17,7 @@ package server import ( "sync" + "fmt" "github.com/golang/protobuf/jsonpb" "github.com/heroiclabs/nakama/rtapi" "github.com/satori/go.uuid" @@ -33,6 +34,8 @@ const ( StreamModeMatchAuthoritative ) +const EventsQueueSize = 512 + type PresenceID struct { Node string SessionID uuid.UUID @@ -66,10 +69,11 @@ type PresenceEvent struct { } type Tracker interface { + SetMatchLeaveListener(func(id uuid.UUID, leaves []*MatchPresence)) Stop() - // Individual presence and user operations. - Track(sessionID uuid.UUID, stream PresenceStream, userID uuid.UUID, meta PresenceMeta) bool + // Track returns success true/false, and new presence true/false. + Track(sessionID uuid.UUID, stream PresenceStream, userID uuid.UUID, meta PresenceMeta, allowIfFirstForSession bool) (bool, bool) Untrack(sessionID uuid.UUID, stream PresenceStream, userID uuid.UUID) UntrackAll(sessionID uuid.UUID) Update(sessionID uuid.UUID, stream PresenceStream, userID uuid.UUID, meta PresenceMeta) bool @@ -88,12 +92,17 @@ type Tracker interface { Count() int // Get the number of presences in the given stream. CountByStream(stream PresenceStream) int + // Get a snapshot of current presence counts for streams with one of the given stream modes. + CountByStreamModeFilter(modes map[uint8]*uint8) map[*PresenceStream]int32 // Check if a single presence on the current node exists. GetLocalBySessionIDStreamUserID(sessionID uuid.UUID, stream PresenceStream, userID uuid.UUID) *PresenceMeta // List presences by stream. - ListByStream(stream PresenceStream) []Presence - // List presences on the current node by stream. - ListLocalByStream(stream PresenceStream) []Presence + ListByStream(stream PresenceStream) []*Presence + + // Fast lookup of local session IDs to use for message delivery. + ListLocalSessionIDByStream(stream PresenceStream) []uuid.UUID + // Fast lookup of node + session IDs to use for message delivery. + ListPresenceIDByStream(stream PresenceStream) []*PresenceID } type presenceCompact struct { @@ -105,24 +114,25 @@ type presenceCompact struct { type LocalTracker struct { sync.RWMutex logger *zap.Logger - registry *SessionRegistry + matchLeaveListener func(id uuid.UUID, leaves []*MatchPresence) + sessionRegistry *SessionRegistry jsonpbMarshaler *jsonpb.Marshaler name string eventsCh chan *PresenceEvent stopCh chan struct{} - presencesByStream map[PresenceStream]map[presenceCompact]PresenceMeta + presencesByStream map[uint8]map[PresenceStream]map[presenceCompact]PresenceMeta presencesBySession map[uuid.UUID]map[presenceCompact]PresenceMeta } -func StartLocalTracker(logger *zap.Logger, registry *SessionRegistry, jsonpbMarshaler *jsonpb.Marshaler, name string) Tracker { +func StartLocalTracker(logger *zap.Logger, sessionRegistry *SessionRegistry, jsonpbMarshaler *jsonpb.Marshaler, name string) Tracker { t := &LocalTracker{ logger: logger, - registry: registry, + sessionRegistry: sessionRegistry, jsonpbMarshaler: jsonpbMarshaler, name: name, - eventsCh: make(chan *PresenceEvent, 128), + eventsCh: make(chan *PresenceEvent, EventsQueueSize), stopCh: make(chan struct{}), - presencesByStream: make(map[PresenceStream]map[presenceCompact]PresenceMeta), + presencesByStream: make(map[uint8]map[PresenceStream]map[presenceCompact]PresenceMeta), presencesBySession: make(map[uuid.UUID]map[presenceCompact]PresenceMeta), } go func() { @@ -139,47 +149,58 @@ func StartLocalTracker(logger *zap.Logger, registry *SessionRegistry, jsonpbMars return t } +func (t *LocalTracker) SetMatchLeaveListener(f func(id uuid.UUID, leaves []*MatchPresence)) { + t.matchLeaveListener = f +} + func (t *LocalTracker) Stop() { // No need to explicitly clean up the events channel, just let the application exit. close(t.stopCh) } -func (t *LocalTracker) Track(sessionID uuid.UUID, stream PresenceStream, userID uuid.UUID, meta PresenceMeta) bool { +func (t *LocalTracker) Track(sessionID uuid.UUID, stream PresenceStream, userID uuid.UUID, meta PresenceMeta, allowIfFirstForSession bool) (bool, bool) { pc := presenceCompact{ID: PresenceID{Node: t.name, SessionID: sessionID}, Stream: stream, UserID: userID} - alreadyTracked := false t.Lock() // See if this session has any presences tracked at all. - bySession, anyTracked := t.presencesBySession[sessionID] - if anyTracked { + if bySession, anyTracked := t.presencesBySession[sessionID]; anyTracked { // Then see if the exact presence we need is tracked. - _, alreadyTracked = bySession[pc] - } - - // Maybe update tracking for session. - if !anyTracked { + if _, alreadyTracked := bySession[pc]; !alreadyTracked { + // If the current session had others tracked, but not this presence. + bySession[pc] = meta + } else { + t.Unlock() + return true, false + } + } else { + if !allowIfFirstForSession { + // If it's the first presence for this session, only allow it if explicitly permitted to. + t.Unlock() + return false, false + } // If nothing at all was tracked for the current session, begin tracking. bySession = make(map[presenceCompact]PresenceMeta) bySession[pc] = meta t.presencesBySession[sessionID] = bySession - } else if !alreadyTracked { - // If the current session had others tracked, but not this presence. - bySession[pc] = meta } - // Maybe update tracking for stream. - if !alreadyTracked { - if byStream, ok := t.presencesByStream[stream]; !ok { - byStream = make(map[presenceCompact]PresenceMeta) - byStream[pc] = meta - t.presencesByStream[stream] = byStream - } else { - byStream[pc] = meta - } + // Update tracking for stream. + byStreamMode, ok := t.presencesByStream[stream.Mode] + if !ok { + byStreamMode = make(map[PresenceStream]map[presenceCompact]PresenceMeta) + t.presencesByStream[stream.Mode] = byStreamMode + } + + if byStream, ok := byStreamMode[stream]; !ok { + byStream = make(map[presenceCompact]PresenceMeta) + byStream[pc] = meta + byStreamMode[stream] = byStream + } else { + byStream[pc] = meta } t.Unlock() - if !alreadyTracked && !meta.Hidden { + if !meta.Hidden { t.queueEvent( []Presence{ Presence{ID: pc.ID, Stream: stream, UserID: userID, Meta: meta}, @@ -187,7 +208,7 @@ func (t *LocalTracker) Track(sessionID uuid.UUID, stream PresenceStream, userID nil, ) } - return !alreadyTracked + return true, true } func (t *LocalTracker) Untrack(sessionID uuid.UUID, stream PresenceStream, userID uuid.UUID) { @@ -217,12 +238,24 @@ func (t *LocalTracker) Untrack(sessionID uuid.UUID, stream PresenceStream, userI } // Update the tracking for stream. - if byStream := t.presencesByStream[stream]; len(byStream) == 1 { - // This was the only presence for the stream, discard the whole list. - delete(t.presencesByStream, stream) + if byStreamMode := t.presencesByStream[stream.Mode]; len(byStreamMode) == 1 { + // This is the only stream for this stream mode. + if byStream := byStreamMode[stream]; len(byStream) == 1 { + // This was the only presence in the only stream for this stream mode, discard the whole list. + delete(t.presencesByStream, stream.Mode) + } else { + // There were other presences for the stream, drop just this one. + delete(byStream, pc) + } } else { - // There were other presences for the stream, drop just this one. - delete(byStream, pc) + // There are other streams for this stream mode. + if byStream := byStreamMode[stream]; len(byStream) == 1 { + // This was the only presence for the stream, discard the whole list. + delete(byStreamMode, stream) + } else { + // There were other presences for the stream, drop just this one. + delete(byStream, pc) + } } t.Unlock() @@ -249,12 +282,24 @@ func (t *LocalTracker) UntrackAll(sessionID uuid.UUID) { leaves := make([]Presence, 0, len(bySession)) for pc, meta := range bySession { // Update the tracking for stream. - if byStream := t.presencesByStream[pc.Stream]; len(byStream) == 1 { - // This was the only presence for the stream, discard the whole list. - delete(t.presencesByStream, pc.Stream) + if byStreamMode := t.presencesByStream[pc.Stream.Mode]; len(byStreamMode) == 1 { + // This is the only stream for this stream mode. + if byStream := byStreamMode[pc.Stream]; len(byStream) == 1 { + // This was the only presence in the only stream for this stream mode, discard the whole list. + delete(t.presencesByStream, pc.Stream.Mode) + } else { + // There were other presences for the stream, drop just this one. + delete(byStream, pc) + } } else { - // There were other presences for the stream, drop just this one. - delete(byStream, pc) + // There are other streams for this stream mode. + if byStream := byStreamMode[pc.Stream]; len(byStream) == 1 { + // This was the only presence for the stream, discard the whole list. + delete(byStreamMode, pc.Stream) + } else { + // There were other presences for the stream, drop just this one. + delete(byStream, pc) + } } // Check if there should be an event for this presence. @@ -294,7 +339,7 @@ func (t *LocalTracker) Update(sessionID uuid.UUID, stream PresenceStream, userID // Update the tracking for session. bySession[pc] = meta // Update the tracking for stream. - t.presencesByStream[stream][pc] = meta + t.presencesByStream[stream.Mode][stream][pc] = meta t.Unlock() if !meta.Hidden || !previousMeta.Hidden { @@ -322,7 +367,7 @@ func (t *LocalTracker) UntrackLocalByStream(stream PresenceStream) { // NOTE: Generates no presence notifications as everyone on the stream is going away all at once. t.Lock() - byStream, anyTracked := t.presencesByStream[stream] + byStream, anyTracked := t.presencesByStream[stream.Mode][stream] if !anyTracked { // Nothing tracked for the stream. t.Unlock() @@ -339,8 +384,15 @@ func (t *LocalTracker) UntrackLocalByStream(stream PresenceStream) { delete(bySession, pc) } } + // Discard the tracking for stream. - delete(t.presencesByStream, stream) + if byStreamMode := t.presencesByStream[stream.Mode]; len(byStreamMode) == 1 { + // This is the only stream for this stream mode. + delete(t.presencesByStream, stream.Mode) + } else { + // There are other streams for this stream mode. + delete(byStreamMode, stream) + } t.Unlock() } @@ -349,7 +401,7 @@ func (t *LocalTracker) UntrackByStream(stream PresenceStream) { // NOTE: Generates no presence notifications as everyone on the stream is going away all at once. t.Lock() - byStream, anyTracked := t.presencesByStream[stream] + byStream, anyTracked := t.presencesByStream[stream.Mode][stream] if !anyTracked { // Nothing tracked for the stream. t.Unlock() @@ -366,15 +418,22 @@ func (t *LocalTracker) UntrackByStream(stream PresenceStream) { delete(bySession, pc) } } + // Discard the tracking for stream. - delete(t.presencesByStream, stream) + if byStreamMode := t.presencesByStream[stream.Mode]; len(byStreamMode) == 1 { + // This is the only stream for this stream mode. + delete(t.presencesByStream, stream.Mode) + } else { + // There are other streams for this stream mode. + delete(byStreamMode, stream) + } t.Unlock() } func (t *LocalTracker) ListNodesForStream(stream PresenceStream) []string { t.RLock() - _, anyTracked := t.presencesByStream[stream] + _, anyTracked := t.presencesByStream[stream.Mode][stream] t.RUnlock() if anyTracked { // For the local tracker having any presences for this stream is enough. @@ -386,7 +445,7 @@ func (t *LocalTracker) ListNodesForStream(stream PresenceStream) []string { func (t *LocalTracker) StreamExists(stream PresenceStream) bool { var exists bool t.RLock() - exists = t.presencesByStream[stream] != nil + exists = t.presencesByStream[stream.Mode][stream] != nil t.RUnlock() return exists } @@ -394,9 +453,9 @@ func (t *LocalTracker) StreamExists(stream PresenceStream) bool { func (t *LocalTracker) Count() int { var count int t.RLock() - // For each stream add together their presence count. - for _, byStream := range t.presencesByStream { - count += len(byStream) + // For each session add together their presence count. + for _, bySession := range t.presencesBySession { + count += len(bySession) } t.RUnlock() return count @@ -406,14 +465,28 @@ func (t *LocalTracker) CountByStream(stream PresenceStream) int { var count int t.RLock() // If the stream exists use its presence count, otherwise 0. - byStream, anyTracked := t.presencesByStream[stream] - if anyTracked { + if byStream, anyTracked := t.presencesByStream[stream.Mode][stream]; anyTracked { count = len(byStream) } t.RUnlock() return count } +func (t *LocalTracker) CountByStreamModeFilter(modes map[uint8]*uint8) map[*PresenceStream]int32 { + counts := make(map[*PresenceStream]int32) + t.RLock() + for mode, byStreamMode := range t.presencesByStream { + if modes[mode] == nil { + continue + } + for s, ps := range byStreamMode { + counts[&s] = int32(len(ps)) + } + } + t.RUnlock() + return counts +} + func (t *LocalTracker) GetLocalBySessionIDStreamUserID(sessionID uuid.UUID, stream PresenceStream, userID uuid.UUID) *PresenceMeta { pc := presenceCompact{ID: PresenceID{Node: t.name, SessionID: sessionID}, Stream: stream, UserID: userID} t.RLock() @@ -431,31 +504,47 @@ func (t *LocalTracker) GetLocalBySessionIDStreamUserID(sessionID uuid.UUID, stre return &meta } -func (t *LocalTracker) ListByStream(stream PresenceStream) []Presence { +func (t *LocalTracker) ListByStream(stream PresenceStream) []*Presence { t.RLock() - byStream, anyTracked := t.presencesByStream[stream] + byStream, anyTracked := t.presencesByStream[stream.Mode][stream] if !anyTracked { t.RUnlock() - return []Presence{} + return []*Presence{} } - ps := make([]Presence, 0, len(byStream)) + ps := make([]*Presence, 0, len(byStream)) for pc, meta := range byStream { - ps = append(ps, Presence{ID: pc.ID, Stream: stream, UserID: pc.UserID, Meta: meta}) + ps = append(ps, &Presence{ID: pc.ID, Stream: stream, UserID: pc.UserID, Meta: meta}) } t.RUnlock() return ps } -func (t *LocalTracker) ListLocalByStream(stream PresenceStream) []Presence { +func (t *LocalTracker) ListLocalSessionIDByStream(stream PresenceStream) []uuid.UUID { t.RLock() - byStream, anyTracked := t.presencesByStream[stream] + byStream, anyTracked := t.presencesByStream[stream.Mode][stream] if !anyTracked { t.RUnlock() - return []Presence{} + return []uuid.UUID{} } - ps := make([]Presence, 0, len(byStream)) - for pc, meta := range byStream { - ps = append(ps, Presence{ID: pc.ID, Stream: stream, UserID: pc.UserID, Meta: meta}) + ps := make([]uuid.UUID, 0, len(byStream)) + for pc, _ := range byStream { + ps = append(ps, pc.ID.SessionID) + } + t.RUnlock() + return ps +} + +func (t *LocalTracker) ListPresenceIDByStream(stream PresenceStream) []*PresenceID { + t.RLock() + byStream, anyTracked := t.presencesByStream[stream.Mode][stream] + if !anyTracked { + t.RUnlock() + return []*PresenceID{} + } + ps := make([]*PresenceID, 0, len(byStream)) + for pc, _ := range byStream { + pid := pc.ID + ps = append(ps, &pid) } t.RUnlock() return ps @@ -481,12 +570,18 @@ func (t *LocalTracker) queueEvent(joins, leaves []Presence) { } func (t *LocalTracker) processEvent(e *PresenceEvent) { - t.logger.Debug("Processing presence event", zap.Int("joins", len(e.joins)), zap.Int("leaves", len(e.leaves))) + if t.logger.Core().Enabled(zap.DebugLevel) { + t.logger.Debug("Processing presence event", zap.Int("joins", len(e.joins)), zap.Int("leaves", len(e.leaves))) + } // Group joins/leaves by stream to allow batching. // Convert to wire representation at the same time. streamJoins := make(map[PresenceStream][]*rtapi.StreamPresence, 0) streamLeaves := make(map[PresenceStream][]*rtapi.StreamPresence, 0) + + // Track grouped authoritative match leaves separately from client-bound events. + matchLeaves := make(map[uuid.UUID][]*MatchPresence, 0) + for _, p := range e.joins { pWire := &rtapi.StreamPresence{ UserId: p.UserID.String(), @@ -509,11 +604,31 @@ func (t *LocalTracker) processEvent(e *PresenceEvent) { Persistence: p.Meta.Persistence, Status: p.Meta.Status, } - if j, ok := streamLeaves[p.Stream]; ok { - streamLeaves[p.Stream] = append(j, pWire) + if l, ok := streamLeaves[p.Stream]; ok { + streamLeaves[p.Stream] = append(l, pWire) } else { streamLeaves[p.Stream] = []*rtapi.StreamPresence{pWire} } + + // We only care about authoritative match leaves where the match host is the current node. + if p.Stream.Mode == StreamModeMatchAuthoritative && p.Stream.Label == t.name { + mp := &MatchPresence{ + Node: p.ID.Node, + UserID: p.UserID, + SessionID: p.ID.SessionID, + Username: p.Meta.Username, + } + if l, ok := matchLeaves[p.Stream.Subject]; ok { + matchLeaves[p.Stream.Subject] = append(l, mp) + } else { + matchLeaves[p.Stream.Subject] = []*MatchPresence{mp} + } + } + } + + // Notify locally hosted authoritative matches of leave events. + for matchID, leaves := range matchLeaves { + t.matchLeaveListener(matchID, leaves) } // Send joins, together with any leaves for the same topic. @@ -535,13 +650,30 @@ func (t *LocalTracker) processEvent(e *PresenceEvent) { streamWire.Descriptor_ = stream.Descriptor.String() } - // Construct the wire representation of the event. - envelope := &rtapi.Envelope{Message: &rtapi.Envelope_StreamPresenceEvent{StreamPresenceEvent: &rtapi.StreamPresenceEvent{ - Stream: streamWire, - Joins: joins, - Leaves: leaves, - }, - }} + // Find the list of event recipients first so we can skip event encoding work if it's not necessary. + sessionIDs := t.ListLocalSessionIDByStream(stream) + if len(sessionIDs) == 0 { + continue + } + + // Construct the wire representation of the event based on the stream mode. + var envelope *rtapi.Envelope + switch stream.Mode { + case StreamModeMatchRelayed: + fallthrough + case StreamModeMatchAuthoritative: + envelope = &rtapi.Envelope{Message: &rtapi.Envelope_MatchPresenceEvent{MatchPresenceEvent: &rtapi.MatchPresenceEvent{ + MatchId: fmt.Sprintf("%v:%v", stream.Subject.String(), stream.Label), + Joins: joins, + Leaves: leaves, + }}} + default: + envelope = &rtapi.Envelope{Message: &rtapi.Envelope_StreamPresenceEvent{StreamPresenceEvent: &rtapi.StreamPresenceEvent{ + Stream: streamWire, + Joins: joins, + Leaves: leaves, + }}} + } payload, err := t.jsonpbMarshaler.MarshalToString(envelope) if err != nil { t.logger.Warn("Could not marshal presence event to json", zap.Error(err)) @@ -549,14 +681,12 @@ func (t *LocalTracker) processEvent(e *PresenceEvent) { } payloadByte := []byte(payload) - // Find the list of event recipients. - presences := t.ListLocalByStream(stream) - for _, p := range presences { - // Deliver event. - if s := t.registry.Get(p.ID.SessionID); s != nil { + // Deliver event. + for _, sessionID := range sessionIDs { + if s := t.sessionRegistry.Get(sessionID); s != nil { s.SendBytes(payloadByte) - } else { - t.logger.Warn("Could not deliver presence event, no session", zap.String("sid", p.ID.SessionID.String())) + } else if t.logger.Core().Enabled(zap.DebugLevel) { + t.logger.Debug("Could not deliver presence event, no session", zap.String("sid", sessionID.String())) } } } @@ -575,13 +705,30 @@ func (t *LocalTracker) processEvent(e *PresenceEvent) { streamWire.Descriptor_ = stream.Descriptor.String() } - // Construct the wire representation of the event. - envelope := &rtapi.Envelope{Message: &rtapi.Envelope_StreamPresenceEvent{StreamPresenceEvent: &rtapi.StreamPresenceEvent{ - Stream: streamWire, - // No joins. - Leaves: leaves, - }, - }} + // Find the list of event recipients first so we can skip event encoding work if it's not necessary. + sessionIDs := t.ListLocalSessionIDByStream(stream) + if len(sessionIDs) == 0 { + continue + } + + // Construct the wire representation of the event based on the stream mode. + var envelope *rtapi.Envelope + switch stream.Mode { + case StreamModeMatchRelayed: + fallthrough + case StreamModeMatchAuthoritative: + envelope = &rtapi.Envelope{Message: &rtapi.Envelope_MatchPresenceEvent{MatchPresenceEvent: &rtapi.MatchPresenceEvent{ + MatchId: fmt.Sprintf("%v:%v", stream.Subject.String(), stream.Label), + // No joins. + Leaves: leaves, + }}} + default: + envelope = &rtapi.Envelope{Message: &rtapi.Envelope_StreamPresenceEvent{StreamPresenceEvent: &rtapi.StreamPresenceEvent{ + Stream: streamWire, + // No joins. + Leaves: leaves, + }}} + } payload, err := t.jsonpbMarshaler.MarshalToString(envelope) if err != nil { t.logger.Warn("Could not marshal presence event to json", zap.Error(err)) @@ -589,14 +736,12 @@ func (t *LocalTracker) processEvent(e *PresenceEvent) { } payloadByte := []byte(payload) - // Find the list of event recipients. - presences := t.ListLocalByStream(stream) - for _, p := range presences { - // Deliver event. - if s := t.registry.Get(p.ID.SessionID); s != nil { + // Deliver event. + for _, sessionID := range sessionIDs { + if s := t.sessionRegistry.Get(sessionID); s != nil { s.SendBytes(payloadByte) - } else { - t.logger.Warn("Could not deliver presence event, no session", zap.String("sid", p.ID.SessionID.String())) + } else if t.logger.Core().Enabled(zap.DebugLevel) { + t.logger.Debug("Could not deliver presence event, no session", zap.String("sid", sessionID.String())) } } }