GameKit, GKGameSession how to add players to the session and send data? - ios

I created a session, shared it to another player and now I want to start a game.
I can see a session with two players on both devices. So, it looks like we're ready to game but I need to change connected status before the game and I can do this on both devices.
But... when I do this on device A, I see that user A is connected and user B isn't. And after that, when I repeat the process on device B, I see vice versa situation. B is connected and A is not.
Here is my code that connect player and send the data:
session.setConnectionState(.connected) { (error) in
if let err = error {
assertionFailure(err.localizedDescription)
}
else {
print("NC:",session.players(with: .notConnected))
print(" C:",session.players(with: .connected))
let m = MoveTransfer(move:1) // test struct to send/receive data
session.send(m.data(), with: .reliable) { (error) in
if let err = error {
assertionFailure(err.localizedDescription)
}
}
}
}
I'm getting error:
The requested operation could not be completed because there are no recipients connected to the session
By the way, I'm unable to change connected state on the simulator (iCloud is logged in).
I forgot to mention, that I'm working on a turn based game.
Edit
Tried again and now after several iterations I got this:
I have both players connected to session. But send data still doesn't work.
here is console output:
NC: [] // not connected array and connected array below
C: [<GKCloudPlayer: 0x17402e700>, id: playerID1, name: Player1,<GKCloudPlayer: 0x17402e900>, id: playerID2, name: Player2]
fatal error: The requested operation could not be completed because there are no recipients connected to the session.
Got this on two real devices.

I was able to send data and receive it on the other device. I used loadSessions function to load all session (I think that loadSession by id would do the trick too).
I did the following, D1 is a device 1 and D2 device 2:
1. D1: load all sessions and set player state to connected
2. D2: load all sessions and set player state to connected
3. D1: load all sessions again and set player state to connected
4. D1 || D2: send data
5. D2 || D1: data 256 bytes from player: <GKCloudPlayer: 0x1c022b380>, id: playerID, name: (null)
Although, I wasn't able to transfer data back and forth on both devices since if we'll add step 6. that sends data from device that just received it, we'll get an error: The requested operation could not be completed because there are no recipients connected to the session.
I do not sure what is the reason of it but I stop my efforts on this stage, since I think now, that I don't need to be connected to session to play turn based game.
I found this "new" iOS 10 APIs abandoned. By Apple and by devs consequently. If you one of those who still trying to use it drop me a note at my twitter (link in my bio) if you want to discuss GKGameSession and related topics.

Related

Error when sending data between connected devices in Game Center

I am working on a local multiplayer, real time game in swift 5. In order to achieve the real time gameplay, I am sending data back and forth between two devices with the function GKMatch.sendData(data:, to:, withDataSendingMethod:). It works fairly inconsistently, regardless of if I use .reliable or .unreliable, however the error it gives me when it is unable to send data is consistent. It is as follow:
2020-07-27 21:07:22.433631-0400 Teacher Brawl[19336:5244039] [ViceroyTrace] [ERROR] AGPSessionRecvFrom:1954 0x103f11600 sack: SEARCH FAILURE SERIAL NUMBER (0000000B) FROM (5682ABEE)...
Where Teacher Brawl is the name of the project.
I was wondering if anyone is able to provide insight as to why I am getting the error, as I do not fully understand it being relatively new to swift and newer to GameKit. The code I am using to send the data is shown below, and it is being called anytime there is a tap on the screen, which in the context of this game is fairly minimal. If you need any further details please let me know, I would be happy to provide them. All help is greatly appreciated as the inconsistency of data sending has stopped any progress I can make for this game. :)
func sendButtons(button: String) {
let sendableString: Data? = button.data(using: .utf8)
do {
try localMatch.send(sendableString!, to: localMatch.players, dataMode: .unreliable)
}
catch {
print("")
}
}
For reference, the variable localMatch is my variable for the GKMatch that was returned when both players joined the game.
This error message is common and shouldn't interfere with your game sending data. My app gets this error but still sends data fine.
If you are sending data to all players, you should use the built in function func sendData(toAllPlayers data: Data, with mode: GKMatch.SendDataMode) throws. You should also send data reliably if it is not being sent very often. For debugging reasons, you might want to print when data is sent and print errors. Here is the full code you can try.
func sendButtons(button: String) {
let sendableString: Data? = button.data(using: .utf8)
do {
try localMatch.sendData(toAllPlayers: sendableString!, with: .reliable)
print("Data sent")
}
catch {
print("Data not sent")
print(error)
}
}
If the data is not sent, check if "Data sent" is printed and check for errors

