From e71f872ac1bdce2de716fd13d3370b80f7b9f07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20L=C3=B3pez=20Guimaraes?= Date: Fri, 28 Jun 2024 22:02:09 +0100 Subject: [PATCH] feat!: Matchmaking rewrite --- globals/matchmaking_globals.go | 13 +- globals/matchmaking_utils.go | 627 ------------------ globals/utils.go | 92 +++ match-making-ext/end_participation.go | 81 +-- match-making-ext/protocol.go | 8 + .../database/disconnect_participant.go | 147 ++++ .../database/end_gathering_participation.go | 119 ++++ match-making/database/find_gathering_by_id.go | 65 ++ match-making/database/join_gathering.go | 112 ++++ .../join_gathering_with_participants.go | 121 ++++ .../database/migrate_gathering_ownership.go | 69 ++ match-making/database/register_gathering.go | 60 ++ .../remove_participant_from_gathering.go | 33 + match-making/database/unregister_gathering.go | 26 + match-making/database/update_session_host.go | 27 + match-making/find_by_single_id.go | 25 +- match-making/get_session_urls.go | 33 +- match-making/protocol.go | 55 +- match-making/unregister_gathering.go | 81 +-- match-making/update_session_host.go | 109 ++- match-making/update_session_host_v1.go | 50 +- match-making/update_session_url.go | 91 +-- .../auto_matchmake_postpone.go | 45 +- .../auto_matchmake_with_param_postpone.go | 53 +- ...matchmake_with_search_criteria_postpone.go | 58 +- .../browse_matchmake_session.go | 30 +- matchmake-extension/close_participation.go | 25 +- .../create_matchmake_session.go | 33 +- .../create_matchmake_session_with_param.go | 28 +- .../database/create_matchmake_session.go | 99 +++ .../database/find_matchmake_session.go | 138 ++++ ...nd_matchmake_session_by_search_criteria.go | 297 +++++++++ .../get_matchmake_session_by_gathering.go | 73 ++ .../database/get_matchmake_session_by_id.go | 107 +++ .../database/get_simple_playing_session.go | 42 ++ .../database/update_application_buffer.go | 27 + .../database/update_game_attribute.go | 26 + .../database/update_participation.go | 26 + .../database/update_progress_score.go | 26 + .../get_simple_playing_session.go | 48 +- matchmake-extension/join_matchmake_session.go | 32 +- .../join_matchmake_session_ex.go | 32 +- .../join_matchmake_session_with_param.go | 34 +- .../modify_current_game_attribute.go | 24 +- matchmake-extension/open_participation.go | 22 +- matchmake-extension/protocol.go | 56 +- .../update_application_buffer.go | 23 +- matchmake-extension/update_progress_score.go | 27 +- 48 files changed, 2355 insertions(+), 1120 deletions(-) delete mode 100644 globals/matchmaking_utils.go create mode 100644 match-making/database/disconnect_participant.go create mode 100644 match-making/database/end_gathering_participation.go create mode 100644 match-making/database/find_gathering_by_id.go create mode 100644 match-making/database/join_gathering.go create mode 100644 match-making/database/join_gathering_with_participants.go create mode 100644 match-making/database/migrate_gathering_ownership.go create mode 100644 match-making/database/register_gathering.go create mode 100644 match-making/database/remove_participant_from_gathering.go create mode 100644 match-making/database/unregister_gathering.go create mode 100644 match-making/database/update_session_host.go create mode 100644 matchmake-extension/database/create_matchmake_session.go create mode 100644 matchmake-extension/database/find_matchmake_session.go create mode 100644 matchmake-extension/database/find_matchmake_session_by_search_criteria.go create mode 100644 matchmake-extension/database/get_matchmake_session_by_gathering.go create mode 100644 matchmake-extension/database/get_matchmake_session_by_id.go create mode 100644 matchmake-extension/database/get_simple_playing_session.go create mode 100644 matchmake-extension/database/update_application_buffer.go create mode 100644 matchmake-extension/database/update_game_attribute.go create mode 100644 matchmake-extension/database/update_participation.go create mode 100644 matchmake-extension/database/update_progress_score.go diff --git a/globals/matchmaking_globals.go b/globals/matchmaking_globals.go index 135f91f..3deec97 100644 --- a/globals/matchmaking_globals.go +++ b/globals/matchmaking_globals.go @@ -1,17 +1,8 @@ package common_globals import ( - "github.com/PretendoNetwork/nex-go/v2" - match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + "sync" ) -type CommonMatchmakeSession struct { - GameMatchmakeSession *match_making_types.MatchmakeSession // * Used by the game, contains the current state of the MatchmakeSession - SearchMatchmakeSession *match_making_types.MatchmakeSession // * Used by the server when searching for matches, contains the state of the MatchmakeSession during the search process for easy compares - ConnectionIDs *nex.MutexSlice[uint32] // * Players in the room, referenced by their connection IDs. This is used instead of the PID in order to ensure we're talking to the correct client (in case of e.g. multiple logins) -} - -var Sessions map[uint32]*CommonMatchmakeSession +var MatchmakingMutex *sync.RWMutex = &sync.RWMutex{} var GetUserFriendPIDsHandler func(pid uint32) []uint32 -var CurrentGatheringID = nex.NewCounter[uint32](0) -var CurrentMatchmakingCallID = nex.NewCounter[uint32](0) diff --git a/globals/matchmaking_utils.go b/globals/matchmaking_utils.go deleted file mode 100644 index 1151cac..0000000 --- a/globals/matchmaking_utils.go +++ /dev/null @@ -1,627 +0,0 @@ -package common_globals - -import ( - "crypto/rand" - "fmt" - "strconv" - "strings" - - "github.com/PretendoNetwork/nex-go/v2" - "github.com/PretendoNetwork/nex-go/v2/constants" - "github.com/PretendoNetwork/nex-go/v2/types" - match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" - match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" - notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" - notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" - "golang.org/x/exp/slices" -) - -// GetAvailableGatheringID returns a gathering ID which doesn't belong to any session -// Returns 0 if no IDs are available (math.MaxUint32 has been reached) -func GetAvailableGatheringID() uint32 { - return CurrentGatheringID.Next() -} - -// FindOtherConnectionID searches a connection ID on the session that isn't the given one -// Returns 0 if no connection ID could be found -func FindOtherConnectionID(excludedConnectionID uint32, gatheringID uint32) uint32 { - var otherConnectionID uint32 = 0 - if session, ok := Sessions[gatheringID]; ok { - session.ConnectionIDs.Each(func(_ int, connectionID uint32) bool { - if connectionID != excludedConnectionID { - otherConnectionID = connectionID - return true - } - - return false - }) - } - - return otherConnectionID -} - -// RemoveConnectionIDFromSession removes a PRUDP connection from the session -func RemoveConnectionIDFromSession(id uint32, gathering uint32) { - Sessions[gathering].ConnectionIDs.DeleteAll(id) - - if Sessions[gathering].ConnectionIDs.Size() == 0 { - delete(Sessions, gathering) - } else { - // Update the participation count with the new connection ID count - Sessions[gathering].GameMatchmakeSession.ParticipationCount.Value = uint32(Sessions[gathering].ConnectionIDs.Size()) - } -} - -// FindConnectionSession searches for session the given connection ID is connected to -func FindConnectionSession(id uint32) uint32 { - for gatheringID := range Sessions { - if Sessions[gatheringID].ConnectionIDs.Has(id) { - return gatheringID - } - } - - return 0 -} - -// RemoveConnectionFromAllSessions removes a connection from every session -func RemoveConnectionFromAllSessions(connection *nex.PRUDPConnection) { - // * Keep checking until no session is found - for gid := FindConnectionSession(connection.ID); gid != 0; { - session := Sessions[gid] - lenParticipants := session.ConnectionIDs.Size() - - RemoveConnectionIDFromSession(connection.ID, gid) - - if lenParticipants <= 1 { - gid = FindConnectionSession(connection.ID) - continue - } - - ownerPID := session.GameMatchmakeSession.Gathering.OwnerPID - - if ownerPID.Equals(connection.PID()) { - // * This flag tells the server to change the matchmake session owner if they disconnect - // * If the flag is not set, delete the session - // * More info: https://nintendo-wiki.pretendo.network/docs/nex/protocols/match-making/types#flags - if session.GameMatchmakeSession.Gathering.Flags.PAND(match_making.GatheringFlags.DisconnectChangeOwner) == 0 { - delete(Sessions, gid) - } else { - ChangeSessionOwner(connection, gid, true) - } - } else { - endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - server := endpoint.Server - - category := notifications.NotificationCategories.Participation - subtype := notifications.NotificationSubTypes.Participation.Disconnected - - oEvent := notifications_types.NewNotificationEvent() - oEvent.PIDSource = connection.PID() - oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(category, subtype)) - oEvent.Param1 = types.NewPrimitiveU32(gid) - oEvent.Param2 = types.NewPrimitiveU32(connection.PID().LegacyValue()) // TODO - This assumes a legacy client. This won't work on the Switch - - stream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - - oEvent.WriteTo(stream) - - rmcRequest := nex.NewRMCRequest(endpoint) - rmcRequest.ProtocolID = notifications.ProtocolID - rmcRequest.CallID = CurrentMatchmakingCallID.Next() - rmcRequest.MethodID = notifications.MethodProcessNotificationEvent - rmcRequest.Parameters = stream.Bytes() - - rmcRequestBytes := rmcRequest.Bytes() - - target := endpoint.FindConnectionByPID(ownerPID.Value()) - if target == nil { - Logger.Warning("Target connection not found") - gid = FindConnectionSession(connection.ID) - continue - } - - var messagePacket nex.PRUDPPacketInterface - - if connection.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, target, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, target, nil) - } - - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(connection.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(connection.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(connection.StreamID) - messagePacket.SetPayload(rmcRequestBytes) - - server.Send(messagePacket) - } - - gid = FindConnectionSession(connection.ID) - } -} - -// CreateSessionByMatchmakeSession creates a gathering from a MatchmakeSession -func CreateSessionByMatchmakeSession(matchmakeSession *match_making_types.MatchmakeSession, searchMatchmakeSession *match_making_types.MatchmakeSession, hostPID *types.PID) (*CommonMatchmakeSession, *nex.Error) { - sessionIndex := GetAvailableGatheringID() - if sessionIndex == 0 { - sessionIndex = GetAvailableGatheringID() // * Skip to index 1 - } - - session := CommonMatchmakeSession{ - SearchMatchmakeSession: searchMatchmakeSession, - GameMatchmakeSession: matchmakeSession, - ConnectionIDs: nex.NewMutexSlice[uint32](), - } - - session.GameMatchmakeSession.Gathering.ID = types.NewPrimitiveU32(sessionIndex) - session.GameMatchmakeSession.Gathering.OwnerPID = hostPID - session.GameMatchmakeSession.Gathering.HostPID = hostPID - - session.GameMatchmakeSession.StartedTime = types.NewDateTime(0).Now() - session.GameMatchmakeSession.SessionKey = types.NewBuffer(make([]byte, 32)) - - rand.Read(session.GameMatchmakeSession.SessionKey.Value) - - SR := types.NewVariant() - SR.TypeID = types.NewPrimitiveU8(3) - SR.Type = types.NewPrimitiveBool(true) - - GIR := types.NewVariant() - GIR.TypeID = types.NewPrimitiveU8(1) - GIR.Type = types.NewPrimitiveS64(3) - - session.GameMatchmakeSession.MatchmakeParam.Params.Set(types.NewString("@SR"), SR) - session.GameMatchmakeSession.MatchmakeParam.Params.Set(types.NewString("@GIR"), GIR) - - Sessions[sessionIndex] = &session - - return Sessions[sessionIndex], nil -} - -// FindSessionByMatchmakeSession finds a gathering that matches with a MatchmakeSession -func FindSessionByMatchmakeSession(pid *types.PID, searchMatchmakeSession *match_making_types.MatchmakeSession) uint32 { - // * This portion finds any sessions that match the search session - // * It does not care about anything beyond that, such as if the match is already full - // * This is handled below - candidateSessionIndexes := make([]uint32, 0, len(Sessions)) - for index, session := range Sessions { - if session.SearchMatchmakeSession.Equals(searchMatchmakeSession) { - candidateSessionIndexes = append(candidateSessionIndexes, index) - } - } - - // TODO - This whole section assumes legacy clients. None of it will work on the Switch - var friendList []uint32 - for _, sessionIndex := range candidateSessionIndexes { - sessionToCheck := Sessions[sessionIndex] - if sessionToCheck.ConnectionIDs.Size() >= int(sessionToCheck.GameMatchmakeSession.MaximumParticipants.Value) { - continue - } - - if !sessionToCheck.GameMatchmakeSession.OpenParticipation.Value { - continue - } - - // * If the session only allows friends, check if the owner is in the friend list of the PID - // TODO - Is this a flag or a constant? - if sessionToCheck.GameMatchmakeSession.ParticipationPolicy.Value == 98 { - if GetUserFriendPIDsHandler == nil { - Logger.Warning("Missing GetUserFriendPIDsHandler!") - continue - } - - if len(friendList) == 0 { - friendList = GetUserFriendPIDsHandler(pid.LegacyValue()) // TODO - This grpc method needs to support the Switch - } - - if !slices.Contains(friendList, sessionToCheck.GameMatchmakeSession.OwnerPID.LegacyValue()) { - continue - } - } - - return sessionIndex // * Found a match - } - - return 0 -} - -// FindSessionsByMatchmakeSessionSearchCriterias finds a gathering that matches with the given search criteria -func FindSessionsByMatchmakeSessionSearchCriterias(pid *types.PID, searchCriterias []*match_making_types.MatchmakeSessionSearchCriteria, gameSpecificChecks func(searchCriteria *match_making_types.MatchmakeSessionSearchCriteria, matchmakeSession *match_making_types.MatchmakeSession) bool) []*CommonMatchmakeSession { - candidateSessions := make([]*CommonMatchmakeSession, 0, len(Sessions)) - - // TODO - This whole section assumes legacy clients. None of it will work on the Switch - var friendList []uint32 - for _, session := range Sessions { - for _, criteria := range searchCriterias { - // * Check things like game specific attributes - if gameSpecificChecks != nil { - if !gameSpecificChecks(criteria, session.GameMatchmakeSession) { - continue - } - } else { - if !compareAttributesSearchCriteria(session.GameMatchmakeSession.Attributes.Slice(), criteria.Attribs.Slice()) { - continue - } - } - - if !compareSearchCriteria(session.GameMatchmakeSession.MaximumParticipants.Value, criteria.MaxParticipants.Value) { - continue - } - - if !compareSearchCriteria(session.GameMatchmakeSession.MinimumParticipants.Value, criteria.MinParticipants.Value) { - continue - } - - if !compareSearchCriteria(session.GameMatchmakeSession.MatchmakeSystemType.Value, criteria.MatchmakeSystemType.Value) { - continue - } - - if !compareSearchCriteria(session.GameMatchmakeSession.GameMode.Value, criteria.GameMode.Value) { - continue - } - - if session.ConnectionIDs.Size() >= int(session.GameMatchmakeSession.MaximumParticipants.Value) { - continue - } - - if !session.GameMatchmakeSession.OpenParticipation.Value { - continue - } - - // * If the session only allows friends, check if the owner is in the friend list of the PID - // TODO - Is this a flag or a constant? - if session.GameMatchmakeSession.ParticipationPolicy.Value == 98 { - if GetUserFriendPIDsHandler == nil { - Logger.Warning("Missing GetUserFriendPIDsHandler!") - continue - } - - if len(friendList) == 0 { - friendList = GetUserFriendPIDsHandler(pid.LegacyValue()) // TODO - Support the Switch - } - - if !slices.Contains(friendList, session.GameMatchmakeSession.OwnerPID.LegacyValue()) { - continue - } - } - - candidateSessions = append(candidateSessions, session) - - // We don't have to compare with other search criterias - break - } - } - - return candidateSessions -} - -func compareAttributesSearchCriteria(original []*types.PrimitiveU32, search []*types.String) bool { - if len(original) != len(search) { - return false - } - - for index, originalAttribute := range original { - searchAttribute := search[index] - - if !compareSearchCriteria(originalAttribute.Value, searchAttribute.Value) { - return false - } - } - - return true -} - -func compareSearchCriteria[T ~uint16 | ~uint32](original T, search string) bool { - if search == "" { // * Accept any value - return true - } - - before, after, found := strings.Cut(search, ",") - if found { - min, err := strconv.ParseUint(before, 10, 64) - if err != nil { - return false - } - - max, err := strconv.ParseUint(after, 10, 64) - if err != nil { - return false - } - - return min <= uint64(original) && max >= uint64(original) - } else { - searchNum, err := strconv.ParseUint(before, 10, 64) - if err != nil { - return false - } - - return searchNum == uint64(original) - } -} - -// AddPlayersToSession updates the given sessions state to include the provided connection IDs -// Returns a NEX error code if failed -func AddPlayersToSession(session *CommonMatchmakeSession, connectionIDs []uint32, initiatingConnection *nex.PRUDPConnection, joinMessage string) *nex.Error { - if (session.ConnectionIDs.Size() + len(connectionIDs)) > int(session.GameMatchmakeSession.Gathering.MaximumParticipants.Value) { - return nex.NewError(nex.ResultCodes.RendezVous.SessionFull, fmt.Sprintf("Gathering %d is full", session.GameMatchmakeSession.Gathering.ID)) - } - - for _, connectedID := range connectionIDs { - if session.ConnectionIDs.Has(connectedID) { - return nex.NewError(nex.ResultCodes.RendezVous.AlreadyParticipatedGathering, fmt.Sprintf("Connection ID %d is already in gathering %d", connectedID, session.GameMatchmakeSession.Gathering.ID)) - } - - session.ConnectionIDs.Add(connectedID) - - // Update the participation count with the new connection ID count - session.GameMatchmakeSession.ParticipationCount.Value = uint32(session.ConnectionIDs.Size()) - } - - endpoint := initiatingConnection.Endpoint().(*nex.PRUDPEndPoint) - server := endpoint.Server - - session.ConnectionIDs.Each(func(_ int, connectionID uint32) bool { - target := endpoint.FindConnectionByID(connectionID) - if target == nil { - // TODO - Error here? - Logger.Warning("Player not found") - return false - } - - notificationCategory := notifications.NotificationCategories.Participation - notificationSubtype := notifications.NotificationSubTypes.Participation.NewParticipant - - oEvent := notifications_types.NewNotificationEvent() - oEvent.PIDSource = initiatingConnection.PID() - oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(notificationCategory, notificationSubtype)) - oEvent.Param1 = session.GameMatchmakeSession.ID.Copy().(*types.PrimitiveU32) - oEvent.Param2 = types.NewPrimitiveU32(target.PID().LegacyValue()) // TODO - This assumes a legacy client. Will not work on the Switch - oEvent.StrParam = types.NewString(joinMessage) - oEvent.Param3 = types.NewPrimitiveU32(uint32(len(connectionIDs))) - - notificationStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - - oEvent.WriteTo(notificationStream) - - notificationRequest := nex.NewRMCRequest(endpoint) - notificationRequest.ProtocolID = notifications.ProtocolID - notificationRequest.CallID = CurrentMatchmakingCallID.Next() - notificationRequest.MethodID = notifications.MethodProcessNotificationEvent - notificationRequest.Parameters = notificationStream.Bytes() - - notificationRequestBytes := notificationRequest.Bytes() - - var messagePacket nex.PRUDPPacketInterface - - if target.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, target, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, target, nil) - } - - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(target.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(target.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(target.StreamID) - messagePacket.SetPayload(notificationRequestBytes) - - server.Send(messagePacket) - - return false - }) - - // * This appears to be correct. Tri-Force Heroes uses 3.9.0, - // * and has issues if these notifications are sent. - // * Minecraft, however, requires these to be sent - // TODO - Check other games both pre and post 3.10.0 and validate - if server.LibraryVersions.MatchMaking.GreaterOrEqual("3.10.0") { - session.ConnectionIDs.Each(func(_ int, connectionID uint32) bool { - target := endpoint.FindConnectionByID(connectionID) - if target == nil { - // TODO - Error here? - Logger.Warning("Player not found") - return false - } - - notificationCategory := notifications.NotificationCategories.Participation - notificationSubtype := notifications.NotificationSubTypes.Participation.NewParticipant - - oEvent := notifications_types.NewNotificationEvent() - oEvent.PIDSource = initiatingConnection.PID() - oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(notificationCategory, notificationSubtype)) - oEvent.Param1 = session.GameMatchmakeSession.ID.Copy().(*types.PrimitiveU32) - oEvent.Param2 = types.NewPrimitiveU32(target.PID().LegacyValue()) // TODO - This assumes a legacy client. Will not work on the Switch - oEvent.StrParam = types.NewString(joinMessage) - oEvent.Param3 = types.NewPrimitiveU32(uint32(len(connectionIDs))) - - notificationStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - - oEvent.WriteTo(notificationStream) - - notificationRequest := nex.NewRMCRequest(endpoint) - notificationRequest.ProtocolID = notifications.ProtocolID - notificationRequest.CallID = CurrentMatchmakingCallID.Next() - notificationRequest.MethodID = notifications.MethodProcessNotificationEvent - notificationRequest.Parameters = notificationStream.Bytes() - - notificationRequestBytes := notificationRequest.Bytes() - - var messagePacket nex.PRUDPPacketInterface - - if target.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, initiatingConnection, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, initiatingConnection, nil) - } - - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(target.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(target.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(target.StreamID) - messagePacket.SetPayload(notificationRequestBytes) - - server.Send(messagePacket) - - return false - }) - - notificationCategory := notifications.NotificationCategories.Participation - notificationSubtype := notifications.NotificationSubTypes.Participation.NewParticipant - - oEvent := notifications_types.NewNotificationEvent() - oEvent.PIDSource = initiatingConnection.PID() - oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(notificationCategory, notificationSubtype)) - oEvent.Param1 = session.GameMatchmakeSession.ID - oEvent.Param2 = types.NewPrimitiveU32(initiatingConnection.PID().LegacyValue()) // TODO - This assumes a legacy client. Will not work on the Switch - oEvent.StrParam = types.NewString(joinMessage) - oEvent.Param3 = types.NewPrimitiveU32(uint32(len(connectionIDs))) - - notificationStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - - oEvent.WriteTo(notificationStream) - - notificationRequest := nex.NewRMCRequest(endpoint) - notificationRequest.ProtocolID = notifications.ProtocolID - notificationRequest.CallID = CurrentMatchmakingCallID.Next() - notificationRequest.MethodID = notifications.MethodProcessNotificationEvent - notificationRequest.Parameters = notificationStream.Bytes() - - notificationRequestBytes := notificationRequest.Bytes() - - var messagePacket nex.PRUDPPacketInterface - - if initiatingConnection.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, initiatingConnection, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, initiatingConnection, nil) - } - - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(initiatingConnection.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(initiatingConnection.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(initiatingConnection.StreamID) - messagePacket.SetPayload(notificationRequestBytes) - - server.Send(messagePacket) - - target := endpoint.FindConnectionByPID(session.GameMatchmakeSession.Gathering.OwnerPID.Value()) - if target == nil { - // TODO - Error here? - Logger.Warning("Player not found") - return nil - } - - if target.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, target, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, target, nil) - } - - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(target.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(target.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(target.StreamID) - messagePacket.SetPayload(notificationRequestBytes) - - server.Send(messagePacket) - } - - return nil -} - -// ChangeSessionOwner changes the session owner to a different connection -func ChangeSessionOwner(currentOwner *nex.PRUDPConnection, gathering uint32, isLeaving bool) { - endpoint := currentOwner.Endpoint().(*nex.PRUDPEndPoint) - server := endpoint.Server - session := Sessions[gathering] - - var newOwner *nex.PRUDPConnection - - newOwnerConnectionID := FindOtherConnectionID(currentOwner.ID, gathering) - if newOwnerConnectionID != 0 { - newOwner = endpoint.FindConnectionByID(newOwnerConnectionID) - if newOwner == nil { - Logger.Warning("Other connection not found") - return - } - - // If the current owner is the host and they are leaving, change it by the new owner - if session.GameMatchmakeSession.Gathering.HostPID.Equals(currentOwner.PID()) && isLeaving { - session.GameMatchmakeSession.Gathering.HostPID = newOwner.PID() - } - session.GameMatchmakeSession.Gathering.OwnerPID = newOwner.PID() - } else { - return - } - - category := notifications.NotificationCategories.OwnershipChanged - subtype := notifications.NotificationSubTypes.OwnershipChanged.None - - oEvent := notifications_types.NewNotificationEvent() - oEvent.PIDSource = currentOwner.PID() - oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(category, subtype)) - oEvent.Param1 = types.NewPrimitiveU32(gathering) - oEvent.Param2 = types.NewPrimitiveU32(newOwner.PID().LegacyValue()) // TODO - This assumes a legacy client. Will not work on the Switch - - // TODO - StrParam doesn't have this value on some servers - // * https://github.com/kinnay/NintendoClients/issues/101 - // * unixTime := time.Now() - // * oEvent.StrParam = strconv.FormatInt(unixTime.UnixMicro(), 10) - - stream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - - oEvent.WriteTo(stream) - - rmcRequest := nex.NewRMCRequest(endpoint) - rmcRequest.ProtocolID = notifications.ProtocolID - rmcRequest.CallID = CurrentMatchmakingCallID.Next() - rmcRequest.MethodID = notifications.MethodProcessNotificationEvent - rmcRequest.Parameters = stream.Bytes() - - rmcRequestBytes := rmcRequest.Bytes() - - session.ConnectionIDs.Each(func(_ int, connectionID uint32) bool { - target := endpoint.FindConnectionByID(connectionID) - if target == nil { - Logger.Warning("Connection not found") - return false - } - - var messagePacket nex.PRUDPPacketInterface - - if target.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, target, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, target, nil) - } - - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(target.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(target.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(target.StreamID) - messagePacket.SetPayload(rmcRequestBytes) - - server.Send(messagePacket) - - return false - }) -} diff --git a/globals/utils.go b/globals/utils.go index 93f8528..f7f4364 100644 --- a/globals/utils.go +++ b/globals/utils.go @@ -1,7 +1,99 @@ package common_globals +import ( + "math" + "slices" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/constants" + "github.com/PretendoNetwork/nex-go/v2/types" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" + notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" +) + +var OutgoingCallID *nex.Counter[uint32] = nex.NewCounter[uint32](0) + // DeleteIndex removes a value from a slice with the given index func DeleteIndex(s []uint32, index int) []uint32 { s[index] = s[len(s)-1] return s[:len(s)-1] } + +// CheckValidParticipant validates if a participant isn't an additional participant +func CheckValidParticipant(participant uint64) bool { + // * Additional participants are stored as the negative value of the parent participant. + // * This seems to only be possible on the 3DS and Wii U, so we don't have to check the uint64 range + return (participant <= math.MaxInt32) || (participant > math.MaxUint32) +} + +// CanJoinMatchmakeSession checks if a PID is allowed to join a matchmake session +func CanJoinMatchmakeSession(pid *types.PID, matchmakeSession *match_making_types.MatchmakeSession) *nex.Error { + // TODO - Is this the right error? + if !matchmakeSession.OpenParticipation.Value { + return nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") + } + + // * Only allow friends + // TODO - This won't work on Switch! + if matchmakeSession.ParticipationPolicy.Value == 98 { + if GetUserFriendPIDsHandler == nil { + Logger.Warning("Missing GetUserFriendPIDsHandler!") + return nex.NewError(nex.ResultCodes.Core.NotImplemented, "change_error") + } + + friendList := GetUserFriendPIDsHandler(pid.LegacyValue()) + if !slices.Contains(friendList, matchmakeSession.OwnerPID.LegacyValue()) { + return nex.NewError(nex.ResultCodes.RendezVous.NotFriend, "change_error") + } + } + + return nil +} + +// SendNotificationEvent sends a notification event to the specified targets +func SendNotificationEvent(endpoint *nex.PRUDPEndPoint, event *notifications_types.NotificationEvent, targets []uint64) { + server := endpoint.Server + stream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) + + event.WriteTo(stream) + + rmcRequest := nex.NewRMCRequest(endpoint) + rmcRequest.ProtocolID = notifications.ProtocolID + rmcRequest.CallID = OutgoingCallID.Next() + rmcRequest.MethodID = notifications.MethodProcessNotificationEvent + rmcRequest.Parameters = stream.Bytes() + + rmcRequestBytes := rmcRequest.Bytes() + + for _, pid := range targets { + if !CheckValidParticipant(pid) { + continue + } + + target := endpoint.FindConnectionByPID(pid) + if target == nil { + Logger.Warning("Client not found") + continue + } + + var messagePacket nex.PRUDPPacketInterface + + if target.DefaultPRUDPVersion == 0 { + messagePacket, _ = nex.NewPRUDPPacketV0(server, target, nil) + } else { + messagePacket, _ = nex.NewPRUDPPacketV1(server, target, nil) + } + + messagePacket.SetType(constants.DataPacket) + messagePacket.AddFlag(constants.PacketFlagNeedsAck) + messagePacket.AddFlag(constants.PacketFlagReliable) + messagePacket.SetSourceVirtualPortStreamType(target.StreamType) + messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) + messagePacket.SetDestinationVirtualPortStreamType(target.StreamType) + messagePacket.SetDestinationVirtualPortStreamID(target.StreamID) + messagePacket.SetPayload(rmcRequestBytes) + + server.Send(messagePacket) + } +} diff --git a/match-making-ext/end_participation.go b/match-making-ext/end_participation.go index e7664eb..d46bbf6 100644 --- a/match-making-ext/end_participation.go +++ b/match-making-ext/end_participation.go @@ -2,13 +2,10 @@ package match_making_ext import ( "github.com/PretendoNetwork/nex-go/v2" - "github.com/PretendoNetwork/nex-go/v2/constants" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" - match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" match_making_ext "github.com/PretendoNetwork/nex-protocols-go/v2/match-making-ext" - notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" - notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" ) func (commonProtocol *CommonProtocol) endParticipation(err error, packet nex.PacketInterface, callID uint32, idGathering *types.PrimitiveU32, strMessage *types.String) (*nex.RMCMessage, *nex.Error) { @@ -17,35 +14,18 @@ func (commonProtocol *CommonProtocol) endParticipation(err error, packet nex.Pac return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[idGathering.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") - } + common_globals.MatchmakingMutex.Lock() connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - server := endpoint.Server - - matchmakeSession := session.GameMatchmakeSession - ownerPID := matchmakeSession.Gathering.OwnerPID - var deleteSession bool = false - if connection.PID().Equals(matchmakeSession.Gathering.OwnerPID) { - // * This flag tells the server to change the matchmake session owner if they disconnect - // * If the flag is not set, delete the session - // * More info: https://nintendo-wiki.pretendo.network/docs/nex/protocols/match-making/types#flags - if matchmakeSession.Gathering.Flags.PAND(match_making.GatheringFlags.DisconnectChangeOwner) == 0 { - deleteSession = true - } else { - common_globals.ChangeSessionOwner(connection, idGathering.Value, true) - } + nexError := database.EndGatheringParticipation(commonProtocol.db, idGathering.Value, connection, strMessage.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - if deleteSession { - delete(common_globals.Sessions, idGathering.Value) - } else { - common_globals.RemoveConnectionIDFromSession(connection.ID, idGathering.Value) - } + common_globals.MatchmakingMutex.Unlock() retval := types.NewPrimitiveBool(true) @@ -60,53 +40,6 @@ func (commonProtocol *CommonProtocol) endParticipation(err error, packet nex.Pac rmcResponse.MethodID = match_making_ext.MethodEndParticipation rmcResponse.CallID = callID - category := notifications.NotificationCategories.Participation - subtype := notifications.NotificationSubTypes.Participation.Ended - - oEvent := notifications_types.NewNotificationEvent() - oEvent.PIDSource = connection.PID().Copy().(*types.PID) - oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(category, subtype)) - oEvent.Param1 = idGathering.Copy().(*types.PrimitiveU32) - oEvent.Param2 = types.NewPrimitiveU32(connection.PID().LegacyValue()) // TODO - This assumes a legacy client. Will not work on the Switch - oEvent.StrParam = strMessage.Copy().(*types.String) - - stream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - - oEvent.WriteTo(stream) - - rmcRequest := nex.NewRMCRequest(endpoint) - rmcRequest.ProtocolID = notifications.ProtocolID - rmcRequest.MethodID = notifications.MethodProcessNotificationEvent - rmcRequest.CallID = common_globals.CurrentMatchmakingCallID.Next() - rmcRequest.Parameters = stream.Bytes() - - rmcRequestBytes := rmcRequest.Bytes() - - target := endpoint.FindConnectionByPID(ownerPID.Value()) - if target == nil { - common_globals.Logger.Warning("Owner client not found") - return nil, nil - } - - var messagePacket nex.PRUDPPacketInterface - - if target.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, target, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, target, nil) - } - - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(target.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(target.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(target.StreamID) - messagePacket.SetPayload(rmcRequestBytes) - - server.Send(messagePacket) - if commonProtocol.OnAfterEndParticipation != nil { go commonProtocol.OnAfterEndParticipation(packet, idGathering, strMessage) } diff --git a/match-making-ext/protocol.go b/match-making-ext/protocol.go index 64a4300..b7c124f 100644 --- a/match-making-ext/protocol.go +++ b/match-making-ext/protocol.go @@ -1,6 +1,8 @@ package match_making_ext import ( + "database/sql" + "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" match_making_ext "github.com/PretendoNetwork/nex-protocols-go/v2/match-making-ext" @@ -9,9 +11,15 @@ import ( type CommonProtocol struct { endpoint nex.EndpointInterface protocol match_making_ext.Interface + db *sql.DB OnAfterEndParticipation func(acket nex.PacketInterface, idGathering *types.PrimitiveU32, strMessage *types.String) } +// SetDatabase defines the SQL database to be used by the common protocol +func (commonProtocol *CommonProtocol) SetDatabase(db *sql.DB) { + commonProtocol.db = db +} + // NewCommonProtocol returns a new CommonProtocol func NewCommonProtocol(protocol match_making_ext.Interface) *CommonProtocol { commonProtocol := &CommonProtocol{ diff --git a/match-making/database/disconnect_participant.go b/match-making/database/disconnect_participant.go new file mode 100644 index 0000000..b75a3b7 --- /dev/null +++ b/match-making/database/disconnect_participant.go @@ -0,0 +1,147 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" + notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// DisconnectParticipant disconnects a participant from all non-persistent gatherings +func DisconnectParticipant(db *sql.DB, connection *nex.PRUDPConnection) { + var nexError *nex.Error + + rows, err := db.Query(`SELECT id, owner_pid, host_pid, min_participants, max_participants, participation_policy, policy_argument, flags, state, description, type, participants FROM matchmaking.gatherings WHERE $1 = ANY(participants) AND registered=true`, connection.PID().Value()) + if err != nil { + common_globals.Logger.Critical(err.Error()) + return + } + + for rows.Next() { + var gatheringID uint32 + var ownerPID uint64 + var hostPID uint64 + var minParticipants uint16 + var maxParticipants uint16 + var participationPolicy uint32 + var policyArgument uint32 + var flags uint32 + var state uint32 + var description string + var gatheringType string + var participants []uint64 + + err = rows.Scan( + &gatheringID, + &ownerPID, + &hostPID, + &minParticipants, + &maxParticipants, + &participationPolicy, + &policyArgument, + &flags, + &state, + &description, + &gatheringType, + pqextended.Array(&participants), + ) + if err != nil { + common_globals.Logger.Critical(err.Error()) + continue + } + + // * If the gathering is a PersistentGathering, ignore and continue + if gatheringType == "PersistentGathering" { + continue + } + + gathering := match_making_types.NewGathering() + gathering.ID.Value = gatheringID + gathering.OwnerPID = types.NewPID(ownerPID) + gathering.HostPID = types.NewPID(hostPID) + gathering.MinimumParticipants.Value = minParticipants + gathering.MaximumParticipants.Value = maxParticipants + gathering.ParticipationPolicy.Value = participationPolicy + gathering.PolicyArgument.Value = policyArgument + gathering.Flags.Value = flags + gathering.State.Value = state + gathering.Description.Value = description + + // * Since the participant is leaving, override the participant list to avoid sending notifications to them + participants, nexError = RemoveParticipantFromGathering(db, gatheringID, connection.PID().Value()) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + continue + } + + if len(participants) == 0 { + // * There are no more participants, so we only have to unregister the gathering + // * Since the participant is disconnecting, we don't send notification events + nexError = UnregisterGathering(db, gatheringID) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + } + + continue + } + + if connection.PID().Equals(gathering.OwnerPID) { + // * This flag tells the server to change the matchmake session owner if they disconnect + // * If the flag is not set, delete the session + // * More info: https://nintendo-wiki.pretendo.network/docs/nex/protocols/match-making/types#flags + if gathering.Flags.PAND(match_making.GatheringFlags.DisconnectChangeOwner) == 0 { + nexError = UnregisterGathering(db, gatheringID) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + continue + } + + category := notifications.NotificationCategories.GatheringUnregistered + subtype := notifications.NotificationSubTypes.GatheringUnregistered.None + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID().Copy().(*types.PID) + oEvent.Type.Value = notifications.BuildNotificationType(category, subtype) + oEvent.Param1.Value = gatheringID + + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, participants) + + continue + } + + nexError = MigrateGatheringOwnership(db, connection, gathering, participants) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + } + } + + category := notifications.NotificationCategories.Participation + subtype := notifications.NotificationSubTypes.Participation.Disconnected + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID().Copy().(*types.PID) + oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(category, subtype)) + oEvent.Param1.Value = gatheringID + oEvent.Param2.Value = connection.PID().LegacyValue() // TODO - This assumes a legacy client. Will not work on the Switch + + var participantEndedTargets []uint64 + + // * When the VerboseParticipants or VerboseParticipantsEx flags are set, all participant notification events are sent to everyone + if gathering.Flags.PAND(match_making.GatheringFlags.VerboseParticipants | match_making.GatheringFlags.VerboseParticipantsEx) != 0 { + participantEndedTargets = participants + } else { + participantEndedTargets = []uint64{gathering.OwnerPID.Value()} + } + + // * Only send the notification event to the owner + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, participantEndedTargets) + } + + rows.Close() +} diff --git a/match-making/database/end_gathering_participation.go b/match-making/database/end_gathering_participation.go new file mode 100644 index 0000000..c609703 --- /dev/null +++ b/match-making/database/end_gathering_participation.go @@ -0,0 +1,119 @@ +package database + +import ( + "database/sql" + "slices" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" + notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" + notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" +) + +// EndGatheringParticipation ends the participation of a connection within a gathering and performs any additional handling required +func EndGatheringParticipation(db *sql.DB, gatheringID uint32, connection *nex.PRUDPConnection, message string) *nex.Error { + gathering, gatheringType, participants, _, nexError := FindGatheringByID(db, gatheringID) + if nexError != nil { + return nexError + } + + // TODO - Is this the right error? + if !slices.Contains(participants, connection.PID().Value()) { + return nex.NewError(nex.ResultCodes.RendezVous.NotParticipatedGathering, "change_error") + } + + // * If the gathering is a PersistentGathering, only remove the participant from the gathering + if gatheringType == "PersistentGathering" { + _, nexError = RemoveParticipantFromGathering(db, gatheringID, connection.PID().Value()) + return nexError + } + + newParticipants, nexError := RemoveParticipantFromGathering(db, gatheringID, connection.PID().Value()) + if nexError != nil { + return nexError + } + + var targetParticipants []uint64 + + // * Minecraft (NEX 3.10) sends all gathering notification events to everyone + // TODO - Is this the case for all NEX 3.10+ games? + if connection.Endpoint().(*nex.PRUDPEndPoint).LibraryVersions().MatchMaking.GreaterOrEqual("v3.10") { + targetParticipants = participants + } else { + targetParticipants = newParticipants + } + + if len(newParticipants) == 0 { + // * There are no more participants, so we just unregister the gathering + nexError = UnregisterGathering(db, gatheringID) + if nexError != nil { + return nexError + } + + category := notifications.NotificationCategories.GatheringUnregistered + subtype := notifications.NotificationSubTypes.GatheringUnregistered.None + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID().Copy().(*types.PID) + oEvent.Type.Value = notifications.BuildNotificationType(category, subtype) + oEvent.Param1.Value = gatheringID + + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, targetParticipants) + + return nil + } + + if connection.PID().Equals(gathering.OwnerPID) { + // * This flag tells the server to change the matchmake session owner if they disconnect + // * If the flag is not set, delete the session + // * More info: https://nintendo-wiki.pretendo.network/docs/nex/protocols/match-making/types#flags + if gathering.Flags.PAND(match_making.GatheringFlags.DisconnectChangeOwner) == 0 { + nexError = UnregisterGathering(db, gatheringID) + if nexError != nil { + return nexError + } + + category := notifications.NotificationCategories.GatheringUnregistered + subtype := notifications.NotificationSubTypes.GatheringUnregistered.None + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID().Copy().(*types.PID) + oEvent.Type.Value = notifications.BuildNotificationType(category, subtype) + oEvent.Param1.Value = gatheringID + + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, targetParticipants) + + return nil + } + + nexError = MigrateGatheringOwnership(db, connection, gathering, targetParticipants) + if nexError != nil { + return nexError + } + } + + category := notifications.NotificationCategories.Participation + subtype := notifications.NotificationSubTypes.Participation.Ended + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID().Copy().(*types.PID) + oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(category, subtype)) + oEvent.Param1.Value = gatheringID + oEvent.Param2.Value = connection.PID().LegacyValue() // TODO - This assumes a legacy client. Will not work on the Switch + oEvent.StrParam.Value = message + + var participationEndedTargets []uint64 + + // * When the VerboseParticipants or VerboseParticipantsEx flags are set, all participant notification events are sent to everyone + if gathering.Flags.PAND(match_making.GatheringFlags.VerboseParticipants | match_making.GatheringFlags.VerboseParticipantsEx) != 0 { + participationEndedTargets = participants + } else { + participationEndedTargets = []uint64{gathering.OwnerPID.Value()} + } + + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, participationEndedTargets) + + return nil +} diff --git a/match-making/database/find_gathering_by_id.go b/match-making/database/find_gathering_by_id.go new file mode 100644 index 0000000..7f286ae --- /dev/null +++ b/match-making/database/find_gathering_by_id.go @@ -0,0 +1,65 @@ +package database + +import ( + "database/sql" + "time" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// FindGatheringByID finds a gathering on a database with the given ID. Returns the gathering, its type, the participant list and the started time +func FindGatheringByID(db *sql.DB, id uint32) (*match_making_types.Gathering, string, []uint64, *types.DateTime, *nex.Error) { + row := db.QueryRow(`SELECT owner_pid, host_pid, min_participants, max_participants, participation_policy, policy_argument, flags, state, description, type, participants, started_time FROM matchmaking.gatherings WHERE id=$1 AND registered=true`, id) + + var ownerPID uint64 + var hostPID uint64 + var minParticipants uint16 + var maxParticipants uint16 + var participationPolicy uint32 + var policyArgument uint32 + var flags uint32 + var state uint32 + var description string + var gatheringType string + var participants []uint64 + var startedTime time.Time + + err := row.Scan( + &ownerPID, + &hostPID, + &minParticipants, + &maxParticipants, + &participationPolicy, + &policyArgument, + &flags, + &state, + &description, + &gatheringType, + pqextended.Array(&participants), + &startedTime, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, "", nil, nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, err.Error()) + } else { + return nil, "", nil, nil, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + } + + gathering := match_making_types.NewGathering() + gathering.ID.Value = id + gathering.OwnerPID = types.NewPID(ownerPID) + gathering.HostPID = types.NewPID(hostPID) + gathering.MinimumParticipants.Value = minParticipants + gathering.MaximumParticipants.Value = maxParticipants + gathering.ParticipationPolicy.Value = participationPolicy + gathering.PolicyArgument.Value = policyArgument + gathering.Flags.Value = flags + gathering.State.Value = state + gathering.Description.Value = description + + return gathering, gatheringType, participants, types.NewDateTime(0).FromTimestamp(startedTime), nil +} diff --git a/match-making/database/join_gathering.go b/match-making/database/join_gathering.go new file mode 100644 index 0000000..212c4d7 --- /dev/null +++ b/match-making/database/join_gathering.go @@ -0,0 +1,112 @@ +package database + +import ( + "database/sql" + "slices" + + "github.com/PretendoNetwork/nex-go/v2" + common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" + notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" + notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// JoinGathering joins participants from the same connection into a gathering. Returns the new number of participants +func JoinGathering(db *sql.DB, gatheringID uint32, connection *nex.PRUDPConnection, vacantParticipants uint16, joinMessage string) (uint32, *nex.Error) { + // * vacantParticipants represents the total number of participants that are joining (including the main participant) + // * Prevent underflow below if vacantParticipants is set to zero + if vacantParticipants == 0 { + vacantParticipants = 1 + } + + var ownerPID uint64 + var maxParticipants uint32 + var flags uint32 + var participants []uint64 + err := db.QueryRow(`SELECT owner_pid, max_participants, flags, participants FROM matchmaking.gatherings WHERE id=$1`, gatheringID).Scan(&ownerPID, &maxParticipants, &flags, pqextended.Array(&participants)) + if err != nil { + if err == sql.ErrNoRows { + return 0, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } else { + return 0, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + } + + if uint32(len(participants)) + uint32(vacantParticipants) > maxParticipants { + return 0, nex.NewError(nex.ResultCodes.RendezVous.SessionFull, "change_error") + } + + if slices.Contains(participants, connection.PID().Value()) { + return 0, nex.NewError(nex.ResultCodes.RendezVous.AlreadyParticipatedGathering, "change_error") + } + + // TODO - This won't work on the Switch! + newParticipants := append(participants, connection.PID().Value()) + + // * Additional participants are represented as the negative of the main participant PID + // * These are casted to an unsigned value for compatibility with uint32 and uint64 + additionalParticipant := int32(-connection.PID().LegacyValue()) + for range vacantParticipants - 1 { + newParticipants = append(newParticipants, uint64(uint32(additionalParticipant))) + } + + // * We have already checked that the gathering exists above, so we don't have to check the rows affected on sql.Result + _, err = db.Exec(`UPDATE matchmaking.gatherings SET participants=$1 WHERE id=$2`, pqextended.Array(newParticipants), gatheringID) + if err != nil { + if err == sql.ErrNoRows { + return 0, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } else { + return 0, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + } + + var participatJoinedTargets []uint64 + + // * When the VerboseParticipants or VerboseParticipantsEx flags are set, all participant notification events are sent to everyone + if flags & (match_making.GatheringFlags.VerboseParticipants | match_making.GatheringFlags.VerboseParticipantsEx) != 0 { + participatJoinedTargets = newParticipants + } else { + // * If the new participant is the same as the owner, then we are creating a new gathering. + // * We don't need to send notification events in that case + if connection.PID().Value() == ownerPID { + return uint32(len(newParticipants)), nil + } + + participatJoinedTargets = []uint64{ownerPID} + } + + notificationCategory := notifications.NotificationCategories.Participation + notificationSubtype := notifications.NotificationSubTypes.Participation.NewParticipant + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID() + oEvent.Type.Value = notifications.BuildNotificationType(notificationCategory, notificationSubtype) + oEvent.Param1.Value = gatheringID + oEvent.Param2.Value = uint32(connection.PID().LegacyValue()) // TODO - This assumes a legacy client. Will not work on the Switch + oEvent.StrParam.Value = joinMessage + oEvent.Param3.Value = uint32(len(newParticipants)) + + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, participatJoinedTargets) + + // * This flag also sends a recap of all currently connected players on the gathering to the participant that is connecting + if flags & match_making.GatheringFlags.VerboseParticipantsEx != 0 { + for _, participant := range participants { + notificationCategory := notifications.NotificationCategories.Participation + notificationSubtype := notifications.NotificationSubTypes.Participation.NewParticipant + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID() + oEvent.Type.Value = notifications.BuildNotificationType(notificationCategory, notificationSubtype) + oEvent.Param1.Value = gatheringID + oEvent.Param2.Value = uint32(participant) // TODO - This assumes a legacy client. Will not work on the Switch + oEvent.StrParam.Value = joinMessage + oEvent.Param3.Value = uint32(len(newParticipants)) + + // * Send the notification to the joining participant + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, []uint64{connection.PID().Value()}) + } + } + + return uint32(len(newParticipants)), nil +} diff --git a/match-making/database/join_gathering_with_participants.go b/match-making/database/join_gathering_with_participants.go new file mode 100644 index 0000000..045a72c --- /dev/null +++ b/match-making/database/join_gathering_with_participants.go @@ -0,0 +1,121 @@ +package database + +import ( + "database/sql" + "slices" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" + notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" + notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// JoinGatheringWithParticipants joins participants into a gathering. Returns the new number of participants +func JoinGatheringWithParticipants(db *sql.DB, gatheringID uint32, connection *nex.PRUDPConnection, additionalParticipants []*types.PID, joinMessage string) (uint32, *nex.Error) { + var ownerPID uint64 + var maxParticipants uint32 + var flags uint32 + var oldParticipants []uint64 + err := db.QueryRow(`SELECT owner_pid, max_participants, flags, participants FROM matchmaking.gatherings WHERE id=$1`, gatheringID).Scan(&ownerPID, &maxParticipants, &flags, pqextended.Array(&oldParticipants)) + if err != nil { + if err == sql.ErrNoRows { + return 0, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } else { + return 0, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + } + + if uint32(len(oldParticipants) + 1 + len(additionalParticipants)) > maxParticipants { + return 0, nex.NewError(nex.ResultCodes.RendezVous.SessionFull, "change_error") + } + + if slices.Contains(oldParticipants, connection.PID().Value()) { + return 0, nex.NewError(nex.ResultCodes.RendezVous.AlreadyParticipatedGathering, "change_error") + } + + newParticipants := []uint64{connection.PID().Value()} + for _, participant := range additionalParticipants { + newParticipants = append(newParticipants, participant.Value()) + } + + participants := append(oldParticipants, newParticipants...) + + // * We have already checked that the gathering exists above, so we don't have to check the rows affected on sql.Result + _, err = db.Exec(`UPDATE matchmaking.gatherings SET participants=$1 WHERE id=$2`, pqextended.Array(participants), gatheringID) + if err != nil { + if err == sql.ErrNoRows { + return 0, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } else { + return 0, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + } + + var participantJoinedTargets []uint64 + + // * When the VerboseParticipants or the VerboseParticipantsEx flags are set, all participant notification events are sent to everyone + if flags & (match_making.GatheringFlags.VerboseParticipants | match_making.GatheringFlags.VerboseParticipantsEx) != 0 { + participantJoinedTargets = participants + } else { + participantJoinedTargets = []uint64{ownerPID} + } + + for _, participant := range newParticipants { + // * If the new participant is the same as the owner, then we are creating a new gathering. + // * We don't need to send the new participant notification event in that case + if flags & (match_making.GatheringFlags.VerboseParticipants | match_making.GatheringFlags.VerboseParticipantsEx) != 0 || connection.PID().Value() != ownerPID { + notificationCategory := notifications.NotificationCategories.Participation + notificationSubtype := notifications.NotificationSubTypes.Participation.NewParticipant + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID() + oEvent.Type.Value = notifications.BuildNotificationType(notificationCategory, notificationSubtype) + oEvent.Param1.Value = gatheringID + oEvent.Param2.Value = uint32(participant) // TODO - This assumes a legacy client. Will not work on the Switch + oEvent.StrParam.Value = joinMessage + oEvent.Param3.Value = uint32(len(participants)) + + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, participantJoinedTargets) + } + + // * This flag also sends a recap of all currently connected players on the gathering to the participant that is connecting + if flags & match_making.GatheringFlags.VerboseParticipantsEx != 0 { + for _, oldParticipant := range oldParticipants { + notificationCategory := notifications.NotificationCategories.Participation + notificationSubtype := notifications.NotificationSubTypes.Participation.NewParticipant + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID() + oEvent.Type.Value = notifications.BuildNotificationType(notificationCategory, notificationSubtype) + oEvent.Param1.Value = gatheringID + oEvent.Param2.Value = uint32(oldParticipant) // TODO - This assumes a legacy client. Will not work on the Switch + oEvent.StrParam.Value = joinMessage + oEvent.Param3.Value = uint32(len(participants)) + + // * Send the notification to the joining participant + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, []uint64{participant}) + } + } + + // * Don't send the SwitchGathering notification to the participant that requested the join + if connection.PID().Value() == uint64(participant) { + continue + } + + notificationCategory := notifications.NotificationCategories.SwitchGathering + notificationSubtype := notifications.NotificationSubTypes.SwitchGathering.None + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID() + oEvent.Type.Value = notifications.BuildNotificationType(notificationCategory, notificationSubtype) + oEvent.Param1.Value = gatheringID + oEvent.Param2.Value = uint32(participant) // TODO - This assumes a legacy client. Will not work on the Switch + + // * Send the notification to the participant + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, []uint64{participant}) + } + + return uint32(len(participants)), nil +} diff --git a/match-making/database/migrate_gathering_ownership.go b/match-making/database/migrate_gathering_ownership.go new file mode 100644 index 0000000..52cd948 --- /dev/null +++ b/match-making/database/migrate_gathering_ownership.go @@ -0,0 +1,69 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" + notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" +) + +// MigrateGatheringOwnership switches the owner of the gathering with a different one +func MigrateGatheringOwnership(db *sql.DB, connection *nex.PRUDPConnection, gathering *match_making_types.Gathering, participants []uint64) *nex.Error { + var nexError *nex.Error + + var newOwner uint64 + for _, participant := range participants { + if participant != gathering.OwnerPID.Value() && common_globals.CheckValidParticipant(participant) { + newOwner = participant + break + } + } + + // * We couldn't find a new owner, so we unregister the gathering + if newOwner == 0 { + nexError = UnregisterGathering(db, gathering.ID.Value) + if nexError != nil { + return nexError + } + + category := notifications.NotificationCategories.GatheringUnregistered + subtype := notifications.NotificationSubTypes.GatheringUnregistered.None + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID().Copy().(*types.PID) + oEvent.Type.Value = notifications.BuildNotificationType(category, subtype) + oEvent.Param1.Value = gathering.ID.Value + + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, participants) + return nil + } + + // * Set the new owner + gathering.OwnerPID = types.NewPID(newOwner) + + nexError = UpdateSessionHost(db, gathering.ID.Value, gathering.OwnerPID, gathering.HostPID) + if nexError != nil { + return nexError + } + + category := notifications.NotificationCategories.OwnershipChanged + subtype := notifications.NotificationSubTypes.OwnershipChanged.None + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID().Copy().(*types.PID) + oEvent.Type.Value = notifications.BuildNotificationType(category, subtype) + oEvent.Param1.Value = gathering.ID.Value + oEvent.Param2.Value = uint32(newOwner) // TODO - This assumes a legacy client. Will not work on the Switch + + // TODO - StrParam doesn't have this value on some servers + // * https://github.com/kinnay/NintendoClients/issues/101 + // * unixTime := time.Now() + // * oEvent.StrParam = strconv.FormatInt(unixTime.UnixMicro(), 10) + + common_globals.SendNotificationEvent(connection.Endpoint().(*nex.PRUDPEndPoint), oEvent, participants) + return nil +} diff --git a/match-making/database/register_gathering.go b/match-making/database/register_gathering.go new file mode 100644 index 0000000..fb533c3 --- /dev/null +++ b/match-making/database/register_gathering.go @@ -0,0 +1,60 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" +) + +// RegisterGathering registers a new gathering on the databse. No participants are added +func RegisterGathering(db *sql.DB, pid *types.PID, gathering *match_making_types.Gathering, gatheringType string) (*types.DateTime, *nex.Error) { + startedTime := types.NewDateTime(0).Now() + + err := db.QueryRow(`INSERT INTO matchmaking.gatherings ( + owner_pid, + host_pid, + min_participants, + max_participants, + participation_policy, + policy_argument, + flags, + state, + description, + type, + started_time + ) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11 + ) RETURNING id`, + pid.Value(), + pid.Value(), + gathering.MinimumParticipants.Value, + gathering.MaximumParticipants.Value, + gathering.ParticipationPolicy.Value, + gathering.PolicyArgument.Value, + gathering.Flags.Value, + gathering.State.Value, + gathering.Description.Value, + gatheringType, + startedTime.Standard(), + ).Scan(&gathering.ID.Value) + if err != nil { + return nil, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + gathering.OwnerPID = pid.Copy().(*types.PID) + gathering.HostPID = pid.Copy().(*types.PID) + + return startedTime, nil +} diff --git a/match-making/database/remove_participant_from_gathering.go b/match-making/database/remove_participant_from_gathering.go new file mode 100644 index 0000000..510f8d7 --- /dev/null +++ b/match-making/database/remove_participant_from_gathering.go @@ -0,0 +1,33 @@ +package database + +import ( + "database/sql" + "fmt" + + "github.com/PretendoNetwork/nex-go/v2" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// RemoveParticipantFromGathering removes a participant from a gathering. Returns the new list of participants +func RemoveParticipantFromGathering(db *sql.DB, gatheringID uint32, participant uint64) ([]uint64, *nex.Error) { + newParticipantsStatement := fmt.Sprintf("array_remove(participants, %d)", participant) + + // * If the participant fits within a int32, then it is compatible with the 3DS and Wii U. + // * Thus, we have to remove the additional participants too + if uint64(int32(participant)) == participant { + additionalParticipants := int32(participant) + newParticipantsStatement = fmt.Sprintf("array_remove(array_remove(participants, %d), %d)", -additionalParticipants, participant) + } + + var newParticipants []uint64 + err := db.QueryRow(`UPDATE matchmaking.gatherings SET participants=` + newParticipantsStatement + ` WHERE id=$1 RETURNING participants`, gatheringID).Scan(pqextended.Array(&newParticipants)) + if err != nil { + if err == sql.ErrNoRows { + return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } else { + return nil, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + } + + return newParticipants, nil +} diff --git a/match-making/database/unregister_gathering.go b/match-making/database/unregister_gathering.go new file mode 100644 index 0000000..d6c71c4 --- /dev/null +++ b/match-making/database/unregister_gathering.go @@ -0,0 +1,26 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" +) + +// UnregisterGathering unregisters a given gathering on a database +func UnregisterGathering(db *sql.DB, id uint32) *nex.Error { + result, err := db.Exec(`UPDATE matchmaking.gatherings SET registered=false WHERE id=$1`, id) + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + if rowsAffected == 0 { + return nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } + + return nil +} diff --git a/match-making/database/update_session_host.go b/match-making/database/update_session_host.go new file mode 100644 index 0000000..788c73e --- /dev/null +++ b/match-making/database/update_session_host.go @@ -0,0 +1,27 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" +) + +// UpdateSessionHost updates the owner and host PID of the session +func UpdateSessionHost(db *sql.DB, gatheringID uint32, ownerPID *types.PID, hostPID *types.PID) *nex.Error { + result, err := db.Exec(`UPDATE matchmaking.gatherings SET owner_pid=$1, host_pid=$2 WHERE id=$3`, ownerPID.Value(), hostPID.Value(), gatheringID) + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + if rowsAffected == 0 { + return nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } + + return nil +} diff --git a/match-making/find_by_single_id.go b/match-making/find_by_single_id.go index 973b80d..42280d5 100644 --- a/match-making/find_by_single_id.go +++ b/match-making/find_by_single_id.go @@ -4,6 +4,8 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + matchmake_extension_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" ) @@ -13,19 +15,32 @@ func (commonProtocol *CommonProtocol) findBySingleID(err error, packet nex.Packe return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[id.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + common_globals.MatchmakingMutex.RLock() + gathering, gatheringType, participants, startedTime, nexError := database.FindGatheringByID(commonProtocol.db, id.Value) + if nexError != nil { + common_globals.MatchmakingMutex.RUnlock() + return nil, nexError + } + + // TODO - Add PersistentGathering + if gatheringType != "MatchmakeSession" { + common_globals.MatchmakingMutex.RUnlock() + return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) + matchmakeSession, nexError := matchmake_extension_database.GetMatchmakeSessionByGathering(commonProtocol.db, endpoint, gathering, uint32(len(participants)), startedTime) + if nexError != nil { + return nil, nexError + } + bResult := types.NewPrimitiveBool(true) pGathering := types.NewAnyDataHolder() - pGathering.TypeName = types.NewString("MatchmakeSession") - pGathering.ObjectData = session.GameMatchmakeSession.Copy() + pGathering.TypeName = types.NewString(gatheringType) + pGathering.ObjectData = matchmakeSession.Copy() rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) diff --git a/match-making/get_session_urls.go b/match-making/get_session_urls.go index 783a54d..920715d 100644 --- a/match-making/get_session_urls.go +++ b/match-making/get_session_urls.go @@ -1,9 +1,12 @@ package matchmaking import ( + "slices" + "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" ) @@ -13,30 +16,44 @@ func (commonProtocol *CommonProtocol) getSessionURLs(err error, packet nex.Packe return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[gid.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + common_globals.MatchmakingMutex.RLock() + gathering, _, participants, _, nexError := database.FindGatheringByID(commonProtocol.db, gid.Value) + if nexError != nil { + common_globals.MatchmakingMutex.RUnlock() + return nil, nexError } connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - hostPID := session.GameMatchmakeSession.Gathering.HostPID - host := endpoint.FindConnectionByPID(hostPID.Value()) + if !slices.Contains(participants, connection.PID().Value()) { + common_globals.MatchmakingMutex.RUnlock() + return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") + } + + host := endpoint.FindConnectionByPID(gathering.HostPID.Value()) if host == nil { // * This popped up once during testing. Leaving it noted here in case it becomes a problem. common_globals.Logger.Warning("Host client not found, trying with owner client") - host = endpoint.FindConnectionByPID(session.GameMatchmakeSession.Gathering.OwnerPID.Value()) + host = endpoint.FindConnectionByPID(gathering.OwnerPID.Value()) if host == nil { // * This popped up once during testing. Leaving it noted here in case it becomes a problem. common_globals.Logger.Error("Owner client not found") - return nil, nex.NewError(nex.ResultCodes.Core.Exception, "change_error") } } + common_globals.MatchmakingMutex.RUnlock() + rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - host.StationURLs.WriteTo(rmcResponseStream) + // * If no host was found, return an empty list of station URLs + if host == nil { + stationURLs := types.NewList[*types.StationURL]() + stationURLs.Type = types.NewStationURL("") + stationURLs.WriteTo(rmcResponseStream) + } else { + host.StationURLs.WriteTo(rmcResponseStream) + } rmcResponseBody := rmcResponseStream.Bytes() diff --git a/match-making/protocol.go b/match-making/protocol.go index 96d7271..0fda7bb 100644 --- a/match-making/protocol.go +++ b/match-making/protocol.go @@ -1,9 +1,12 @@ package matchmaking import ( + "database/sql" + "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" _ "github.com/PretendoNetwork/nex-protocols-go/v2" match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" ) @@ -11,6 +14,7 @@ import ( type CommonProtocol struct { endpoint *nex.PRUDPEndPoint protocol match_making.Interface + db *sql.DB OnAfterUnregisterGathering func(packet nex.PacketInterface, idGathering *types.PrimitiveU32) OnAfterFindBySingleID func(packet nex.PacketInterface, id *types.PrimitiveU32) OnAfterUpdateSessionURL func(packet nex.PacketInterface, idGathering *types.PrimitiveU32, strURL *types.String) @@ -19,6 +23,51 @@ type CommonProtocol struct { OnAfterUpdateSessionHost func(packet nex.PacketInterface, gid *types.PrimitiveU32, isMigrateOwner *types.PrimitiveBool) } +// SetDatabase defines the SQL database to be used by the common protocol +func (commonProtocol *CommonProtocol) SetDatabase(db *sql.DB) { + var err error + + commonProtocol.db = db + + _, err = db.Exec(`CREATE SCHEMA IF NOT EXISTS matchmaking`) + if err != nil { + common_globals.Logger.Error(err.Error()) + return + } + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS matchmaking.gatherings ( + id bigserial PRIMARY KEY, + owner_pid numeric(10), + host_pid numeric(10), + min_participants integer, + max_participants integer, + participation_policy bigint, + policy_argument bigint, + flags bigint, + state bigint, + description text, + registered boolean NOT NULL DEFAULT true, + type text NOT NULL DEFAULT '', + started_time timestamp, + participants numeric(10)[] NOT NULL DEFAULT array[]::numeric(10)[] + )`) + if err != nil { + common_globals.Logger.Error(err.Error()) + return + } + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS matchmaking.participants ( + pid numeric(10) PRIMARY KEY, + owned_gatherings bigint[] NOT NULL DEFAULT array[]::bigint[], + hosted_gatherings bigint[] NOT NULL DEFAULT array[]::bigint[], + participated_gatherings bigint[] NOT NULL DEFAULT array[]::bigint[] + )`) + if err != nil { + common_globals.Logger.Error(err.Error()) + return + } +} + // NewCommonProtocol returns a new CommonProtocol func NewCommonProtocol(protocol match_making.Interface) *CommonProtocol { endpoint := protocol.Endpoint().(*nex.PRUDPEndPoint) @@ -28,8 +77,6 @@ func NewCommonProtocol(protocol match_making.Interface) *CommonProtocol { protocol: protocol, } - common_globals.Sessions = make(map[uint32]*common_globals.CommonMatchmakeSession) - protocol.SetHandlerUnregisterGathering(commonProtocol.unregisterGathering) protocol.SetHandlerFindBySingleID(commonProtocol.findBySingleID) protocol.SetHandlerUpdateSessionURL(commonProtocol.updateSessionURL) @@ -38,7 +85,9 @@ func NewCommonProtocol(protocol match_making.Interface) *CommonProtocol { protocol.SetHandlerUpdateSessionHost(commonProtocol.updateSessionHost) endpoint.OnConnectionEnded(func(connection *nex.PRUDPConnection) { - common_globals.RemoveConnectionFromAllSessions(connection) + common_globals.MatchmakingMutex.Lock() + database.DisconnectParticipant(commonProtocol.db, connection) + common_globals.MatchmakingMutex.Unlock() }) return commonProtocol diff --git a/match-making/unregister_gathering.go b/match-making/unregister_gathering.go index f77abb4..e2fce5b 100644 --- a/match-making/unregister_gathering.go +++ b/match-making/unregister_gathering.go @@ -2,9 +2,9 @@ package matchmaking import ( "github.com/PretendoNetwork/nex-go/v2" - "github.com/PretendoNetwork/nex-go/v2/constants" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" @@ -16,22 +16,38 @@ func (commonProtocol *CommonProtocol) unregisterGathering(err error, packet nex. return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[idGathering.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + common_globals.MatchmakingMutex.Lock() + gathering, _, participants, _, nexError := database.FindGatheringByID(commonProtocol.db, idGathering.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - server := endpoint.Server - if !session.GameMatchmakeSession.Gathering.OwnerPID.Equals(connection.PID()) { + if !gathering.OwnerPID.Equals(connection.PID()) { + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") } - gatheringPlayers := session.ConnectionIDs + nexError = database.UnregisterGathering(commonProtocol.db, idGathering.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + category := notifications.NotificationCategories.GatheringUnregistered + subtype := notifications.NotificationSubTypes.GatheringUnregistered.None + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID().Copy().(*types.PID) + oEvent.Type.Value = notifications.BuildNotificationType(category, subtype) + oEvent.Param1.Value = idGathering.Value + + common_globals.SendNotificationEvent(endpoint, oEvent, participants) - delete(common_globals.Sessions, idGathering.Value) + common_globals.MatchmakingMutex.Unlock() retval := types.NewPrimitiveBool(true) @@ -46,55 +62,6 @@ func (commonProtocol *CommonProtocol) unregisterGathering(err error, packet nex. rmcResponse.MethodID = match_making.MethodUnregisterGathering rmcResponse.CallID = callID - category := notifications.NotificationCategories.GatheringUnregistered - subtype := notifications.NotificationSubTypes.GatheringUnregistered.None - - oEvent := notifications_types.NewNotificationEvent() - oEvent.PIDSource = connection.PID().Copy().(*types.PID) - oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(category, subtype)) - oEvent.Param1 = idGathering.Copy().(*types.PrimitiveU32) - - stream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - - oEvent.WriteTo(stream) - - rmcRequest := nex.NewRMCRequest(endpoint) - rmcRequest.ProtocolID = notifications.ProtocolID - rmcRequest.CallID = common_globals.CurrentMatchmakingCallID.Next() - rmcRequest.MethodID = notifications.MethodProcessNotificationEvent - rmcRequest.Parameters = stream.Bytes() - - rmcRequestBytes := rmcRequest.Bytes() - - gatheringPlayers.Each(func(_ int, connectionID uint32) bool { - target := endpoint.FindConnectionByID(connectionID) - if target == nil { - common_globals.Logger.Warning("Client not found") - return false - } - - var messagePacket nex.PRUDPPacketInterface - - if target.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, target, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, target, nil) - } - - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(target.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(target.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(target.StreamID) - messagePacket.SetPayload(rmcRequestBytes) - - server.Send(messagePacket) - - return false - }) - if commonProtocol.OnAfterUnregisterGathering != nil { go commonProtocol.OnAfterUnregisterGathering(packet, idGathering) } diff --git a/match-making/update_session_host.go b/match-making/update_session_host.go index ee8b413..6376682 100644 --- a/match-making/update_session_host.go +++ b/match-making/update_session_host.go @@ -1,10 +1,12 @@ package matchmaking import ( + "slices" + "github.com/PretendoNetwork/nex-go/v2" - "github.com/PretendoNetwork/nex-go/v2/constants" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" @@ -16,91 +18,58 @@ func (commonProtocol *CommonProtocol) updateSessionHost(err error, packet nex.Pa return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[gid.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + common_globals.MatchmakingMutex.Lock() + gathering, _, participants, _, nexError := database.FindGatheringByID(commonProtocol.db, gid.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - server := endpoint.Server - if common_globals.FindConnectionSession(connection.ID) != gid.Value { + if !slices.Contains(participants, connection.PID().Value()) { + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") } - session.GameMatchmakeSession.Gathering.HostPID = connection.PID().Copy().(*types.PID) - - rmcResponse := nex.NewRMCSuccess(endpoint, nil) - rmcResponse.ProtocolID = match_making.ProtocolID - rmcResponse.MethodID = match_making.MethodUpdateSessionHost - rmcResponse.CallID = callID - + // TODO - Should this check for match_making.GatheringFlags.ParticipantsChangeOwner too? if !isMigrateOwner.Value { - if commonProtocol.OnAfterUpdateSessionHost != nil { - go commonProtocol.OnAfterUpdateSessionHost(packet, gid, isMigrateOwner) + nexError = database.UpdateSessionHost(commonProtocol.db, gid.Value, gathering.OwnerPID, connection.PID()) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - - return rmcResponse, nil - } - - originalOwner := session.GameMatchmakeSession.Gathering.OwnerPID - session.GameMatchmakeSession.Gathering.OwnerPID = connection.PID().Copy().(*types.PID) - - category := notifications.NotificationCategories.OwnershipChanged - subtype := notifications.NotificationSubTypes.OwnershipChanged.None - - oEvent := notifications_types.NewNotificationEvent() - oEvent.PIDSource = originalOwner.Copy().(*types.PID) - oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(category, subtype)) - oEvent.Param1 = gid.Copy().(*types.PrimitiveU32) - oEvent.Param2 = types.NewPrimitiveU32(connection.PID().LegacyValue()) // TODO - This assumes a legacy client. Will not work on the Switch - - // TODO - StrParam doesn't have this value on some servers - // * https://github.com/kinnay/NintendoClients/issues/101 - // * unixTime := time.Now() - // * oEvent.StrParam = strconv.FormatInt(unixTime.UnixMicro(), 10) - - stream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - - oEvent.WriteTo(stream) - - rmcRequest := nex.NewRMCRequest(endpoint) - rmcRequest.ProtocolID = notifications.ProtocolID - rmcRequest.CallID = common_globals.CurrentMatchmakingCallID.Next() - rmcRequest.MethodID = notifications.MethodProcessNotificationEvent - rmcRequest.Parameters = stream.Bytes() - - rmcRequestBytes := rmcRequest.Bytes() - - common_globals.Sessions[gid.Value].ConnectionIDs.Each(func(_ int, connectionID uint32) bool { - target := endpoint.FindConnectionByID(connectionID) - if target == nil { - common_globals.Logger.Warning("Client not found") - return false + } else { + nexError = database.UpdateSessionHost(commonProtocol.db, gid.Value, connection.PID(), connection.PID()) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - var messagePacket nex.PRUDPPacketInterface + category := notifications.NotificationCategories.OwnershipChanged + subtype := notifications.NotificationSubTypes.OwnershipChanged.None - if target.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, target, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, target, nil) - } + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID() + oEvent.Type.Value = notifications.BuildNotificationType(category, subtype) + oEvent.Param1.Value = gid.Value + oEvent.Param2.Value = connection.PID().LegacyValue() // TODO - This assumes a legacy client. Will not work on the Switch - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(target.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(target.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(target.StreamID) - messagePacket.SetPayload(rmcRequestBytes) + // TODO - StrParam doesn't have this value on some servers + // * https://github.com/kinnay/NintendoClients/issues/101 + // * unixTime := time.Now() + // * oEvent.StrParam = strconv.FormatInt(unixTime.UnixMicro(), 10) - server.Send(messagePacket) + common_globals.SendNotificationEvent(endpoint, oEvent, participants) + } + + common_globals.MatchmakingMutex.Unlock() - return false - }) + rmcResponse := nex.NewRMCSuccess(endpoint, nil) + rmcResponse.ProtocolID = match_making.ProtocolID + rmcResponse.MethodID = match_making.MethodUpdateSessionHost + rmcResponse.CallID = callID if commonProtocol.OnAfterUpdateSessionHost != nil { go commonProtocol.OnAfterUpdateSessionHost(packet, gid, isMigrateOwner) diff --git a/match-making/update_session_host_v1.go b/match-making/update_session_host_v1.go index 10bc7c6..5810cb5 100644 --- a/match-making/update_session_host_v1.go +++ b/match-making/update_session_host_v1.go @@ -1,10 +1,15 @@ package matchmaking import ( + "slices" + "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" + notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" + notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" ) func (commonProtocol *CommonProtocol) updateSessionHostV1(err error, packet nex.PacketInterface, callID uint32, gid *types.PrimitiveU32) (*nex.RMCMessage, *nex.Error) { @@ -13,23 +18,54 @@ func (commonProtocol *CommonProtocol) updateSessionHostV1(err error, packet nex. return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[gid.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + common_globals.MatchmakingMutex.Lock() + + gathering, _, participants, _, nexError := database.FindGatheringByID(commonProtocol.db, gid.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - if common_globals.FindConnectionSession(connection.ID) != gid.Value { + if !slices.Contains(participants, connection.PID().Value()) { + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") } - session.GameMatchmakeSession.Gathering.HostPID = connection.PID() - if session.GameMatchmakeSession.Gathering.Flags.PAND(match_making.GatheringFlags.DisconnectChangeOwner) != 0 { - session.GameMatchmakeSession.Gathering.OwnerPID = connection.PID() + if gathering.Flags.PAND(match_making.GatheringFlags.ParticipantsChangeOwner) == 0 { + nexError = database.UpdateSessionHost(commonProtocol.db, gid.Value, gathering.OwnerPID, connection.PID()) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + } else { + nexError = database.UpdateSessionHost(commonProtocol.db, gid.Value, connection.PID(), connection.PID()) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + category := notifications.NotificationCategories.OwnershipChanged + subtype := notifications.NotificationSubTypes.OwnershipChanged.None + + oEvent := notifications_types.NewNotificationEvent() + oEvent.PIDSource = connection.PID() + oEvent.Type.Value = notifications.BuildNotificationType(category, subtype) + oEvent.Param1.Value = gid.Value + oEvent.Param2.Value = connection.PID().LegacyValue() // TODO - This assumes a legacy client. Will not work on the Switch + + // TODO - StrParam doesn't have this value on some servers + // * https://github.com/kinnay/NintendoClients/issues/101 + // * unixTime := time.Now() + // * oEvent.StrParam = strconv.FormatInt(unixTime.UnixMicro(), 10) + + common_globals.SendNotificationEvent(endpoint, oEvent, participants) } + common_globals.MatchmakingMutex.Unlock() + rmcResponse := nex.NewRMCSuccess(endpoint, nil) rmcResponse.ProtocolID = match_making.ProtocolID rmcResponse.MethodID = match_making.MethodUpdateSessionHostV1 diff --git a/match-making/update_session_url.go b/match-making/update_session_url.go index f61bc3a..0aeb4eb 100644 --- a/match-making/update_session_url.go +++ b/match-making/update_session_url.go @@ -1,13 +1,13 @@ package matchmaking import ( + "slices" + "github.com/PretendoNetwork/nex-go/v2" - "github.com/PretendoNetwork/nex-go/v2/constants" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" match_making "github.com/PretendoNetwork/nex-protocols-go/v2/match-making" - notifications "github.com/PretendoNetwork/nex-protocols-go/v2/notifications" - notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types" ) func (commonProtocol *CommonProtocol) updateSessionURL(err error, packet nex.PacketInterface, callID uint32, idGathering *types.PrimitiveU32, strURL *types.String) (*nex.RMCMessage, *nex.Error) { @@ -16,77 +16,32 @@ func (commonProtocol *CommonProtocol) updateSessionURL(err error, packet nex.Pac return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[idGathering.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + common_globals.MatchmakingMutex.Lock() + gathering, _, participants, _, nexError := database.FindGatheringByID(commonProtocol.db, idGathering.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - server := endpoint.Server - - // * Mario Kart 7 seems to set an empty strURL, so I assume this is what the method does? - session.GameMatchmakeSession.Gathering.HostPID = connection.PID().Copy().(*types.PID) - if session.GameMatchmakeSession.Gathering.Flags.PAND(match_making.GatheringFlags.DisconnectChangeOwner) != 0 { - originalOwner := session.GameMatchmakeSession.Gathering.OwnerPID - session.GameMatchmakeSession.Gathering.OwnerPID = connection.PID().Copy().(*types.PID) - - category := notifications.NotificationCategories.OwnershipChanged - subtype := notifications.NotificationSubTypes.OwnershipChanged.None - - oEvent := notifications_types.NewNotificationEvent() - oEvent.PIDSource = originalOwner.Copy().(*types.PID) - oEvent.Type = types.NewPrimitiveU32(notifications.BuildNotificationType(category, subtype)) - oEvent.Param1 = idGathering.Copy().(*types.PrimitiveU32) - oEvent.Param2 = types.NewPrimitiveU32(connection.PID().LegacyValue()) // TODO - This assumes a legacy client. Will not work on the Switch - - // TODO - StrParam doesn't have this value on some servers - // * https://github.com/kinnay/NintendoClients/issues/101 - // * unixTime := time.Now() - // * oEvent.StrParam = strconv.FormatInt(unixTime.UnixMicro(), 10) - - stream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - - oEvent.WriteTo(stream) - - rmcRequest := nex.NewRMCRequest(endpoint) - rmcRequest.ProtocolID = notifications.ProtocolID - rmcRequest.CallID = common_globals.CurrentMatchmakingCallID.Next() - rmcRequest.MethodID = notifications.MethodProcessNotificationEvent - rmcRequest.Parameters = stream.Bytes() - - rmcRequestBytes := rmcRequest.Bytes() - - common_globals.Sessions[idGathering.Value].ConnectionIDs.Each(func(_ int, connectionID uint32) bool { - target := endpoint.FindConnectionByID(connectionID) - if target == nil { - common_globals.Logger.Warning("Client not found") - return false - } - - var messagePacket nex.PRUDPPacketInterface - - if target.DefaultPRUDPVersion == 0 { - messagePacket, _ = nex.NewPRUDPPacketV0(server, target, nil) - } else { - messagePacket, _ = nex.NewPRUDPPacketV1(server, target, nil) - } - - messagePacket.SetType(constants.DataPacket) - messagePacket.AddFlag(constants.PacketFlagNeedsAck) - messagePacket.AddFlag(constants.PacketFlagReliable) - messagePacket.SetSourceVirtualPortStreamType(target.StreamType) - messagePacket.SetSourceVirtualPortStreamID(endpoint.StreamID) - messagePacket.SetDestinationVirtualPortStreamType(target.StreamType) - messagePacket.SetDestinationVirtualPortStreamID(target.StreamID) - messagePacket.SetPayload(rmcRequestBytes) - - server.Send(messagePacket) - - return false - }) + + if !slices.Contains(participants, connection.PID().Value()) { + common_globals.MatchmakingMutex.Unlock() + return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") + } + + // TODO - Mario Kart 7 seems to set an empty strURL. What does that do if it's actually set? + + // * Only update the host + nexError = database.UpdateSessionHost(commonProtocol.db, idGathering.Value, gathering.OwnerPID, connection.PID()) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } + common_globals.MatchmakingMutex.Unlock() + retval := types.NewPrimitiveBool(true) rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) diff --git a/matchmake-extension/auto_matchmake_postpone.go b/matchmake-extension/auto_matchmake_postpone.go index e3d4c23..a0bebde 100644 --- a/matchmake-extension/auto_matchmake_postpone.go +++ b/matchmake-extension/auto_matchmake_postpone.go @@ -6,6 +6,8 @@ import ( common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" + match_making_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + database "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" ) func (commonProtocol *CommonProtocol) autoMatchmakePostpone(err error, packet nex.PacketInterface, callID uint32, anyGathering *types.AnyDataHolder, message *types.String) (*nex.RMCMessage, *nex.Error) { @@ -22,9 +24,11 @@ func (commonProtocol *CommonProtocol) autoMatchmakePostpone(err error, packet ne connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) + common_globals.MatchmakingMutex.Lock() + // * A client may disconnect from a session without leaving reliably, // * so let's make sure the client is removed from the session - common_globals.RemoveConnectionFromAllSessions(connection) + match_making_database.DisconnectParticipant(commonProtocol.db, connection) var matchmakeSession *match_making_types.MatchmakeSession anyGatheringDataType := anyGathering.TypeName @@ -33,35 +37,42 @@ func (commonProtocol *CommonProtocol) autoMatchmakePostpone(err error, packet ne matchmakeSession = anyGathering.ObjectData.(*match_making_types.MatchmakeSession) } else { common_globals.Logger.Critical("Non-MatchmakeSession DataType?!") + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } searchMatchmakeSession := matchmakeSession.Copy().(*match_making_types.MatchmakeSession) commonProtocol.CleanupSearchMatchmakeSession(searchMatchmakeSession) - sessionIndex := common_globals.FindSessionByMatchmakeSession(connection.PID(), searchMatchmakeSession) - var session *common_globals.CommonMatchmakeSession - - if sessionIndex == 0 { - var errCode *nex.Error - session, errCode = common_globals.CreateSessionByMatchmakeSession(matchmakeSession, searchMatchmakeSession, connection.PID()) - if err != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + resultSession, nexError := database.FindMatchmakeSession(commonProtocol.db, connection, searchMatchmakeSession) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + if resultSession == nil { + resultSession = searchMatchmakeSession.Copy().(*match_making_types.MatchmakeSession) + nexError = database.CreateMatchmakeSession(commonProtocol.db, connection, resultSession) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - } else { - session = common_globals.Sessions[sessionIndex] } - errCode := common_globals.AddPlayersToSession(session, []uint32{connection.ID}, connection, message.Value) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + participants, nexError := match_making_database.JoinGathering(commonProtocol.db, resultSession.Gathering.ID.Value, connection, 1, message.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } + resultSession.ParticipationCount.Value = participants + + common_globals.MatchmakingMutex.Unlock() + matchmakeDataHolder := types.NewAnyDataHolder() matchmakeDataHolder.TypeName = types.NewString("MatchmakeSession") - matchmakeDataHolder.ObjectData = session.GameMatchmakeSession.Copy() + matchmakeDataHolder.ObjectData = resultSession.Copy() rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) diff --git a/matchmake-extension/auto_matchmake_with_param_postpone.go b/matchmake-extension/auto_matchmake_with_param_postpone.go index c601397..966cc3a 100644 --- a/matchmake-extension/auto_matchmake_with_param_postpone.go +++ b/matchmake-extension/auto_matchmake_with_param_postpone.go @@ -4,11 +4,18 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) func (commonProtocol *CommonProtocol) autoMatchmakeWithParamPostpone(err error, packet nex.PacketInterface, callID uint32, autoMatchmakeParam *match_making_types.AutoMatchmakeParam) (*nex.RMCMessage, *nex.Error) { + if commonProtocol.CleanupMatchmakeSessionSearchCriterias == nil { + common_globals.Logger.Warning("MatchmakeExtension::AutoMatchmakeWithParam_Postpone missing CleanupMatchmakeSessionSearchCriterias!") + return nil, nex.NewError(nex.ResultCodes.Core.NotImplemented, "change_error") + } + if err != nil { common_globals.Logger.Error(err.Error()) return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") @@ -17,40 +24,48 @@ func (commonProtocol *CommonProtocol) autoMatchmakeWithParamPostpone(err error, connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) + common_globals.MatchmakingMutex.Lock() + // * A client may disconnect from a session without leaving reliably, // * so let's make sure the client is removed from the session - common_globals.RemoveConnectionFromAllSessions(connection) + match_making_database.DisconnectParticipant(commonProtocol.db, connection) - matchmakeSession := autoMatchmakeParam.SourceMatchmakeSession + commonProtocol.CleanupMatchmakeSessionSearchCriterias(autoMatchmakeParam.LstSearchCriteria) - sessions := common_globals.FindSessionsByMatchmakeSessionSearchCriterias(connection.PID(), autoMatchmakeParam.LstSearchCriteria.Slice(), commonProtocol.GameSpecificMatchmakeSessionSearchCriteriaChecks) - var session *common_globals.CommonMatchmakeSession + resultRange := types.NewResultRange() + resultRange.Length.Value = 1 + resultSessions, nexError := database.FindMatchmakeSessionBySearchCriteria(commonProtocol.db, connection, autoMatchmakeParam.LstSearchCriteria.Slice(), resultRange) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } - if len(sessions) == 0 { - var errCode *nex.Error - session, errCode = common_globals.CreateSessionByMatchmakeSession(matchmakeSession, nil, connection.PID()) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + var resultSession *match_making_types.MatchmakeSession + if len(resultSessions) == 0 { + resultSession = autoMatchmakeParam.SourceMatchmakeSession.Copy().(*match_making_types.MatchmakeSession) + nexError = database.CreateMatchmakeSession(commonProtocol.db, connection, resultSession) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } } else { - session = sessions[0] + resultSession = resultSessions[0] } - errCode := common_globals.AddPlayersToSession(session, []uint32{connection.ID}, connection, "") - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + participants, nexError := match_making_database.JoinGatheringWithParticipants(commonProtocol.db, resultSession.ID.Value, connection, autoMatchmakeParam.AdditionalParticipants.Slice(), autoMatchmakeParam.JoinMessage.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - matchmakeDataHolder := types.NewAnyDataHolder() + resultSession.ParticipationCount.Value = participants - matchmakeDataHolder.TypeName = types.NewString("MatchmakeSession") - matchmakeDataHolder.ObjectData = session.GameMatchmakeSession.Copy() + common_globals.MatchmakingMutex.Unlock() rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - session.GameMatchmakeSession.WriteTo(rmcResponseStream) + resultSession.WriteTo(rmcResponseStream) rmcResponseBody := rmcResponseStream.Bytes() diff --git a/matchmake-extension/auto_matchmake_with_search_criteria_postpone.go b/matchmake-extension/auto_matchmake_with_search_criteria_postpone.go index 4d478ee..be8420e 100644 --- a/matchmake-extension/auto_matchmake_with_search_criteria_postpone.go +++ b/matchmake-extension/auto_matchmake_with_search_criteria_postpone.go @@ -4,11 +4,18 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) func (commonProtocol *CommonProtocol) autoMatchmakeWithSearchCriteriaPostpone(err error, packet nex.PacketInterface, callID uint32, lstSearchCriteria *types.List[*match_making_types.MatchmakeSessionSearchCriteria], anyGathering *types.AnyDataHolder, strMessage *types.String) (*nex.RMCMessage, *nex.Error) { + if commonProtocol.CleanupMatchmakeSessionSearchCriterias == nil { + common_globals.Logger.Warning("MatchmakeExtension::AutoMatchmakeWithSearchCriteria_Postpone missing CleanupMatchmakeSessionSearchCriterias!") + return nil, nex.NewError(nex.ResultCodes.Core.NotImplemented, "change_error") + } + if err != nil { common_globals.Logger.Error(err.Error()) return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") @@ -17,9 +24,11 @@ func (commonProtocol *CommonProtocol) autoMatchmakeWithSearchCriteriaPostpone(er connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) + common_globals.MatchmakingMutex.Lock() + // * A client may disconnect from a session without leaving reliably, // * so let's make sure the client is removed from the session - common_globals.RemoveConnectionFromAllSessions(connection) + match_making_database.DisconnectParticipant(commonProtocol.db, connection) var matchmakeSession *match_making_types.MatchmakeSession @@ -27,33 +36,52 @@ func (commonProtocol *CommonProtocol) autoMatchmakeWithSearchCriteriaPostpone(er matchmakeSession = anyGathering.ObjectData.(*match_making_types.MatchmakeSession) } else { common_globals.Logger.Critical("Non-MatchmakeSession DataType?!") + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - sessions := common_globals.FindSessionsByMatchmakeSessionSearchCriterias(connection.PID(), lstSearchCriteria.Slice(), commonProtocol.GameSpecificMatchmakeSessionSearchCriteriaChecks) - var session *common_globals.CommonMatchmakeSession + commonProtocol.CleanupMatchmakeSessionSearchCriterias(lstSearchCriteria) + + resultRange := types.NewResultRange() + resultRange.Length.Value = 1 + resultSessions, nexError := database.FindMatchmakeSessionBySearchCriteria(commonProtocol.db, connection, lstSearchCriteria.Slice(), resultRange) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } - if len(sessions) == 0 { - var errCode *nex.Error - session, errCode = common_globals.CreateSessionByMatchmakeSession(matchmakeSession, nil, connection.PID()) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + var resultSession *match_making_types.MatchmakeSession + if len(resultSessions) == 0 { + resultSession = matchmakeSession.Copy().(*match_making_types.MatchmakeSession) + nexError = database.CreateMatchmakeSession(commonProtocol.db, connection, resultSession) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } } else { - session = sessions[0] + resultSession = resultSessions[0] } - errCode := common_globals.AddPlayersToSession(session, []uint32{connection.ID}, connection, strMessage.Value) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + var vacantParticipants uint16 = 1 + if searchCriteria, err := lstSearchCriteria.Get(0); err == nil { + vacantParticipants = searchCriteria.VacantParticipants.Value } + participants, nexError := match_making_database.JoinGathering(commonProtocol.db, resultSession.Gathering.ID.Value, connection, vacantParticipants, strMessage.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + resultSession.ParticipationCount.Value = participants + + common_globals.MatchmakingMutex.Unlock() + matchmakeDataHolder := types.NewAnyDataHolder() matchmakeDataHolder.TypeName = types.NewString("MatchmakeSession") - matchmakeDataHolder.ObjectData = session.GameMatchmakeSession.Copy() + matchmakeDataHolder.ObjectData = resultSession.Copy() rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) diff --git a/matchmake-extension/browse_matchmake_session.go b/matchmake-extension/browse_matchmake_session.go index 18e8c64..20758ab 100644 --- a/matchmake-extension/browse_matchmake_session.go +++ b/matchmake-extension/browse_matchmake_session.go @@ -1,11 +1,10 @@ package matchmake_extension import ( - "math" - "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -19,35 +18,32 @@ func (commonProtocol *CommonProtocol) browseMatchmakeSession(err error, packet n connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - searchCriterias := []*match_making_types.MatchmakeSessionSearchCriteria{searchCriteria} - - sessions := common_globals.FindSessionsByMatchmakeSessionSearchCriterias(connection.PID(), searchCriterias, commonProtocol.GameSpecificMatchmakeSessionSearchCriteriaChecks) - - // TODO - Is this right? - if resultRange.Offset.Value != math.MaxUint32 { - if len(sessions) < int(resultRange.Offset.Value) { - return nil, nex.NewError(nex.ResultCodes.Core.InvalidIndex, "change_error") - } - - sessions = sessions[resultRange.Offset.Value:] - } + common_globals.MatchmakingMutex.RLock() + searchCriterias := []*match_making_types.MatchmakeSessionSearchCriteria{searchCriteria} - if len(sessions) > int(resultRange.Length.Value) { - sessions = sessions[:resultRange.Length.Value] + sessions, nexError := database.FindMatchmakeSessionBySearchCriteria(commonProtocol.db, connection, searchCriterias, resultRange) + if nexError != nil { + common_globals.MatchmakingMutex.RUnlock() + return nil, nexError } lstGathering := types.NewList[*types.AnyDataHolder]() lstGathering.Type = types.NewAnyDataHolder() for _, session := range sessions { + // * Scrap session key + session.SessionKey.Value = make([]byte, 0) + matchmakeSessionDataHolder := types.NewAnyDataHolder() matchmakeSessionDataHolder.TypeName = types.NewString("MatchmakeSession") - matchmakeSessionDataHolder.ObjectData = session.GameMatchmakeSession.Copy() + matchmakeSessionDataHolder.ObjectData = session.Copy() lstGathering.Append(matchmakeSessionDataHolder) } + common_globals.MatchmakingMutex.RUnlock() + rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) lstGathering.WriteTo(rmcResponseStream) diff --git a/matchmake-extension/close_participation.go b/matchmake-extension/close_participation.go index 6bc1b00..08437e9 100644 --- a/matchmake-extension/close_participation.go +++ b/matchmake-extension/close_participation.go @@ -4,6 +4,7 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -13,19 +14,29 @@ func (commonProtocol *CommonProtocol) closeParticipation(err error, packet nex.P return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[gid.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") - } - connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - if !session.GameMatchmakeSession.Gathering.OwnerPID.Equals(connection.PID()) { + common_globals.MatchmakingMutex.Lock() + + session, nexError := database.GetMatchmakeSessionByID(commonProtocol.db, endpoint, gid.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + if !session.Gathering.OwnerPID.Equals(connection.PID()) { + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") } - session.GameMatchmakeSession.OpenParticipation = types.NewPrimitiveBool(false) + nexError = database.UpdateParticipation(commonProtocol.db, gid.Value, false) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + common_globals.MatchmakingMutex.Unlock() rmcResponse := nex.NewRMCSuccess(endpoint, nil) rmcResponse.ProtocolID = matchmake_extension.ProtocolID diff --git a/matchmake-extension/create_matchmake_session.go b/matchmake-extension/create_matchmake_session.go index b67f76a..94eeeef 100644 --- a/matchmake-extension/create_matchmake_session.go +++ b/matchmake-extension/create_matchmake_session.go @@ -4,6 +4,8 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -18,9 +20,11 @@ func (commonProtocol *CommonProtocol) createMatchmakeSession(err error, packet n endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) server := endpoint.Server + common_globals.MatchmakingMutex.Lock() + // * A client may disconnect from a session without leaving reliably, // * so let's make sure the client is removed from the session - common_globals.RemoveConnectionFromAllSessions(connection) + match_making_database.DisconnectParticipant(commonProtocol.db, connection) var matchmakeSession *match_making_types.MatchmakeSession @@ -28,27 +32,34 @@ func (commonProtocol *CommonProtocol) createMatchmakeSession(err error, packet n matchmakeSession = anyGathering.ObjectData.(*match_making_types.MatchmakeSession) } else { common_globals.Logger.Critical("Non-MatchmakeSession DataType?!") + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, errCode := common_globals.CreateSessionByMatchmakeSession(matchmakeSession, nil, connection.PID()) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + nexError := database.CreateMatchmakeSession(commonProtocol.db, connection, matchmakeSession) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - errCode = common_globals.AddPlayersToSession(session, []uint32{connection.ID}, connection, message.Value) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + participants, nexError := match_making_database.JoinGathering(commonProtocol.db, matchmakeSession.Gathering.ID.Value, connection, participationCount.Value, message.Value) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } + matchmakeSession.ParticipationCount.Value = participants + + common_globals.MatchmakingMutex.Unlock() + rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - session.GameMatchmakeSession.Gathering.ID.WriteTo(rmcResponseStream) + matchmakeSession.Gathering.ID.WriteTo(rmcResponseStream) if server.LibraryVersions.MatchMaking.GreaterOrEqual("3.0.0") { - session.GameMatchmakeSession.SessionKey.WriteTo(rmcResponseStream) + matchmakeSession.SessionKey.WriteTo(rmcResponseStream) } rmcResponseBody := rmcResponseStream.Bytes() diff --git a/matchmake-extension/create_matchmake_session_with_param.go b/matchmake-extension/create_matchmake_session_with_param.go index 056ddad..f6b0e7c 100644 --- a/matchmake-extension/create_matchmake_session_with_param.go +++ b/matchmake-extension/create_matchmake_session_with_param.go @@ -3,6 +3,8 @@ package matchmake_extension import ( "github.com/PretendoNetwork/nex-go/v2" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -16,26 +18,32 @@ func (commonProtocol *CommonProtocol) createMatchmakeSessionWithParam(err error, connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) + common_globals.MatchmakingMutex.Lock() + // * A client may disconnect from a session without leaving reliably, // * so let's make sure the client is removed from all sessions - common_globals.RemoveConnectionFromAllSessions(connection) + match_making_database.DisconnectParticipant(commonProtocol.db, connection) joinedMatchmakeSession := createMatchmakeSessionParam.SourceMatchmakeSession.Copy().(*match_making_types.MatchmakeSession) - session, errCode := common_globals.CreateSessionByMatchmakeSession(joinedMatchmakeSession, nil, connection.PID()) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + nexError := database.CreateMatchmakeSession(commonProtocol.db, connection, joinedMatchmakeSession) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - errCode = common_globals.AddPlayersToSession(session, []uint32{connection.ID}, connection, createMatchmakeSessionParam.JoinMessage.Value) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + participants, nexError := match_making_database.JoinGatheringWithParticipants(commonProtocol.db, joinedMatchmakeSession.Gathering.ID.Value, connection, createMatchmakeSessionParam.AdditionalParticipants.Slice(), createMatchmakeSessionParam.JoinMessage.Value) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } + joinedMatchmakeSession.ParticipationCount.Value = participants + rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - session.GameMatchmakeSession.WriteTo(rmcResponseStream) + joinedMatchmakeSession.WriteTo(rmcResponseStream) rmcResponseBody := rmcResponseStream.Bytes() diff --git a/matchmake-extension/database/create_matchmake_session.go b/matchmake-extension/database/create_matchmake_session.go new file mode 100644 index 0000000..1368cf0 --- /dev/null +++ b/matchmake-extension/database/create_matchmake_session.go @@ -0,0 +1,99 @@ +package database + +import ( + "crypto/rand" + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + match_making_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// CreateMatchmakeSession creates a new MatchmakeSession on the database. No participants are added +func CreateMatchmakeSession(db *sql.DB, connection *nex.PRUDPConnection, matchmakeSession *match_making_types.MatchmakeSession) *nex.Error { + startedTime, nexError := match_making_database.RegisterGathering(db, connection.PID(), matchmakeSession.Gathering, "MatchmakeSession") + if nexError != nil { + return nexError + } + + attribs := make([]uint32, matchmakeSession.Attributes.Length()) + for i, value := range matchmakeSession.Attributes.Slice() { + attribs[i] = value.Value + } + + endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) + + matchmakeParam := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) + + srVariant := types.NewVariant() + srVariant.TypeID.Value = 3 + srVariant.Type = types.NewPrimitiveBool(true) + matchmakeSession.MatchmakeParam.Params.Set(types.NewString("@SR"), srVariant) + girVariant := types.NewVariant() + girVariant.TypeID.Value = 1 + girVariant.Type = types.NewPrimitiveS64(3) + matchmakeSession.MatchmakeParam.Params.Set(types.NewString("@GIR"), srVariant) + + matchmakeSession.MatchmakeParam.WriteTo(matchmakeParam) + + matchmakeSession.StartedTime = startedTime + matchmakeSession.SessionKey.Value = make([]byte, 32) + rand.Read(matchmakeSession.SessionKey.Value) + + _, err := db.Exec(`INSERT INTO matchmaking.matchmake_sessions ( + id, + game_mode, + attribs, + open_participation, + matchmake_system_type, + application_buffer, + progress_score, + session_key, + option_zero, + matchmake_param, + user_password, + refer_gid, + user_password_enabled, + system_password_enabled, + codeword + ) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 + )`, + matchmakeSession.Gathering.ID.Value, + matchmakeSession.GameMode.Value, + pqextended.Array(attribs), + matchmakeSession.OpenParticipation.Value, + matchmakeSession.MatchmakeSystemType.Value, + matchmakeSession.ApplicationBuffer.Value, + matchmakeSession.ProgressScore.Value, + matchmakeSession.SessionKey.Value, + matchmakeSession.Option.Value, + matchmakeParam.Bytes(), + matchmakeSession.UserPassword.Value, + matchmakeSession.ReferGID.Value, + matchmakeSession.UserPasswordEnabled.Value, + matchmakeSession.SystemPasswordEnabled.Value, + matchmakeSession.CodeWord.Value, + ) + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + return nil +} diff --git a/matchmake-extension/database/find_matchmake_session.go b/matchmake-extension/database/find_matchmake_session.go new file mode 100644 index 0000000..0853f3a --- /dev/null +++ b/matchmake-extension/database/find_matchmake_session.go @@ -0,0 +1,138 @@ +package database + +import ( + "database/sql" + "time" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// FindMatchmakeSession finds a matchmake session with the given search matchmake session +func FindMatchmakeSession(db *sql.DB, connection *nex.PRUDPConnection, searchMatchmakeSession *match_making_types.MatchmakeSession) (*match_making_types.MatchmakeSession, *nex.Error) { + attribs := make([]uint32, searchMatchmakeSession.Attributes.Length()) + for i, value := range searchMatchmakeSession.Attributes.Slice() { + attribs[i] = value.Value + } + + endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) + + searchStatement := `SELECT + g.id, + g.owner_pid, + g.host_pid, + g.min_participants, + g.max_participants, + g.participation_policy, + g.policy_argument, + g.flags, + g.state, + g.description, + array_length(g.participants, 1), + g.started_time, + ms.game_mode, + ms.attribs, + ms.open_participation, + ms.matchmake_system_type, + ms.application_buffer, + ms.progress_score, + ms.session_key, + ms.option_zero, + ms.matchmake_param, + ms.user_password, + ms.refer_gid, + ms.user_password_enabled, + ms.system_password_enabled, + ms.codeword + FROM matchmaking.gatherings AS g + INNER JOIN matchmaking.matchmake_sessions AS ms ON ms.id = g.id + WHERE + g.registered=true AND + g.type='MatchmakeSession' AND + ms.open_participation=true AND + array_length(g.participants, 1) < g.max_participants AND + g.max_participants=$1 AND + g.min_participants=$2 AND + ms.game_mode=$3 AND + ms.attribs=$4 AND + ms.matchmake_system_type=$5 AND + ms.refer_gid=$6 AND + ms.codeword=$7 AND (CASE WHEN g.participation_policy=98 THEN g.owner_pid=ANY($8) ELSE true END)` + + var friendList []uint32 + // * Prevent access to friend rooms if not implemented + if common_globals.GetUserFriendPIDsHandler != nil { + friendList = common_globals.GetUserFriendPIDsHandler(connection.PID().LegacyValue()) + } + + resultMatchmakeSession := match_making_types.NewMatchmakeSession() + var ownerPID uint64 + var hostPID uint64 + var startedTime time.Time + var resultAttribs []uint32 + var resultMatchmakeParam []byte + + // * For simplicity, we will only compare the values that exist on a MatchmakeSessionSearchCriteria + err := db.QueryRow(searchStatement, + searchMatchmakeSession.Gathering.MaximumParticipants.Value, + searchMatchmakeSession.Gathering.MinimumParticipants.Value, + searchMatchmakeSession.GameMode.Value, + pqextended.Array(attribs), + searchMatchmakeSession.MatchmakeSystemType.Value, + searchMatchmakeSession.ReferGID.Value, + searchMatchmakeSession.CodeWord.Value, + pqextended.Array(friendList), + ).Scan( + &resultMatchmakeSession.Gathering.ID.Value, + &ownerPID, + &hostPID, + &resultMatchmakeSession.Gathering.MinimumParticipants.Value, + &resultMatchmakeSession.Gathering.MaximumParticipants.Value, + &resultMatchmakeSession.Gathering.ParticipationPolicy.Value, + &resultMatchmakeSession.Gathering.PolicyArgument.Value, + &resultMatchmakeSession.Gathering.Flags.Value, + &resultMatchmakeSession.Gathering.State.Value, + &resultMatchmakeSession.Gathering.Description.Value, + &resultMatchmakeSession.ParticipationCount.Value, + &startedTime, + &resultMatchmakeSession.GameMode.Value, + pqextended.Array(&resultAttribs), + &resultMatchmakeSession.OpenParticipation.Value, + &resultMatchmakeSession.MatchmakeSystemType.Value, + &resultMatchmakeSession.ApplicationBuffer.Value, + &resultMatchmakeSession.ProgressScore.Value, + &resultMatchmakeSession.SessionKey.Value, + &resultMatchmakeSession.Option.Value, + &resultMatchmakeParam, + &resultMatchmakeSession.UserPassword.Value, + &resultMatchmakeSession.ReferGID.Value, + &resultMatchmakeSession.UserPasswordEnabled.Value, + &resultMatchmakeSession.SystemPasswordEnabled.Value, + &resultMatchmakeSession.CodeWord.Value, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } else { + return nil, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + } + + resultMatchmakeSession.OwnerPID = types.NewPID(ownerPID) + resultMatchmakeSession.HostPID = types.NewPID(hostPID) + resultMatchmakeSession.StartedTime = resultMatchmakeSession.StartedTime.FromTimestamp(startedTime) + + attributesSlice := make([]*types.PrimitiveU32, len(resultAttribs)) + for i, value := range resultAttribs { + attributesSlice[i] = types.NewPrimitiveU32(value) + } + resultMatchmakeSession.Attributes.SetFromData(attributesSlice) + + matchmakeParamBytes := nex.NewByteStreamIn(resultMatchmakeParam, endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) + resultMatchmakeSession.MatchmakeParam.ExtractFrom(matchmakeParamBytes) + + return resultMatchmakeSession, nil +} diff --git a/matchmake-extension/database/find_matchmake_session_by_search_criteria.go b/matchmake-extension/database/find_matchmake_session_by_search_criteria.go new file mode 100644 index 0000000..305dde4 --- /dev/null +++ b/matchmake-extension/database/find_matchmake_session_by_search_criteria.go @@ -0,0 +1,297 @@ +package database + +import ( + "database/sql" + "fmt" + "math" + "strconv" + "strings" + "time" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-go/v2/globals" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// FindMatchmakeSessionBySearchCriteria finds matchmake sessions with the given search criterias +func FindMatchmakeSessionBySearchCriteria(db *sql.DB, connection *nex.PRUDPConnection, searchCriterias []*match_making_types.MatchmakeSessionSearchCriteria, resultRange *types.ResultRange) ([]*match_making_types.MatchmakeSession, *nex.Error) { + resultMatchmakeSessions := make([]*match_making_types.MatchmakeSession, 0) + + endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) + + var friendList []uint32 + if common_globals.GetUserFriendPIDsHandler != nil { + friendList = common_globals.GetUserFriendPIDsHandler(connection.PID().LegacyValue()) + } + + // TODO - Is this right? + if resultRange.Offset.Value == math.MaxUint32 { + resultRange.Offset.Value = 0 + } + + for _, searchCriteria := range searchCriterias { + searchStatement := `SELECT + g.id, + g.owner_pid, + g.host_pid, + g.min_participants, + g.max_participants, + g.participation_policy, + g.policy_argument, + g.flags, + g.state, + g.description, + array_length(g.participants, 1), + g.started_time, + ms.game_mode, + ms.attribs, + ms.open_participation, + ms.matchmake_system_type, + ms.application_buffer, + ms.progress_score, + ms.session_key, + ms.option_zero, + ms.matchmake_param, + ms.user_password, + ms.refer_gid, + ms.user_password_enabled, + ms.system_password_enabled, + ms.codeword + FROM matchmaking.gatherings AS g + INNER JOIN matchmaking.matchmake_sessions AS ms ON ms.id = g.id + WHERE + g.registered=true AND + g.type='MatchmakeSession' AND + ms.open_participation=true AND + ms.refer_gid=$1 AND + ms.codeword=$2 AND + array_length(ms.attribs, 1)=$3 AND + (CASE WHEN g.participation_policy=98 THEN g.owner_pid=ANY($4) ELSE true END)` + + var valid bool = true + for i, attrib := range searchCriteria.Attribs.Slice() { + if attrib.Value != "" { + before, after, found := strings.Cut(attrib.Value, ",") + if found { + min, err := strconv.ParseUint(before, 10, 32) + if err != nil { + valid = false + break + } + + max, err := strconv.ParseUint(after, 10, 32) + if err != nil { + valid = false + break + } + + searchStatement += fmt.Sprintf(` AND ms.attribs[%d] BETWEEN %d AND %d`, i + 1, min, max) + } else { + value, err := strconv.ParseUint(before, 10, 32) + if err != nil { + valid = false + break + } + + searchStatement += fmt.Sprintf(` AND ms.attribs[%d]=%d`, i + 1, value) + } + } + } + + // * Search criteria is invalid, continue to next one + if !valid { + continue + } + + if searchCriteria.MaxParticipants.Value != "" { + before, after, found := strings.Cut(searchCriteria.MaxParticipants.Value, ",") + if found { + min, err := strconv.ParseUint(before, 10, 16) + if err != nil { + continue + } + + max, err := strconv.ParseUint(after, 10, 16) + if err != nil { + continue + } + + searchStatement += fmt.Sprintf(` AND ms.max_participants BETWEEN %d AND %d`, min, max) + } else { + value, err := strconv.ParseUint(before, 10, 16) + if err != nil { + continue + } + + searchStatement += fmt.Sprintf(` AND ms.max_participants=%d`, value) + } + } + + if searchCriteria.MinParticipants.Value != "" { + before, after, found := strings.Cut(searchCriteria.MinParticipants.Value, ",") + if found { + min, err := strconv.ParseUint(before, 10, 16) + if err != nil { + continue + } + + max, err := strconv.ParseUint(after, 10, 16) + if err != nil { + continue + } + + searchStatement += fmt.Sprintf(` AND ms.min_participants BETWEEN %d AND %d`, min, max) + } else { + value, err := strconv.ParseUint(before, 10, 16) + if err != nil { + continue + } + + searchStatement += fmt.Sprintf(` AND ms.min_participants=%d`, value) + } + } + + if searchCriteria.GameMode.Value != "" { + before, after, found := strings.Cut(searchCriteria.GameMode.Value, ",") + if found { + min, err := strconv.ParseUint(before, 10, 32) + if err != nil { + continue + } + + max, err := strconv.ParseUint(after, 10, 32) + if err != nil { + continue + } + + searchStatement += fmt.Sprintf(` AND ms.game_mode BETWEEN %d AND %d`, min, max) + } else { + value, err := strconv.ParseUint(before, 10, 32) + if err != nil { + continue + } + + searchStatement += fmt.Sprintf(` AND ms.game_mode=%d`, value) + } + } + + if searchCriteria.MatchmakeSystemType.Value != "" { + before, after, found := strings.Cut(searchCriteria.MatchmakeSystemType.Value, ",") + if found { + min, err := strconv.ParseUint(before, 10, 32) + if err != nil { + continue + } + + max, err := strconv.ParseUint(after, 10, 32) + if err != nil { + continue + } + + searchStatement += fmt.Sprintf(` AND ms.matchmake_system_type BETWEEN %d AND %d`, min, max) + } else { + value, err := strconv.ParseUint(before, 10, 32) + if err != nil { + continue + } + + searchStatement += fmt.Sprintf(` AND ms.matchmake_system_type=%d`, value) + } + } + + // * Filter full sessions if necessary + if searchCriteria.VacantOnly.Value { + // * Account for the VacantParticipants when searching for sessions (if given) + if searchCriteria.VacantParticipants.Value == 0 { + searchStatement += ` AND array_length(g.participants, 1) + 1 <= g.max_participants` + } else { + searchStatement += fmt.Sprintf(` AND array_length(g.participants, 1) + %d <= g.max_participants`, searchCriteria.VacantParticipants.Value) + } + } + + // * If the ResultRange inside the MatchmakeSessionSearchCriteria is valid (only present on NEX 4.0+), use that + // * Otherwise, use the one given as argument + if searchCriteria.ResultRange.Length.Value != 0 { + searchStatement += fmt.Sprintf(` LIMIT %d OFFSET %d`, searchCriteria.ResultRange.Length.Value, searchCriteria.ResultRange.Offset.Value) + } else { + // * Since we use one ResultRange for all searches, limit the total length to the one specified + // * but apply the same offset to all queries + searchStatement += fmt.Sprintf(` LIMIT %d OFFSET %d`, resultRange.Length.Value - uint32(len(resultMatchmakeSessions)), resultRange.Offset.Value) + } + + rows, err := db.Query(searchStatement, + searchCriteria.ReferGID.Value, + searchCriteria.CodeWord.Value, + searchCriteria.Attribs.Length(), + pqextended.Array(friendList), + ) + if err != nil { + globals.Logger.Critical(err.Error()) + continue + } + + for rows.Next() { + resultMatchmakeSession := match_making_types.NewMatchmakeSession() + var ownerPID uint64 + var hostPID uint64 + var startedTime time.Time + var resultAttribs []uint32 + var resultMatchmakeParam []byte + + err = rows.Scan( + &resultMatchmakeSession.Gathering.ID.Value, + &ownerPID, + &hostPID, + &resultMatchmakeSession.Gathering.MinimumParticipants.Value, + &resultMatchmakeSession.Gathering.MaximumParticipants.Value, + &resultMatchmakeSession.Gathering.ParticipationPolicy.Value, + &resultMatchmakeSession.Gathering.PolicyArgument.Value, + &resultMatchmakeSession.Gathering.Flags.Value, + &resultMatchmakeSession.Gathering.State.Value, + &resultMatchmakeSession.Gathering.Description.Value, + &resultMatchmakeSession.ParticipationCount.Value, + &startedTime, + &resultMatchmakeSession.GameMode.Value, + pqextended.Array(&resultAttribs), + &resultMatchmakeSession.OpenParticipation.Value, + &resultMatchmakeSession.MatchmakeSystemType.Value, + &resultMatchmakeSession.ApplicationBuffer.Value, + &resultMatchmakeSession.ProgressScore.Value, + &resultMatchmakeSession.SessionKey.Value, + &resultMatchmakeSession.Option.Value, + &resultMatchmakeParam, + &resultMatchmakeSession.UserPassword.Value, + &resultMatchmakeSession.ReferGID.Value, + &resultMatchmakeSession.UserPasswordEnabled.Value, + &resultMatchmakeSession.SystemPasswordEnabled.Value, + &resultMatchmakeSession.CodeWord.Value, + ) + if err != nil { + globals.Logger.Critical(err.Error()) + continue + } + + resultMatchmakeSession.OwnerPID = types.NewPID(ownerPID) + resultMatchmakeSession.HostPID = types.NewPID(hostPID) + resultMatchmakeSession.StartedTime = resultMatchmakeSession.StartedTime.FromTimestamp(startedTime) + + attributesSlice := make([]*types.PrimitiveU32, len(resultAttribs)) + for i, value := range resultAttribs { + attributesSlice[i] = types.NewPrimitiveU32(value) + } + resultMatchmakeSession.Attributes.SetFromData(attributesSlice) + + matchmakeParamBytes := nex.NewByteStreamIn(resultMatchmakeParam, endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) + resultMatchmakeSession.MatchmakeParam.ExtractFrom(matchmakeParamBytes) + + resultMatchmakeSessions = append(resultMatchmakeSessions, resultMatchmakeSession) + } + + rows.Close() + } + + return resultMatchmakeSessions, nil +} diff --git a/matchmake-extension/database/get_matchmake_session_by_gathering.go b/matchmake-extension/database/get_matchmake_session_by_gathering.go new file mode 100644 index 0000000..510f027 --- /dev/null +++ b/matchmake-extension/database/get_matchmake_session_by_gathering.go @@ -0,0 +1,73 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// GetMatchmakeSessionByGathering gets a matchmake session with the given gathering data +func GetMatchmakeSessionByGathering(db *sql.DB, endpoint *nex.PRUDPEndPoint, gathering *match_making_types.Gathering, participationCount uint32, startedTime *types.DateTime) (*match_making_types.MatchmakeSession, *nex.Error) { + resultMatchmakeSession := match_making_types.NewMatchmakeSession() + var resultAttribs []uint32 + var resultMatchmakeParam []byte + + err := db.QueryRow(`SELECT + game_mode, + attribs, + open_participation, + matchmake_system_type, + application_buffer, + progress_score, + session_key, + option_zero, + matchmake_param, + user_password, + refer_gid, + user_password_enabled, + system_password_enabled, + codeword + FROM matchmaking.matchmake_sessions WHERE id=$1`, + gathering.ID.Value, + ).Scan( + &resultMatchmakeSession.GameMode.Value, + pqextended.Array(&resultAttribs), + &resultMatchmakeSession.OpenParticipation.Value, + &resultMatchmakeSession.MatchmakeSystemType.Value, + &resultMatchmakeSession.ApplicationBuffer.Value, + &resultMatchmakeSession.ProgressScore.Value, + &resultMatchmakeSession.SessionKey.Value, + &resultMatchmakeSession.Option.Value, + &resultMatchmakeParam, + &resultMatchmakeSession.UserPassword.Value, + &resultMatchmakeSession.ReferGID.Value, + &resultMatchmakeSession.UserPasswordEnabled.Value, + &resultMatchmakeSession.SystemPasswordEnabled.Value, + &resultMatchmakeSession.CodeWord.Value, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } else { + return nil, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + } + + resultMatchmakeSession.Gathering = gathering + resultMatchmakeSession.ParticipationCount.Value = participationCount + resultMatchmakeSession.StartedTime = startedTime + + attributesSlice := make([]*types.PrimitiveU32, len(resultAttribs)) + for i, value := range resultAttribs { + attributesSlice[i] = types.NewPrimitiveU32(value) + } + resultMatchmakeSession.Attributes.SetFromData(attributesSlice) + + matchmakeParamBytes := nex.NewByteStreamIn(resultMatchmakeParam, endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) + resultMatchmakeSession.MatchmakeParam.ExtractFrom(matchmakeParamBytes) + + return resultMatchmakeSession, nil +} diff --git a/matchmake-extension/database/get_matchmake_session_by_id.go b/matchmake-extension/database/get_matchmake_session_by_id.go new file mode 100644 index 0000000..72125b8 --- /dev/null +++ b/matchmake-extension/database/get_matchmake_session_by_id.go @@ -0,0 +1,107 @@ +package database + +import ( + "database/sql" + "time" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" + pqextended "github.com/PretendoNetwork/pq-extended" +) + +// GetMatchmakeSessionByID gets a matchmake session with the given gathering ID +func GetMatchmakeSessionByID(db *sql.DB, endpoint *nex.PRUDPEndPoint, gatheringID uint32) (*match_making_types.MatchmakeSession, *nex.Error) { + resultMatchmakeSession := match_making_types.NewMatchmakeSession() + var ownerPID uint64 + var hostPID uint64 + var startedTime time.Time + var resultAttribs []uint32 + var resultMatchmakeParam []byte + + // * For simplicity, we will only compare the values that exist on a MatchmakeSessionSearchCriteria + err := db.QueryRow(`SELECT + g.id, + g.owner_pid, + g.host_pid, + g.min_participants, + g.max_participants, + g.participation_policy, + g.policy_argument, + g.flags, + g.state, + g.description, + array_length(g.participants, 1), + g.started_time, + ms.game_mode, + ms.attribs, + ms.open_participation, + ms.matchmake_system_type, + ms.application_buffer, + ms.progress_score, + ms.session_key, + ms.option_zero, + ms.matchmake_param, + ms.user_password, + ms.refer_gid, + ms.user_password_enabled, + ms.system_password_enabled, + ms.codeword + FROM matchmaking.gatherings AS g + INNER JOIN matchmaking.matchmake_sessions AS ms ON ms.id = g.id + WHERE + g.registered=true AND + g.type='MatchmakeSession' AND + g.id=$1`, + gatheringID, + ).Scan( + &resultMatchmakeSession.Gathering.ID.Value, + &ownerPID, + &hostPID, + &resultMatchmakeSession.Gathering.MinimumParticipants.Value, + &resultMatchmakeSession.Gathering.MaximumParticipants.Value, + &resultMatchmakeSession.Gathering.ParticipationPolicy.Value, + &resultMatchmakeSession.Gathering.PolicyArgument.Value, + &resultMatchmakeSession.Gathering.Flags.Value, + &resultMatchmakeSession.Gathering.State.Value, + &resultMatchmakeSession.Gathering.Description.Value, + &resultMatchmakeSession.ParticipationCount.Value, + &startedTime, + &resultMatchmakeSession.GameMode.Value, + pqextended.Array(&resultAttribs), + &resultMatchmakeSession.OpenParticipation.Value, + &resultMatchmakeSession.MatchmakeSystemType.Value, + &resultMatchmakeSession.ApplicationBuffer.Value, + &resultMatchmakeSession.ProgressScore.Value, + &resultMatchmakeSession.SessionKey.Value, + &resultMatchmakeSession.Option.Value, + &resultMatchmakeParam, + &resultMatchmakeSession.UserPassword.Value, + &resultMatchmakeSession.ReferGID.Value, + &resultMatchmakeSession.UserPasswordEnabled.Value, + &resultMatchmakeSession.SystemPasswordEnabled.Value, + &resultMatchmakeSession.CodeWord.Value, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } else { + return nil, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + } + + resultMatchmakeSession.OwnerPID = types.NewPID(ownerPID) + resultMatchmakeSession.HostPID = types.NewPID(hostPID) + resultMatchmakeSession.StartedTime = resultMatchmakeSession.StartedTime.FromTimestamp(startedTime) + + attributesSlice := make([]*types.PrimitiveU32, len(resultAttribs)) + for i, value := range resultAttribs { + attributesSlice[i] = types.NewPrimitiveU32(value) + } + resultMatchmakeSession.Attributes.SetFromData(attributesSlice) + + matchmakeParamBytes := nex.NewByteStreamIn(resultMatchmakeParam, endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) + resultMatchmakeSession.MatchmakeParam.ExtractFrom(matchmakeParamBytes) + + return resultMatchmakeSession, nil +} diff --git a/matchmake-extension/database/get_simple_playing_session.go b/matchmake-extension/database/get_simple_playing_session.go new file mode 100644 index 0000000..273f954 --- /dev/null +++ b/matchmake-extension/database/get_simple_playing_session.go @@ -0,0 +1,42 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" + common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" +) + +// GetSimplePlayingSession returns the simple playing sessions of the given PIDs +func GetSimplePlayingSession(db *sql.DB, listPID []*types.PID) ([]*match_making_types.SimplePlayingSession, *nex.Error) { + simplePlayingSessions := make([]*match_making_types.SimplePlayingSession, 0) + for _, pid := range listPID { + simplePlayingSession := match_making_types.NewSimplePlayingSession() + err := db.QueryRow(`SELECT + g.id, + ms.attribs[1], + ms.game_mode + FROM matchmaking.gatherings AS g + INNER JOIN matchmaking.matchmake_sessions AS ms ON ms.id = g.id + WHERE + g.registered=true AND + g.type='MatchmakeSession' AND + $1=ANY(g.participants)`, pid.Value()).Scan( + &simplePlayingSession.GatheringID.Value, + &simplePlayingSession.Attribute0.Value, + &simplePlayingSession.GameMode.Value) + if err != nil { + if err != sql.ErrNoRows { + common_globals.Logger.Critical(err.Error()) + } + continue + } + + simplePlayingSession.PrincipalID = pid + simplePlayingSessions = append(simplePlayingSessions, simplePlayingSession) + } + + return simplePlayingSessions, nil +} diff --git a/matchmake-extension/database/update_application_buffer.go b/matchmake-extension/database/update_application_buffer.go new file mode 100644 index 0000000..bc284f2 --- /dev/null +++ b/matchmake-extension/database/update_application_buffer.go @@ -0,0 +1,27 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" + "github.com/PretendoNetwork/nex-go/v2/types" +) + +// UpdateApplicationBuffer updates the application buffer of a matchmake session +func UpdateApplicationBuffer(db *sql.DB, gatheringID uint32, applicationBuffer *types.Buffer) *nex.Error { + result, err := db.Exec(`UPDATE matchmaking.matchmake_sessions SET application_buffer=$1 WHERE id=$2`, applicationBuffer.Value, gatheringID) + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + if rowsAffected == 0 { + return nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } + + return nil +} diff --git a/matchmake-extension/database/update_game_attribute.go b/matchmake-extension/database/update_game_attribute.go new file mode 100644 index 0000000..8f74ae1 --- /dev/null +++ b/matchmake-extension/database/update_game_attribute.go @@ -0,0 +1,26 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" +) + +// UpdateGameAttribute updates an attribute on a matchmake session +func UpdateGameAttribute(db *sql.DB, gatheringID uint32, attributeIndex uint32, newValue uint32) *nex.Error { + result, err := db.Exec(`UPDATE matchmaking.matchmake_sessions SET attribs[$1]=$2 WHERE id=$3`, attributeIndex + 1, newValue, gatheringID) + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + if rowsAffected == 0 { + return nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } + + return nil +} diff --git a/matchmake-extension/database/update_participation.go b/matchmake-extension/database/update_participation.go new file mode 100644 index 0000000..cc9ce23 --- /dev/null +++ b/matchmake-extension/database/update_participation.go @@ -0,0 +1,26 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" +) + +// UpdateParticipation updates the participation of a matchmake session +func UpdateParticipation(db *sql.DB, gatheringID uint32, participation bool) *nex.Error { + result, err := db.Exec(`UPDATE matchmaking.matchmake_sessions SET open_participation=$1 WHERE id=$2`, participation, gatheringID) + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + if rowsAffected == 0 { + return nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } + + return nil +} diff --git a/matchmake-extension/database/update_progress_score.go b/matchmake-extension/database/update_progress_score.go new file mode 100644 index 0000000..5c6aaa2 --- /dev/null +++ b/matchmake-extension/database/update_progress_score.go @@ -0,0 +1,26 @@ +package database + +import ( + "database/sql" + + "github.com/PretendoNetwork/nex-go/v2" +) + +// UpdateProgressScore updates the progress score on a matchmake session +func UpdateProgressScore(db *sql.DB, gatheringID uint32, progressScore uint8) *nex.Error { + result, err := db.Exec(`UPDATE matchmaking.matchmake_sessions SET progress_score=$1 WHERE id=$2`, progressScore, gatheringID) + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error()) + } + + if rowsAffected == 0 { + return nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + } + + return nil +} diff --git a/matchmake-extension/get_simple_playing_session.go b/matchmake-extension/get_simple_playing_session.go index 5059426..c0ac0a6 100644 --- a/matchmake-extension/get_simple_playing_session.go +++ b/matchmake-extension/get_simple_playing_session.go @@ -1,14 +1,12 @@ package matchmake_extension import ( - "fmt" - "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" - "golang.org/x/exp/slices" ) func (commonProtocol *CommonProtocol) getSimplePlayingSession(err error, packet nex.PacketInterface, callID uint32, listPID *types.List[*types.PID], includeLoginUser *types.PrimitiveBool) (*nex.RMCMessage, *nex.Error) { @@ -27,46 +25,18 @@ func (commonProtocol *CommonProtocol) getSimplePlayingSession(err error, packet listPID.Append(connection.PID().Copy().(*types.PID)) } - simplePlayingSessions := make(map[string]*match_making_types.SimplePlayingSession) - - for gatheringID, session := range common_globals.Sessions { - for _, pid := range listPID.Slice() { - key := fmt.Sprintf("%d-%d", gatheringID, pid.Value()) - if simplePlayingSessions[key] == nil { - connectedPIDs := make([]uint64, 0) - session.ConnectionIDs.Each(func(_ int, connectionID uint32) bool { - player := endpoint.FindConnectionByID(connectionID) - if player == nil { - common_globals.Logger.Warning("Player not found") - return false - } - - connectedPIDs = append(connectedPIDs, player.PID().Value()) - return false - }) - - if slices.Contains(connectedPIDs, pid.Value()) { - attribute0, err := session.GameMatchmakeSession.Attributes.Get(0) - if err != nil { - common_globals.Logger.Error(err.Error()) - return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") - } + common_globals.MatchmakingMutex.RLock() - simplePlayingSessions[key] = match_making_types.NewSimplePlayingSession() - simplePlayingSessions[key].PrincipalID = pid.Copy().(*types.PID) - simplePlayingSessions[key].GatheringID = types.NewPrimitiveU32(gatheringID) - simplePlayingSessions[key].GameMode = session.GameMatchmakeSession.GameMode.Copy().(*types.PrimitiveU32) - simplePlayingSessions[key].Attribute0 = attribute0.Copy().(*types.PrimitiveU32) - } - } - } + simplePlayingSessions, nexError := database.GetSimplePlayingSession(commonProtocol.db, listPID.Slice()) + if nexError != nil { + common_globals.MatchmakingMutex.RUnlock() + return nil, nexError } - lstSimplePlayingSession := types.NewList[*match_making_types.SimplePlayingSession]() + common_globals.MatchmakingMutex.RUnlock() - for _, simplePlayingSession := range simplePlayingSessions { - lstSimplePlayingSession.Append(simplePlayingSession) - } + lstSimplePlayingSession := types.NewList[*match_making_types.SimplePlayingSession]() + lstSimplePlayingSession.SetFromData(simplePlayingSessions) rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) diff --git a/matchmake-extension/join_matchmake_session.go b/matchmake-extension/join_matchmake_session.go index 789523e..7010927 100644 --- a/matchmake-extension/join_matchmake_session.go +++ b/matchmake-extension/join_matchmake_session.go @@ -4,6 +4,8 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -13,23 +15,33 @@ func (commonProtocol *CommonProtocol) joinMatchmakeSession(err error, packet nex return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[gid.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") - } + common_globals.MatchmakingMutex.Lock() connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) server := endpoint.Server - // TODO - More checks here - errCode := common_globals.AddPlayersToSession(session, []uint32{connection.ID}, connection, strMessage.Value) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + joinedMatchmakeSession, nexError := database.GetMatchmakeSessionByID(commonProtocol.db, endpoint, gid.Value) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + nexError = common_globals.CanJoinMatchmakeSession(connection.PID(), joinedMatchmakeSession) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + _, nexError = match_making_database.JoinGathering(commonProtocol.db, joinedMatchmakeSession.Gathering.ID.Value, connection, 1, strMessage.Value) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - joinedMatchmakeSession := session.GameMatchmakeSession + common_globals.MatchmakingMutex.Unlock() rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) diff --git a/matchmake-extension/join_matchmake_session_ex.go b/matchmake-extension/join_matchmake_session_ex.go index fb3d647..cac84fc 100644 --- a/matchmake-extension/join_matchmake_session_ex.go +++ b/matchmake-extension/join_matchmake_session_ex.go @@ -4,6 +4,8 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -13,23 +15,33 @@ func (commonProtocol *CommonProtocol) joinMatchmakeSessionEx(err error, packet n return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[gid.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") - } + common_globals.MatchmakingMutex.Lock() connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) server := endpoint.Server - // TODO - More checks here - errCode := common_globals.AddPlayersToSession(session, []uint32{connection.ID}, connection, strMessage.Value) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + joinedMatchmakeSession, nexError := database.GetMatchmakeSessionByID(commonProtocol.db, endpoint, gid.Value) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + nexError = common_globals.CanJoinMatchmakeSession(connection.PID(), joinedMatchmakeSession) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + _, nexError = match_making_database.JoinGathering(commonProtocol.db, joinedMatchmakeSession.Gathering.ID.Value, connection, participationCount.Value, strMessage.Value) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - joinedMatchmakeSession := session.GameMatchmakeSession + common_globals.MatchmakingMutex.Unlock() rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) diff --git a/matchmake-extension/join_matchmake_session_with_param.go b/matchmake-extension/join_matchmake_session_with_param.go index 22cf55c..45669a9 100644 --- a/matchmake-extension/join_matchmake_session_with_param.go +++ b/matchmake-extension/join_matchmake_session_with_param.go @@ -3,6 +3,8 @@ package matchmake_extension import ( "github.com/PretendoNetwork/nex-go/v2" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + match_making_database "github.com/PretendoNetwork/nex-protocols-common-go/v2/match-making/database" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -13,24 +15,36 @@ func (commonProtocol *CommonProtocol) joinMatchmakeSessionWithParam(err error, p return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session, ok := common_globals.Sessions[joinMatchmakeSessionParam.GID.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") - } + common_globals.MatchmakingMutex.Lock() connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - // TODO - More checks here - errCode := common_globals.AddPlayersToSession(session, []uint32{connection.ID}, connection, joinMatchmakeSessionParam.JoinMessage.Value) - if errCode != nil { - common_globals.Logger.Error(errCode.Error()) - return nil, errCode + joinedMatchmakeSession, nexError := database.GetMatchmakeSessionByID(commonProtocol.db, endpoint, joinMatchmakeSessionParam.GID.Value) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + nexError = common_globals.CanJoinMatchmakeSession(connection.PID(), joinedMatchmakeSession) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } + _, nexError = match_making_database.JoinGatheringWithParticipants(commonProtocol.db, joinedMatchmakeSession.Gathering.ID.Value, connection, joinMatchmakeSessionParam.AdditionalParticipants.Slice(), joinMatchmakeSessionParam.JoinMessage.Value) + if nexError != nil { + common_globals.Logger.Error(nexError.Error()) + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + common_globals.MatchmakingMutex.Unlock() + rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings()) - session.GameMatchmakeSession.WriteTo(rmcResponseStream) + joinMatchmakeSessionParam.WriteTo(rmcResponseStream) rmcResponseBody := rmcResponseStream.Bytes() diff --git a/matchmake-extension/modify_current_game_attribute.go b/matchmake-extension/modify_current_game_attribute.go index 8a12be3..033081d 100644 --- a/matchmake-extension/modify_current_game_attribute.go +++ b/matchmake-extension/modify_current_game_attribute.go @@ -4,6 +4,7 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -16,22 +17,33 @@ func (commonProtocol *CommonProtocol) modifyCurrentGameAttribute(err error, pack connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - session, ok := common_globals.Sessions[gid.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + common_globals.MatchmakingMutex.Lock() + + session, nexError := database.GetMatchmakeSessionByID(commonProtocol.db, endpoint, gid.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - if !session.GameMatchmakeSession.Gathering.OwnerPID.Equals(connection.PID()) { + if !session.Gathering.OwnerPID.Equals(connection.PID()) { + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") } index := int(attribIndex.Value) - if index > session.GameMatchmakeSession.Attributes.Length() { + if index >= session.Attributes.Length() { + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.Core.InvalidIndex, "change_error") } - session.GameMatchmakeSession.Attributes.SetIndex(index, newValue.Copy().(*types.PrimitiveU32)) + nexError = database.UpdateGameAttribute(commonProtocol.db, gid.Value, attribIndex.Value, newValue.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + common_globals.MatchmakingMutex.Unlock() rmcResponse := nex.NewRMCSuccess(endpoint, nil) rmcResponse.ProtocolID = matchmake_extension.ProtocolID diff --git a/matchmake-extension/open_participation.go b/matchmake-extension/open_participation.go index dde2d8a..244eef0 100644 --- a/matchmake-extension/open_participation.go +++ b/matchmake-extension/open_participation.go @@ -4,6 +4,7 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -16,16 +17,27 @@ func (commonProtocol *CommonProtocol) openParticipation(err error, packet nex.Pa connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - session, ok := common_globals.Sessions[gid.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + + common_globals.MatchmakingMutex.Lock() + + session, nexError := database.GetMatchmakeSessionByID(commonProtocol.db, endpoint, gid.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError } - if !session.GameMatchmakeSession.Gathering.OwnerPID.Equals(connection.PID()) { + if !session.Gathering.OwnerPID.Equals(connection.PID()) { + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") } - session.GameMatchmakeSession.OpenParticipation = types.NewPrimitiveBool(true) + nexError = database.UpdateParticipation(commonProtocol.db, gid.Value, true) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + common_globals.MatchmakingMutex.Unlock() rmcResponse := nex.NewRMCSuccess(endpoint, nil) rmcResponse.ProtocolID = matchmake_extension.ProtocolID diff --git a/matchmake-extension/protocol.go b/matchmake-extension/protocol.go index 34fb51d..7d982fb 100644 --- a/matchmake-extension/protocol.go +++ b/matchmake-extension/protocol.go @@ -1,6 +1,8 @@ package matchmake_extension import ( + "database/sql" + "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" match_making_types "github.com/PretendoNetwork/nex-protocols-go/v2/match-making/types" @@ -12,8 +14,9 @@ import ( type CommonProtocol struct { endpoint nex.EndpointInterface protocol matchmake_extension.Interface + db *sql.DB CleanupSearchMatchmakeSession func(matchmakeSession *match_making_types.MatchmakeSession) - GameSpecificMatchmakeSessionSearchCriteriaChecks func(searchCriteria *match_making_types.MatchmakeSessionSearchCriteria, matchmakeSession *match_making_types.MatchmakeSession) bool + CleanupMatchmakeSessionSearchCriterias func(searchCriterias *types.List[*match_making_types.MatchmakeSessionSearchCriteria]) OnAfterOpenParticipation func(packet nex.PacketInterface, gid *types.PrimitiveU32) OnAfterCloseParticipation func(packet nex.PacketInterface, gid *types.PrimitiveU32) OnAfterCreateMatchmakeSession func(packet nex.PacketInterface, anyGathering *types.AnyDataHolder, message *types.String, participationCount *types.PrimitiveU16) @@ -36,6 +39,57 @@ func (commonProtocol *CommonProtocol) GetUserFriendPIDs(handler func(pid uint32) common_globals.GetUserFriendPIDsHandler = handler } +// SetDatabase defines the SQL database to be used by the common protocol +func (commonProtocol *CommonProtocol) SetDatabase(db *sql.DB) { + var err error + + commonProtocol.db = db + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS matchmaking.matchmake_sessions ( + id bigserial PRIMARY KEY, + game_mode bigint, + attribs bigint[], + open_participation boolean, + matchmake_system_type bigint, + application_buffer bytea, + flags bigint, + state bigint, + progress_score smallint, + session_key bytea, + option_zero bigint, + matchmake_param bytea, + user_password text, + refer_gid bigint, + user_password_enabled boolean, + system_password_enabled boolean, + codeword text + )`) + if err != nil { + common_globals.Logger.Error(err.Error()) + return + } + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS matchmaking.persistent_gatherings ( + id bigserial PRIMARY KEY, + password text, + attribs bigint[], + application_buffer bytea, + participation_start_date timestamp, + participation_end_date timestamp + )`) + if err != nil { + common_globals.Logger.Error(err.Error()) + return + } + + // * In case the server is restarted, unregister any previous matchmake sessions + _, err = db.Exec(`UPDATE matchmaking.gatherings SET registered=false WHERE type='MatchmakeSession'`) + if err != nil { + common_globals.Logger.Error(err.Error()) + return + } +} + // NewCommonProtocol returns a new CommonProtocol func NewCommonProtocol(protocol matchmake_extension.Interface) *CommonProtocol { commonProtocol := &CommonProtocol{ diff --git a/matchmake-extension/update_application_buffer.go b/matchmake-extension/update_application_buffer.go index 325fc16..7f4bed0 100644 --- a/matchmake-extension/update_application_buffer.go +++ b/matchmake-extension/update_application_buffer.go @@ -4,6 +4,7 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -16,14 +17,26 @@ func (commonProtocol *CommonProtocol) updateApplicationBuffer(err error, packet connection := packet.Sender().(*nex.PRUDPConnection) endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) - session, ok := common_globals.Sessions[gid.Value] - if !ok { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") + common_globals.MatchmakingMutex.Lock() + + session, nexError := database.GetMatchmakeSessionByID(commonProtocol.db, endpoint, gid.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + if !session.Gathering.OwnerPID.Equals(connection.PID()) { + common_globals.MatchmakingMutex.Unlock() + return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") } - // TODO - Should ANYONE be allowed to do this?? + nexError = database.UpdateApplicationBuffer(commonProtocol.db, gid.Value, applicationBuffer) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } - session.GameMatchmakeSession.ApplicationBuffer = applicationBuffer.Copy().(*types.Buffer) + common_globals.MatchmakingMutex.Unlock() rmcResponse := nex.NewRMCSuccess(endpoint, nil) rmcResponse.ProtocolID = matchmake_extension.ProtocolID diff --git a/matchmake-extension/update_progress_score.go b/matchmake-extension/update_progress_score.go index 57a7773..0a4b69e 100644 --- a/matchmake-extension/update_progress_score.go +++ b/matchmake-extension/update_progress_score.go @@ -4,6 +4,7 @@ import ( "github.com/PretendoNetwork/nex-go/v2" "github.com/PretendoNetwork/nex-go/v2/types" common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + "github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database" matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension" ) @@ -13,23 +14,33 @@ func (commonProtocol *CommonProtocol) updateProgressScore(err error, packet nex. return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - session := common_globals.Sessions[gid.Value] - if session == nil { - return nil, nex.NewError(nex.ResultCodes.RendezVous.SessionVoid, "change_error") - } + connection := packet.Sender().(*nex.PRUDPConnection) + endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) if progressScore.Value > 100 { return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error") } - connection := packet.Sender().(*nex.PRUDPConnection) - endpoint := connection.Endpoint().(*nex.PRUDPEndPoint) + common_globals.MatchmakingMutex.Lock() - if !session.GameMatchmakeSession.Gathering.OwnerPID.Equals(connection.PID()) { + session, nexError := database.GetMatchmakeSessionByID(commonProtocol.db, endpoint, gid.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + if !session.Gathering.OwnerPID.Equals(connection.PID()) { + common_globals.MatchmakingMutex.Unlock() return nil, nex.NewError(nex.ResultCodes.RendezVous.PermissionDenied, "change_error") } - session.GameMatchmakeSession.ProgressScore.Value += progressScore.Value + nexError = database.UpdateProgressScore(commonProtocol.db, gid.Value, progressScore.Value) + if nexError != nil { + common_globals.MatchmakingMutex.Unlock() + return nil, nexError + } + + common_globals.MatchmakingMutex.Unlock() rmcResponse := nex.NewRMCSuccess(endpoint, nil) rmcResponse.ProtocolID = matchmake_extension.ProtocolID