Skip to content

Commit

Permalink
Party feature (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
fsufyan authored Oct 16, 2023
1 parent 0137a6b commit 727b336
Show file tree
Hide file tree
Showing 6 changed files with 536 additions and 21 deletions.
32 changes: 32 additions & 0 deletions Sources/Nakama/Mappings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
* limitations under the License.
*/

import SwiftProtobuf

// MARK: - API

extension Nakama_Api_Session {
func toSession() async -> Session {
return DefaultSession(token: self.token, refreshToken: self.refreshToken, created: self.created)
Expand Down Expand Up @@ -323,6 +327,21 @@ extension Nakama_Api_Notification {
}
}

// MARK: - Socket

extension UserPresence {
func toApiUserPresence() -> Nakama_Realtime_UserPresence {
var presence = Nakama_Realtime_UserPresence()
presence.userID = self.userId
presence.sessionID = self.sessionId
presence.username = self.username
presence.persistence = self.persistence
presence.status = Google_Protobuf_StringValue(self.status ?? "")

return presence
}
}

extension Nakama_Realtime_UserPresence {
func toUserPresence() -> UserPresence {
return UserPresence(
Expand All @@ -333,3 +352,16 @@ extension Nakama_Realtime_UserPresence {
)
}
}

extension Nakama_Realtime_Party {
func toParty() -> Party {
return Party(
id: self.partyID,
open: self.open,
maxSize: Int(self.maxSize),
self_p: self.self_p.toUserPresence(),
leader: self.leader.toUserPresence(),
presences: self.presences.map { $0.toUserPresence() }
)
}
}
60 changes: 60 additions & 0 deletions Sources/Nakama/Models/Party.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright © 2023 Heroic Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation

/// Incoming information about a party.
public struct Party {
/// The unique party identifier.
let id: String

/// If the party is open to join.
let open: Bool

/// The maximum number of party members.
let maxSize: Int

/// The current user in this party. i.e. Yourself.
let self_p: UserPresence

/// The current party leader.
let leader: UserPresence

/// All members currently in the party.
var presences: [UserPresence]

/// Apply the joins and leaves from a presence event to the presences tracked by the party.
mutating func updatePresences(event: PartyPresenceEvent) {
guard event.partyId == self.id else {
return
}

// Append joins and leaves
self.presences.copyJoinsAndLeaves(joins: event.joins, leaves: event.leaves)
}
}

public struct PartyPresenceEvent
{
/// The ID of the party.
let partyId: String

/// The user presences that have just joined the party.
let joins: [UserPresence]

/// The user presences that have just left the party.
let leaves: [UserPresence]
}
18 changes: 17 additions & 1 deletion Sources/Nakama/Models/UserPresence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import Foundation

class UserPresence {
public final class UserPresence: Hashable {
let userId: String
let sessionId: String
let username: String
Expand All @@ -40,4 +40,20 @@ class UserPresence {
status: rtUserPresence.status.value.isEmpty ? nil : rtUserPresence.status.value
)
}

public static func == (lhs: UserPresence, rhs: UserPresence) -> Bool {
return lhs.userId == rhs.userId
}

public func hash(into hasher: inout Hasher) {
hasher.combine(self.userId)
}
}

extension [UserPresence] {
func copyJoinsAndLeaves(joins: [UserPresence], leaves: [UserPresence]) {
var newPresences = Set(self)
newPresences.formUnion(joins)
newPresences.formUnion(leaves)
}
}
160 changes: 149 additions & 11 deletions Sources/Nakama/Socket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ public final class Socket : SocketProtocol {

public var onStreamData: StreamDataHandler?

public var onPartyReceived: PartyReceivedHandler?

public var onPartyClosed: PartyCloseHandler?

public var onPartyData: PartyDataHandler?

public var onPartyJoin: PartyJoinRequestHandler?

public var onPartyPresence: PartyPresenceHandler?

public var onPartyMatchmakerTicket: PartyMatchmakerTicketHandler?

public var onPartyLeader: PartyLeaderHandler?

let collationCounter = ManagedAtomic<Int>(0)
let eventLoopGroup: EventLoopGroup
let logger: Logger?
Expand Down Expand Up @@ -154,24 +168,30 @@ public final class Socket : SocketProtocol {
self.onStreamData?(streamData)
case .streamPresenceEvent(let streamPresenceEvent):
self.onStreamPresence?(streamPresenceEvent)
case .party(let party):
self.onPartyReceived?(party.toParty())
case .partyJoinRequest(let joinRequest):
self.onPartyJoin?(joinRequest)
case .partyData(let data):
self.onPartyData?(data)
case .partyPresenceEvent(let partyPresenceEvent):
self.onPartyPresence?(partyPresenceEvent)
case .partyMatchmakerTicket(let ticket):
self.onPartyMatchmakerTicket?(ticket)
case .partyClose(let close):
self.onPartyClosed?(close)
case .partyLeader(let leader):
self.onPartyLeader?(leader)
default:
self.logger?.error("Unrecognised incoming uncollated message from server: \(try! response.jsonString())")
}
} else {
if let collatedPromise = self.collatedPromises[response.cid] {
switch response.message {
case .error(let error):
if let promise = collatedPromise as? EventLoopPromise<Nakama_Api_Rpc> {
promise.fail(NakamaRealtimeError(error: error))
} else if let promise = collatedPromise as? EventLoopPromise<Nakama_Realtime_Channel> {
promise.fail(NakamaRealtimeError(error: error))
} else if let promise = collatedPromise as? EventLoopPromise<Nakama_Realtime_ChannelMessageAck> {
promise.fail(NakamaRealtimeError(error: error))
} else if let promise = collatedPromise as? EventLoopPromise<Nakama_Realtime_Match> {
if let promise = collatedPromise as? EventLoopPromise<Any> {
promise.fail(NakamaRealtimeError(error: error))
} else if let promise = collatedPromise as? EventLoopPromise<Nakama_Realtime_MatchmakerTicket> {
promise.fail(NakamaRealtimeError(error: error))
} else if let promise = collatedPromise as? EventLoopPromise<Nakama_Realtime_Status> {
} else if let promise = collatedPromise as? EventLoopPromise<Google_Protobuf_Empty> {
promise.fail(NakamaRealtimeError(error: error))
}
case .rpc(let rpc):
Expand All @@ -192,6 +212,12 @@ public final class Socket : SocketProtocol {
case .status(let status):
let promise = collatedPromise as! EventLoopPromise<Nakama_Realtime_Status>
promise.succeed(status)
case .party(let party):
let promise = collatedPromise as! EventLoopPromise<Nakama_Realtime_Party>
promise.succeed(party)
case .partyMatchmakerTicket(let ticket):
let promise = collatedPromise as! EventLoopPromise<Nakama_Realtime_PartyMatchmakerTicket>
promise.succeed(ticket)
default:
self.logger?.error("Unrecognised incoming collated message from server: \(try! response.jsonString())")
// Handle empty or nil response from server
Expand Down Expand Up @@ -341,7 +367,7 @@ public final class Socket : SocketProtocol {

public func sendMatchData(matchId: String, opCode: Int64, data: String, presences: [Nakama_Realtime_UserPresence]? = nil) async throws {
guard let data = data.data(using: .utf8) else {
throw SocketError("Unable to convert string to Data")
throw NakamaRealtimeError(text: "Unable to convert string to Data")
}

var req = Nakama_Realtime_MatchDataSend()
Expand Down Expand Up @@ -426,4 +452,116 @@ public final class Socket : SocketProtocol {

let _: Google_Protobuf_Empty = try await self.send(env: &env)
}

public func createParty(open: Bool, maxSize: Int) async throws -> Nakama_Realtime_Party {
var env = Nakama_Realtime_Envelope()
env.partyCreate.open = open
env.partyCreate.maxSize = Int32(maxSize)

return try await self.send(env: &env)
}

public func joinParty(partyId: String) async throws {
var env = Nakama_Realtime_Envelope()
env.partyJoin.partyID = partyId

let _: Google_Protobuf_Empty = try await self.send(env: &env)
}

public func leaveParty(partyId: String) async throws {
var env = Nakama_Realtime_Envelope()
env.partyLeave.partyID = partyId

let _: Google_Protobuf_Empty = try await self.send(env: &env)
}

public func closeParty(partyId: String) async throws {
var env = Nakama_Realtime_Envelope()
env.partyClose.partyID = partyId

let _: Google_Protobuf_Empty = try await self.send(env: &env)
}

public func sendPartyData(partyId: String, opCode: Int, data: Data) async throws {
var env = Nakama_Realtime_Envelope()
env.partyDataSend.partyID = partyId
env.partyDataSend.opCode = Int64(opCode)
env.partyDataSend.data = data

let _: Google_Protobuf_Empty = try await self.send(env: &env)
}

public func sendPartyData(partyId: String, opCode: Int, data: String) async throws {
guard let data = data.data(using: .utf8) else {
throw NakamaRealtimeError(text: "Unable to convert string to Data")
}

var env = Nakama_Realtime_Envelope()
env.partyDataSend.partyID = partyId
env.partyDataSend.opCode = Int64(opCode)
env.partyDataSend.data = data
}

public func acceptPartyMember(partyId: String, presence: UserPresence) async throws {
var req = Nakama_Realtime_PartyAccept()
req.partyID = partyId
req.presence = presence.toApiUserPresence()
var env = Nakama_Realtime_Envelope()
env.partyAccept = req

let _: Google_Protobuf_Empty = try await self.send(env: &env)
}

public func removePartyMember(partyId: String, presence: UserPresence) async throws {
var env = Nakama_Realtime_Envelope()
env.partyRemove.partyID = partyId
env.partyRemove.presence = presence.toApiUserPresence()

let _: Google_Protobuf_Empty = try await self.send(env: &env)
}

public func addMatchmakerParty(partyId: String, query: String, minCount: Int, maxCount: Int, stringProperties: [String:String]? = nil, numericProperties: [String:Double]? = nil, countMultiple: Int? = nil) async throws -> Nakama_Realtime_PartyMatchmakerTicket {
var env = Nakama_Realtime_Envelope()
var add = Nakama_Realtime_PartyMatchmakerAdd()
add.partyID = partyId
add.query = query
add.minCount = Int32(minCount)
add.maxCount = Int32(maxCount)
if let stringProperties {
add.stringProperties = stringProperties
}
if let numericProperties {
add.numericProperties = numericProperties
}
if let countMultiple {
add.countMultiple = countMultiple.pbInt32Value
}

env.partyMatchmakerAdd = add

return try await self.send(env: &env)
}

public func removeMatchmakerParty(partyId: String, ticket: String) async throws -> Void {
var env = Nakama_Realtime_Envelope()
env.partyMatchmakerRemove.partyID = partyId
env.partyMatchmakerRemove.ticket = ticket

let _: Google_Protobuf_Empty = try await self.send(env: &env)
}

public func listPartyJoinRequests(partyId: String) async throws -> Nakama_Realtime_PartyJoinRequest {
var env = Nakama_Realtime_Envelope()
env.partyJoinRequestList.partyID = partyId

return try await self.send(env: &env)
}

public func promotePartyMember(partyId: String, partyMember: UserPresence) async throws -> Void {
var env = Nakama_Realtime_Envelope()
env.partyPromote.partyID = partyId
env.partyPromote.presence = partyMember.toApiUserPresence()

let _: Google_Protobuf_Empty = try await self.send(env: &env)
}
}
Loading

0 comments on commit 727b336

Please sign in to comment.