How does a player programmatically using GameKit advertise they want to join some match?

I am having trouble finding information in the standard GameKit documentation on how does a player advertise that they want to join some match? There is documentation on how to create a match and invite waiting players, but I can't decipher how does a player advertise to join a match and become this "waiting" player?
Here is a link to Apple's documentation https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/GameKit_Guide/MatchmakingwithGameCenter/MatchmakingwithGameCenter.html
Calling findMatch on a GKMatchmaker will probably do the trick:
https://developer.apple.com/documentation/gamekit/gkmatchmaker/1520777-findmatch
// Auto-matching or invites to find a peer-to-peer match for the specified request. Error will be nil on success:
// Possible reasons for error:
// 1. Communications failure
// 2. Unauthenticated player
// 3. Timeout
// Note that the players property on the returned GKMatch instance will only contain connected players. It will initially be empty as players are connecting. Implement the GKMatchDelegate method match:player:didChangeConnectionState: to listen for updates to the GKMatch instance's players property.
open func findMatch(for request: GKMatchRequest, withCompletionHandler completionHandler: ((GKMatch?, Error?) -> Void)? = nil)
I believe that providing an invitation handler is the answer:
https://developer.apple.com/documentation/gamekit/gkmatchmaker
To receive invitations from other players, your game must provide an
invitation handler. After your game successfully authenticates the local
player, it should immediately set the inviteHandler property. The invite
handler is called immediately if your game was launched in response to a
push notification.
EDIT: NOPE, this property is apparently DEPRECATED since iOS 7

Data Consistency on very close events in Firebase

