Keep my player number after disconnection

Hello, I know there is a way we can say to the LoadBalancing client that when we leave a room we will return to the room and thus, make the server will give us the same player number in the room, but, how do i set this option if we get disconnected my timeout?

Comments

  • Hi @jbelonalea.

    Please see https://doc.photonengine.com/en-us/realtime/current/tutorials/async-and-turnbased and https://doc.photonengine.com/en-us/realtime/current/tutorials/persistence-guide

    When rejoining after leaving a room works, then no additional options are needed to activate rejoin after disconnect.

    To be able to rejoin a room you, need to set a playerTtl on room creation, which is the time in ms that a player that has disconnected or left the room with 'willComeBack' set to true, will be treated as inactive, and is able to rejoin a room in which there is still at least one active player. To be able to rejoin an empty room (a room without any active player inside), you also need to set the emptyRoomTTl to the value in ms for which you wish it to stay alive when empty.
  • jbelonalea
    edited August 2017
    What we need is to be able to have the willComeBack with a timeout, not a player disconnect.
    If I set player TTL to -1 and emptyRoomTTL to 500, and then 2 players enter the room, one left by disconnecting with timeout (not using the opLeaveRoom (willComeBack) function ) and re-join the room, this player will have a different number but the number that the player had is still used ...so, I don't get it, what values do we need to set for this scenario, where the room must delete if all players leave but all the players that enters will always have the same player number in this particular room for all the time?.
  • Hi @jbelonalea.

    I have just tested exactly that with demo_loadBalancing and the timeout-disconnected client just happily reconnects for me with its old playerNr.

    This is what I did:
    - set a valid appID in the demo
    - change the player ttl and empty room ttl that the demos uses to the values that you have mentioned (-1 && 500)
    - enable rejoin (lookout for the "// remove false to enable rejoin" comment in NetworkLogic::run() and remove the 'false' in the line below it)
    - add another switch-case in NetworkLogic::run(), that sets the state to STATE_INITIALIZED whenever it is in state STATE_DISCONNECTED so that the demo will automatically reconnect after a timeout disconnect
    - start two instances of the demo, let one timeout disconnect, but keep the other one inside the room, then after reconnecting just rejoin the room again
  • jbelonalea
    edited August 2017
    What i did in the unreal demo:
    1) first, set the ttl's:
    void LoadBalancingListener::createRoom() { char name[16]; sprintf(name, "UE-%d", GETTIMEMS()); Hashtable props; props.put("s", mGridSize); props.put("m", mMap); if(mLbc->opCreateRoom(name, ExitGames::LoadBalancing::RoomOptions() .setPlayerTtl(-1) .setEmptyRoomTtl(500) .setCustomRoomProperties(props)) ) mView->info("Creating room %s", name); else mView->error("Can't create room in current state"); }

    2) then added the var
    public: ExitGames::Common::JString mLastJoinedRoom;
    in class LoadBalancingListener : public ExitGames::LoadBalancing::Listener of PhotonDemoParticle-UE4\Source\PhotonDemoParticle\demo_particle_common\LoadBalancingListener.h
    and set it in

    void LoadBalancingListener::afterRoomJoined(int localPlayerNr) { mView->info("afterRoomJoined: localPlayerNr=", localPlayerNr); this->mLocalPlayerNr = localPlayerNr; MutableRoom& myRoom = mLbc->getCurrentlyJoinedRoom(); this->mLastJoinedRoom = myRoom.getName(); ... }

    3) Use this last joined room name (gameid) for the joinroom function of photonlbclient:

    void APhotonLBClient::JoinRoom(FString gameId) { client->opJoinRoom( ExitGames::Common::JString(*gameId), gameId.Equals(*FString(listener->mLastJoinedRoom.UTF8Representation().cstr())) ); }
    if PhotonLBClient.cpp
    (offtopic: do you think it's a good idea to use the utf8 conversion?).


    4) Last Step, change all the random join to join the same room:
    from

    mLbc->opJoinRandomRoom();

    to

    mLbc->opJoinOrCreateRoom("room01");

    in LoadBalancingListener.cpp


    when i launch the game with 3 instances it works, and I got player1,player2,player3 numbers, the next time, the old player numbers are still there:


    what do i have to change in the code to use the same player numbers again?, if i do this in my game project i got this errors:

    LogNetwork:Warning: : 2017-08-25 10:29:57,655 ERROR Client.cpp ExitGames::LoadBalancing::Client::onOperationResponse() line: 1296 - OperationResponse - operationCode: 226, returnCode: 32748 (User does not exist in this game)
    LogNetwork:Warning: : 2017-08-25 10:29:57,656 ERROR Client.cpp ExitGames::LoadBalancing::Client::onOperationResponse() line: 1416 - opJoinRoom failed with errorcode 32748: User does not exist in this game. Client is therefore returning to masterserver!
    LogNetwork:Warning: : 2017-08-25 10:29:57,935 ERROR NetworkLogic.cpp NetworkLogic::joinRoomReturn() line: 999 - User does not exist in this game
    LogNetwork:Warning: : opJoinRoom() failed: User does not exist in this game

    so, for the room i'm not there?...
  • Kaiserludi
    Kaiserludi admin
    edited September 2017
    Hi @jbelonalea.

    The call to connect() takes an AuthValues instance as optional first parameter.
    In this AuthValues instance you can set a user ID.
    Now if you don't set one, then the server will just generate one for you that the client will then use.
    What is probably happening here is the following:
    You don't set a user ID yourself, so you get one assigned from the server on connect. Now after a disconnect, you again connect without giving any userID, so Photon sees you as a new Client and assigns a new generated userID to you that does not match the previous one.
    Now when you try to rejoin the room, that rejoin is expected to fail, because you are a new different user, who has not been in that room before.

    You have 2 options:
    a)
    Just generate a userID yourself, store it and ruse it on every connect. For developments purposes you could just do it like this:
    static const ExitGames::Common::JString USER_ID;// = ExitGames::Common::JString()+GETTIMEMS()
    The current timestamp in ms should be by far unique enough for development purposes.
    However before going live you need to make sure you generate a true GUID (globally unique identifier) to make 100% sure no 2 clients will ever generate the same ID.

    b)
    You might want to just let the Photon server deal with generating that production-quality GUID.
    In that case just set the user ID to an empty string on the initial connect after the game got installed. Photon now generated a GUID for you. After you got connected, store the return value of LoadBalancingClient::getUserID().
    Now for every further connects just pass in the stored user id.


    You likely want to store the id in a file and read it out from there on application start, if that file exists, so that the user will still get recognized after restarting the application / the computer.


    3) Use this last joined room name (gameid) for the joinroom function of photonlbclient:

    void APhotonLBClient::JoinRoom(FString gameId) { client->opJoinRoom( ExitGames::Common::JString(*gameId), gameId.Equals(*FString(listener->mLastJoinedRoom.UTF8Representation().cstr())) ); }
    if PhotonLBClient.cpp
    (offtopic: do you think it's a good idea to use the utf8 conversion?).

    It is save to do that, but when converting between FString and JString your adding unnecessary overhead both in terms of performance as well as in terms of code complexity, as both use UTF16/UTF32 internally (at least when you stay with the default definition of TCHAR in Unreal).

    I would just have done the following:

    Or the following:
    
    void APhotonLBClient::JoinRoom(FString gameId)
    {
        client->opJoinRoom(ExitGames::Common::JString(*gameId), listener->mLastJoinedRoom==*gameId);
    }
    
    No UTF8 conversion is needed at all.
  • PS:
    After a connection-loss while inside a room, you may also just call reconnectAndRejoin(). This way the client automatically reuses the cached user ID from the previous connection.
    Also reconnectAndRejoin() results in a quicker rejoin procedure than connect() + opJoinRoom() as it takes advantage of the fact that the client already knows on which game server to find that room, so it just directly connect there, instead of connecting to name server and master server first.

    PPS:
    I have just changed the implementation of LoadBalancingClient::connect():
    Starting with the next release it will just reuse any previously set user ID, when the caller has not provided one. That way, on the initial connect, if the app does not set a user ID, then the server will set one, but on any further connects without providing a user ID, the client will just reuse the one that it already got from the server and not override it with the empty value that it got from the app.

    Still in the future you should still store the ID in a file, as Photon won't cache it beyond the lifetime of the Client instance.
  • jbelonalea
    edited August 2017

    Hi @jbelonalea.
    ...
    There is already a conversion happening when creating the JString from the char*, so the first snippet takes advantage of the fact, that we needed to create a JString anyway and just compares that JString to listener->mLastJoinedRoom, so that we don't need any conversion in that comparison. This should be the optimal variant performance-wise.
    In the second snippet listener->mLastJoinedRoom==*gameId compares a JString with a char*, so internally a temporary JString instance of that char* will be created, so that we still have a conversion happening for the comparison, but in difference to your snippet, it happens implicitly, which makes the code simpler and easier to read. Other than in the former snippet however we still can get away with a one-liner without any local variables.

    Ok, what about this function:

    void NetworkLogic::writeLog(const JString& sLog) { UE_LOG(LogNetwork, Warning, TEXT("<NetworkLogic>: %s"), *FString(sLog.UTF8Representation().cstr())); }

    If I don't use this conversion the strings in the logs are junk characters, and sometimes I need to bring FString from blueprint functions so there is no way i can use const char* or JString directly, I'm wondering what cases are inevitable to avoid the UTF8Representation cstr conversion.

    Ok, I've changed the connection so I save in the disk the userID that PhotonCloud gave me onConnection and then I use it the next time I connect, and now it uses the same player number, cool! ,thanks

  • Kaiserludi
    Kaiserludi admin
    edited September 2017
    *FString(sLog.UTF8Representation().cstr())
    Let's think about what's actually happening here:
    - sLog is a JString
    - you convert that into a UTF8String with JString::UTF8Representation()
    - then you grab the char* representation of that one with UTF8String::cstr()
    - and construct an FString from it with FString::FString() (which implies a conversion back from UTF8 into the original UTF16/UTF32 format, which both, JString and FString use)
    - just to grab the wchar_t* representation of that one with FString::operator*()

    So the whole procedure is pointless and you can remove it as JString::cstr() returns a const reference to it's internal buffer that already is a wchar_t* in the first place.

    Just replace the "%s" with "%ls". The former tells the function to expect a narrow string (char*), so when you parse a wide string (wchar_t*) instead it still interprets it as a narrow one and that is where you junk output characters come from. The latter however tells it to expect a wide string, so it would output junk when you actually pass in a narrow string, but it interprets your input correctly when you pass in a wide string.

    The following will work just fine without any conversions to or from UTF8:
    
    void NetworkLogic::writeLog(const JString& sLog)
    {
        UE_LOG(LogNetwork, Warning, TEXT("<NetworkLogic>: %ls"), sLog.cstr());
    } 
    
  • Thanks K. That I can understand, now I would like to know more...I have a JSON class, it uses this type of functions:
    
    UJsonFieldData* UJsonFieldData::SetString(const FString& key, const FString& value) {
    	Data->SetStringField(*key, *value);
    	return this;
    }
    
    where TSharedPtr<FJsonObject> Data;
    I have a room property that's a json string converted from a class,
    
    template <typename T>
    void NetworkLogic::SetRoomProperty(const JString& name, const T& data)
    {
    	if (!mLoadBalancingClient.getIsInGameRoom())
    	{
    		UE_LOG(LogNetwork, Error, TEXT("NetworkLogic::SetRoomProperty - client is not in a room."));
    		return;
    	}
    
    	MutableRoom& currentRoom = mLoadBalancingClient.getCurrentlyJoinedRoom();
    	currentRoom.addCustomProperty<JString, T>(name, data);
    }
    
    so, first for receiving it, I create a UJsonFieldData, then I set a string from a Hashtable when room properties change:
    
    void NetworkLogic::onRoomPropertiesChange(const Hashtable& changes)
    {
     //Create json class
      //...checks...
     //--- 
    //...
     const JVector<Object>& crpKeys = changes.getKeys();
     for (unsigned int i = 0; i < changes.getSize(); i++)
     {
    	FString key = ValueObject<JString>(crpKeys[i]).getDataCopy().UTF8Representation().cstr();				
            switch (changes[i].getType()){
                          default: case TypeCode::STRING: {
    					FString sValue = FString(ValueObject<JString>(changes[i]).getDataCopy().cstr());
    					json->SetString(*key, *sValue);
    				}
    				break;
                      //other cases
             }
    }
    
    I need a copy of the string in the case it is deleted in the origin (weak pointer?), so I can use it in C++ and blueprints, now, if i remove this utf8 conversion, i have problems with the strings (chinese characters?=)...
    Another case, when you send the data over photon
    
    void NetworkLogic::sendActorFunctionJSON(const JString& actorName, const JString& funcName, const JString& json_string)
    {
    	Hashtable trsData = Hashtable();
    	trsData.put<const char*, const char*>("aname", actorName.UTF8Representation().cstr());
    	trsData.put<const char*, const char*>("fname", funcName.UTF8Representation().cstr());
    	trsData.put<const char*, const char*>("json", json_string.UTF8Representation().cstr());
    	
    	//UE_LOG(LogNetwork, Warning, TEXT("JSON to send:%s from actor %s function %s"),json_string.UTF8Representation().cstr(),actorName.UTF8Representation().cstr(),funcName.UTF8Representation().cstr());
    	mLoadBalancingClient.opRaiseEvent(true, trsData, CODE_SEND_USER_CUSTOM_EVENT);
    }
    
    
    and receiving it:
    
    Hashtable custom_event = ValueObject<Hashtable>
    			(eventContent).getDataCopy();
    		FString aname(ValueObject<JString>(
    			ValueObject<const char*>(custom_event.getValue("aname"))
    			).getDataCopy().UTF8Representation().cstr());
    		FString fname(ValueObject<JString>(
    			ValueObject<const char*>(custom_event.getValue("fname"))
    			).getDataCopy().UTF8Representation().cstr());
    		FString json_param(ValueObject<JString>(
    			ValueObject<const char*>(custom_event.getValue("json"))
    			).getDataCopy().UTF8Representation().cstr());
    		UMainGameDataSingleton_Library::GetPhotonCloudAPI()->OnReceivedActorFunctionJSONString(
    			playerNr,*aname,*fname,	*json_param
    		);
    
    looks like it wont work properly without the utf8 neither...so, how can I keep a copy of the string in the json class and why do i need this utf8 conversion in this 2 cases? thanks!


  • Maybe i should start a new topic with this last question...
  • Kaiserludi
    Kaiserludi admin
    edited September 2017
    Hi @jbelonalea.

    As UE4 on default uses wchar_t*, not char*, for the internal representation of FString, and JString also uses wchar_t* internally, the conversion between JString and FString is very straightforward and does not involve any UTF8 conversion at all.

    However you should get into the habit of not using "string", but instead use L"string", to get rid of a lot of silent implicit conversions.

    Furthermore instead of
    trsData.put<const char*, const char*>("aname", actorName.UTF8Representation().cstr());
    you should simply write
    trsData.put(L"aname", actorName);
    The first variant involves 1 explicit conversion to UTF8 and 2 implicit ones from UTF8, while the second one involves no conversion at all and also makes the code a lot shorter and simpler.

    Accordingly
    FString aname(ValueObject<JString>(ValueObject<const char*>(custom_event.getValue("aname"))).getDataCopy().UTF8Representation().cstr());
    can be simplified to
    FString aname(ValueObject<JString>(ValueObject<JString>(custom_event.getValue("aname"))).getDataCopy().cstr();
    and the same holds true for all the other samples.

    Please also read this post for more information about this topic:
    http://forum.photonengine.com/discussion/comment/36230/#Comment_36230

    EDIT:
    I have just updated all my string conversion related posts in this thread and removed the misleading information that was written under the impression that FString would use char*.
  • Hello, this is a reply from EPIC
    Technically speaking, it's platform dependent on whether or not we use WIDECHAR (2 bytes) or ANSICHAR (1 byte). Take a look at Engine\Source\Runtime\Core\Public\HAL\Platform.h, and you'll see how we define them. However, in practice I do believe all currently supported systems to use WIDECHAR.


    Before going down a rabbit hole of UTF8 conversion, my first question would be have you done network profiling to see that sending these strings is actually causing bandwidth issues (or that they are frequent enough for concern).


    If sending strings does cause a bandwidth concern, the next step would be to determine if it's really necessary to send that many strings.


    If both of the above are true, the next step would be figuring out if there's a better structure for the data to begin with.


    If the set of possible strings is well defined (a list of Map Names, Game Modes, Weapon Types, Modifiers, etc.) you could send Enums or other primitive types instead that you can do dictionary lookups against on either side.


    If the set of possible strings is not well defined, but they are repeated frequently, you could create a cache that only sends strings the first time they are seen, and then sends a Unique ID in subsequent sends (effectively building a dictionary). If this becomes a memory concern, you could limit cache size by removing less used entries as the cache fills up.


    If you're doing something like sending JSON, you might consider parsing the string data and packing it into a struct. The engine does allow you to have custom net serialization code, so you could do something like create a wrapper type for these strings and let it handle of the packing / unpacking / converting. Take a look at FVector_NetQuantize100 for an example of how this can be done.


    Any of the above approaches would likely offer you much greater bandwidth savings than simply converting to / from UTF8.


    If there's absolutely no way to avoid sending the raw string data, and it definitely is a bandwidth issue, then you'd have to determine whether or not converting to UTF8 will offer you enough benefit to mitigate the issues. If it does, then it sounds like that's a viable solution. If it doesn't, then you'll have to work something else out.


    Thanks,
    Jon N.
  • @juaxix:
    Interesting. That response seems to imply that UE4 uses widestrings not just inside the API and other code, but also as the format for transmitting the string over the network.

    This is different in Photon.
    Photon uses UTF16 (on Microsoft platforms) / UTF32 (on every other platform) encoded wide strings in the Client API, but UTF8 encoded narrow strings for the actual transmission over the network (Photon automatically converts all contained strings to and from UTF8 when serializing/deserializing data) as especially compared to UTF32 strings and with mainly Western characters UTF8 saves a lot (about 75% per character) of network traffic for strings. With East Asian (i.e. Chinese, Japanese and Korean) characters the difference naturally is a lot smaller, especially between UTF8 and UTF16, as most of those characters need 2bytes and can even take up to 5 bytes in UTF8.

    So why are we using wide string in the API, when we use UTF8 in the network anyway? Couldn't we then just use narrow strings everywhere ans get rid of those conversions?
    Well the main reasons against using UTF8 everywhere are a) that wide strings allow for much faster performance in practically all string operations, while the extra memory costs for storing the string variables are negligible (CPU cycles are expensive, RAM is cheap) and b) that on some platforms UTF8 support is (or was) rather limited.
  • @juaxix:
    Interesting. That response seems to imply that UE4 uses widestrings not just inside the API and other code, but also as the format for transmitting the string over the network.

    So why are we using wide string in the API, when we use UTF8 in the network anyway? Couldn't we then just use narrow strings everywhere ans get rid of those conversions?
    Well the main reasons against using UTF8 everywhere are a) that wide strings allow for much faster performance in practically all string operations, while the extra memory costs for storing the string variables are negligible (CPU cycles are expensive, RAM is cheap) and b) that on some platforms UTF8 support is (or was) rather limited.

    Ok, so we could have problems for utf8 chars converstions from iOS to Android, right?
  • juaxix said:

    @juaxix:
    Interesting. That response seems to imply that UE4 uses widestrings not just inside the API and other code, but also as the format for transmitting the string over the network.

    So why are we using wide string in the API, when we use UTF8 in the network anyway? Couldn't we then just use narrow strings everywhere ans get rid of those conversions?
    Well the main reasons against using UTF8 everywhere are a) that wide strings allow for much faster performance in practically all string operations, while the extra memory costs for storing the string variables are negligible (CPU cycles are expensive, RAM is cheap) and b) that on some platforms UTF8 support is (or was) rather limited.

    Ok, so we could have problems for utf8 chars converstions from iOS to Android, right?
    No, there should be no problems with that. What leads you to the thought that there might be issues?
  • SaravanaKumar
    SaravanaKumar ✭✭
    edited May 2022

    @Kaiserludi I was facing the similar issue. Where in my case the SDK which I was using are almost similar.

    What I was trying to implement was when the user got lost due to internet disconnection or not proper internet availability. User should rejoin with same user ID as he/she was entered into before.

    I was using the following code when the user is rejoining. Which is almost same as one you were referrring

    ExitGames::LoadBalancing::Client::opJoinRoom( ExitGames::Common::JString(*gameId), true);


    Reference:

    void APhotonLBClient::JoinRoom(FString gameId)
    {
        client->opJoinRoom(ExitGames::Common::JString(*gameId), listener->mLastJoinedRoom==*gameId);
    }
    


    It returns the incremental userID instead of retrieving the old userID in which the user was joined before. As you referrred in this post, we need to pass the previously created userID into this call. But, there is no option for passing an user ID on the above function.


    Can you share the function which I should pass the previous UserID inorder to maintain the same userID and also join the same room?

  • Kaiserludi
    Kaiserludi admin
    edited May 2022

    Hi @SaravanaKumar.


    The observed behavior happens if the player has left the room for good.

    For a rejoin to be possible the player still needs to exist inside the room as an inactive player.

    On default a player gets removed from a room as soon as the server considers it disconnected (so either when it receives an disconnect-request from the client or when the disconnect timeout kicks in or when the server disconnects the clients for some reason).

    For the player to become inactive on disconnect instead of getting removed from the room, you need to set a 'playerTtl' of either -1 or a value greater than 0 in the 'RoomOptions' instance that you provide on room creation (The room options are the optional second argument for all of the API functions for room creation). This value specifies the time in ms for which the player stays inside the room after it became inactive.

    See https://doc-api.photonengine.com/en/cpp/current/a05610.html#ac4ef38546f06519e58a906f810600ce5 for more information on the 'playerTtl'.


    Also please be aware that when there is no active player in a room, the room gets destroyed either instantly (when no 'emptyRoomTtl' has been set in the 'RoomOptions' for that room) or after the 'emptyRoomTtl' in ms has run out, unless you have configured Webhooks for room persistence, so that the room can be stored and reloaded at a later point. Once a room gets destroyed, rejoining it is no longer possible, even if the 'playerTtl' for the player who attempts to rejoin has not run out yet, as all data about the room, including inactive players, has been destroyed and creating a room with the same name will result in an entirely new room without any data from the previously existing one.

    See https://doc-api.photonengine.com/en/cpp/current/a05610.html#a554389e6e0e4b724613caeac1116aeb0 for more information on the 'emptyRoomTtl'.