I am building an iOS game. Each game room is constructed from two users. After two users get matched, I'm displaying a "waiting for respond" timer on both devices: they need to click am "I'm Ready" button within 8 second or both of them will get kicked from the room + deleted from Firebase.
The correct state of the users (before each of them clicked on the "I'm Ready Button"):
Parent
Matches
User 1
opponent : User2
state : "NotReady"
User 2
opponent : User1
state : "NotReady"
Critical note - The timer on both devices time difference is +-2 seconds, In other words - once device timer is going to end before the other one
When a user press the "I'm Ready" button i'm updating the state : "userReady",and check if the other user is ready too (observing its state value).
If both users are userReady - game on.
PROBLEM
so we have already cleared that in 100% of the cases i have a small time difference between both devices. BUT, if for instance ,
User1 clicked I'm Ready button. So now User2 got an ChildUpdate event, and as far as he knows - User2 is completely ready to play.
User1 timer is ending first(fact), so when his timer will finish, User2 timer will remain 1 seconds. NOW, on User1 time just reached zero, so he get kicked out of the room, and send a removeValue event on each of the Users nodes. While this is happening, at this very small "gap",(between the zero time of User1 timer ending,User2 clock show's 1 sec(fact) - and he press the ready button. than he thinks that User1 is ready to play, and he as well - than the game starts playing.
Final Results -
Both players got deleted from Firebase
User1 is out of the room
User2 starts the game(and he think he is an opponent)
How can i solve this end-case scenario?, I have already tried calling startGame function only when the "UpdateChild state" is done, but it still gets in, maybe because the order for updateChild,and removeValue"?
Any suggestions? And BIG thank you Firebase team for reaching how all the time!!!
It doesn't make sense that User 1 would accept and then expire anyway. User 2 should be the one expiring if the time limit is reached and he hasn't accepted yet.
To prevent this, you're looking for transactions. When you would "expire" a user, use a transaction to do so, so that there are no data conflicts.
var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/");
ref.child('Parent/Matches').child(user1).transaction(function(currentValue) {
if( currentValue && currentValue.state === 'NotReady' ) {
return null; // delete the user
}
else {
return undefined; // abort the transaction; status changed while we were attempting to remove it
}
});
Possibly correct in swift:
var ref = Firebase(url: "https://<YOUR-FIREBASE-APP>.firebaseio.com/Parent/Matches/<user id>")
upvotesRef.runTransactionBlock({
(currentData:FMutableData!) in
if currentData && currentData.value["state"] != "NotReady" {
return FTransactionResult.successWithValue(NSNull)
}
return FTransactionResult.abort();
});
Furthermore, you could simplify things and reduce the opportunity for chaos by not deleting/expiring the records until both users have reached their accept time limit, and then doing both at once in a transaction.

online/offline data management

I have to create an application that has functionality similar to the contacts app. You can add a contact on the client's iPhone and it should get uploaded onto the client's iPad. If the client updates the contact on their iPad, it should get updated on their iPhone.
Most of this is fairly straight forward. I am using Parse.com as my back end and saving contacts locally with Core Data. The only problem I'm encountering is managing contacts when the user is offline.
Let's say I have an iPhone and an iPad. Both of them currently have the same version of the online database. My iPhone is now offline. It is 9AM.
At 10AM I update the phone number for a contact on my iPad. It saves the change locally and online. At 11AM I update the email address for the same contact on my iPhone but I'm still offline.
At noon, my iPhone connects to the internet and checks the server for changes. It sees that its changes are more recent than the latest update (checking an updatedAt timestamp property), so instead of downloading the new phone number for the contact (which is "obsolete"), it overrides the phone number along with the email address (updates the new phone number to the old version it has because it was offline during the phone number update at 10AM and its changes are supposedly more recent).
How am I supposed to manage the online/offline problems encountered such as the one above? A solution I can think of would be to keep updated timestamps on every attribute for a contact instead of just a general updatedAt property for the entire contact, e.g. when was first name updated, when was last name updated, and then manually check if an offline device has more recent changes on every attribute instead of overwriting the whole object, but that seems sloppy.
I was also thinking on having an updatedLocally and updatedOnline timestamp property on every Core Data object. This way if the two don't match I can do a diff-check and use the most recent one for conflicts but this still doesn't seem like the cleanest solution. Has anyone else encountered something similar? If so, how did you solve it?
Pseudocode/Summary for what I think? covers every test case but still isn't very elegant/complete:
2 Entities on Parse.com: Contact and Contact History
Contact has first, last, phone, email, onlineUpdate
Contact History has a Primary Key to a Contact to refer to and the same attributes but with history. e.g. first: [{value:"josue",onlineUpdate:"9AM"},{value:"j",onlineUpdate:"10AM"},{value:"JOSUEESP",onlineUpdate:"11AM"}]
1 Entity on Core Data, Contact:
Contact has first, last phone, email, onlineUpdate, and offlineUpdate (IMPORTANT: this is only on Core Data, not on Parse)
for every contact in parse database as onlineContact {
if onlineContact does not exist in core data {
create contact in core data
}
else {
// found matching local object to online object, check for changes
var localContact = core data contact with same UID as onlineContact
if localContact.offlineUpdate more recent than onlineContact.onlineUpdate {
for every attribute in localContact as attribute {
var lastOnlineValueReceived = Parse database Contact History at the time localContact.onlineUpdate for attribute
if lastOnlineValueReceived == localContact.attribute {
// this attribute did not change in the offline update. use latest available online value
localContact.attribute = onlineContact.attribute
}
else{
// this attribute changed during the more recent offline update, update it online
onlineContact.attribute = localContact.attribute
}
}
}
else if onlineContact.onlineUpdate more recent than localContact.offlineUpdate {
// another device updated the contact. use the online contact.
localContact = offlineContact
}
else{
// when a device is connected to the internet, and it saves a contact
// the offline/online update times are the same
// therefore contacts should be equivalent in this else statement
// do nothing
}
}
TL;DR: How are you supposed to structure a kind of version-control system for online/offline updates without accidental overwriting? I'd like to limit bandwidth usage to a minimum.
I would suggest to use key based updates instead of contact based updates.
You should not send the whole contact to the server, in most cases the user would just change a few attributes anyways (things like 'last name' usually don't change very often). This also reduces bandwith usage.
Along with the applied changes of your offline contact you send the
old version number/last update timestamp of your local contact to the server. The server can now
determine whether or not your local data is up to date, simply by looking at your old version number. If your old version number matches the current version number of the server there is no need for your client to update any other information. If this is not the case the server should send you the new contact (after applying your requested update).
You can also save those commits, this would result in a contact history
which does not store the whole contact each time a key was changed but only the changes themselves.
A simple implementation in pseudo code could look like this:
for( each currentContact in offlineContacts ) do
{
if( localChanges.length > 0){ // updates to be made
commitAllChanges();
answer = getServerAnswer();
if(answer.containsContact() == true){
// server sent us a contact as answer so
// we should overwrite the contact
currentContact = answer.contact;
} else {
// the server does not want us to overwrite the contact, so we are up to date!
}
// ...
}
} // end of iterating over contacts
The server side would look just as simple:
for (currentContactToUpdate in contactsToUpdate) do
{
sendBackContact = false; // only send back the updated contact if the client missed updates
for( each currentUpdate in incomingUpdates ) do {
oldClientVersion = currentUpdate.oldversion;
oldServerVersion = currentContact.getVersion();
if( oldClientVersion != oldServerVersion ){
sendBackContact = true;
// the client missed some updates from other devices
// because he tries to update an old version
}
currentContactToUpdate.apply(currentUpdate);
}
if(sendBackContact == true){
sendBack(currentUpdate);
}
}
To get a better understanding of the workflow I will provide an example:
8 AM both clients and the server are up to date, each device is online
Each device has an entry (in this case a row) for the contact 'Foo Bar' which has the primary key ID.
The version is the same for each entry, so all of them are up to date.
_ Server iPhone iPad
ID 42 42 42
Ver 1 1 1
First Foo Foo Foo
Last Bar Bar Bar
Mail f#b f#b f#b
(excuse this terrible format, SO sadly does not support any sort of tables...)
9 AM your iPhone is offline. You notice Foo Bar's email changed to 'foo#b'.
You change the contact information on your phone like this:
UPDATE 42 FROM 1 TO 2 Mail=foo#b
// ^ID ^old version ^new version ^changed attribute(s)
so now the contact in your phone would look like this:
_ iPhone
ID 42
Ver 2
First Foo
Last Bar
Mail foo#b
10 AM your iPad is offline. You notice 'Foo Bar' is actually written as 'Voo Bar'! You apply the changes immediatly on your iPad.
UPDATE 42 FROM 1 TO 2 First=Voo
Notice that the iPad still thinks the current version of contact 42 is 1. Neither the server nor the iPad did notice how you changed the mail address and increased the version number, since no devices were connected to the network. Those changes are only locally stored and visible on your iPad.
11 AM you connect your iPad to the network. The iPad sends the recent update
to the server. Before:
_ Server iPad
ID 42 42
Ver 1 2
First Foo Voo
Last Bar Bar
Mail f#b f#b
iPad -> Server:
UPDATE 42 FROM 1 TO 2 First=Voo
The server can now see that you are updating Version 1 of contact 42. Since version 1 is the current version your client is up to date (no changes commited in the mean time while you were offline).
Server -> iPad
UPDATED 42 FROM 1 TO 2 - OK
After:
_ Server iPad
ID 42 42
Ver 2 2
First Voo Voo
Last Bar Bar
Mail f#b f#b
12 AM you disconnected your iPad from the network and connect your iPhone.
The iPhone tries to commit the recent changes. Before:
_ Server iPhone
ID 42 42
Ver 2 2
First Voo Voo
Last Bar Bar
Mail f#b foo#b
iPhone -> Server
UPDATE 42 FROM 1 TO 2 Mail=foo#b
The server notices how you try to update an old version of the same contact.
He will apply your update since it is more recent than the iPad's update but
will send you the new contact data to make sure you get the updated first name aswell.After:
_ Server iPhone
ID 42 42
Ver 2 2
First Voo Voo
Last Bar Bar
Mail foo#b foo#b
Server -> iPad
UPDATED 42 FROM 1 TO 3 - Ver=2;First=Voo;.... // send the whole contact
/* Note how the version number was changed to 3, and not to 2, as requested.
* If the new version number was (still) 2 the iPad would miss the update
*/
The next time your iPad connects to the network and has no changes to commit it should just send the current version of the contact and see whether it is still up to date.
Now you have committed two offline changes without overwriting each other.
You can easily extend this approach and so some optimizations. For example:
If the client tries to update an old version of the contact, don't send them the whole contact as answer. Rather send them the commits they missed and let them update their contact by themselves. This is useful if you store lots of information about your client and expect few changes to be done between updates.
If the client updated all information about a contact we can assume he does not need to know about the missed updates, however we would let him know about everything he missed (but it would/should have no effect to him)
I hope this helps.
I know nothing about iOs, core data and parse.com, so I can suggest only a general algorithmic solution. I think you can an approach similar to what is done in version control systems.
The simplest thing is to keep all the history on the server: keep all the revisions of the contact list. Now during the synchronization the phone sends information about the last server revision it has seen, and this revision will be the "common parent" for both the current phone revision and current server revision.
Now you can see what has changed on server and on phone since that revision, and apply the usual 3-way comparison: if some field has changed only on the server, then send the new field to the phone; if some field has changed only on the phone, then change it on server too, if some field has been changed both on phone and on server and the changes are different, then you have a conflict and have to ask user.
A variation of this approach might be to work with changes, not revisions. The primary data at both the server and the client will be not the contact list, but a history of its changes. (The current contact list, as well as a set of 'keyframes' can also be kept if needed; it will not be used for conflict resolving algorithm, but can be used so that it can be quickly shown and used.)
Then, when a user synchronizes the data, you download/upload only the changes. If there are any conflict changes, you have nothing left but to ask a user, otherwise you just merge them. How you define a change and which changes are considered conflicting, is up to you. A simple approach can be defining a change as a pair (field, new-value), and two change are conflicting if they have the same field. You can also employ a more advanced conflict resolving logic such as if one change changes only the first half of the email, and the other the second half, then you can merge them.
The correct way to do this is to keep a transaction log. Whenever you save in Core Data you create a log entry in your transaction log. When you are next online you play back the transaction log against the server.
This is how iCloud and other sync services work.
Instead of having separate flag for each of the core data objects, you can have separate table which will store IDs(primary key from database table which stores contact information) of all updated contacts.
Later when user comes online you just fetch those contacts from your actual contact detail table and upload them on your server.

does anyone know _UIApplicationDidRemoveDeactivationReasonNotification?

when ever a _UIApplicationDidRemoveDeactivationReasonNotification notification is posted
(which is an apple private NSNotification) it holds a NSNumber in it's userInfo,
does anyone know, what does each number mean?
the key is _UIApplicationDeactivationReasonUserInfoKey
all I know so far number 5 means app resigned active for InAppPurchase-verification
(which is an OS message and makes your app inactive until user clicks 'Cancel' or 'Buy')
So I needed this and I got a few values decoded.
I am looking at another similar notification _UIApplicationWillAddDeactivationReasonNotification
with same value key _UIApplicationDeactivationReasonUserInfoKey
here they are (for ios6/7):
// notification/control center overlay
kDeactivationReasonControlOverlay = 1,
// double tapped for the task manager mode
kDeactivationReasonTaskManager = 2,
// app is going inactive, eg: locked, enter background, power off screen, phone call, voice control (not siri)
kDeactivationReasonLockScreen = 3,
// system alerts, eg: privacy permissions, IAP dialogs
kDeactivationReasonSystemAlert = 5,
// siri overlay
kDeactivationReasonSiriOverlay = 6,
However I don't have a test case which i can reproduce 4.
And I don't know if 0 or values above 6 are valid values, if anyone knows please comment!

Resources