12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553 |
- // -----------------------------------------------------------------------
- // <copyright file="LoadBalancingClient.cs" company="Exit Games GmbH">
- // Loadbalancing Framework for Photon - Copyright (C) 2018 Exit Games GmbH
- // </copyright>
- // <summary>
- // Provides the operations and a state for games using the
- // Photon LoadBalancing server.
- // </summary>
- // <author>developer@photonengine.com</author>
- // ----------------------------------------------------------------------------
- #if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER
- #define SUPPORTED_UNITY
- #endif
- namespace Photon.Realtime
- {
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Diagnostics;
- using ExitGames.Client.Photon;
- #if SUPPORTED_UNITY
- using UnityEngine;
- using Debug = UnityEngine.Debug;
- #endif
- #if SUPPORTED_UNITY || NETFX_CORE
- using Hashtable = ExitGames.Client.Photon.Hashtable;
- using SupportClass = ExitGames.Client.Photon.SupportClass;
- #endif
- #region Enums
- /// <summary>
- /// State values for a client, which handles switching Photon server types, some operations, etc.
- /// </summary>
- /// \ingroup publicApi
- public enum ClientState
- {
- /// <summary>Peer is created but not used yet.</summary>
- PeerCreated,
- /// <summary>Transition state while connecting to a server. On the Photon Cloud this sends the AppId and AuthenticationValues (UserID).</summary>
- Authenticating,
- /// <summary>Not Used.</summary>
- Authenticated,
- /// <summary>The client sent an OpJoinLobby and if this was done on the Master Server, it will result in. Depending on the lobby, it gets room listings.</summary>
- JoiningLobby,
- /// <summary>The client is in a lobby, connected to the MasterServer. Depending on the lobby, it gets room listings.</summary>
- JoinedLobby,
- /// <summary>Transition from MasterServer to GameServer.</summary>
- DisconnectingFromMasterServer,
- [Obsolete("Renamed to DisconnectingFromMasterServer")]
- DisconnectingFromMasterserver = DisconnectingFromMasterServer,
- /// <summary>Transition to GameServer (client authenticates and joins/creates a room).</summary>
- ConnectingToGameServer,
- [Obsolete("Renamed to ConnectingToGameServer")]
- ConnectingToGameserver = ConnectingToGameServer,
- /// <summary>Connected to GameServer (going to auth and join game).</summary>
- ConnectedToGameServer,
- [Obsolete("Renamed to ConnectedToGameServer")]
- ConnectedToGameserver = ConnectedToGameServer,
- /// <summary>Transition state while joining or creating a room on GameServer.</summary>
- Joining,
- /// <summary>The client entered a room. The CurrentRoom and Players are known and you can now raise events.</summary>
- Joined,
- /// <summary>Transition state when leaving a room.</summary>
- Leaving,
- /// <summary>Transition from GameServer to MasterServer (after leaving a room/game).</summary>
- DisconnectingFromGameServer,
- [Obsolete("Renamed to DisconnectingFromGameServer")]
- DisconnectingFromGameserver = DisconnectingFromGameServer,
- /// <summary>Connecting to MasterServer (includes sending authentication values).</summary>
- ConnectingToMasterServer,
- [Obsolete("Renamed to ConnectingToMasterServer.")]
- ConnectingToMasterserver = ConnectingToMasterServer,
- /// <summary>The client disconnects (from any server). This leads to state Disconnected.</summary>
- Disconnecting,
- /// <summary>The client is no longer connected (to any server). Connect to MasterServer to go on.</summary>
- Disconnected,
- /// <summary>Connected to MasterServer. You might use matchmaking or join a lobby now.</summary>
- ConnectedToMasterServer,
- [Obsolete("Renamed to ConnectedToMasterServer.")]
- ConnectedToMasterserver = ConnectedToMasterServer,
- [Obsolete("Renamed to ConnectedToMasterServer.")]
- ConnectedToMaster = ConnectedToMasterServer,
- /// <summary>Client connects to the NameServer. This process includes low level connecting and setting up encryption. When done, state becomes ConnectedToNameServer.</summary>
- ConnectingToNameServer,
- /// <summary>Client is connected to the NameServer and established encryption already. You should call OpGetRegions or ConnectToRegionMaster.</summary>
- ConnectedToNameServer,
- /// <summary>Clients disconnects (specifically) from the NameServer (usually to connect to the MasterServer).</summary>
- DisconnectingFromNameServer,
- /// <summary>Client was unable to connect to Name Server and will attempt to connect with an alternative network protocol (TCP).</summary>
- ConnectWithFallbackProtocol,
- ConnectWithoutAuthOnceWss
- }
- /// <summary>
- /// Internal state, how this peer gets into a particular room (joining it or creating it).
- /// </summary>
- internal enum JoinType
- {
- /// <summary>This client creates a room, gets into it (no need to join) and can set room properties.</summary>
- CreateRoom,
- /// <summary>The room existed already and we join into it (not setting room properties).</summary>
- JoinRoom,
- /// <summary>Done on Master Server and (if successful) followed by a Join on Game Server.</summary>
- JoinRandomRoom,
- /// <summary>Done on Master Server and (if successful) followed by a Join or Create on Game Server.</summary>
- JoinRandomOrCreateRoom,
- /// <summary>Client is either joining or creating a room. On Master- and Game-Server.</summary>
- JoinOrCreateRoom
- }
- /// <summary>Enumeration of causes for Disconnects (used in LoadBalancingClient.DisconnectedCause).</summary>
- /// <remarks>Read the individual descriptions to find out what to do about this type of disconnect.</remarks>
- public enum DisconnectCause
- {
- /// <summary>No error was tracked.</summary>
- None,
- /// <summary>OnStatusChanged: The server is not available or the address is wrong. Make sure the port is provided and the server is up.</summary>
- ExceptionOnConnect,
- /// <summary>OnStatusChanged: Dns resolution for a hostname failed. The exception for this is being caught and logged with error level.</summary>
- DnsExceptionOnConnect,
- /// <summary>OnStatusChanged: The server address was parsed as IPv4 illegally. An illegal address would be e.g. 192.168.1.300. IPAddress.TryParse() will let this pass but our check won't.</summary>
- ServerAddressInvalid,
- /// <summary>OnStatusChanged: Some internal exception caused the socket code to fail. This may happen if you attempt to connect locally but the server is not available. In doubt: Contact Exit Games.</summary>
- Exception,
- /// <summary>Send exception.</summary>
- SendException,
- /// <summary>Receive exception.</summary>
- ReceiveException,
- /// <summary>OnStatusChanged: The server disconnected this client due to timing out (missing acknowledgement from the client).</summary>
- ServerTimeout,
- /// <summary>OnStatusChanged: This client detected that the server's responses are not received in due time.</summary>
- ClientTimeout,
- /// <summary>OnStatusChanged: The server disconnected this client from within the room's logic (the C# code).</summary>
- DisconnectByServerLogic,
- /// <summary>OnStatusChanged: The server disconnected this client for unknown reasons.</summary>
- DisconnectByServerReasonUnknown,
- /// <summary>OnOperationResponse: Authenticate in the Photon Cloud with invalid AppId. Update your subscription or contact Exit Games.</summary>
- InvalidAuthentication,
- /// <summary>OnOperationResponse: Authenticate in the Photon Cloud with invalid client values or custom authentication setup in Cloud Dashboard.</summary>
- CustomAuthenticationFailed,
- /// <summary>The authentication ticket should provide access to any Photon Cloud server without doing another authentication-service call. However, the ticket expired.</summary>
- AuthenticationTicketExpired,
- /// <summary>OnOperationResponse: Authenticate (temporarily) failed when using a Photon Cloud subscription without CCU Burst. Update your subscription.</summary>
- MaxCcuReached,
- /// <summary>OnOperationResponse: Authenticate when the app's Photon Cloud subscription is locked to some (other) region(s). Update your subscription or master server address.</summary>
- InvalidRegion,
- /// <summary>OnOperationResponse: Operation that's (currently) not available for this client (not authorized usually). Only tracked for op Authenticate.</summary>
- OperationNotAllowedInCurrentState,
- /// <summary>OnStatusChanged: The client disconnected from within the logic (the C# code).</summary>
- DisconnectByClientLogic,
- /// <summary>The client called an operation too frequently and got disconnected due to hitting the OperationLimit. This triggers a client-side disconnect, too.</summary>
- /// <remarks>To protect the server, some operations have a limit. When an OperationResponse fails with ErrorCode.OperationLimitReached, the client disconnects.</remarks>
- DisconnectByOperationLimit,
- /// <summary>The client received a "Disconnect Message" from the server. Check the debug logs for details.</summary>
- DisconnectByDisconnectMessage,
- /// <summary>Used in case the application quits. Can be useful to not load new scenes or re-connect in OnDisconnected.</summary>
- /// <remarks>ConnectionHandler.OnDisable() will use this, if the Unity engine already called OnApplicationQuit (ConnectionHandler.AppQuits = true).</remarks>
- ApplicationQuit
- }
- /// <summary>Available server (types) for internally used field: server.</summary>
- /// <remarks>Photon uses 3 different roles of servers: Name Server, Master Server and Game Server.</remarks>
- public enum ServerConnection
- {
- /// <summary>This server is where matchmaking gets done and where clients can get lists of rooms in lobbies.</summary>
- MasterServer,
- /// <summary>This server handles a number of rooms to execute and relay the messages between players (in a room).</summary>
- GameServer,
- /// <summary>This server is used initially to get the address (IP) of a Master Server for a specific region. Not used for Photon OnPremise (self hosted).</summary>
- NameServer
- }
- /// <summary>Defines which sort of app the LoadBalancingClient is used for: Realtime or Voice.</summary>
- public enum ClientAppType
- {
- /// <summary>Realtime apps are for gaming / interaction. Also used by PUN 2.</summary>
- Realtime,
- /// <summary>Voice apps stream audio.</summary>
- Voice,
- /// <summary>Fusion clients are for matchmaking and relay in Photon Fusion.</summary>
- Fusion
- }
- /// <summary>
- /// Defines how the communication gets encrypted.
- /// </summary>
- public enum EncryptionMode
- {
- /// <summary>
- /// This is the default encryption mode: Messages get encrypted only on demand (when you send operations with the "encrypt" parameter set to true).
- /// </summary>
- PayloadEncryption,
- /// <summary>
- /// With this encryption mode for UDP, the connection gets setup and all further datagrams get encrypted almost entirely. On-demand message encryption (like in PayloadEncryption) is unavailable.
- /// </summary>
- DatagramEncryption = 10,
- /// <summary>
- /// With this encryption mode for UDP, the connection gets setup with random sequence numbers and all further datagrams get encrypted almost entirely. On-demand message encryption (like in PayloadEncryption) is unavailable.
- /// </summary>
- DatagramEncryptionRandomSequence = 11,
- ///// <summary>
- ///// Same as above except that GCM mode is used to encrypt data.
- ///// </summary>
- //DatagramEncryptionGCMRandomSequence = 12,
- /// <summary>
- /// Datagram Encryption with GCM.
- /// </summary>
- DatagramEncryptionGCM = 13,
- }
- /// <summary>Container for port definitions.</summary>
- public struct PhotonPortDefinition
- {
- public static readonly PhotonPortDefinition AlternativeUdpPorts = new PhotonPortDefinition() { NameServerPort = 27000, MasterServerPort = 27001, GameServerPort = 27002};
- /// <summary>Typical ports: UDP: 5058 or 27000, TCP: 4533, WSS: 19093 or 443.</summary>
- public ushort NameServerPort;
- /// <summary>Typical ports: UDP: 5056 or 27002, TCP: 4530, WSS: 19090 or 443.</summary>
- public ushort MasterServerPort;
- /// <summary>Typical ports: UDP: 5055 or 27001, TCP: 4531, WSS: 19091 or 443.</summary>
- public ushort GameServerPort;
- }
- #endregion
- /// <summary>
- /// This class implements the Photon LoadBalancing workflow by using a LoadBalancingPeer.
- /// It keeps a state and will automatically execute transitions between the Master and Game Servers.
- /// </summary>
- /// <remarks>
- /// This class (and the Player class) should be extended to implement your own game logic.
- /// You can override CreatePlayer as "factory" method for Players and return your own Player instances.
- /// The State of this class is essential to know when a client is in a lobby (or just on the master)
- /// and when in a game where the actual gameplay should take place.
- /// Extension notes:
- /// An extension of this class should override the methods of the IPhotonPeerListener, as they
- /// are called when the state changes. Call base.method first, then pick the operation or state you
- /// want to react to and put it in a switch-case.
- /// We try to provide demo to each platform where this api can be used, so lookout for those.
- /// </remarks>
- public class LoadBalancingClient : IPhotonPeerListener
- {
- /// <summary>
- /// The client uses a LoadBalancingPeer as API to communicate with the server.
- /// This is public for ease-of-use: Some methods like OpRaiseEvent are not relevant for the connection state and don't need a override.
- /// </summary>
- public LoadBalancingPeer LoadBalancingPeer { get; private set; }
- /// <summary>
- /// Gets or sets the binary protocol version used by this client
- /// </summary>
- /// <remarks>
- /// Use this always instead of setting it via <see cref="LoadBalancingClient.LoadBalancingPeer"/>
- /// (<see cref="PhotonPeer.SerializationProtocolType"/>) directly, especially when WSS protocol is used.
- /// </remarks>
- public SerializationProtocol SerializationProtocol
- {
- get
- {
- return this.LoadBalancingPeer.SerializationProtocolType;
- }
- set
- {
- this.LoadBalancingPeer.SerializationProtocolType = value;
- }
- }
- /// <summary>The version of your client. A new version also creates a new "virtual app" to separate players from older client versions.</summary>
- public string AppVersion { get; set; }
- /// <summary>The AppID as assigned from the Photon Cloud. If you host yourself, this is the "regular" Photon Server Application Name (most likely: "LoadBalancing").</summary>
- public string AppId { get; set; }
- /// <summary>The ClientAppType defines which sort of AppId should be expected. The LoadBalancingClient supports Realtime and Voice app types. Default: Realtime.</summary>
- public ClientAppType ClientType { get; set; }
- /// <summary>User authentication values to be sent to the Photon server right after connecting.</summary>
- /// <remarks>Set this property or pass AuthenticationValues by Connect(..., authValues).</remarks>
- public AuthenticationValues AuthValues { get; set; }
- /// <summary>Enables the new Authentication workflow.</summary>
- public AuthModeOption AuthMode = AuthModeOption.Auth;
- /// <summary>Defines how the communication gets encrypted.</summary>
- public EncryptionMode EncryptionMode = EncryptionMode.PayloadEncryption;
- /// <summary>Optionally contains a protocol which will be used on Master- and GameServer. </summary>
- /// <remarks>
- /// When using AuthMode = AuthModeOption.AuthOnceWss, the client uses a wss-connection on the NameServer but another protocol on the other servers.
- /// As the NameServer sends an address, which is different per protocol, it needs to know the expected protocol.
- ///
- /// This is nullable by design. In many cases, the protocol on the NameServer is not different from the other servers.
- /// If set, the operation AuthOnce will contain this value and the OpAuth response on the NameServer will execute a protocol switch.
- /// </remarks>
- public ConnectionProtocol? ExpectedProtocol { get; set; }
- ///<summary>Simplifies getting the token for connect/init requests, if this feature is enabled.</summary>
- private object TokenForInit
- {
- get
- {
- if (this.AuthMode == AuthModeOption.Auth)
- {
- return null;
- }
- return (this.AuthValues != null) ? this.AuthValues.Token : null;
- }
- }
- /// <summary>Internally used cache for the server's token. Identifies a user/session and can be used to rejoin.</summary>
- private object tokenCache;
- /// <summary>True if this client uses a NameServer to get the Master Server address.</summary>
- /// <remarks>This value is public, despite being an internal value, which should only be set by this client.</remarks>
- public bool IsUsingNameServer { get; set; }
- /// <summary>Name Server Host Name for Photon Cloud. Without port and without any prefix.</summary>
- public string NameServerHost = "ns.photonengine.io";
- /// <summary>Name Server Address for Photon Cloud (based on current protocol). You can use the default values and usually won't have to set this value.</summary>
- public string NameServerAddress { get { return this.GetNameServerAddress(); } }
- /// <summary>Name Server port per protocol (the UDP port is different than TCP, etc).</summary>
- private static readonly Dictionary<ConnectionProtocol, int> ProtocolToNameServerPort = new Dictionary<ConnectionProtocol, int>() { { ConnectionProtocol.Udp, 5058 }, { ConnectionProtocol.Tcp, 4533 }, { ConnectionProtocol.WebSocket, 80 }, { ConnectionProtocol.WebSocketSecure, 443 } };
- /// <summary>Replaced by ServerPortOverrides.</summary>
- [Obsolete("Set port overrides in ServerPortOverrides. Not used anymore!")]
- public bool UseAlternativeUdpPorts { get; set; }
- /// <summary>Defines overrides for server ports. Used per server-type if > 0. Important: You must change these when the protocol changes!</summary>
- /// <remarks>
- /// Typical ports are listed in PhotonPortDefinition.
- ///
- /// Instead of using the port provided from the servers, the specified port is used (independent of the protocol).
- /// If a value is 0 (default), the port is not being replaced.
- ///
- /// Different protocols have different typical ports per server-type.
- /// https://doc.photonengine.com/en-us/pun/current/reference/tcp-and-udp-port-numbers
- ///
- /// In case of using the AuthMode AutOnceWss, the name server's protocol is wss, while udp or tcp will be used on the master server and game server.
- /// Set the ports accordingly per protocol and server.
- /// </remarks>
- public PhotonPortDefinition ServerPortOverrides;
- /// <summary>Enables a fallback to another protocol in case a connect to the Name Server fails.</summary>
- /// <remarks>
- /// When connecting to the Name Server fails for a first time, the client will select an alternative
- /// network protocol and re-try to connect.
- ///
- /// The fallback will use the default Name Server port as defined by ProtocolToNameServerPort.
- ///
- /// The fallback for TCP is UDP. All other protocols fallback to TCP.
- /// </remarks>
- public bool EnableProtocolFallback { get; set; }
- /// <summary>The currently used server address (if any). The type of server is define by Server property.</summary>
- public string CurrentServerAddress { get { return this.LoadBalancingPeer.ServerAddress; } }
- /// <summary>Your Master Server address. In PhotonCloud, call ConnectToRegionMaster() to find your Master Server.</summary>
- /// <remarks>
- /// In the Photon Cloud, explicit definition of a Master Server Address is not best practice.
- /// The Photon Cloud has a "Name Server" which redirects clients to a specific Master Server (per Region and AppId).
- /// </remarks>
- public string MasterServerAddress { get; set; }
- /// <summary>The game server's address for a particular room. In use temporarily, as assigned by master.</summary>
- public string GameServerAddress { get; protected internal set; }
- /// <summary>The server this client is currently connected or connecting to.</summary>
- /// <remarks>
- /// Each server (NameServer, MasterServer, GameServer) allow some operations and reject others.
- /// </remarks>
- public ServerConnection Server { get; private set; }
- /// <summary>
- /// Defines a proxy URL for WebSocket connections. Can be the proxy or point to a .pac file.
- /// </summary>
- /// <remarks>
- /// This URL supports various definitions:
- ///
- /// "user:pass@proxyaddress:port"<br/>
- /// "proxyaddress:port"<br/>
- /// "system:"<br/>
- /// "pac:"<br/>
- /// "pac:http://host/path/pacfile.pac"<br/>
- ///
- /// Important: Don't define a protocol, except to point to a pac file. the proxy address should not begin with http:// or https://.
- /// </remarks>
- public string ProxyServerAddress;
- /// <summary>Backing field for property.</summary>
- private ClientState state = ClientState.PeerCreated;
- /// <summary>Current state this client is in. Careful: several states are "transitions" that lead to other states.</summary>
- public ClientState State
- {
- get
- {
- return this.state;
- }
- set
- {
- if (this.state == value)
- {
- return;
- }
- ClientState previousState = this.state;
- this.state = value;
- if (StateChanged != null) StateChanged(previousState, this.state);
- }
- }
- /// <summary>Returns if this client is currently connected or connecting to some type of server.</summary>
- /// <remarks>This is even true while switching servers. Use IsConnectedAndReady to check only for those states that enable you to send Operations.</remarks>
- public bool IsConnected { get { return this.LoadBalancingPeer != null && this.State != ClientState.PeerCreated && this.State != ClientState.Disconnected; } }
- /// <summary>
- /// A refined version of IsConnected which is true only if your connection is ready to send operations.
- /// </summary>
- /// <remarks>
- /// Not all operations can be called on all types of servers. If an operation is unavailable on the currently connected server,
- /// this will result in a OperationResponse with ErrorCode != 0.
- ///
- /// Examples: The NameServer allows OpGetRegions which is not available anywhere else.
- /// The MasterServer does not allow you to send events (OpRaiseEvent) and on the GameServer you are unable to join a lobby (OpJoinLobby).
- ///
- /// To check which server you are on, use: <see cref="Server"/>.
- /// </remarks>
- public bool IsConnectedAndReady
- {
- get
- {
- if (this.LoadBalancingPeer == null)
- {
- return false;
- }
- switch (this.State)
- {
- case ClientState.PeerCreated:
- case ClientState.Disconnected:
- case ClientState.Disconnecting:
- case ClientState.DisconnectingFromGameServer:
- case ClientState.DisconnectingFromMasterServer:
- case ClientState.DisconnectingFromNameServer:
- case ClientState.Authenticating:
- case ClientState.ConnectingToGameServer:
- case ClientState.ConnectingToMasterServer:
- case ClientState.ConnectingToNameServer:
- case ClientState.Joining:
- case ClientState.Leaving:
- return false; // we are not ready to execute any operations
- }
- return true;
- }
- }
- /// <summary>Register a method to be called when this client's ClientState gets set.</summary>
- /// <remarks>This can be useful to react to being connected, joined into a room, etc.</remarks>
- public event Action<ClientState, ClientState> StateChanged;
- /// <summary>Register a method to be called when an event got dispatched. Gets called after the LoadBalancingClient handled the internal events first.</summary>
- /// <remarks>
- /// This is an alternative to extending LoadBalancingClient to override OnEvent().
- ///
- /// Note that OnEvent is calling EventReceived after it handled internal events first.
- /// That means for example: Joining players will already be in the player list but leaving
- /// players will already be removed from the room.
- /// </remarks>
- public event Action<EventData> EventReceived;
- /// <summary>Register a method to be called when an operation response is received.</summary>
- /// <remarks>
- /// This is an alternative to extending LoadBalancingClient to override OnOperationResponse().
- ///
- /// Note that OnOperationResponse gets executed before your Action is called.
- /// That means for example: The OpJoinLobby response already set the state to "JoinedLobby"
- /// and the response to OpLeave already triggered the Disconnect before this is called.
- /// </remarks>
- public event Action<OperationResponse> OpResponseReceived;
- /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
- /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
- public ConnectionCallbacksContainer ConnectionCallbackTargets;
- /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
- /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
- public MatchMakingCallbacksContainer MatchMakingCallbackTargets;
- /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
- /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
- internal InRoomCallbacksContainer InRoomCallbackTargets;
- /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
- /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
- internal LobbyCallbacksContainer LobbyCallbackTargets;
- /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
- /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
- internal WebRpcCallbacksContainer WebRpcCallbackTargets;
- /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
- /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
- internal ErrorInfoCallbacksContainer ErrorInfoCallbackTargets;
- /// <summary>Summarizes (aggregates) the different causes for disconnects of a client.</summary>
- /// <remarks>
- /// A disconnect can be caused by: errors in the network connection or some vital operation failing
- /// (which is considered "high level"). While operations always trigger a call to OnOperationResponse,
- /// connection related changes are treated in OnStatusChanged.
- /// The DisconnectCause is set in either case and summarizes the causes for any disconnect in a single
- /// state value which can be used to display (or debug) the cause for disconnection.
- /// </remarks>
- public DisconnectCause DisconnectedCause { get; protected set; }
- /// <summary>
- /// After a to a connection loss or timeout, this summarizes the most relevant system conditions which might have contributed to the loss.
- /// </summary>
- /// <remarks>
- /// </remarks>
- public SystemConnectionSummary SystemConnectionSummary;
- /// <summary>Internal value if the client is in a lobby.</summary>
- /// <remarks>This is used to re-set this.State, when joining/creating a room fails.</remarks>
- public bool InLobby
- {
- get { return this.State == ClientState.JoinedLobby; }
- }
- /// <summary>The lobby this client currently uses. Defined when joining a lobby or creating rooms</summary>
- public TypedLobby CurrentLobby { get; internal set; }
- /// <summary>
- /// If enabled, the client will get a list of available lobbies from the Master Server.
- /// </summary>
- /// <remarks>
- /// Set this value before the client connects to the Master Server. While connected to the Master
- /// Server, a change has no effect.
- ///
- /// Implement OptionalInfoCallbacks.OnLobbyStatisticsUpdate, to get the list of used lobbies.
- ///
- /// The lobby statistics can be useful if your title dynamically uses lobbies, depending (e.g.)
- /// on current player activity or such.
- /// In this case, getting a list of available lobbies, their room-count and player-count can
- /// be useful info.
- ///
- /// ConnectUsingSettings sets this to the PhotonServerSettings value.
- /// </remarks>
- public bool EnableLobbyStatistics;
- /// <summary>Internal lobby stats cache, used by LobbyStatistics.</summary>
- private readonly List<TypedLobbyInfo> lobbyStatistics = new List<TypedLobbyInfo>();
- /// <summary>The local player is never null but not valid unless the client is in a room, too. The ID will be -1 outside of rooms.</summary>
- public Player LocalPlayer { get; internal set; }
- /// <summary>
- /// The nickname of the player (synced with others). Same as client.LocalPlayer.NickName.
- /// </summary>
- public string NickName
- {
- get
- {
- return this.LocalPlayer.NickName;
- }
- set
- {
- if (this.LocalPlayer == null)
- {
- return;
- }
- this.LocalPlayer.NickName = value;
- }
- }
- /// <summary>An ID for this user. Sent in OpAuthenticate when you connect. If not set, the PlayerName is applied during connect.</summary>
- /// <remarks>
- /// On connect, if the UserId is null or empty, the client will copy the PlayName to UserId. If PlayerName is not set either
- /// (before connect), the server applies a temporary ID which stays unknown to this client and other clients.
- ///
- /// The UserId is what's used in FindFriends and for fetching data for your account (with WebHooks e.g.).
- ///
- /// By convention, set this ID before you connect, not while being connected.
- /// There is no error but the ID won't change while being connected.
- /// </remarks>
- public string UserId
- {
- get
- {
- if (this.AuthValues != null)
- {
- return this.AuthValues.UserId;
- }
- return null;
- }
- set
- {
- if (this.AuthValues == null)
- {
- this.AuthValues = new AuthenticationValues();
- }
- this.AuthValues.UserId = value;
- }
- }
- /// <summary>The current room this client is connected to (null if none available).</summary>
- public Room CurrentRoom { get; set; }
- /// <summary>Is true while being in a room (this.state == ClientState.Joined).</summary>
- /// <remarks>
- /// Aside from polling this value, game logic should implement IMatchmakingCallbacks in some class
- /// and react when that gets called.<br/>
- /// OpRaiseEvent, OpLeave and some other operations can only be used (successfully) when the client is in a room..
- /// </remarks>
- public bool InRoom
- {
- get
- {
- return this.state == ClientState.Joined && this.CurrentRoom != null;
- }
- }
- /// <summary>Statistic value available on master server: Players on master (looking for games).</summary>
- public int PlayersOnMasterCount { get; internal set; }
- /// <summary>Statistic value available on master server: Players in rooms (playing).</summary>
- public int PlayersInRoomsCount { get; internal set; }
- /// <summary>Statistic value available on master server: Rooms currently created.</summary>
- public int RoomsCount { get; internal set; }
- /// <summary>Internally used to decide if a room must be created or joined on game server.</summary>
- private JoinType lastJoinType;
- /// <summary>Used when the client arrives on the GS, to join the room with the correct values.</summary>
- private EnterRoomParams enterRoomParamsCache;
- /// <summary>Used to cache a failed "enter room" operation on the Game Server, to return to the Master Server before calling a fail-callback.</summary>
- private OperationResponse failedRoomEntryOperation;
- /// <summary>Maximum of userIDs that can be sent in one friend list request.</summary>
- private const int FriendRequestListMax = 512;
- /// <summary>Contains the list of names of friends to look up their state on the server.</summary>
- private string[] friendListRequested;
- /// <summary>Internal flag to know if the client currently fetches a friend list.</summary>
- public bool IsFetchingFriendList { get { return this.friendListRequested != null; } }
- /// <summary>The cloud region this client connects to. Set by ConnectToRegionMaster(). Not set if you don't use a NameServer!</summary>
- public string CloudRegion { get; private set; }
- /// <summary>The cluster name provided by the Name Server.</summary>
- /// <remarks>
- /// The value is provided by the OpResponse for OpAuthenticate/OpAuthenticateOnce.
- /// Default: null. This value only ever updates from the Name Server authenticate response.
- /// </remarks>
- public string CurrentCluster { get; private set; }
- /// <summary>Contains the list if enabled regions this client may use. Null, unless the client got a response to OpGetRegions.</summary>
- public RegionHandler RegionHandler;
- /// <summary>Stores the best region summary of a previous session to speed up connecting.</summary>
- private string bestRegionSummaryFromStorage;
- /// <summary>Set when the best region pinging is done.</summary>
- public string SummaryToCache;
- /// <summary>Internal connection setting/flag. If the client should connect to the best region or not.</summary>
- /// <remarks>
- /// It's set in the Connect...() methods. Only ConnectUsingSettings() sets it to true.
- /// If true, client will ping available regions and select the best.
- /// A bestRegionSummaryFromStorage can be used to cut the ping time short.
- /// </remarks>
- private bool connectToBestRegion = true;
- /// <summary>Definition of parameters for encryption data (included in Authenticate operation response).</summary>
- private class EncryptionDataParameters
- {
- /// <summary>
- /// Key for encryption mode
- /// </summary>
- public const byte Mode = 0;
- /// <summary>
- /// Key for first secret
- /// </summary>
- public const byte Secret1 = 1;
- /// <summary>
- /// Key for second secret
- /// </summary>
- public const byte Secret2 = 2;
- }
- private class CallbackTargetChange
- {
- public readonly object Target;
- /// <summary>Add if true, remove if false.</summary>
- public readonly bool AddTarget;
- public CallbackTargetChange(object target, bool addTarget)
- {
- this.Target = target;
- this.AddTarget = addTarget;
- }
- }
- private readonly Queue<CallbackTargetChange> callbackTargetChanges = new Queue<CallbackTargetChange>();
- private readonly HashSet<object> callbackTargets = new HashSet<object>();
- /// <summary>Creates a LoadBalancingClient with UDP protocol or the one specified.</summary>
- /// <param name="protocol">Specifies the network protocol to use for connections.</param>
- public LoadBalancingClient(ConnectionProtocol protocol = ConnectionProtocol.Udp)
- {
- this.ConnectionCallbackTargets = new ConnectionCallbacksContainer(this);
- this.MatchMakingCallbackTargets = new MatchMakingCallbacksContainer(this);
- this.InRoomCallbackTargets = new InRoomCallbacksContainer(this);
- this.LobbyCallbackTargets = new LobbyCallbacksContainer(this);
- this.WebRpcCallbackTargets = new WebRpcCallbacksContainer(this);
- this.ErrorInfoCallbackTargets = new ErrorInfoCallbacksContainer(this);
- this.LoadBalancingPeer = new LoadBalancingPeer(this, protocol);
- this.LoadBalancingPeer.OnDisconnectMessage += this.OnDisconnectMessageReceived;
- this.SerializationProtocol = SerializationProtocol.GpBinaryV18;
- this.LocalPlayer = this.CreatePlayer(string.Empty, -1, true, null); //TODO: Check if we can do this later
- #if SUPPORTED_UNITY
- CustomTypesUnity.Register();
- #endif
- #if UNITY_WEBGL
- if (this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Tcp || this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Udp)
- {
- this.LoadBalancingPeer.Listener.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure.");
- this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
- }
- #endif
- this.State = ClientState.PeerCreated;
- }
- /// <summary>Creates a LoadBalancingClient, setting various values needed before connecting.</summary>
- /// <param name="masterAddress">The Master Server's address to connect to. Used in Connect.</param>
- /// <param name="appId">The AppId of this title. Needed for the Photon Cloud. Find it in the Dashboard.</param>
- /// <param name="gameVersion">A version for this client/build. In the Photon Cloud, players are separated by AppId, GameVersion and Region.</param>
- /// <param name="protocol">Specifies the network protocol to use for connections.</param>
- public LoadBalancingClient(string masterAddress, string appId, string gameVersion, ConnectionProtocol protocol = ConnectionProtocol.Udp) : this(protocol)
- {
- this.MasterServerAddress = masterAddress;
- this.AppId = appId;
- this.AppVersion = gameVersion;
- }
- public int NameServerPortInAppSettings;
- /// <summary>
- /// Gets the NameServer Address (with prefix and port), based on the set protocol (this.LoadBalancingPeer.UsedProtocol).
- /// </summary>
- /// <returns>NameServer Address (with prefix and port).</returns>
- private string GetNameServerAddress()
- {
- var protocolPort = 0;
- ProtocolToNameServerPort.TryGetValue(this.LoadBalancingPeer.TransportProtocol, out protocolPort);
- if (this.NameServerPortInAppSettings != 0)
- {
- this.DebugReturn(DebugLevel.INFO, string.Format("Using NameServerPortInAppSettings: {0}", this.NameServerPortInAppSettings));
- protocolPort = this.NameServerPortInAppSettings;
- }
- if (this.ServerPortOverrides.NameServerPort > 0)
- {
- protocolPort = this.ServerPortOverrides.NameServerPort;
- }
- switch (this.LoadBalancingPeer.TransportProtocol)
- {
- case ConnectionProtocol.Udp:
- case ConnectionProtocol.Tcp:
- return string.Format("{0}:{1}", NameServerHost, protocolPort);
- case ConnectionProtocol.WebSocket:
- return string.Format("ws://{0}:{1}", NameServerHost, protocolPort);
- case ConnectionProtocol.WebSocketSecure:
- return string.Format("wss://{0}:{1}", NameServerHost, protocolPort);
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
- #region Operations and Commands
- // needed connect variants:
- // connect to Name Server only (could include getregions) -> end after getregions
- // connect to Region Master via Name Server (specific region/cluster) -> no getregions! authenticates and ends after on connected to master
- // connect to Best Region via Name Server
- // connect to Master Server (no Name Server, no appid)
- public virtual bool ConnectUsingSettings(AppSettings appSettings)
- {
- if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
- {
- this.DebugReturn(DebugLevel.WARNING, "ConnectUsingSettings() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
- return false;
- }
- if (appSettings == null)
- {
- this.DebugReturn(DebugLevel.ERROR, "ConnectUsingSettings failed. The appSettings can't be null.'");
- return false;
- }
- switch (this.ClientType)
- {
- case ClientAppType.Realtime:
- this.AppId = appSettings.AppIdRealtime;
- break;
- case ClientAppType.Voice:
- this.AppId = appSettings.AppIdVoice;
- break;
- case ClientAppType.Fusion:
- this.AppId = appSettings.AppIdFusion;
- break;
- }
- this.AppVersion = appSettings.AppVersion;
- this.IsUsingNameServer = appSettings.UseNameServer;
- this.CloudRegion = appSettings.FixedRegion;
- this.connectToBestRegion = string.IsNullOrEmpty(this.CloudRegion);
- this.EnableLobbyStatistics = appSettings.EnableLobbyStatistics;
- this.LoadBalancingPeer.DebugOut = appSettings.NetworkLogging;
- this.AuthMode = appSettings.AuthMode;
- if (appSettings.AuthMode == AuthModeOption.AuthOnceWss)
- {
- this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
- this.ExpectedProtocol = appSettings.Protocol;
- }
- else
- {
- this.LoadBalancingPeer.TransportProtocol = appSettings.Protocol;
- this.ExpectedProtocol = null;
- }
- this.EnableProtocolFallback = appSettings.EnableProtocolFallback;
- this.bestRegionSummaryFromStorage = appSettings.BestRegionSummaryFromStorage;
- this.DisconnectedCause = DisconnectCause.None;
- this.SystemConnectionSummary = null;
- this.CheckConnectSetupWebGl();
- if (this.IsUsingNameServer)
- {
- this.Server = ServerConnection.NameServer;
- if (!appSettings.IsDefaultNameServer)
- {
- this.NameServerHost = appSettings.Server;
- }
- this.ProxyServerAddress = appSettings.ProxyServer;
- this.NameServerPortInAppSettings = appSettings.Port;
- if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
- {
- return false;
- }
- this.State = ClientState.ConnectingToNameServer;
- }
- else
- {
- this.Server = ServerConnection.MasterServer;
- int portToUse = appSettings.IsDefaultPort ? 5055 : appSettings.Port; // TODO: setup new (default) port config
- this.MasterServerAddress = string.Format("{0}:{1}", appSettings.Server, portToUse);
- if (!this.LoadBalancingPeer.Connect(this.MasterServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
- {
- return false;
- }
- this.State = ClientState.ConnectingToMasterServer;
- }
- return true;
- }
- [Obsolete("Use ConnectToMasterServer() instead.")]
- public bool Connect()
- {
- return this.ConnectToMasterServer();
- }
- /// <summary>
- /// Starts the "process" to connect to a Master Server, using MasterServerAddress and AppId properties.
- /// </summary>
- /// <remarks>
- /// To connect to the Photon Cloud, use ConnectUsingSettings() or ConnectToRegionMaster().
- ///
- /// The process to connect includes several steps: the actual connecting, establishing encryption, authentification
- /// (of app and optionally the user) and connecting to the MasterServer
- ///
- /// Users can connect either anonymously or use "Custom Authentication" to verify each individual player's login.
- /// Custom Authentication in Photon uses external services and communities to verify users. While the client provides a user's info,
- /// the service setup is done in the Photon Cloud Dashboard.
- /// The parameter authValues will set this.AuthValues and use them in the connect process.
- ///
- /// Connecting to the Photon Cloud might fail due to:
- /// - Network issues (OnStatusChanged() StatusCode.ExceptionOnConnect)
- /// - Region not available (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.InvalidRegion)
- /// - Subscription CCU limit reached (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.MaxCcuReached)
- /// </remarks>
- public virtual bool ConnectToMasterServer()
- {
- if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
- {
- this.DebugReturn(DebugLevel.WARNING, "ConnectToMasterServer() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
- return false;
- }
- // when using authMode AuthOnce or AuthOnceWSS, the token must be available for the init request. if it's null in that case, don't connect
- if (this.AuthMode != AuthModeOption.Auth && this.TokenForInit == null)
- {
- this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect to MasterServer with Token == null in AuthMode: " + this.AuthMode);
- return false;
- }
- this.CheckConnectSetupWebGl();
- this.DisconnectedCause = DisconnectCause.None;
- this.SystemConnectionSummary = null;
- if (this.LoadBalancingPeer.Connect(this.MasterServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
- {
- this.connectToBestRegion = false;
- this.State = ClientState.ConnectingToMasterServer;
- this.Server = ServerConnection.MasterServer;
- return true;
- }
- return false;
- }
- /// <summary>
- /// Connects to the NameServer for Photon Cloud, where a region and server list can be obtained.
- /// </summary>
- /// <see cref="OpGetRegions"/>
- /// <returns>If the workflow was started or failed right away.</returns>
- public bool ConnectToNameServer()
- {
- if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
- {
- this.DebugReturn(DebugLevel.WARNING, "ConnectToNameServer() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
- return false;
- }
- this.IsUsingNameServer = true;
- this.CloudRegion = null;
- this.CheckConnectSetupWebGl();
- if (this.AuthMode == AuthModeOption.AuthOnceWss)
- {
- if (this.ExpectedProtocol == null)
- {
- this.ExpectedProtocol = this.LoadBalancingPeer.TransportProtocol;
- }
- this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
- }
- this.DisconnectedCause = DisconnectCause.None;
- this.SystemConnectionSummary = null;
- if (this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, "NameServer", this.TokenForInit))
- {
- this.connectToBestRegion = false;
- this.State = ClientState.ConnectingToNameServer;
- this.Server = ServerConnection.NameServer;
- return true;
- }
- return false;
- }
- /// <summary>
- /// Connects you to a specific region's Master Server, using the Name Server to get the IP.
- /// </summary>
- /// <remarks>
- /// If the region is null or empty, no connection will be made.
- /// If the region (code) provided is not available, the connection process will fail on the Name Server.
- /// This method connects only to the region defined. Any "Best Region" pinging should get done beforehand.
- ///
- /// To support "sharding", a region string may contain a "/*" to pick a random cluster or "/clustername"
- /// to connect to a specific cluster.
- /// With a "/" or no cluster postfix, the client connects to the default cluster (a specific one
- /// for a region).
- ///
- /// By default, the region string provided by the Name Server does not contain a cluster (and only the
- /// default cluster is used).
- /// </remarks>
- /// <returns>If the operation could be sent. If false, no operation was sent.</returns>
- public bool ConnectToRegionMaster(string region)
- {
- if (string.IsNullOrEmpty(region))
- {
- this.DebugReturn(DebugLevel.ERROR, "ConnectToRegionMaster() failed. The region can not be null or empty.");
- return false;
- }
- this.IsUsingNameServer = true;
- if (this.State == ClientState.Authenticating)
- {
- if (this.LoadBalancingPeer.DebugOut >= DebugLevel.INFO)
- {
- this.DebugReturn(DebugLevel.INFO, "ConnectToRegionMaster() will skip calling authenticate, as the current state is 'Authenticating'. Just wait for the result.");
- }
- return true;
- }
- if (this.State == ClientState.ConnectedToNameServer)
- {
- this.CloudRegion = region;
- bool authenticating = this.CallAuthenticate();
- if (authenticating)
- {
- this.State = ClientState.Authenticating;
- }
- return authenticating;
- }
- this.LoadBalancingPeer.Disconnect();
- //if (!string.IsNullOrEmpty(region) && !region.Contains("/"))
- //{
- // region = region + "/*";
- //}
- this.CloudRegion = region;
- this.CheckConnectSetupWebGl();
- if (this.AuthMode == AuthModeOption.AuthOnceWss)
- {
- if (this.ExpectedProtocol == null)
- {
- this.ExpectedProtocol = this.LoadBalancingPeer.TransportProtocol;
- }
- this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
- }
- this.connectToBestRegion = false;
- this.DisconnectedCause = DisconnectCause.None;
- this.SystemConnectionSummary = null;
- if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, "NameServer", null))
- {
- return false;
- }
- this.State = ClientState.ConnectingToNameServer;
- this.Server = ServerConnection.NameServer;
- return true;
- }
- [Conditional("UNITY_WEBGL")]
- private void CheckConnectSetupWebGl()
- {
- #if UNITY_WEBGL
- if (this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocket && this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure)
- {
- this.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure.");
- this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
- }
- this.EnableProtocolFallback = false; // no fallback on WebGL
- #endif
- }
- /// <summary>
- /// Privately used only for reconnecting.
- /// </summary>
- private bool Connect(string serverAddress, string proxyServerAddress, ServerConnection serverType)
- {
- // TODO: Make sure app doesn't quit right now
- if (this.State == ClientState.Disconnecting)
- {
- this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect while disconnecting (still). Current state: " + this.State);
- return false;
- }
- // when using authMode AuthOnce or AuthOnceWSS, the token must be available for the init request. if it's null in that case, don't connect
- if (this.AuthMode != AuthModeOption.Auth && serverType != ServerConnection.NameServer && this.TokenForInit == null)
- {
- this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect to " + serverType + " with Token == null in AuthMode: " + this.AuthMode);
- return false;
- }
- this.DisconnectedCause = DisconnectCause.None;
- this.SystemConnectionSummary = null;
- // connect might fail, if the DNS name can't be resolved or if no network connection is available, etc.
- bool connecting = this.LoadBalancingPeer.Connect(serverAddress, proxyServerAddress, this.AppId, this.TokenForInit);
- if (connecting)
- {
- this.Server = serverType;
- switch (serverType)
- {
- case ServerConnection.NameServer:
- State = ClientState.ConnectingToNameServer;
- break;
- case ServerConnection.MasterServer:
- State = ClientState.ConnectingToMasterServer;
- break;
- case ServerConnection.GameServer:
- State = ClientState.ConnectingToGameServer;
- break;
- }
- }
- return connecting;
- }
- /// <summary>Can be used to reconnect to the master server after a disconnect.</summary>
- /// <remarks>Common use case: Press the Lock Button on a iOS device and you get disconnected immediately.</remarks>
- public bool ReconnectToMaster()
- {
- if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
- {
- this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
- return false;
- }
- if (string.IsNullOrEmpty(this.MasterServerAddress))
- {
- this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() failed. MasterServerAddress is null or empty.");
- return false;
- }
- if (this.tokenCache == null)
- {
- this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() failed. It seems the client doesn't have any previous authentication token to re-connect.");
- return false;
- }
- if (this.AuthValues == null)
- {
- this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() with AuthValues == null is not correct!");
- this.AuthValues = new AuthenticationValues();
- }
- this.AuthValues.Token = this.tokenCache;
- return this.Connect(this.MasterServerAddress, this.ProxyServerAddress, ServerConnection.MasterServer);
- }
- /// <summary>
- /// Can be used to return to a room quickly by directly reconnecting to a game server to rejoin a room.
- /// </summary>
- /// <remarks>
- /// Rejoining room will not send any player properties. Instead client will receive up-to-date ones from server.
- /// If you want to set new player properties, do it once rejoined.
- /// </remarks>
- /// <returns>False, if the conditions are not met. Then, this client does not attempt the ReconnectAndRejoin.</returns>
- public bool ReconnectAndRejoin()
- {
- if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
- {
- this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
- return false;
- }
- if (string.IsNullOrEmpty(this.GameServerAddress))
- {
- this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. It seems the client wasn't connected to a game server before (no address).");
- return false;
- }
- if (this.enterRoomParamsCache == null)
- {
- this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. It seems the client doesn't have any previous room to re-join.");
- return false;
- }
- if (this.tokenCache == null)
- {
- this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. It seems the client doesn't have any previous authentication token to re-connect.");
- return false;
- }
- if (this.AuthValues == null)
- {
- this.AuthValues = new AuthenticationValues();
- }
- this.AuthValues.Token = this.tokenCache;
- if (!string.IsNullOrEmpty(this.GameServerAddress) && this.enterRoomParamsCache != null)
- {
- this.lastJoinType = JoinType.JoinRoom;
- this.enterRoomParamsCache.JoinMode = JoinMode.RejoinOnly;
- return this.Connect(this.GameServerAddress, this.ProxyServerAddress, ServerConnection.GameServer);
- }
- return false;
- }
- /// <summary>Disconnects the peer from a server or stays disconnected. If the client / peer was connected, a callback will be triggered.</summary>
- /// <remarks>
- /// Disconnect will attempt to notify the server of the client closing the connection.
- ///
- /// Clients that are in a room, will leave the room. If the room's playerTTL > 0, the player will just become inactive (and may rejoin).
- ///
- /// This method will not change the current State, if this client State is PeerCreated, Disconnecting or Disconnected.
- /// In those cases, there is also no callback for the disconnect. The DisconnectedCause will only change if the client was connected.
- /// </remarks>
- public void Disconnect(DisconnectCause cause = DisconnectCause.DisconnectByClientLogic)
- {
- if (this.State == ClientState.Disconnecting || this.State == ClientState.PeerCreated)
- {
- this.DebugReturn(DebugLevel.INFO, "Disconnect() call gets skipped due to State " + this.State + ". DisconnectedCause: " + this.DisconnectedCause + " Parameter cause: " + cause);
- return;
- }
- if (this.State != ClientState.Disconnected)
- {
- this.State = ClientState.Disconnecting;
- this.DisconnectedCause = cause;
- this.LoadBalancingPeer.Disconnect();
- }
- }
- /// <summary>
- /// Private Disconnect variant that sets the state, too.
- /// </summary>
- private void DisconnectToReconnect()
- {
- switch (this.Server)
- {
- case ServerConnection.NameServer:
- this.State = ClientState.DisconnectingFromNameServer;
- break;
- case ServerConnection.MasterServer:
- this.State = ClientState.DisconnectingFromMasterServer;
- break;
- case ServerConnection.GameServer:
- this.State = ClientState.DisconnectingFromGameServer;
- break;
- }
- this.LoadBalancingPeer.Disconnect();
- }
- /// <summary>
- /// Useful to test loss of connection which will end in a client timeout. This modifies LoadBalancingPeer.NetworkSimulationSettings. Read remarks.
- /// </summary>
- /// <remarks>
- /// Use with care as this sets LoadBalancingPeer.IsSimulationEnabled.<br/>
- /// Read LoadBalancingPeer.IsSimulationEnabled to check if this is on or off, if needed.<br/>
- ///
- /// If simulateTimeout is true, LoadBalancingPeer.NetworkSimulationSettings.IncomingLossPercentage and
- /// LoadBalancingPeer.NetworkSimulationSettings.OutgoingLossPercentage will be set to 100.<br/>
- /// Obviously, this overrides any network simulation settings done before.<br/>
- ///
- /// If you want fine-grained network simulation control, use the NetworkSimulationSettings.<br/>
- ///
- /// The timeout will lead to a call to <see cref="IConnectionCallbacks.OnDisconnected"/>, as usual in a client timeout.
- ///
- /// You could modify this method (or use NetworkSimulationSettings) to deliberately run into a server timeout by
- /// just setting the OutgoingLossPercentage = 100 and the IncomingLossPercentage = 0.
- /// </remarks>
- /// <param name="simulateTimeout">If true, a connection loss is simulated. If false, the simulation ends.</param>
- public void SimulateConnectionLoss(bool simulateTimeout)
- {
- this.DebugReturn(DebugLevel.WARNING, "SimulateConnectionLoss() set to: "+simulateTimeout);
- if (simulateTimeout)
- {
- this.LoadBalancingPeer.NetworkSimulationSettings.IncomingLossPercentage = 100;
- this.LoadBalancingPeer.NetworkSimulationSettings.OutgoingLossPercentage = 100;
- }
- this.LoadBalancingPeer.IsSimulationEnabled = simulateTimeout;
- }
- private bool CallAuthenticate()
- {
- if (this.IsUsingNameServer && this.Server != ServerConnection.NameServer && (this.AuthValues == null || this.AuthValues.Token == null))
- {
- this.DebugReturn(DebugLevel.ERROR, "Authenticate without Token is only allowed on Name Server. Connecting to: " + this.Server + " on: " + this.CurrentServerAddress + ". State: " + this.State);
- return false;
- }
- if (this.AuthMode == AuthModeOption.Auth)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.Authenticate, this.Server, "Authenticate"))
- {
- return false;
- }
- return this.LoadBalancingPeer.OpAuthenticate(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, (this.EnableLobbyStatistics && this.Server == ServerConnection.MasterServer));
- }
- else
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.AuthenticateOnce, this.Server, "AuthenticateOnce"))
- {
- return false;
- }
- ConnectionProtocol targetProtocolPastNameServer = this.ExpectedProtocol != null ? (ConnectionProtocol) this.ExpectedProtocol : this.LoadBalancingPeer.TransportProtocol;
- return this.LoadBalancingPeer.OpAuthenticateOnce(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, this.EncryptionMode, targetProtocolPastNameServer);
- }
- }
- /// <summary>
- /// This method dispatches all available incoming commands and then sends this client's outgoing commands.
- /// It uses DispatchIncomingCommands and SendOutgoingCommands to do that.
- /// </summary>
- /// <remarks>
- /// The Photon client libraries are designed to fit easily into a game or application. The application
- /// is in control of the context (thread) in which incoming events and responses are executed and has
- /// full control of the creation of UDP/TCP packages.
- ///
- /// Sending packages and dispatching received messages are two separate tasks. Service combines them
- /// into one method at the cost of control. It calls DispatchIncomingCommands and SendOutgoingCommands.
- ///
- /// Call this method regularly (10..50 times a second).
- ///
- /// This will Dispatch ANY received commands (unless a reliable command in-order is still missing) and
- /// events AND will send queued outgoing commands. Fewer calls might be more effective if a device
- /// cannot send many packets per second, as multiple operations might be combined into one package.
- /// </remarks>
- /// <example>
- /// You could replace Service by:
- ///
- /// while (DispatchIncomingCommands()); //Dispatch until everything is Dispatched...
- /// SendOutgoingCommands(); //Send a UDP/TCP package with outgoing messages
- /// </example>
- /// <seealso cref="PhotonPeer.DispatchIncomingCommands"/>
- /// <seealso cref="PhotonPeer.SendOutgoingCommands"/>
- public void Service()
- {
- if (this.LoadBalancingPeer != null)
- {
- this.LoadBalancingPeer.Service();
- }
- }
- /// <summary>
- /// While on the NameServer, this gets you the list of regional servers (short names and their IPs to ping them).
- /// </summary>
- /// <returns>If the operation could be sent. If false, no operation was sent (e.g. while not connected to the NameServer).</returns>
- private bool OpGetRegions()
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.GetRegions, this.Server, "GetRegions"))
- {
- return false;
- }
- bool sent = this.LoadBalancingPeer.OpGetRegions(this.AppId);
- return sent;
- }
- /// <summary>
- /// Request the rooms and online status for a list of friends. All clients should set a unique UserId before connecting. The result is available in this.FriendList.
- /// </summary>
- /// <remarks>
- /// Used on Master Server to find the rooms played by a selected list of users.
- /// The result will be stored in LoadBalancingClient.FriendList, which is null before the first server response.
- ///
- /// Users identify themselves by setting a UserId in the LoadBalancingClient instance.
- /// This will send the ID in OpAuthenticate during connect (to master and game servers).
- /// Note: Changing a player's name doesn't make sense when using a friend list.
- ///
- /// The list of usernames must be fetched from some other source (not provided by Photon).
- ///
- ///
- /// Internal:<br/>
- /// The server response includes 2 arrays of info (each index matching a friend from the request):<br/>
- /// ParameterCode.FindFriendsResponseOnlineList = bool[] of online states<br/>
- /// ParameterCode.FindFriendsResponseRoomIdList = string[] of room names (empty string if not in a room)<br/>
- /// <br/>
- /// The options may be used to define which state a room must match to be returned.
- /// </remarks>
- /// <param name="friendsToFind">Array of friend's names (make sure they are unique).</param>
- /// <param name="options">Options that affect the result of the FindFriends operation.</param>
- /// <returns>If the operation could be sent (requires connection).</returns>
- public bool OpFindFriends(string[] friendsToFind, FindFriendsOptions options = null)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.FindFriends, this.Server, "FindFriends"))
- {
- return false;
- }
- if (this.IsFetchingFriendList)
- {
- this.DebugReturn(DebugLevel.WARNING, "OpFindFriends skipped: already fetching friends list.");
- return false; // fetching friends currently, so don't do it again (avoid changing the list while fetching friends)
- }
- if (friendsToFind == null || friendsToFind.Length == 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpFindFriends skipped: friendsToFind array is null or empty.");
- return false;
- }
- if (friendsToFind.Length > FriendRequestListMax)
- {
- this.DebugReturn(DebugLevel.ERROR, string.Format("OpFindFriends skipped: friendsToFind array exceeds allowed length of {0}.", FriendRequestListMax));
- return false;
- }
- List<string> friendsList = new List<string>(friendsToFind.Length);
- for (int i = 0; i < friendsToFind.Length; i++)
- {
- string friendUserId = friendsToFind[i];
- if (string.IsNullOrEmpty(friendUserId))
- {
- this.DebugReturn(DebugLevel.WARNING,
- string.Format(
- "friendsToFind array contains a null or empty UserId, element at position {0} skipped.",
- i));
- }
- else if (friendUserId.Equals(UserId))
- {
- this.DebugReturn(DebugLevel.WARNING,
- string.Format(
- "friendsToFind array contains local player's UserId \"{0}\", element at position {1} skipped.",
- friendUserId,
- i));
- }
- else if (friendsList.Contains(friendUserId))
- {
- this.DebugReturn(DebugLevel.WARNING,
- string.Format(
- "friendsToFind array contains duplicate UserId \"{0}\", element at position {1} skipped.",
- friendUserId,
- i));
- }
- else
- {
- friendsList.Add(friendUserId);
- }
- }
- if (friendsList.Count == 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpFindFriends skipped: friends list to find is empty.");
- return false;
- }
- string[] filteredArray = friendsList.ToArray();
- bool sent = this.LoadBalancingPeer.OpFindFriends(filteredArray, options);
- this.friendListRequested = sent ? filteredArray : null;
- return sent;
- }
- /// <summary>If already connected to a Master Server, this joins the specified lobby. This request triggers an OnOperationResponse() call and the callback OnJoinedLobby().</summary>
- /// <param name="lobby">The lobby to join. Use null for default lobby.</param>
- /// <returns>If the operation could be sent. False, if the client is not IsConnectedAndReady or when it's not connected to a Master Server.</returns>
- public bool OpJoinLobby(TypedLobby lobby)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.JoinLobby, this.Server, "JoinLobby"))
- {
- return false;
- }
- if (lobby == null)
- {
- lobby = TypedLobby.Default;
- }
- bool sent = this.LoadBalancingPeer.OpJoinLobby(lobby);
- if (sent)
- {
- this.CurrentLobby = lobby;
- this.State = ClientState.JoiningLobby;
- }
- return sent;
- }
- /// <summary>Opposite of joining a lobby. You don't have to explicitly leave a lobby to join another (client can be in one max, at any time).</summary>
- /// <returns>If the operation could be sent (has to be connected).</returns>
- public bool OpLeaveLobby()
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.LeaveLobby, this.Server, "LeaveLobby"))
- {
- return false;
- }
- return this.LoadBalancingPeer.OpLeaveLobby();
- }
- /// <summary>
- /// Joins a random room that matches the filter. Will callback: OnJoinedRoom or OnJoinRandomFailed.
- /// </summary>
- /// <remarks>
- /// Used for random matchmaking. You can join any room or one with specific properties defined in opJoinRandomRoomParams.
- ///
- /// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms.
- /// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties.
- /// You need to define which Custom Room Properties will be available for matchmaking when you create a room.
- /// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby)
- ///
- /// This operation fails if no rooms are fitting or available (all full, closed or not visible).
- /// It may also fail when actually joining the room which was found. Rooms may close, become full or empty anytime.
- ///
- /// This method can only be called while the client is connected to a Master Server so you should
- /// implement the callback OnConnectedToMaster.
- /// Check the return value to make sure the operation will be called on the server.
- /// Note: There will be no callbacks if this method returned false.
- ///
- ///
- /// This client's State is set to ClientState.Joining immediately, when the operation could
- /// be called. In the background, the client will switch servers and call various related operations.
- ///
- /// When you're in the room, this client's State will become ClientState.Joined.
- ///
- ///
- /// When entering a room, this client's Player Custom Properties will be sent to the room.
- /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
- /// Note that the player properties will be cached locally and are not wiped when leaving a room.
- ///
- /// More about matchmaking:
- /// https://doc.photonengine.com/en-us/realtime/current/reference/matchmaking-and-lobby
- ///
- /// You can define an array of expectedUsers, to block player slots in the room for these users.
- /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
- /// </remarks>
- /// <param name="opJoinRandomRoomParams">Optional definition of properties to filter rooms in random matchmaking.</param>
- /// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
- public bool OpJoinRandomRoom(OpJoinRandomRoomParams opJoinRandomRoomParams = null)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.JoinRandomGame, this.Server, "JoinRandomGame"))
- {
- return false;
- }
- if (opJoinRandomRoomParams == null)
- {
- opJoinRandomRoomParams = new OpJoinRandomRoomParams();
- }
- this.enterRoomParamsCache = new EnterRoomParams();
- this.enterRoomParamsCache.Lobby = opJoinRandomRoomParams.TypedLobby;
- this.enterRoomParamsCache.ExpectedUsers = opJoinRandomRoomParams.ExpectedUsers;
- bool sending = this.LoadBalancingPeer.OpJoinRandomRoom(opJoinRandomRoomParams);
- if (sending)
- {
- this.lastJoinType = JoinType.JoinRandomRoom;
- this.State = ClientState.Joining;
- }
- return sending;
- }
- /// <summary>
- /// Attempts to join a room that matches the specified filter and creates a room if none found.
- /// </summary>
- /// <remarks>
- /// This operation is a combination of filter-based random matchmaking with the option to create a new room,
- /// if no fitting room exists.
- /// The benefit of that is that the room creation is done by the same operation and the room can be found
- /// by the very next client, looking for similar rooms.
- ///
- /// There are separate parameters for joining and creating a room.
- ///
- /// This method can only be called while connected to a Master Server.
- /// This client's State is set to ClientState.Joining immediately.
- ///
- /// Either IMatchmakingCallbacks.OnJoinedRoom or IMatchmakingCallbacks.OnCreatedRoom get called.
- ///
- /// More about matchmaking:
- /// https://doc.photonengine.com/en-us/realtime/current/reference/matchmaking-and-lobby
- ///
- /// Check the return value to make sure the operation will be called on the server.
- /// Note: There will be no callbacks if this method returned false.
- /// </remarks>
- /// <returns>If the operation will be sent (requires connection to Master Server).</returns>
- public bool OpJoinRandomOrCreateRoom(OpJoinRandomRoomParams opJoinRandomRoomParams, EnterRoomParams createRoomParams)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.JoinRandomGame, this.Server, "OpJoinRandomOrCreateRoom"))
- {
- return false;
- }
- if (opJoinRandomRoomParams == null)
- {
- opJoinRandomRoomParams = new OpJoinRandomRoomParams();
- }
- if (createRoomParams == null)
- {
- createRoomParams = new EnterRoomParams();
- }
- createRoomParams.JoinMode = JoinMode.CreateIfNotExists;
- this.enterRoomParamsCache = createRoomParams;
- this.enterRoomParamsCache.Lobby = opJoinRandomRoomParams.TypedLobby;
- this.enterRoomParamsCache.ExpectedUsers = opJoinRandomRoomParams.ExpectedUsers;
- bool sending = this.LoadBalancingPeer.OpJoinRandomOrCreateRoom(opJoinRandomRoomParams, createRoomParams);
- if (sending)
- {
- this.lastJoinType = JoinType.JoinRandomOrCreateRoom;
- this.State = ClientState.Joining;
- }
- return sending;
- }
- /// <summary>
- /// Creates a new room. Will callback: OnCreatedRoom and OnJoinedRoom or OnCreateRoomFailed.
- /// </summary>
- /// <remarks>
- /// When successful, the client will enter the specified room and callback both OnCreatedRoom and OnJoinedRoom.
- /// In all error cases, OnCreateRoomFailed gets called.
- ///
- /// Creating a room will fail if the room name is already in use or when the RoomOptions clashing
- /// with one another. Check the EnterRoomParams reference for the various room creation options.
- ///
- ///
- /// This method can only be called while the client is connected to a Master Server so you should
- /// implement the callback OnConnectedToMaster.
- /// Check the return value to make sure the operation will be called on the server.
- /// Note: There will be no callbacks if this method returned false.
- ///
- ///
- /// When you're in the room, this client's State will become ClientState.Joined.
- ///
- ///
- /// When entering a room, this client's Player Custom Properties will be sent to the room.
- /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
- /// Note that the player properties will be cached locally and are not wiped when leaving a room.
- ///
- /// You can define an array of expectedUsers, to block player slots in the room for these users.
- /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
- /// </remarks>
- /// <param name="enterRoomParams">Definition of properties for the room to create.</param>
- /// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
- public bool OpCreateRoom(EnterRoomParams enterRoomParams)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.CreateGame, this.Server, "CreateGame"))
- {
- return false;
- }
- bool onGameServer = this.Server == ServerConnection.GameServer;
- enterRoomParams.OnGameServer = onGameServer;
- if (!onGameServer)
- {
- this.enterRoomParamsCache = enterRoomParams;
- }
- bool sending = this.LoadBalancingPeer.OpCreateRoom(enterRoomParams);
- if (sending)
- {
- this.lastJoinType = JoinType.CreateRoom;
- this.State = ClientState.Joining;
- }
- return sending;
- }
- /// <summary>
- /// Joins a specific room by name and creates it on demand. Will callback: OnJoinedRoom or OnJoinRoomFailed.
- /// </summary>
- /// <remarks>
- /// Useful when players make up a room name to meet in:
- /// All involved clients call the same method and whoever is first, also creates the room.
- ///
- /// When successful, the client will enter the specified room.
- /// The client which creates the room, will callback both OnCreatedRoom and OnJoinedRoom.
- /// Clients that join an existing room will only callback OnJoinedRoom.
- /// In all error cases, OnJoinRoomFailed gets called.
- ///
- /// Joining a room will fail, if the room is full, closed or when the user
- /// already is present in the room (checked by userId).
- ///
- /// To return to a room, use OpRejoinRoom.
- ///
- /// This method can only be called while the client is connected to a Master Server so you should
- /// implement the callback OnConnectedToMaster.
- /// Check the return value to make sure the operation will be called on the server.
- /// Note: There will be no callbacks if this method returned false.
- ///
- /// This client's State is set to ClientState.Joining immediately, when the operation could
- /// be called. In the background, the client will switch servers and call various related operations.
- ///
- /// When you're in the room, this client's State will become ClientState.Joined.
- ///
- ///
- /// If you set room properties in roomOptions, they get ignored when the room is existing already.
- /// This avoids changing the room properties by late joining players.
- ///
- /// When entering a room, this client's Player Custom Properties will be sent to the room.
- /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
- /// Note that the player properties will be cached locally and are not wiped when leaving a room.
- ///
- /// You can define an array of expectedUsers, to block player slots in the room for these users.
- /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
- /// </remarks>
- /// <param name="enterRoomParams">Definition of properties for the room to create or join.</param>
- /// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
- public bool OpJoinOrCreateRoom(EnterRoomParams enterRoomParams)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.JoinGame, this.Server, "JoinOrCreateRoom"))
- {
- return false;
- }
- bool onGameServer = this.Server == ServerConnection.GameServer;
- enterRoomParams.JoinMode = JoinMode.CreateIfNotExists;
- enterRoomParams.OnGameServer = onGameServer;
- if (!onGameServer)
- {
- this.enterRoomParamsCache = enterRoomParams;
- }
- bool sending = this.LoadBalancingPeer.OpJoinRoom(enterRoomParams);
- if (sending)
- {
- this.lastJoinType = JoinType.JoinOrCreateRoom;
- this.State = ClientState.Joining;
- }
- return sending;
- }
- /// <summary>
- /// Joins a room by name. Will callback: OnJoinedRoom or OnJoinRoomFailed.
- /// </summary>
- /// <remarks>
- /// Useful when using lobbies or when players follow friends or invite each other.
- ///
- /// When successful, the client will enter the specified room and callback via OnJoinedRoom.
- /// In all error cases, OnJoinRoomFailed gets called.
- ///
- /// Joining a room will fail if the room is full, closed, not existing or when the user
- /// already is present in the room (checked by userId).
- ///
- /// To return to a room, use OpRejoinRoom.
- /// When players invite each other and it's unclear who's first to respond, use OpJoinOrCreateRoom instead.
- ///
- /// This method can only be called while the client is connected to a Master Server so you should
- /// implement the callback OnConnectedToMaster.
- /// Check the return value to make sure the operation will be called on the server.
- /// Note: There will be no callbacks if this method returned false.
- ///
- /// A room's name has to be unique (per region, appid and gameversion).
- /// When your title uses a global matchmaking or invitations (e.g. an external solution),
- /// keep regions and the game versions in mind to join a room.
- ///
- ///
- /// This client's State is set to ClientState.Joining immediately, when the operation could
- /// be called. In the background, the client will switch servers and call various related operations.
- ///
- /// When you're in the room, this client's State will become ClientState.Joined.
- ///
- ///
- /// When entering a room, this client's Player Custom Properties will be sent to the room.
- /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
- /// Note that the player properties will be cached locally and are not wiped when leaving a room.
- ///
- /// You can define an array of expectedUsers, to reserve player slots in the room for friends or party members.
- /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
- /// </remarks>
- /// <param name="enterRoomParams">Definition of properties for the room to join.</param>
- /// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
- public bool OpJoinRoom(EnterRoomParams enterRoomParams)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.JoinGame, this.Server, "JoinRoom"))
- {
- return false;
- }
- bool onGameServer = this.Server == ServerConnection.GameServer;
- enterRoomParams.OnGameServer = onGameServer;
- if (!onGameServer)
- {
- this.enterRoomParamsCache = enterRoomParams;
- }
- bool sending = this.LoadBalancingPeer.OpJoinRoom(enterRoomParams);
- if (sending)
- {
- this.lastJoinType = enterRoomParams.JoinMode == JoinMode.CreateIfNotExists ? JoinType.JoinOrCreateRoom : JoinType.JoinRoom;
- this.State = ClientState.Joining;
- }
- return sending;
- }
- /// <summary>
- /// Rejoins a room by roomName (using the userID internally to return). Will callback: OnJoinedRoom or OnJoinRoomFailed.
- /// </summary>
- /// <remarks>
- /// Used to return to a room, before this user was removed from the players list.
- /// Internally, the userID will be checked by the server, to make sure this user is in the room (active or inactice).
- ///
- /// In contrast to join, this operation never adds a players to a room. It will attempt to retake an existing
- /// spot in the playerlist or fail. This makes sure the client doesn't accidentally join a room when the
- /// game logic meant to re-activate an existing actor in an existing room.
- ///
- /// This method will fail on the server, when the room does not exist, can't be loaded (persistent rooms) or
- /// when the userId is not in the player list of this room. This will lead to a callback OnJoinRoomFailed.
- ///
- /// Rejoining room will not send any player properties. Instead client will receive up-to-date ones from server.
- /// If you want to set new player properties, do it once rejoined.
- /// </remarks>
- public bool OpRejoinRoom(string roomName)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.JoinGame, this.Server, "RejoinRoom"))
- {
- return false;
- }
- bool onGameServer = this.Server == ServerConnection.GameServer;
- EnterRoomParams opParams = new EnterRoomParams();
- this.enterRoomParamsCache = opParams;
- opParams.RoomName = roomName;
- opParams.OnGameServer = onGameServer;
- opParams.JoinMode = JoinMode.RejoinOnly;
- bool sending = this.LoadBalancingPeer.OpJoinRoom(opParams);
- if (sending)
- {
- this.lastJoinType = JoinType.JoinRoom;
- this.State = ClientState.Joining;
- }
- return sending;
- }
- /// <summary>
- /// Leaves the current room, optionally telling the server that the user is just becoming inactive. Will callback: OnLeftRoom.
- /// </summary>
- ///
- /// <remarks>
- /// OpLeaveRoom skips execution when the room is null or the server is not GameServer or the client is disconnecting from GS already.
- /// OpLeaveRoom returns false in those cases and won't change the state, so check return of this method.
- ///
- /// In some cases, this method will skip the OpLeave call and just call Disconnect(),
- /// which not only leaves the room but also the server. Disconnect also triggers a leave and so that workflow is is quicker.
- /// </remarks>
- /// <param name="becomeInactive">If true, this player becomes inactive in the game and can return later (if PlayerTTL of the room is != 0).</param>
- /// <param name="sendAuthCookie">WebFlag: Securely transmit the encrypted object AuthCookie to the web service in PathLeave webhook when available</param>
- /// <returns>If the current room could be left (impossible while not in a room).</returns>
- public bool OpLeaveRoom(bool becomeInactive, bool sendAuthCookie = false)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.Leave, this.Server, "LeaveRoom"))
- {
- return false;
- }
- this.State = ClientState.Leaving;
- this.GameServerAddress = String.Empty;
- this.enterRoomParamsCache = null;
- return this.LoadBalancingPeer.OpLeaveRoom(becomeInactive, sendAuthCookie);
- }
- /// <summary>Gets a list of rooms matching the (non empty) SQL filter for the given SQL-typed lobby.</summary>
- /// <remarks>
- /// Operation is only available for lobbies of type SqlLobby and the filter can not be empty.
- /// It will check those conditions and fail locally, returning false.
- ///
- /// This is an async request which triggers a OnOperationResponse() call.
- /// </remarks>
- /// <see cref="https://doc.photonengine.com/en-us/realtime/current/reference/matchmaking-and-lobby#sql_lobby_type"/>
- /// <param name="typedLobby">The lobby to query. Has to be of type SqlLobby.</param>
- /// <param name="sqlLobbyFilter">The sql query statement.</param>
- /// <returns>If the operation could be sent (has to be connected).</returns>
- public bool OpGetGameList(TypedLobby typedLobby, string sqlLobbyFilter)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.GetGameList, this.Server, "GetGameList"))
- {
- return false;
- }
- if (string.IsNullOrEmpty(sqlLobbyFilter))
- {
- this.DebugReturn(DebugLevel.ERROR, "Operation GetGameList requires a filter.");
- return false;
- }
- if (typedLobby.Type != LobbyType.SqlLobby)
- {
- this.DebugReturn(DebugLevel.ERROR, "Operation GetGameList can only be used for lobbies of type SqlLobby.");
- return false;
- }
- return this.LoadBalancingPeer.OpGetGameList(typedLobby, sqlLobbyFilter);
- }
- /// <summary>
- /// Updates and synchronizes a Player's Custom Properties. Optionally, expectedProperties can be provided as condition.
- /// </summary>
- /// <remarks>
- /// Custom Properties are a set of string keys and arbitrary values which is synchronized
- /// for the players in a Room. They are available when the client enters the room, as
- /// they are in the response of OpJoin and OpCreate.
- ///
- /// Custom Properties either relate to the (current) Room or a Player (in that Room).
- ///
- /// Both classes locally cache the current key/values and make them available as
- /// property: CustomProperties. This is provided only to read them.
- /// You must use the method SetCustomProperties to set/modify them.
- ///
- /// Any client can set any Custom Properties anytime (when in a room).
- /// It's up to the game logic to organize how they are best used.
- ///
- /// You should call SetCustomProperties only with key/values that are new or changed. This reduces
- /// traffic and performance.
- ///
- /// Unless you define some expectedProperties, setting key/values is always permitted.
- /// In this case, the property-setting client will not receive the new values from the server but
- /// instead update its local cache in SetCustomProperties.
- ///
- /// If you define expectedProperties, the server will skip updates if the server property-cache
- /// does not contain all expectedProperties with the same values.
- /// In this case, the property-setting client will get an update from the server and update it's
- /// cached key/values at about the same time as everyone else.
- ///
- /// The benefit of using expectedProperties can be only one client successfully sets a key from
- /// one known value to another.
- /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
- /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
- /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
- /// take the item will have it (and the others fail to set the ownership).
- ///
- /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
- /// </remarks>
- /// <param name="actorNr">Defines which player the Custom Properties belong to. ActorID of a player.</param>
- /// <param name="propertiesToSet">Hashtable of Custom Properties that changes.</param>
- /// <param name="expectedProperties">Provide some keys/values to use as condition for setting the new values. Client must be in room.</param>
- /// <param name="webFlags">Defines if the set properties should be forwarded to a WebHook. Client must be in room.</param>
- /// <returns>
- /// False if propertiesToSet is null or empty or have zero string keys.
- /// If not in a room, returns true if local player and expectedProperties and webFlags are null.
- /// False if actorNr is lower than or equal to zero.
- /// Otherwise, returns if the operation could be sent to the server.
- /// </returns>
- public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
- {
- if (propertiesToSet == null || propertiesToSet.Count == 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. propertiesToSet must not be null nor empty.");
- return false;
- }
- if (this.CurrentRoom == null)
- {
- // if you attempt to set this player's values without conditions, then fine:
- if (expectedProperties == null && webFlags == null && this.LocalPlayer != null && this.LocalPlayer.ActorNumber == actorNr)
- {
- return this.LocalPlayer.SetCustomProperties(propertiesToSet);
- }
- if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ERROR)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. To use expectedProperties or webForward, you have to be in a room. State: " + this.State);
- }
- return false;
- }
- Hashtable customActorProperties = new Hashtable();
- customActorProperties.MergeStringKeys(propertiesToSet);
- if (customActorProperties.Count == 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. Only string keys allowed for custom properties.");
- return false;
- }
- return this.OpSetPropertiesOfActor(actorNr, customActorProperties, expectedProperties, webFlags);
- }
- /// <summary>Internally used to cache and set properties (including well known properties).</summary>
- /// <remarks>Requires being in a room (because this attempts to send an operation which will fail otherwise).</remarks>
- protected internal bool OpSetPropertiesOfActor(int actorNr, Hashtable actorProperties, Hashtable expectedProperties = null, WebFlags webFlags = null)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.SetProperties, this.Server, "SetProperties"))
- {
- return false;
- }
- if (actorProperties == null || actorProperties.Count == 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfActor() failed. actorProperties must not be null nor empty.");
- return false;
- }
- bool res = this.LoadBalancingPeer.OpSetPropertiesOfActor(actorNr, actorProperties, expectedProperties, webFlags);
- if (res && !this.CurrentRoom.BroadcastPropertiesChangeToAll && (expectedProperties == null || expectedProperties.Count == 0))
- {
- Player target = this.CurrentRoom.GetPlayer(actorNr);
- if (target != null)
- {
- target.InternalCacheProperties(actorProperties);
- this.InRoomCallbackTargets.OnPlayerPropertiesUpdate(target, actorProperties);
- }
- }
- return res;
- }
- /// <summary>
- /// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition.
- /// </summary>
- /// <remarks>
- /// Custom Properties are a set of string keys and arbitrary values which is synchronized
- /// for the players in a Room. They are available when the client enters the room, as
- /// they are in the response of OpJoin and OpCreate.
- ///
- /// Custom Properties either relate to the (current) Room or a Player (in that Room).
- ///
- /// Both classes locally cache the current key/values and make them available as
- /// property: CustomProperties. This is provided only to read them.
- /// You must use the method SetCustomProperties to set/modify them.
- ///
- /// Any client can set any Custom Properties anytime (when in a room).
- /// It's up to the game logic to organize how they are best used.
- ///
- /// You should call SetCustomProperties only with key/values that are new or changed. This reduces
- /// traffic and performance.
- ///
- /// Unless you define some expectedProperties, setting key/values is always permitted.
- /// In this case, the property-setting client will not receive the new values from the server but
- /// instead update its local cache in SetCustomProperties.
- ///
- /// If you define expectedProperties, the server will skip updates if the server property-cache
- /// does not contain all expectedProperties with the same values.
- /// In this case, the property-setting client will get an update from the server and update it's
- /// cached key/values at about the same time as everyone else.
- ///
- /// The benefit of using expectedProperties can be only one client successfully sets a key from
- /// one known value to another.
- /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
- /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
- /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
- /// take the item will have it (and the others fail to set the ownership).
- ///
- /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
- /// </remarks>
- /// <param name="propertiesToSet">Hashtable of Custom Properties that changes.</param>
- /// <param name="expectedProperties">Provide some keys/values to use as condition for setting the new values.</param>
- /// <param name="webFlags">Defines web flags for an optional PathProperties webhook.</param>
- /// <returns>
- /// False if propertiesToSet is null or empty or have zero string keys.
- /// Otherwise, returns if the operation could be sent to the server.
- /// </returns>
- public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
- {
- if (propertiesToSet == null || propertiesToSet.Count == 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfRoom() failed. propertiesToSet must not be null nor empty.");
- return false;
- }
- Hashtable customGameProps = new Hashtable();
- customGameProps.MergeStringKeys(propertiesToSet);
- if (customGameProps.Count == 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfRoom() failed. Only string keys are allowed for custom properties.");
- return false;
- }
- return this.OpSetPropertiesOfRoom(customGameProps, expectedProperties, webFlags);
- }
- protected internal bool OpSetPropertyOfRoom(byte propCode, object value)
- {
- Hashtable properties = new Hashtable();
- properties[propCode] = value;
- return this.OpSetPropertiesOfRoom(properties);
- }
- /// <summary>Internally used to cache and set properties (including well known properties).</summary>
- /// <remarks>Requires being in a room (because this attempts to send an operation which will fail otherwise).</remarks>
- protected internal bool OpSetPropertiesOfRoom(Hashtable gameProperties, Hashtable expectedProperties = null, WebFlags webFlags = null)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.SetProperties, this.Server, "SetProperties"))
- {
- return false;
- }
- if (gameProperties == null || gameProperties.Count == 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfRoom() failed. gameProperties must not be null nor empty.");
- return false;
- }
- bool res = this.LoadBalancingPeer.OpSetPropertiesOfRoom(gameProperties, expectedProperties, webFlags);
- if (res && !this.CurrentRoom.BroadcastPropertiesChangeToAll && (expectedProperties == null || expectedProperties.Count == 0))
- {
- this.CurrentRoom.InternalCacheProperties(gameProperties);
- this.InRoomCallbackTargets.OnRoomPropertiesUpdate(gameProperties);
- }
- return res;
- }
- /// <summary>
- /// Send an event with custom code/type and any content to the other players in the same room.
- /// </summary>
- /// <param name="eventCode">Identifies this type of event (and the content). Your game's event codes can start with 0.</param>
- /// <param name="customEventContent">Any serializable datatype (including Hashtable like the other OpRaiseEvent overloads).</param>
- /// <param name="raiseEventOptions">Contains used send options. If you pass null, the default options will be used.</param>
- /// <param name="sendOptions">Send options for reliable, encryption etc</param>
- /// <returns>If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands.</returns>
- public virtual bool OpRaiseEvent(byte eventCode, object customEventContent, RaiseEventOptions raiseEventOptions, SendOptions sendOptions)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.RaiseEvent, this.Server, "RaiseEvent"))
- {
- return false;
- }
- return this.LoadBalancingPeer.OpRaiseEvent(eventCode, customEventContent, raiseEventOptions, sendOptions);
- }
- /// <summary>
- /// Operation to handle this client's interest groups (for events in room).
- /// </summary>
- /// <remarks>
- /// Note the difference between passing null and byte[0]:
- /// null won't add/remove any groups.
- /// byte[0] will add/remove all (existing) groups.
- /// First, removing groups is executed. This way, you could leave all groups and join only the ones provided.
- ///
- /// Changes become active not immediately but when the server executes this operation (approximately RTT/2).
- /// </remarks>
- /// <param name="groupsToRemove">Groups to remove from interest. Null will not remove any. A byte[0] will remove all.</param>
- /// <param name="groupsToAdd">Groups to add to interest. Null will not add any. A byte[0] will add all current.</param>
- /// <returns>If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands.</returns>
- public virtual bool OpChangeGroups(byte[] groupsToRemove, byte[] groupsToAdd)
- {
- if (!this.CheckIfOpCanBeSent(OperationCode.ChangeGroups, this.Server, "ChangeGroups"))
- {
- return false;
- }
- return this.LoadBalancingPeer.OpChangeGroups(groupsToRemove, groupsToAdd);
- }
- #endregion
- #region Helpers
- /// <summary>
- /// Privately used to read-out properties coming from the server in events and operation responses (which might be a bit tricky).
- /// </summary>
- private void ReadoutProperties(Hashtable gameProperties, Hashtable actorProperties, int targetActorNr)
- {
- // read game properties and cache them locally
- if (this.CurrentRoom != null && gameProperties != null)
- {
- this.CurrentRoom.InternalCacheProperties(gameProperties);
- if (this.InRoom)
- {
- this.InRoomCallbackTargets.OnRoomPropertiesUpdate(gameProperties);
- }
- }
- if (actorProperties != null && actorProperties.Count > 0)
- {
- if (targetActorNr > 0)
- {
- // we have a single entry in the actorProperties with one user's name
- // targets MUST exist before you set properties
- Player target = this.CurrentRoom.GetPlayer(targetActorNr);
- if (target != null)
- {
- Hashtable props = this.ReadoutPropertiesForActorNr(actorProperties, targetActorNr);
- target.InternalCacheProperties(props);
- this.InRoomCallbackTargets.OnPlayerPropertiesUpdate(target, props);
- }
- }
- else
- {
- // in this case, we've got a key-value pair per actor (each
- // value is a hashtable with the actor's properties then)
- int actorNr;
- Hashtable props;
- string newName;
- Player target;
- foreach (object key in actorProperties.Keys)
- {
- actorNr = (int)key;
- if (actorNr == 0)
- {
- continue;
- }
- props = (Hashtable)actorProperties[key];
- newName = (string)props[ActorProperties.PlayerName];
- target = this.CurrentRoom.GetPlayer(actorNr);
- if (target == null)
- {
- target = this.CreatePlayer(newName, actorNr, false, props);
- this.CurrentRoom.StorePlayer(target);
- }
- target.InternalCacheProperties(props);
- }
- }
- }
- }
- /// <summary>
- /// Privately used only to read properties for a distinct actor (which might be the hashtable OR a key-pair value IN the actorProperties).
- /// </summary>
- private Hashtable ReadoutPropertiesForActorNr(Hashtable actorProperties, int actorNr)
- {
- if (actorProperties.ContainsKey(actorNr))
- {
- return (Hashtable)actorProperties[actorNr];
- }
- return actorProperties;
- }
- /// <summary>
- /// Internally used to set the LocalPlayer's ID (from -1 to the actual in-room ID).
- /// </summary>
- /// <param name="newID">New actor ID (a.k.a actorNr) assigned when joining a room.</param>
- public void ChangeLocalID(int newID)
- {
- if (this.LocalPlayer == null)
- {
- this.DebugReturn(DebugLevel.WARNING, string.Format("Local actor is null or not in mActors! mLocalActor: {0} mActors==null: {1} newID: {2}", this.LocalPlayer, this.CurrentRoom.Players == null, newID));
- }
- if (this.CurrentRoom == null)
- {
- // change to new actor/player ID and make sure the player does not have a room reference left
- this.LocalPlayer.ChangeLocalID(newID);
- this.LocalPlayer.RoomReference = null;
- }
- else
- {
- // remove old actorId from actor list
- this.CurrentRoom.RemovePlayer(this.LocalPlayer);
- // change to new actor/player ID
- this.LocalPlayer.ChangeLocalID(newID);
- // update the room's list with the new reference
- this.CurrentRoom.StorePlayer(this.LocalPlayer);
- }
- }
- /// <summary>
- /// Called internally, when a game was joined or created on the game server successfully.
- /// </summary>
- /// <remarks>
- /// This reads the response, finds out the local player's actorNumber (a.k.a. Player.ID) and applies properties of the room and players.
- /// Errors for these operations are to be handled before this method is called.
- /// </remarks>
- /// <param name="operationResponse">Contains the server's response for an operation called by this peer.</param>
- private void GameEnteredOnGameServer(OperationResponse operationResponse)
- {
- this.CurrentRoom = this.CreateRoom(this.enterRoomParamsCache.RoomName, this.enterRoomParamsCache.RoomOptions);
- this.CurrentRoom.LoadBalancingClient = this;
- // first change the local id, instead of first updating the actorList since actorList uses ID to update itself
- // the local player's actor-properties are not returned in join-result. add this player to the list
- int localActorNr = (int)operationResponse[ParameterCode.ActorNr];
- this.ChangeLocalID(localActorNr);
- if (operationResponse.Parameters.ContainsKey(ParameterCode.ActorList))
- {
- int[] actorsInRoom = (int[])operationResponse.Parameters[ParameterCode.ActorList];
- this.UpdatedActorList(actorsInRoom);
- }
- Hashtable actorProperties = (Hashtable)operationResponse[ParameterCode.PlayerProperties];
- Hashtable gameProperties = (Hashtable)operationResponse[ParameterCode.GameProperties];
- this.ReadoutProperties(gameProperties, actorProperties, 0);
- object temp;
- if (operationResponse.Parameters.TryGetValue(ParameterCode.RoomOptionFlags, out temp))
- {
- this.CurrentRoom.InternalCacheRoomFlags((int)temp);
- }
- // the callbacks OnCreatedRoom and OnJoinedRoom are called in the event join. it contains important info about the room and players.
- // unless there will be no room events (RoomOptions.SuppressRoomEvents = true)
- if (this.CurrentRoom.SuppressRoomEvents)
- {
- // setting the state was moved into this condition in v4.1.6.26.
- // the state should be set before OnCreateRoom/OnJoinedRoom is called, which may be due to the event join.
- // if you need the old behavior, move the following line out of this block (below the block that could call InternalCacheRoomFlags().
- this.State = ClientState.Joined;
- if (this.lastJoinType == JoinType.CreateRoom || (this.lastJoinType == JoinType.JoinOrCreateRoom && this.LocalPlayer.ActorNumber == 1))
- {
- this.MatchMakingCallbackTargets.OnCreatedRoom();
- }
- this.MatchMakingCallbackTargets.OnJoinedRoom();
- }
- }
- private void UpdatedActorList(int[] actorsInGame)
- {
- if (actorsInGame != null)
- {
- foreach (int actorNumber in actorsInGame)
- {
- if (actorNumber == 0)
- {
- continue;
- }
- Player target = this.CurrentRoom.GetPlayer(actorNumber);
- if (target == null)
- {
- this.CurrentRoom.StorePlayer(this.CreatePlayer(string.Empty, actorNumber, false, null));
- }
- }
- }
- }
- /// <summary>
- /// Factory method to create a player instance - override to get your own player-type with custom features.
- /// </summary>
- /// <param name="actorName">The name of the player to be created. </param>
- /// <param name="actorNumber">The player ID (a.k.a. actorNumber) of the player to be created.</param>
- /// <param name="isLocal">Sets the distinction if the player to be created is your player or if its assigned to someone else.</param>
- /// <param name="actorProperties">The custom properties for this new player</param>
- /// <returns>The newly created player</returns>
- protected internal virtual Player CreatePlayer(string actorName, int actorNumber, bool isLocal, Hashtable actorProperties)
- {
- Player newPlayer = new Player(actorName, actorNumber, isLocal, actorProperties);
- return newPlayer;
- }
- /// <summary>Internal "factory" method to create a room-instance.</summary>
- protected internal virtual Room CreateRoom(string roomName, RoomOptions opt)
- {
- Room r = new Room(roomName, opt);
- return r;
- }
- private bool CheckIfOpAllowedOnServer(byte opCode, ServerConnection serverConnection)
- {
- switch (serverConnection)
- {
- case ServerConnection.MasterServer:
- switch (opCode)
- {
- case OperationCode.CreateGame:
- case OperationCode.Authenticate:
- case OperationCode.AuthenticateOnce:
- case OperationCode.FindFriends:
- case OperationCode.GetGameList:
- case OperationCode.GetLobbyStats:
- case OperationCode.JoinGame:
- case OperationCode.JoinLobby:
- case OperationCode.LeaveLobby:
- case OperationCode.WebRpc:
- case OperationCode.ServerSettings:
- case OperationCode.JoinRandomGame:
- return true;
- }
- break;
- case ServerConnection.GameServer:
- switch (opCode)
- {
- case OperationCode.CreateGame:
- case OperationCode.Authenticate:
- case OperationCode.AuthenticateOnce:
- case OperationCode.ChangeGroups:
- case OperationCode.GetProperties:
- case OperationCode.JoinGame:
- case OperationCode.Leave:
- case OperationCode.WebRpc:
- case OperationCode.ServerSettings:
- case OperationCode.SetProperties:
- case OperationCode.RaiseEvent:
- return true;
- }
- break;
- case ServerConnection.NameServer:
- switch (opCode)
- {
- case OperationCode.Authenticate:
- case OperationCode.AuthenticateOnce:
- case OperationCode.GetRegions:
- case OperationCode.ServerSettings:
- return true;
- }
- break;
- default:
- throw new ArgumentOutOfRangeException("serverConnection", serverConnection, null);
- }
- return false;
- }
- private bool CheckIfOpCanBeSent(byte opCode, ServerConnection serverConnection, string opName)
- {
- if (this.LoadBalancingPeer == null)
- {
- this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) can't be sent because peer is null", opName, opCode));
- return false;
- }
- if (!this.CheckIfOpAllowedOnServer(opCode, serverConnection))
- {
- if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ERROR)
- {
- this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) not allowed on current server ({2})", opName, opCode, serverConnection));
- }
- return false;
- }
- if (!this.CheckIfClientIsReadyToCallOperation(opCode))
- {
- DebugLevel levelToReport = DebugLevel.ERROR;
- if (opCode == OperationCode.RaiseEvent && (this.State == ClientState.Leaving || this.State == ClientState.Disconnecting || this.State == ClientState.DisconnectingFromGameServer))
- {
- levelToReport = DebugLevel.INFO;
- }
- if (this.LoadBalancingPeer.DebugOut >= levelToReport)
- {
- this.DebugReturn(levelToReport, string.Format("Operation {0} ({1}) not called because client is not connected or not ready yet, client state: {2}", opName, opCode, Enum.GetName(typeof(ClientState), this.State)));
- }
- return false;
- }
- if (this.LoadBalancingPeer.PeerState != PeerStateValue.Connected)
- {
- this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) can't be sent because peer is not connected, peer state: {2}", opName, opCode, this.LoadBalancingPeer.PeerState));
- return false;
- }
- return true;
- }
- private bool CheckIfClientIsReadyToCallOperation(byte opCode)
- {
- switch (opCode)
- {
- //case OperationCode.ServerSettings: // ??
- //case OperationCode.WebRpc: // WebRPC works on MS and GS and I think it does not need the client to be ready
- case OperationCode.Authenticate:
- case OperationCode.AuthenticateOnce:
- return this.IsConnectedAndReady ||
- this.State == ClientState.ConnectingToNameServer || // this is required since we do not set state to ConnectedToNameServer before authentication
- this.State == ClientState.ConnectingToMasterServer || // this is required since we do not set state to ConnectedToMasterServer before authentication
- this.State == ClientState.ConnectingToGameServer; // this is required since we do not set state to ConnectedToGameServer before authentication
- case OperationCode.ChangeGroups:
- case OperationCode.GetProperties:
- case OperationCode.SetProperties:
- case OperationCode.RaiseEvent:
- case OperationCode.Leave:
- return this.InRoom;
- case OperationCode.JoinGame:
- case OperationCode.CreateGame:
- return this.State == ClientState.ConnectedToMasterServer || this.InLobby || this.State == ClientState.ConnectedToGameServer; // CurrentRoom can be not null in case of quick rejoin
- case OperationCode.LeaveLobby:
- return this.InLobby;
- case OperationCode.JoinRandomGame:
- case OperationCode.FindFriends:
- case OperationCode.GetGameList:
- case OperationCode.GetLobbyStats: // do we need to be inside lobby to call this?
- case OperationCode.JoinLobby: // You don't have to explicitly leave a lobby to join another (client can be in one max, at any time)
- return this.State == ClientState.ConnectedToMasterServer || this.InLobby;
- case OperationCode.GetRegions:
- return this.State == ClientState.ConnectedToNameServer;
- }
- return this.IsConnected;
- }
- #endregion
- #region Implementation of IPhotonPeerListener
- /// <summary>Debug output of low level api (and this client).</summary>
- /// <remarks>This method is not responsible to keep up the state of a LoadBalancingClient. Calling base.DebugReturn on overrides is optional.</remarks>
- public virtual void DebugReturn(DebugLevel level, string message)
- {
- if (this.LoadBalancingPeer.DebugOut != DebugLevel.ALL && level > this.LoadBalancingPeer.DebugOut)
- {
- return;
- }
- #if !SUPPORTED_UNITY
- Debug.WriteLine(message);
- #else
- if (level == DebugLevel.ERROR)
- {
- Debug.LogError(message);
- }
- else if (level == DebugLevel.WARNING)
- {
- Debug.LogWarning(message);
- }
- else if (level == DebugLevel.INFO)
- {
- Debug.Log(message);
- }
- else if (level == DebugLevel.ALL)
- {
- Debug.Log(message);
- }
- #endif
- }
- private void CallbackRoomEnterFailed(OperationResponse operationResponse)
- {
- if (operationResponse.ReturnCode != 0)
- {
- if (operationResponse.OperationCode == OperationCode.JoinGame)
- {
- this.MatchMakingCallbackTargets.OnJoinRoomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage);
- }
- else if (operationResponse.OperationCode == OperationCode.CreateGame)
- {
- this.MatchMakingCallbackTargets.OnCreateRoomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage);
- }
- else if (operationResponse.OperationCode == OperationCode.JoinRandomGame)
- {
- this.MatchMakingCallbackTargets.OnJoinRandomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage);
- }
- }
- }
- /// <summary>
- /// Uses the OperationResponses provided by the server to advance the internal state and call ops as needed.
- /// </summary>
- /// <remarks>
- /// When this method finishes, it will call your OnOpResponseAction (if any). This way, you can get any
- /// operation response without overriding this class.
- ///
- /// To implement a more complex game/app logic, you should implement your own class that inherits the
- /// LoadBalancingClient. Override this method to use your own operation-responses easily.
- ///
- /// This method is essential to update the internal state of a LoadBalancingClient, so overriding methods
- /// must call base.OnOperationResponse().
- /// </remarks>
- /// <param name="operationResponse">Contains the server's response for an operation called by this peer.</param>
- public virtual void OnOperationResponse(OperationResponse operationResponse)
- {
- // if (operationResponse.ReturnCode != 0) this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull());
- // use the "secret" or "token" whenever we get it. doesn't really matter if it's in AuthResponse.
- if (operationResponse.Parameters.ContainsKey(ParameterCode.Token))
- {
- if (this.AuthValues == null)
- {
- this.AuthValues = new AuthenticationValues();
- //this.DebugReturn(DebugLevel.ERROR, "Server returned secret. Created AuthValues.");
- }
- this.AuthValues.Token = operationResponse.Parameters[ParameterCode.Token];
- this.tokenCache = this.AuthValues.Token;
- }
- // if the operation limit was reached, disconnect (but still execute the operation response).
- if (operationResponse.ReturnCode == ErrorCode.OperationLimitReached)
- {
- this.Disconnect(DisconnectCause.DisconnectByOperationLimit);
- }
- switch (operationResponse.OperationCode)
- {
- case OperationCode.Authenticate:
- case OperationCode.AuthenticateOnce:
- {
- if (operationResponse.ReturnCode != 0)
- {
- this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull() + " Server: " + this.Server + " Address: " + this.LoadBalancingPeer.ServerAddress);
- switch (operationResponse.ReturnCode)
- {
- case ErrorCode.InvalidAuthentication:
- this.DisconnectedCause = DisconnectCause.InvalidAuthentication;
- break;
- case ErrorCode.CustomAuthenticationFailed:
- this.DisconnectedCause = DisconnectCause.CustomAuthenticationFailed;
- this.ConnectionCallbackTargets.OnCustomAuthenticationFailed(operationResponse.DebugMessage);
- break;
- case ErrorCode.InvalidRegion:
- this.DisconnectedCause = DisconnectCause.InvalidRegion;
- break;
- case ErrorCode.MaxCcuReached:
- this.DisconnectedCause = DisconnectCause.MaxCcuReached;
- break;
- case ErrorCode.InvalidOperation:
- case ErrorCode.OperationNotAllowedInCurrentState:
- this.DisconnectedCause = DisconnectCause.OperationNotAllowedInCurrentState;
- break;
- case ErrorCode.AuthenticationTicketExpired:
- this.DisconnectedCause = DisconnectCause.AuthenticationTicketExpired;
- break;
- }
- this.Disconnect(this.DisconnectedCause);
- break; // if auth didn't succeed, we disconnect (above) and exit this operation's handling
- }
- if (this.Server == ServerConnection.NameServer || this.Server == ServerConnection.MasterServer)
- {
- if (operationResponse.Parameters.ContainsKey(ParameterCode.UserId))
- {
- string incomingId = (string)operationResponse.Parameters[ParameterCode.UserId];
- if (!string.IsNullOrEmpty(incomingId))
- {
- this.UserId = incomingId;
- this.LocalPlayer.UserId = incomingId;
- this.DebugReturn(DebugLevel.INFO, string.Format("Received your UserID from server. Updating local value to: {0}", this.UserId));
- }
- }
- if (operationResponse.Parameters.ContainsKey(ParameterCode.NickName))
- {
- this.NickName = (string)operationResponse.Parameters[ParameterCode.NickName];
- this.DebugReturn(DebugLevel.INFO, string.Format("Received your NickName from server. Updating local value to: {0}", this.NickName));
- }
- if (operationResponse.Parameters.ContainsKey(ParameterCode.EncryptionData))
- {
- this.SetupEncryption((Dictionary<byte, object>)operationResponse.Parameters[ParameterCode.EncryptionData]);
- }
- }
- if (this.Server == ServerConnection.NameServer)
- {
- string receivedCluster = operationResponse[ParameterCode.Cluster] as string;
- if (!string.IsNullOrEmpty(receivedCluster))
- {
- this.CurrentCluster = receivedCluster;
- }
- // on the NameServer, authenticate returns the MasterServer address for a region and we hop off to there
- this.MasterServerAddress = operationResponse[ParameterCode.Address] as string;
- if (this.ServerPortOverrides.MasterServerPort != 0)
- {
- //Debug.LogWarning("Incoming MasterServer Address: "+this.MasterServerAddress);
- this.MasterServerAddress = ReplacePortWithAlternative(this.MasterServerAddress, this.ServerPortOverrides.MasterServerPort);
- //Debug.LogWarning("New MasterServer Address: "+this.MasterServerAddress);
- }
- if (this.AuthMode == AuthModeOption.AuthOnceWss && this.ExpectedProtocol != null)
- {
- this.DebugReturn(DebugLevel.INFO, string.Format("AuthOnceWss mode. Auth response switches TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol));
- this.LoadBalancingPeer.TransportProtocol = (ConnectionProtocol)this.ExpectedProtocol;
- this.ExpectedProtocol = null;
- }
- this.DisconnectToReconnect();
- }
- else if (this.Server == ServerConnection.MasterServer)
- {
- this.State = ClientState.ConnectedToMasterServer;
- if (this.failedRoomEntryOperation == null)
- {
- this.ConnectionCallbackTargets.OnConnectedToMaster();
- }
- else
- {
- this.CallbackRoomEnterFailed(this.failedRoomEntryOperation);
- this.failedRoomEntryOperation = null;
- }
- if (this.AuthMode != AuthModeOption.Auth)
- {
- this.LoadBalancingPeer.OpSettings(this.EnableLobbyStatistics);
- }
- }
- else if (this.Server == ServerConnection.GameServer)
- {
- this.State = ClientState.Joining;
- if (this.enterRoomParamsCache.JoinMode == JoinMode.RejoinOnly)
- {
- this.enterRoomParamsCache.PlayerProperties = null;
- }
- else
- {
- Hashtable allProps = new Hashtable();
- allProps.Merge(this.LocalPlayer.CustomProperties);
- if (!string.IsNullOrEmpty(this.LocalPlayer.NickName))
- {
- allProps[ActorProperties.PlayerName] = this.LocalPlayer.NickName;
- }
- this.enterRoomParamsCache.PlayerProperties = allProps;
- }
- this.enterRoomParamsCache.OnGameServer = true;
- if (this.lastJoinType == JoinType.JoinRoom || this.lastJoinType == JoinType.JoinRandomRoom || this.lastJoinType == JoinType.JoinRandomOrCreateRoom || this.lastJoinType == JoinType.JoinOrCreateRoom)
- {
- this.LoadBalancingPeer.OpJoinRoom(this.enterRoomParamsCache);
- }
- else if (this.lastJoinType == JoinType.CreateRoom)
- {
- this.LoadBalancingPeer.OpCreateRoom(this.enterRoomParamsCache);
- }
- break;
- }
- // optionally, OpAuth may return some data for the client to use. if it's available, call OnCustomAuthenticationResponse
- Dictionary<string, object> data = (Dictionary<string, object>)operationResponse[ParameterCode.Data];
- if (data != null)
- {
- this.ConnectionCallbackTargets.OnCustomAuthenticationResponse(data);
- }
- break;
- }
- case OperationCode.GetRegions:
- // Debug.Log("GetRegions returned: " + operationResponse.ToStringFull());
- if (operationResponse.ReturnCode == ErrorCode.InvalidAuthentication)
- {
- this.DebugReturn(DebugLevel.ERROR, string.Format("GetRegions failed. AppId is unknown on the (cloud) server. "+operationResponse.DebugMessage));
- this.Disconnect(DisconnectCause.InvalidAuthentication);
- break;
- }
- if (operationResponse.ReturnCode != ErrorCode.Ok)
- {
- this.DebugReturn(DebugLevel.ERROR, "GetRegions failed. Can't provide regions list. ReturnCode: " + operationResponse.ReturnCode + ": " + operationResponse.DebugMessage);
- this.Disconnect(DisconnectCause.InvalidAuthentication);
- break;
- }
- if (this.RegionHandler == null)
- {
- this.RegionHandler = new RegionHandler(this.ServerPortOverrides.MasterServerPort);
- }
- if (this.RegionHandler.IsPinging)
- {
- this.DebugReturn(DebugLevel.WARNING, "Received an response for OpGetRegions while the RegionHandler is pinging regions already. Skipping this response in favor of completing the current region-pinging.");
- return; // in this particular case, we suppress the duplicate GetRegion response. we don't want a callback for this, cause there is a warning already.
- }
- this.RegionHandler.SetRegions(operationResponse);
- this.ConnectionCallbackTargets.OnRegionListReceived(this.RegionHandler);
- if (this.connectToBestRegion)
- {
- // ping minimal regions (if one is known) and connect
- this.RegionHandler.PingMinimumOfRegions(this.OnRegionPingCompleted, this.bestRegionSummaryFromStorage);
- }
- break;
- case OperationCode.JoinRandomGame: // this happens only on the master server. on gameserver this is a "regular" join
- case OperationCode.CreateGame:
- case OperationCode.JoinGame:
- if (operationResponse.ReturnCode != 0)
- {
- if (this.Server == ServerConnection.GameServer)
- {
- this.failedRoomEntryOperation = operationResponse;
- this.DisconnectToReconnect();
- }
- else
- {
- this.State = (this.InLobby) ? ClientState.JoinedLobby : ClientState.ConnectedToMasterServer;
- this.CallbackRoomEnterFailed(operationResponse);
- }
- }
- else
- {
- if (this.Server == ServerConnection.GameServer)
- {
- this.GameEnteredOnGameServer(operationResponse);
- }
- else
- {
- this.GameServerAddress = (string)operationResponse[ParameterCode.Address];
- if (this.ServerPortOverrides.GameServerPort != 0)
- {
- //Debug.LogWarning("Incoming GameServer Address: " + this.GameServerAddress);
- this.GameServerAddress = ReplacePortWithAlternative(this.GameServerAddress, this.ServerPortOverrides.GameServerPort);
- //Debug.LogWarning("New GameServer Address: " + this.GameServerAddress);
- }
- string roomName = operationResponse[ParameterCode.RoomName] as string;
- if (!string.IsNullOrEmpty(roomName))
- {
- this.enterRoomParamsCache.RoomName = roomName;
- }
- this.DisconnectToReconnect();
- }
- }
- break;
- case OperationCode.GetGameList:
- if (operationResponse.ReturnCode != 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "GetGameList failed: " + operationResponse.ToStringFull());
- break;
- }
- List<RoomInfo> _RoomInfoList = new List<RoomInfo>();
- Hashtable games = (Hashtable)operationResponse[ParameterCode.GameList];
- foreach (string gameName in games.Keys)
- {
- _RoomInfoList.Add(new RoomInfo(gameName, (Hashtable)games[gameName]));
- }
- this.LobbyCallbackTargets.OnRoomListUpdate(_RoomInfoList);
- break;
- case OperationCode.JoinLobby:
- this.State = ClientState.JoinedLobby;
- this.LobbyCallbackTargets.OnJoinedLobby();
- break;
- case OperationCode.LeaveLobby:
- this.State = ClientState.ConnectedToMasterServer;
- this.LobbyCallbackTargets.OnLeftLobby();
- break;
- case OperationCode.Leave:
- this.DisconnectToReconnect();
- break;
- case OperationCode.FindFriends:
- if (operationResponse.ReturnCode != 0)
- {
- this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed: " + operationResponse.ToStringFull());
- this.friendListRequested = null;
- break;
- }
- bool[] onlineList = operationResponse[ParameterCode.FindFriendsResponseOnlineList] as bool[];
- string[] roomList = operationResponse[ParameterCode.FindFriendsResponseRoomIdList] as string[];
- //if (onlineList == null || roomList == null || this.friendListRequested == null || onlineList.Length != this.friendListRequested.Length)
- //{
- // // TODO: Check if we should handle this case better / more extensively
- // this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed. Some list is not set. OpResponse: " + operationResponse.ToStringFull());
- // this.friendListRequested = null;
- // this.isFetchingFriendList = false;
- // break;
- //}
- List<FriendInfo> friendList = new List<FriendInfo>(this.friendListRequested.Length);
- for (int index = 0; index < this.friendListRequested.Length; index++)
- {
- FriendInfo friend = new FriendInfo();
- friend.UserId = this.friendListRequested[index];
- friend.Room = roomList[index];
- friend.IsOnline = onlineList[index];
- friendList.Insert(index, friend);
- }
- this.friendListRequested = null;
- this.MatchMakingCallbackTargets.OnFriendListUpdate(friendList);
- break;
- case OperationCode.WebRpc:
- this.WebRpcCallbackTargets.OnWebRpcResponse(operationResponse);
- break;
- }
- if (this.OpResponseReceived != null) this.OpResponseReceived(operationResponse);
- }
- /// <summary>
- /// Uses the connection's statusCodes to advance the internal state and call operations as needed.
- /// </summary>
- /// <remarks>This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnStatusChanged.</remarks>
- public virtual void OnStatusChanged(StatusCode statusCode)
- {
- switch (statusCode)
- {
- case StatusCode.Connect:
- if (this.State == ClientState.ConnectingToNameServer)
- {
- if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ALL)
- {
- this.DebugReturn(DebugLevel.ALL, "Connected to nameserver.");
- }
- this.Server = ServerConnection.NameServer;
- if (this.AuthValues != null)
- {
- this.AuthValues.Token = null; // when connecting to NameServer, invalidate the secret (only)
- }
- }
- if (this.State == ClientState.ConnectingToGameServer)
- {
- if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ALL)
- {
- this.DebugReturn(DebugLevel.ALL, "Connected to gameserver.");
- }
- this.Server = ServerConnection.GameServer;
- }
- if (this.State == ClientState.ConnectingToMasterServer)
- {
- if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ALL)
- {
- this.DebugReturn(DebugLevel.ALL, "Connected to masterserver.");
- }
- this.Server = ServerConnection.MasterServer;
- this.ConnectionCallbackTargets.OnConnected(); // if initial connect
- }
- if (this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure)
- {
- if (this.Server == ServerConnection.NameServer || this.AuthMode == AuthModeOption.Auth)
- {
- this.LoadBalancingPeer.EstablishEncryption();
- }
- }
- else
- {
- goto case StatusCode.EncryptionEstablished;
- }
- break;
- case StatusCode.EncryptionEstablished:
- if (this.Server == ServerConnection.NameServer)
- {
- this.State = ClientState.ConnectedToNameServer;
- // if there is no specific region to connect to, get available regions from the Name Server. the result triggers next actions in workflow
- if (string.IsNullOrEmpty(this.CloudRegion))
- {
- this.OpGetRegions();
- break;
- }
- }
- else
- {
- // auth AuthOnce, no explicit authentication is needed on Master Server and Game Server. this is done via token, so: break
- if (this.AuthMode == AuthModeOption.AuthOnce || this.AuthMode == AuthModeOption.AuthOnceWss)
- {
- break;
- }
- }
- // authenticate in all other cases (using the CloudRegion, if available)
- bool authenticating = this.CallAuthenticate();
- if (authenticating)
- {
- this.State = ClientState.Authenticating;
- }
- else
- {
- this.DebugReturn(DebugLevel.ERROR, "OpAuthenticate failed. Check log output and AuthValues. State: " + this.State);
- }
- break;
- case StatusCode.Disconnect:
- // disconnect due to connection exception is handled below (don't connect to GS or master in that case)
- this.friendListRequested = null;
- bool wasInRoom = this.CurrentRoom != null;
- this.CurrentRoom = null; // players get cleaned up inside this, too, except LocalPlayer (which we keep)
- this.ChangeLocalID(-1); // depends on this.CurrentRoom, so it must be called after updating that
- if (this.Server == ServerConnection.GameServer && wasInRoom)
- {
- this.MatchMakingCallbackTargets.OnLeftRoom();
- }
- if (this.ExpectedProtocol != null && this.LoadBalancingPeer.TransportProtocol != this.ExpectedProtocol)
- {
- this.DebugReturn(DebugLevel.INFO, string.Format("On disconnect switches TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol));
- this.LoadBalancingPeer.TransportProtocol = (ConnectionProtocol)this.ExpectedProtocol;
- this.ExpectedProtocol = null;
- }
- switch (this.State)
- {
- case ClientState.ConnectWithoutAuthOnceWss:
- this.DebugReturn(DebugLevel.INFO, string.Format("AuthOnceWss failed (WSS connection could not be established: " + this.DisconnectedCause + "). Trying again with protocol: " + this.LoadBalancingPeer.TransportProtocol));
- this.AuthMode = AuthModeOption.Auth;
- // switching protocol back to ExpectedProtocol is done above this switch-block
- //this.LoadBalancingPeer.TransportProtocol = (ConnectionProtocol)this.ExpectedProtocol;
- //this.ExpectedProtocol = null;
- this.NameServerPortInAppSettings = 0; // this does not affect the ServerSettings file, just a variable at runtime
- this.ServerPortOverrides = new PhotonPortDefinition(); // use default ports for the fallback
- if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
- {
- return;
- }
- this.State = ClientState.ConnectingToNameServer;
- break;
- case ClientState.ConnectWithFallbackProtocol:
- this.EnableProtocolFallback = false; // the client does a fallback only one time
- this.LoadBalancingPeer.TransportProtocol = (this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.Udp) ? ConnectionProtocol.Udp : ConnectionProtocol.WebSocketSecure;
- this.NameServerPortInAppSettings = 0; // this does not affect the ServerSettings file, just a variable at runtime
- this.ServerPortOverrides = new PhotonPortDefinition(); // use default ports for the fallback
- if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
- {
- return;
- }
- this.State = ClientState.ConnectingToNameServer;
- break;
- case ClientState.PeerCreated:
- case ClientState.Disconnecting:
- if (this.AuthValues != null)
- {
- this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
- }
- this.State = ClientState.Disconnected;
- this.ConnectionCallbackTargets.OnDisconnected(this.DisconnectedCause);
- break;
- case ClientState.DisconnectingFromGameServer:
- case ClientState.DisconnectingFromNameServer:
- this.ConnectToMasterServer(); // this gets the client back to the Master Server
- break;
- case ClientState.DisconnectingFromMasterServer:
- this.Connect(this.GameServerAddress, this.ProxyServerAddress, ServerConnection.GameServer); // this connects the client with the Game Server (when joining/creating a room)
- break;
- case ClientState.Disconnected:
- // this client is already Disconnected, so no further action is needed.
- // this.DebugReturn(DebugLevel.INFO, "LBC.OnStatusChanged(Disconnect) this.State: " + this.State + ". Server: " + this.Server);
- break;
- default:
- string stacktrace = "";
- #if DEBUG && !NETFX_CORE
- stacktrace = new System.Diagnostics.StackTrace(true).ToString();
- #endif
- this.DebugReturn(DebugLevel.WARNING, "Got a unexpected Disconnect in LoadBalancingClient State: " + this.State + ". Server: " + this.Server + " Trace: " + stacktrace);
- if (this.AuthValues != null)
- {
- this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
- }
- this.State = ClientState.Disconnected;
- this.ConnectionCallbackTargets.OnDisconnected(this.DisconnectedCause);
- break;
- }
- break;
- case StatusCode.DisconnectByServerUserLimit:
- this.DebugReturn(DebugLevel.ERROR, "This connection was rejected due to the apps CCU limit.");
- this.DisconnectedCause = DisconnectCause.MaxCcuReached;
- this.State = ClientState.Disconnecting;
- break;
- case StatusCode.DnsExceptionOnConnect:
- this.DisconnectedCause = DisconnectCause.DnsExceptionOnConnect;
- this.State = ClientState.Disconnecting;
- break;
- case StatusCode.ServerAddressInvalid:
- this.DisconnectedCause = DisconnectCause.ServerAddressInvalid;
- this.State = ClientState.Disconnecting;
- break;
- case StatusCode.ExceptionOnConnect:
- case StatusCode.SecurityExceptionOnConnect:
- case StatusCode.EncryptionFailedToEstablish:
- this.SystemConnectionSummary = new SystemConnectionSummary(this);
- this.DebugReturn(DebugLevel.ERROR, $"Connection lost. OnStatusChanged to {statusCode}. Client state was: {this.State}. {this.SystemConnectionSummary.ToString()}");
- this.DisconnectedCause = DisconnectCause.ExceptionOnConnect;
- ClientState nextState = ClientState.Disconnecting;
- if (this.State == ClientState.ConnectingToNameServer)
- {
- // if AuthOnceWss was used, try to connect again with the expected protocol (unless that is also WSS)
- // this is not yet considered as using a fallback protocol
- if (this.AuthMode == AuthModeOption.AuthOnceWss && this.ExpectedProtocol != ConnectionProtocol.WebSocketSecure)
- {
- nextState = ClientState.ConnectWithoutAuthOnceWss;
- }
- else if (this.EnableProtocolFallback)
- {
- // if enabled, the client can attempt to connect with another networking-protocol to check if that connects
- nextState = ClientState.ConnectWithFallbackProtocol;
- }
- this.AuthMode = AuthModeOption.AuthOnce;
- }
- this.State = nextState;
- break;
- case StatusCode.Exception:
- case StatusCode.ExceptionOnReceive:
- case StatusCode.SendError:
- this.SystemConnectionSummary = new SystemConnectionSummary(this);
- this.DebugReturn(DebugLevel.ERROR, $"Connection lost. OnStatusChanged to {statusCode}. Client state was: {this.State}. {this.SystemConnectionSummary.ToString()}");
- this.DisconnectedCause = DisconnectCause.Exception;
- this.State = ClientState.Disconnecting;
- break;
- case StatusCode.DisconnectByServerTimeout:
- this.SystemConnectionSummary = new SystemConnectionSummary(this);
- this.DebugReturn(DebugLevel.ERROR, $"Connection lost. OnStatusChanged to {statusCode}. Client state was: {this.State}. {this.SystemConnectionSummary.ToString()}");
- this.DisconnectedCause = DisconnectCause.ServerTimeout; // could check if app was in background (and is now back)
- this.State = ClientState.Disconnecting;
- break;
- case StatusCode.TimeoutDisconnect:
- this.SystemConnectionSummary = new SystemConnectionSummary(this);
- this.DebugReturn(DebugLevel.ERROR, $"Connection lost. OnStatusChanged to {statusCode}. Client state was: {this.State}. {this.SystemConnectionSummary.ToString()}");
- this.DisconnectedCause = DisconnectCause.ClientTimeout;
- nextState = ClientState.Disconnecting;
- if (this.State == ClientState.ConnectingToNameServer)
- {
- // if AuthOnceWss was used, try to connect again with the expected protocol (unless that is also WSS)
- // this is not yet considered as using a fallback protocol
- if (this.AuthMode == AuthModeOption.AuthOnceWss && this.ExpectedProtocol != ConnectionProtocol.WebSocketSecure)
- {
- nextState = ClientState.ConnectWithoutAuthOnceWss;
- }
- else if (this.EnableProtocolFallback)
- {
- // if enabled, the client can attempt to connect with another networking-protocol to check if that connects
- nextState = ClientState.ConnectWithFallbackProtocol;
- }
- this.AuthMode = AuthModeOption.AuthOnce;
- }
- this.State = nextState;
- break;
- case StatusCode.DisconnectByServerLogic:
- this.DisconnectedCause = DisconnectCause.DisconnectByServerLogic;
- this.State = ClientState.Disconnecting;
- break;
- case StatusCode.DisconnectByServerReasonUnknown:
- this.DisconnectedCause = DisconnectCause.DisconnectByServerReasonUnknown;
- this.State = ClientState.Disconnecting;
- break;
- }
- }
- /// <summary>
- /// Uses the photonEvent's provided by the server to advance the internal state and call ops as needed.
- /// </summary>
- /// <remarks>This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnEvent.</remarks>
- public virtual void OnEvent(EventData photonEvent)
- {
- int actorNr = photonEvent.Sender;
- Player originatingPlayer = (this.CurrentRoom != null) ? this.CurrentRoom.GetPlayer(actorNr) : null;
- switch (photonEvent.Code)
- {
- case EventCode.GameList:
- case EventCode.GameListUpdate:
- List<RoomInfo> _RoomInfoList = new List<RoomInfo>();
- Hashtable games = (Hashtable)photonEvent[ParameterCode.GameList];
- foreach (string gameName in games.Keys)
- {
- _RoomInfoList.Add(new RoomInfo(gameName, (Hashtable)games[gameName]));
- }
- this.LobbyCallbackTargets.OnRoomListUpdate(_RoomInfoList);
- break;
- case EventCode.Join:
- Hashtable actorProperties = (Hashtable)photonEvent[ParameterCode.PlayerProperties];
- if (originatingPlayer == null)
- {
- if (actorNr > 0)
- {
- originatingPlayer = this.CreatePlayer(string.Empty, actorNr, false, actorProperties);
- this.CurrentRoom.StorePlayer(originatingPlayer);
- }
- }
- else
- {
- originatingPlayer.InternalCacheProperties(actorProperties);
- originatingPlayer.IsInactive = false;
- originatingPlayer.HasRejoined = actorNr != this.LocalPlayer.ActorNumber; // event is for non-local player, who is known (by ActorNumber), so it's a returning player
- }
- if (actorNr == this.LocalPlayer.ActorNumber)
- {
- // in this player's own join event, we get a complete list of players in the room, so check if we know each of the
- int[] actorsInRoom = (int[])photonEvent[ParameterCode.ActorList];
- this.UpdatedActorList(actorsInRoom);
- // any operation that does a "rejoin" will set this value to true. this can indicate if the local player returns to a room.
- originatingPlayer.HasRejoined = this.enterRoomParamsCache.JoinMode == JoinMode.RejoinOnly;
- this.State = ClientState.Joined;
- // joinWithCreateOnDemand can turn an OpJoin into creating the room. Then actorNumber is 1 and callback: OnCreatedRoom()
- if (this.lastJoinType == JoinType.CreateRoom || (this.lastJoinType == JoinType.JoinOrCreateRoom && this.LocalPlayer.ActorNumber == 1))
- {
- this.MatchMakingCallbackTargets.OnCreatedRoom();
- }
- this.MatchMakingCallbackTargets.OnJoinedRoom();
- }
- else
- {
- this.InRoomCallbackTargets.OnPlayerEnteredRoom(originatingPlayer);
- }
- break;
- case EventCode.Leave:
- if (originatingPlayer != null)
- {
- bool isInactive = false;
- if (photonEvent.Parameters.ContainsKey(ParameterCode.IsInactive))
- {
- isInactive = (bool)photonEvent.Parameters[ParameterCode.IsInactive];
- }
- originatingPlayer.IsInactive = isInactive;
- originatingPlayer.HasRejoined = false;
- if (!isInactive)
- {
- this.CurrentRoom.RemovePlayer(actorNr);
- }
- }
- if (photonEvent.Parameters.ContainsKey(ParameterCode.MasterClientId))
- {
- int newMaster = (int)photonEvent[ParameterCode.MasterClientId];
- if (newMaster != 0)
- {
- this.CurrentRoom.masterClientId = newMaster;
- this.InRoomCallbackTargets.OnMasterClientSwitched(this.CurrentRoom.GetPlayer(newMaster));
- }
- }
- // finally, send notification that a player left
- this.InRoomCallbackTargets.OnPlayerLeftRoom(originatingPlayer);
- break;
- case EventCode.PropertiesChanged:
- // whenever properties are sent in-room, they can be broadcast as event (which we handle here)
- // we get PLAYERproperties if actorNr > 0 or ROOMproperties if actorNumber is not set or 0
- int targetActorNr = 0;
- if (photonEvent.Parameters.ContainsKey(ParameterCode.TargetActorNr))
- {
- targetActorNr = (int)photonEvent[ParameterCode.TargetActorNr];
- }
- Hashtable gameProperties = null;
- Hashtable actorProps = null;
- if (targetActorNr == 0)
- {
- gameProperties = (Hashtable)photonEvent[ParameterCode.Properties];
- }
- else
- {
- actorProps = (Hashtable)photonEvent[ParameterCode.Properties];
- }
- this.ReadoutProperties(gameProperties, actorProps, targetActorNr);
- break;
- case EventCode.AppStats:
- // only the master server sends these in (1 minute) intervals
- this.PlayersInRoomsCount = (int)photonEvent[ParameterCode.PeerCount];
- this.RoomsCount = (int)photonEvent[ParameterCode.GameCount];
- this.PlayersOnMasterCount = (int)photonEvent[ParameterCode.MasterPeerCount];
- break;
- case EventCode.LobbyStats:
- string[] names = photonEvent[ParameterCode.LobbyName] as string[];
- int[] peers = photonEvent[ParameterCode.PeerCount] as int[];
- int[] rooms = photonEvent[ParameterCode.GameCount] as int[];
- byte[] types;
- ByteArraySlice slice = photonEvent[ParameterCode.LobbyType] as ByteArraySlice;
- bool useByteArraySlice = slice != null;
- if (useByteArraySlice)
- {
- types = slice.Buffer;
- }
- else
- {
- types = photonEvent[ParameterCode.LobbyType] as byte[];
- }
- this.lobbyStatistics.Clear();
- for (int i = 0; i < names.Length; i++)
- {
- TypedLobbyInfo info = new TypedLobbyInfo();
- info.Name = names[i];
- info.Type = (LobbyType)types[i];
- info.PlayerCount = peers[i];
- info.RoomCount = rooms[i];
- this.lobbyStatistics.Add(info);
- }
- if (useByteArraySlice)
- {
- slice.Release();
- }
- this.LobbyCallbackTargets.OnLobbyStatisticsUpdate(this.lobbyStatistics);
- break;
- case EventCode.ErrorInfo:
- this.ErrorInfoCallbackTargets.OnErrorInfo(new ErrorInfo(photonEvent));
- break;
- case EventCode.AuthEvent:
- if (this.AuthValues == null)
- {
- this.AuthValues = new AuthenticationValues();
- }
- this.AuthValues.Token = photonEvent[ParameterCode.Token];
- this.tokenCache = this.AuthValues.Token;
- break;
- }
- this.UpdateCallbackTargets();
- if (this.EventReceived != null)
- {
- this.EventReceived(photonEvent);
- }
- }
- /// <summary>In Photon 4, "raw messages" will get their own callback method in the interface. Not used yet.</summary>
- public virtual void OnMessage(object message)
- {
- this.DebugReturn(DebugLevel.ALL, string.Format("got OnMessage {0}", message));
- }
- #endregion
- private void OnDisconnectMessageReceived(DisconnectMessage obj)
- {
- this.DebugReturn(DebugLevel.ERROR, string.Format("Got DisconnectMessage. Code: {0} Msg: \"{1}\". Debug Info: {2}", obj.Code, obj.DebugMessage, obj.Parameters.ToStringFull()));
- this.Disconnect(DisconnectCause.DisconnectByDisconnectMessage);
- }
- /// <summary>A callback of the RegionHandler, provided in OnRegionListReceived.</summary>
- /// <param name="regionHandler">The regionHandler wraps up best region and other region relevant info.</param>
- private void OnRegionPingCompleted(RegionHandler regionHandler)
- {
- //Debug.Log("OnRegionPingCompleted " + regionHandler.BestRegion);
- //Debug.Log("RegionPingSummary: " + regionHandler.SummaryToCache);
- this.SummaryToCache = regionHandler.SummaryToCache;
- this.ConnectToRegionMaster(regionHandler.BestRegion.Code);
- }
- protected internal static string ReplacePortWithAlternative(string address, ushort replacementPort)
- {
- bool webSocket = address.StartsWith("ws");
- if (webSocket)
- {
- UriBuilder urib = new UriBuilder(address);
- urib.Port = replacementPort;
- return urib.ToString();
- }
- else
- {
- UriBuilder urib = new UriBuilder(string.Format("scheme://{0}", address));
- return string.Format("{0}:{1}", urib.Host, replacementPort);
- }
- }
- private void SetupEncryption(Dictionary<byte, object> encryptionData)
- {
- var mode = (EncryptionMode)(byte)encryptionData[EncryptionDataParameters.Mode];
- switch (mode)
- {
- case EncryptionMode.PayloadEncryption:
- byte[] encryptionSecret = (byte[])encryptionData[EncryptionDataParameters.Secret1];
- this.LoadBalancingPeer.InitPayloadEncryption(encryptionSecret);
- break;
- case EncryptionMode.DatagramEncryption:
- case EncryptionMode.DatagramEncryptionRandomSequence:
- {
- byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1];
- byte[] secret2 = (byte[])encryptionData[EncryptionDataParameters.Secret2];
- this.LoadBalancingPeer.InitDatagramEncryption(secret1, secret2, mode == EncryptionMode.DatagramEncryptionRandomSequence);
- }
- break;
- case EncryptionMode.DatagramEncryptionGCM:
- {
- byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1];
- this.LoadBalancingPeer.InitDatagramEncryption(secret1, null, true, true);
- }
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
- /// <summary>
- /// This operation makes Photon call your custom web-service by path/name with the given parameters (converted into Json).
- /// Use <see cref="IWebRpcCallback.OnWebRpcResponse"/> as a callback.
- /// </summary>
- /// <remarks>
- /// A WebRPC calls a custom, http-based function on a server you provide. The uriPath is relative to a "base path"
- /// which is configured server-side. The sent parameters get converted from C# types to Json. Vice versa, the response
- /// of the web-service will be converted to C# types and sent back as normal operation response.
- ///
- /// To use this feature, you have to setup your server:
- ///
- /// For a Photon Cloud application, <a href="https://doc.photonengine.com/en-us/realtime/current/reference/webhooks">
- /// visit the Dashboard </a> and setup "WebHooks". The BaseUrl is used for WebRPCs as well.
- ///
- /// The class <see cref="WebRpcResponse"/> is a helper-class that extracts the most valuable content from the WebRPC
- /// response.
- /// </remarks>
- /// <param name="uriPath">The url path to call, relative to the baseUrl configured on Photon's server-side.</param>
- /// <param name="parameters">The parameters to send to the web-service method.</param>
- /// <param name="sendAuthCookie">Defines if the authentication cookie gets sent to a WebHook (if setup).</param>
- public bool OpWebRpc(string uriPath, object parameters, bool sendAuthCookie = false)
- {
- if (string.IsNullOrEmpty(uriPath))
- {
- this.DebugReturn(DebugLevel.ERROR, "WebRPC method name must not be null nor empty.");
- return false;
- }
- if (!this.CheckIfOpCanBeSent(OperationCode.WebRpc, this.Server, "WebRpc"))
- {
- return false;
- }
- Dictionary<byte, object> opParameters = new Dictionary<byte, object>();
- opParameters.Add(ParameterCode.UriPath, uriPath);
- if (parameters != null)
- {
- opParameters.Add(ParameterCode.WebRpcParameters, parameters);
- }
- if (sendAuthCookie)
- {
- opParameters.Add(ParameterCode.EventForward, WebFlags.SendAuthCookieConst);
- }
- //return this.LoadBalancingPeer.OpCustom(OperationCode.WebRpc, opParameters, true);
- return this.LoadBalancingPeer.SendOperation(OperationCode.WebRpc, opParameters, SendOptions.SendReliable);
- }
- /// <summary>
- /// Registers an object for callbacks for the implemented callback-interfaces.
- /// </summary>
- /// <remarks>
- /// Adding and removing callback targets is queued to not mess with callbacks in execution.
- /// Internally, this means that the addition/removal is done before the LoadBalancingClient
- /// calls the next callbacks. This detail should not affect a game's workflow.
- ///
- /// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks,
- /// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback.
- ///
- /// See: <a href="https://doc.photonengine.com/en-us/realtime/current/reference/dotnet-callbacks"/>
- /// </remarks>
- /// <param name="target">The object that registers to get callbacks from this client.</param>
- public void AddCallbackTarget(object target)
- {
- this.callbackTargetChanges.Enqueue(new CallbackTargetChange(target, true));
- }
- /// <summary>
- /// Unregisters an object from callbacks for the implemented callback-interfaces.
- /// </summary>
- /// <remarks>
- /// Adding and removing callback targets is queued to not mess with callbacks in execution.
- /// Internally, this means that the addition/removal is done before the LoadBalancingClient
- /// calls the next callbacks. This detail should not affect a game's workflow.
- ///
- /// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks,
- /// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback.
- ///
- /// See: <a href="https://doc.photonengine.com/en-us/realtime/current/reference/dotnet-callbacks"></a>
- /// </remarks>
- /// <param name="target">The object that unregisters from getting callbacks.</param>
- public void RemoveCallbackTarget(object target)
- {
- this.callbackTargetChanges.Enqueue(new CallbackTargetChange(target, false));
- }
- /// <summary>
- /// Applies queued callback cahnges from a queue to the actual containers. Will cause exceptions if used while callbacks execute.
- /// </summary>
- /// <remarks>
- /// There is no explicit check that this is not called during callbacks, however the implemented, private logic takes care of this.
- /// </remarks>
- protected internal void UpdateCallbackTargets()
- {
- while (this.callbackTargetChanges.Count > 0)
- {
- CallbackTargetChange change = this.callbackTargetChanges.Dequeue();
- if (change.AddTarget)
- {
- if (this.callbackTargets.Contains(change.Target))
- {
- //Debug.Log("UpdateCallbackTargets skipped adding a target, as the object is already registered. Target: " + change.Target);
- continue;
- }
- this.callbackTargets.Add(change.Target);
- }
- else
- {
- if (!this.callbackTargets.Contains(change.Target))
- {
- //Debug.Log("UpdateCallbackTargets skipped removing a target, as the object is not registered. Target: " + change.Target);
- continue;
- }
- this.callbackTargets.Remove(change.Target);
- }
- this.UpdateCallbackTarget<IInRoomCallbacks>(change, this.InRoomCallbackTargets);
- this.UpdateCallbackTarget<IConnectionCallbacks>(change, this.ConnectionCallbackTargets);
- this.UpdateCallbackTarget<IMatchmakingCallbacks>(change, this.MatchMakingCallbackTargets);
- this.UpdateCallbackTarget<ILobbyCallbacks>(change, this.LobbyCallbackTargets);
- this.UpdateCallbackTarget<IWebRpcCallback>(change, this.WebRpcCallbackTargets);
- this.UpdateCallbackTarget<IErrorInfoCallback>(change, this.ErrorInfoCallbackTargets);
- IOnEventCallback onEventCallback = change.Target as IOnEventCallback;
- if (onEventCallback != null)
- {
- if (change.AddTarget)
- {
- EventReceived += onEventCallback.OnEvent;
- }
- else
- {
- EventReceived -= onEventCallback.OnEvent;
- }
- }
- }
- }
- /// <summary>Helper method to cast and apply a target per (interface) type.</summary>
- /// <typeparam name="T">Either of the interfaces for callbacks.</typeparam>
- /// <param name="change">The queued change to apply (add or remove) some target.</param>
- /// <param name="container">The container that calls callbacks on it's list of targets.</param>
- private void UpdateCallbackTarget<T>(CallbackTargetChange change, List<T> container) where T : class
- {
- T target = change.Target as T;
- if (target != null)
- {
- if (change.AddTarget)
- {
- container.Add(target);
- }
- else
- {
- container.Remove(target);
- }
- }
- }
- }
- /// <summary>
- /// Collection of "organizational" callbacks for the Realtime Api to cover: Connection and Regions.
- /// </summary>
- /// <remarks>
- /// Classes that implement this interface must be registered to get callbacks for various situations.
- ///
- /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
- /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
- ///
- /// </remarks>
- /// \ingroup callbacks
- public interface IConnectionCallbacks
- {
- /// <summary>
- /// Called to signal that the "low level connection" got established but before the client can call operation on the server.
- /// </summary>
- /// <remarks>
- /// After the (low level transport) connection is established, the client will automatically send
- /// the Authentication operation, which needs to get a response before the client can call other operations.
- ///
- /// Your logic should wait for either: OnRegionListReceived or OnConnectedToMaster.
- ///
- /// This callback is useful to detect if the server can be reached at all (technically).
- /// Most often, it's enough to implement OnDisconnected(DisconnectCause cause) and check for the cause.
- ///
- /// This is not called for transitions from the masterserver to game servers.
- /// </remarks>
- void OnConnected();
- /// <summary>
- /// Called when the client is connected to the Master Server and ready for matchmaking and other tasks.
- /// </summary>
- /// <remarks>
- /// The list of available rooms won't become available unless you join a lobby via LoadBalancingClient.OpJoinLobby.
- /// You can join rooms and create them even without being in a lobby. The default lobby is used in that case.
- /// </remarks>
- void OnConnectedToMaster();
- /// <summary>
- /// Called after disconnecting from the Photon server. It could be a failure or an explicit disconnect call
- /// </summary>
- /// <remarks>
- /// The reason for this disconnect is provided as DisconnectCause.
- /// </remarks>
- void OnDisconnected(DisconnectCause cause);
- /// <summary>
- /// Called when the Name Server provided a list of regions for your title.
- /// </summary>
- /// <remarks>
- /// This callback is called as soon as the list is available. No pings were sent for Best Region selection yet.
- /// If the client is set to connect to the Best Region (lowest ping), one or more regions get pinged.
- /// Not all regions are pinged. As soon as the results are final, the client will connect to the best region,
- /// so you can check the ping results when connected to the Master Server.
- ///
- /// Check the RegionHandler class description, to make use of the provided values.
- /// </remarks>
- /// <param name="regionHandler">The currently used RegionHandler.</param>
- void OnRegionListReceived(RegionHandler regionHandler);
- /// <summary>
- /// Called when your Custom Authentication service responds with additional data.
- /// </summary>
- /// <remarks>
- /// Custom Authentication services can include some custom data in their response.
- /// When present, that data is made available in this callback as Dictionary.
- /// While the keys of your data have to be strings, the values can be either string or a number (in Json).
- /// You need to make extra sure, that the value type is the one you expect. Numbers become (currently) int64.
- ///
- /// Example: void OnCustomAuthenticationResponse(Dictionary<string, object> data) { ... }
- /// </remarks>
- /// <see cref="https://doc.photonengine.com/en-us/realtime/current/reference/custom-authentication"/>
- void OnCustomAuthenticationResponse(Dictionary<string, object> data);
- /// <summary>
- /// Called when the custom authentication failed. Followed by disconnect!
- /// </summary>
- /// <remarks>
- /// Custom Authentication can fail due to user-input, bad tokens/secrets.
- /// If authentication is successful, this method is not called. Implement OnJoinedLobby() or OnConnectedToMaster() (as usual).
- ///
- /// During development of a game, it might also fail due to wrong configuration on the server side.
- /// In those cases, logging the debugMessage is very important.
- ///
- /// Unless you setup a custom authentication service for your app (in the [Dashboard](https://dashboard.photonengine.com)),
- /// this won't be called!
- /// </remarks>
- /// <param name="debugMessage">Contains a debug message why authentication failed. This has to be fixed during development.</param>
- void OnCustomAuthenticationFailed(string debugMessage);
- }
- /// <summary>
- /// Collection of "organizational" callbacks for the Realtime Api to cover the Lobby.
- /// </summary>
- /// <remarks>
- /// Classes that implement this interface must be registered to get callbacks for various situations.
- ///
- /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
- /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
- ///
- /// </remarks>
- /// \ingroup callbacks
- public interface ILobbyCallbacks
- {
- /// <summary>
- /// Called on entering a lobby on the Master Server. The actual room-list updates will call OnRoomListUpdate.
- /// </summary>
- /// <remarks>
- /// While in the lobby, the roomlist is automatically updated in fixed intervals (which you can't modify in the public cloud).
- /// The room list gets available via OnRoomListUpdate.
- /// </remarks>
- void OnJoinedLobby();
- /// <summary>
- /// Called after leaving a lobby.
- /// </summary>
- /// <remarks>
- /// When you leave a lobby, [OpCreateRoom](@ref OpCreateRoom) and [OpJoinRandomRoom](@ref OpJoinRandomRoom)
- /// automatically refer to the default lobby.
- /// </remarks>
- void OnLeftLobby();
- /// <summary>
- /// Called for any update of the room-listing while in a lobby (InLobby) on the Master Server.
- /// </summary>
- /// <remarks>
- /// Each item is a RoomInfo which might include custom properties (provided you defined those as lobby-listed when creating a room).
- /// Not all types of lobbies provide a listing of rooms to the client. Some are silent and specialized for server-side matchmaking.
- ///
- /// The list is sorted using two criteria: open or closed, full or not. So the list is composed of three groups, in this order:
- ///
- /// first group: open and not full (joinable).</br>
- /// second group: full but not closed (not joinable).</br>
- /// third group: closed (not joinable, could be full or not).</br>
- ///
- /// In each group, entries do not have any particular order (random).
- ///
- /// The list of rooms (or rooms' updates) is also limited in number, see Lobby Limits.
- /// </remarks>
- void OnRoomListUpdate(List<RoomInfo> roomList);
- /// <summary>
- /// Called when the Master Server sent an update for the Lobby Statistics.
- /// </summary>
- /// <remarks>
- /// This callback has two preconditions:
- /// EnableLobbyStatistics must be set to true, before this client connects.
- /// And the client has to be connected to the Master Server, which is providing the info about lobbies.
- /// </remarks>
- void OnLobbyStatisticsUpdate(List<TypedLobbyInfo> lobbyStatistics);
- }
- /// <summary>
- /// Collection of "organizational" callbacks for the Realtime Api to cover Matchmaking.
- /// </summary>
- /// <remarks>
- /// Classes that implement this interface must be registered to get callbacks for various situations.
- ///
- /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
- /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
- ///
- /// </remarks>
- /// \ingroup callbacks
- public interface IMatchmakingCallbacks
- {
- /// <summary>
- /// Called when the server sent the response to a FindFriends request.
- /// </summary>
- /// <remarks>
- /// After calling OpFindFriends, the Master Server will cache the friend list and send updates to the friend
- /// list. The friends includes the name, userId, online state and the room (if any) for each requested user/friend.
- ///
- /// Use the friendList to update your UI and store it, if the UI should highlight changes.
- /// </remarks>
- void OnFriendListUpdate(List<FriendInfo> friendList);
- /// <summary>
- /// Called when this client created a room and entered it. OnJoinedRoom() will be called as well.
- /// </summary>
- /// <remarks>
- /// This callback is only called on the client which created a room (see OpCreateRoom).
- ///
- /// As any client might close (or drop connection) anytime, there is a chance that the
- /// creator of a room does not execute OnCreatedRoom.
- ///
- /// If you need specific room properties or a "start signal", implement OnMasterClientSwitched()
- /// and make each new MasterClient check the room's state.
- /// </remarks>
- void OnCreatedRoom();
- /// <summary>
- /// Called when the server couldn't create a room (OpCreateRoom failed).
- /// </summary>
- /// <remarks>
- /// Creating a room may fail for various reasons. Most often, the room already exists (roomname in use) or
- /// the RoomOptions clash and it's impossible to create the room.
- ///
- /// When creating a room fails on a Game Server:
- /// The client will cache the failure internally and returns to the Master Server before it calls the fail-callback.
- /// This way, the client is ready to find/create a room at the moment of the callback.
- /// In this case, the client skips calling OnConnectedToMaster but returning to the Master Server will still call OnConnected.
- /// Treat callbacks of OnConnected as pure information that the client could connect.
- /// </remarks>
- /// <param name="returnCode">Operation ReturnCode from the server.</param>
- /// <param name="message">Debug message for the error.</param>
- void OnCreateRoomFailed(short returnCode, string message);
- /// <summary>
- /// Called when the LoadBalancingClient entered a room, no matter if this client created it or simply joined.
- /// </summary>
- /// <remarks>
- /// When this is called, you can access the existing players in Room.Players, their custom properties and Room.CustomProperties.
- ///
- /// In this callback, you could create player objects. For example in Unity, instantiate a prefab for the player.
- ///
- /// If you want a match to be started "actively", enable the user to signal "ready" (using OpRaiseEvent or a Custom Property).
- /// </remarks>
- void OnJoinedRoom();
- /// <summary>
- /// Called when a previous OpJoinRoom call failed on the server.
- /// </summary>
- /// <remarks>
- /// Joining a room may fail for various reasons. Most often, the room is full or does not exist anymore
- /// (due to someone else being faster or closing the room).
- ///
- /// When joining a room fails on a Game Server:
- /// The client will cache the failure internally and returns to the Master Server before it calls the fail-callback.
- /// This way, the client is ready to find/create a room at the moment of the callback.
- /// In this case, the client skips calling OnConnectedToMaster but returning to the Master Server will still call OnConnected.
- /// Treat callbacks of OnConnected as pure information that the client could connect.
- /// </remarks>
- /// <param name="returnCode">Operation ReturnCode from the server.</param>
- /// <param name="message">Debug message for the error.</param>
- void OnJoinRoomFailed(short returnCode, string message);
- /// <summary>
- /// Called when a previous OpJoinRandom call failed on the server.
- /// </summary>
- /// <remarks>
- /// The most common causes are that a room is full or does not exist (due to someone else being faster or closing the room).
- ///
- /// This operation is only ever sent to the Master Server. Once a room is found by the Master Server, the client will
- /// head off to the designated Game Server and use the operation Join on the Game Server.
- ///
- /// When using multiple lobbies (via OpJoinLobby or a TypedLobby parameter), another lobby might have more/fitting rooms.<br/>
- /// </remarks>
- /// <param name="returnCode">Operation ReturnCode from the server.</param>
- /// <param name="message">Debug message for the error.</param>
- void OnJoinRandomFailed(short returnCode, string message);
- /// <summary>
- /// Called when the local user/client left a room, so the game's logic can clean up it's internal state.
- /// </summary>
- /// <remarks>
- /// When leaving a room, the LoadBalancingClient will disconnect the Game Server and connect to the Master Server.
- /// This wraps up multiple internal actions.
- ///
- /// Wait for the callback OnConnectedToMaster, before you use lobbies and join or create rooms.
- ///
- /// OnLeftRoom also gets called, when the application quits.
- /// It makes sense to check static ConnectionHandler.AppQuits before loading scenes in OnLeftRoom().
- /// </remarks>
- void OnLeftRoom();
- }
- /// <summary>
- /// Collection of "in room" callbacks for the Realtime Api to cover: Players entering or leaving, property updates and Master Client switching.
- /// </summary>
- /// <remarks>
- /// Classes that implement this interface must be registered to get callbacks for various situations.
- ///
- /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
- /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
- ///
- /// </remarks>
- /// \ingroup callbacks
- public interface IInRoomCallbacks
- {
- /// <summary>
- /// Called when a remote player entered the room. This Player is already added to the playerlist.
- /// </summary>
- /// <remarks>
- /// If your game starts with a certain number of players, this callback can be useful to check the
- /// Room.playerCount and find out if you can start.
- /// </remarks>
- void OnPlayerEnteredRoom(Player newPlayer);
- /// <summary>
- /// Called when a remote player left the room or became inactive. Check otherPlayer.IsInactive.
- /// </summary>
- /// <remarks>
- /// If another player leaves the room or if the server detects a lost connection, this callback will
- /// be used to notify your game logic.
- ///
- /// Depending on the room's setup, players may become inactive, which means they may return and retake
- /// their spot in the room. In such cases, the Player stays in the Room.Players dictionary.
- ///
- /// If the player is not just inactive, it gets removed from the Room.Players dictionary, before
- /// the callback is called.
- /// </remarks>
- void OnPlayerLeftRoom(Player otherPlayer);
- /// <summary>
- /// Called when room properties changed. The propertiesThatChanged contain only the keys that changed.
- /// </summary>
- /// <remarks>
- /// In most cases, this method gets called when some player changes the Room Properties.
- /// However, there are also "Well Known Properties" (which use byte keys) and this callback may include them.
- /// Especially when entering a room, the server will also send the required Well Known Properties and they
- /// are not filtered out for the OnRoomPropertiesUpdate callback.
- ///
- /// You can safely ignore the byte typed keys in propertiesThatChanged.
- ///
- /// Changing properties is usually done by Room.SetCustomProperties.
- /// </remarks>
- /// <param name="propertiesThatChanged"></param>
- void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged);
- /// <summary>
- /// Called when custom player-properties are changed.
- /// </summary>
- /// <remarks>
- /// Changing properties must be done by Player.SetCustomProperties, which causes this callback locally, too.
- /// </remarks>
- /// <param name="targetPlayer">Contains Player that changed.</param>
- /// <param name="changedProps">Contains the properties that changed.</param>
- void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps);
- /// <summary>
- /// Called after switching to a new MasterClient when the current one leaves.
- /// </summary>
- /// <remarks>
- /// This is not called when this client enters a room.
- /// The former MasterClient is still in the player list when this method get called.
- /// </remarks>
- void OnMasterClientSwitched(Player newMasterClient);
- }
- /// <summary>
- /// Event callback for the Realtime Api. Covers events from the server and those sent by clients via OpRaiseEvent.
- /// </summary>
- /// <remarks>
- /// Classes that implement this interface must be registered to get callbacks for various situations.
- ///
- /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
- /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
- ///
- /// </remarks>
- /// \ingroup callbacks
- public interface IOnEventCallback
- {
- /// <summary>Called for any incoming events.</summary>
- /// <remarks>
- /// To receive events, implement IOnEventCallback in any class and register it via AddCallbackTarget
- /// (either in LoadBalancingClient or PhotonNetwork).
- ///
- /// With the EventData.Sender you can look up the Player who sent the event.
- ///
- /// It is best practice to assign an eventCode for each different type of content and action, so the Code
- /// will be essential to read the incoming events.
- /// </remarks>
- void OnEvent(EventData photonEvent);
- }
- /// <summary>
- /// Interface for "WebRpc" callbacks for the Realtime Api. Currently includes only responses for Web RPCs.
- /// </summary>
- /// <remarks>
- /// Classes that implement this interface must be registered to get callbacks for various situations.
- ///
- /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
- /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
- ///
- /// </remarks>
- /// \ingroup callbacks
- public interface IWebRpcCallback
- {
- /// <summary>
- /// Called when the response to a WebRPC is available. See <see cref="LoadBalancingClient.OpWebRpc"/>.
- /// </summary>
- /// <remarks>
- /// Important: The response.ReturnCode is 0 if Photon was able to reach your web-service.<br/>
- /// The content of the response is what your web-service sent. You can create a WebRpcResponse from it.<br/>
- /// Example: WebRpcResponse webResponse = new WebRpcResponse(operationResponse);<br/>
- ///
- /// Please note: Class OperationResponse is in a namespace which needs to be "used":<br/>
- /// using ExitGames.Client.Photon; // includes OperationResponse (and other classes)
- /// </remarks>
- /// <example>
- /// public void OnWebRpcResponse(OperationResponse response)
- /// {
- /// Debug.LogFormat("WebRPC operation response {0}", response.ToStringFull());
- /// switch (response.ReturnCode)
- /// {
- /// case ErrorCode.Ok:
- /// WebRpcResponse webRpcResponse = new WebRpcResponse(response);
- /// Debug.LogFormat("Parsed WebRPC response {0}", response.ToStringFull());
- /// if (string.IsNullOrEmpty(webRpcResponse.Name))
- /// {
- /// Debug.LogError("Unexpected: WebRPC response did not contain WebRPC method name");
- /// }
- /// if (webRpcResponse.ResultCode == 0) // success
- /// {
- /// switch (webRpcResponse.Name)
- /// {
- /// // todo: add your code here
- /// case GetGameListWebRpcMethodName: // example
- /// // ...
- /// break;
- /// }
- /// }
- /// else if (webRpcResponse.ResultCode == -1)
- /// {
- /// Debug.LogErrorFormat("Web server did not return ResultCode for WebRPC method=\"{0}\", Message={1}", webRpcResponse.Name, webRpcResponse.Message);
- /// }
- /// else
- /// {
- /// Debug.LogErrorFormat("Web server returned ResultCode={0} for WebRPC method=\"{1}\", Message={2}", webRpcResponse.ResultCode, webRpcResponse.Name, webRpcResponse.Message);
- /// }
- /// break;
- /// case ErrorCode.ExternalHttpCallFailed: // web service unreachable
- /// Debug.LogErrorFormat("WebRPC call failed as request could not be sent to the server. {0}", response.DebugMessage);
- /// break;
- /// case ErrorCode.HttpLimitReached: // too many WebRPCs in a short period of time
- /// // the debug message should contain the limit exceeded
- /// Debug.LogErrorFormat("WebRPCs rate limit exceeded: {0}", response.DebugMessage);
- /// break;
- /// case ErrorCode.InvalidOperation: // WebRPC not configured at all OR not configured properly OR trying to send on name server
- /// if (PhotonNetwork.Server == ServerConnection.NameServer)
- /// {
- /// Debug.LogErrorFormat("WebRPC not supported on NameServer. {0}", response.DebugMessage);
- /// }
- /// else
- /// {
- /// Debug.LogErrorFormat("WebRPC not properly configured or not configured at all. {0}", response.DebugMessage);
- /// }
- /// break;
- /// default:
- /// // other unknown error, unexpected
- /// Debug.LogErrorFormat("Unexpected error, {0} {1}", response.ReturnCode, response.DebugMessage);
- /// break;
- /// }
- /// }
- ///
- /// </example>
- void OnWebRpcResponse(OperationResponse response);
- }
- /// <summary>
- /// Interface for <see cref="EventCode.ErrorInfo"/> event callback for the Realtime Api.
- /// </summary>
- /// <remarks>
- /// Classes that implement this interface must be registered to get callbacks for various situations.
- ///
- /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
- /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
- ///
- /// </remarks>
- /// \ingroup callbacks
- public interface IErrorInfoCallback
- {
- /// <summary>
- /// Called when the client receives an event from the server indicating that an error happened there.
- /// </summary>
- /// <remarks>
- /// In most cases this could be either:
- /// 1. an error from webhooks plugin (if HasErrorInfo is enabled), read more here:
- /// https://doc.photonengine.com/en-us/realtime/current/gameplay/web-extensions/webhooks#options
- /// 2. an error sent from a custom server plugin via PluginHost.BroadcastErrorInfoEvent, see example here:
- /// https://doc.photonengine.com/en-us/server/current/plugins/manual#handling_http_response
- /// 3. an error sent from the server, for example, when the limit of cached events has been exceeded in the room
- /// (all clients will be disconnected and the room will be closed in this case)
- /// read more here: https://doc.photonengine.com/en-us/realtime/current/gameplay/cached-events#special_considerations
- ///
- /// If you implement <see cref="IOnEventCallback.OnEvent"/> or <see cref="LoadBalancingClient.EventReceived"/> you will also get this event.
- /// </remarks>
- /// <param name="errorInfo">Object containing information about the error</param>
- void OnErrorInfo(ErrorInfo errorInfo);
- }
- /// <summary>
- /// Container type for callbacks defined by IConnectionCallbacks. See LoadBalancingCallbackTargets.
- /// </summary>
- /// <remarks>
- /// While the interfaces of callbacks wrap up the methods that will be called,
- /// the container classes implement a simple way to call a method on all registered objects.
- /// </remarks>
- public class ConnectionCallbacksContainer : List<IConnectionCallbacks>, IConnectionCallbacks
- {
- private readonly LoadBalancingClient client;
- public ConnectionCallbacksContainer(LoadBalancingClient client)
- {
- this.client = client;
- }
- public void OnConnected()
- {
- this.client.UpdateCallbackTargets();
- foreach (IConnectionCallbacks target in this)
- {
- target.OnConnected();
- }
- }
- public void OnConnectedToMaster()
- {
- this.client.UpdateCallbackTargets();
- foreach (IConnectionCallbacks target in this)
- {
- target.OnConnectedToMaster();
- }
- }
- public void OnRegionListReceived(RegionHandler regionHandler)
- {
- this.client.UpdateCallbackTargets();
- foreach (IConnectionCallbacks target in this)
- {
- target.OnRegionListReceived(regionHandler);
- }
- }
- public void OnDisconnected(DisconnectCause cause)
- {
- this.client.UpdateCallbackTargets();
- foreach (IConnectionCallbacks target in this)
- {
- target.OnDisconnected(cause);
- }
- }
- public void OnCustomAuthenticationResponse(Dictionary<string, object> data)
- {
- this.client.UpdateCallbackTargets();
- foreach (IConnectionCallbacks target in this)
- {
- target.OnCustomAuthenticationResponse(data);
- }
- }
- public void OnCustomAuthenticationFailed(string debugMessage)
- {
- this.client.UpdateCallbackTargets();
- foreach (IConnectionCallbacks target in this)
- {
- target.OnCustomAuthenticationFailed(debugMessage);
- }
- }
- }
- /// <summary>
- /// Container type for callbacks defined by IMatchmakingCallbacks. See MatchMakingCallbackTargets.
- /// </summary>
- /// <remarks>
- /// While the interfaces of callbacks wrap up the methods that will be called,
- /// the container classes implement a simple way to call a method on all registered objects.
- /// </remarks>
- public class MatchMakingCallbacksContainer : List<IMatchmakingCallbacks>, IMatchmakingCallbacks
- {
- private readonly LoadBalancingClient client;
- public MatchMakingCallbacksContainer(LoadBalancingClient client)
- {
- this.client = client;
- }
- public void OnCreatedRoom()
- {
- this.client.UpdateCallbackTargets();
- foreach (IMatchmakingCallbacks target in this)
- {
- target.OnCreatedRoom();
- }
- }
- public void OnJoinedRoom()
- {
- this.client.UpdateCallbackTargets();
- foreach (IMatchmakingCallbacks target in this)
- {
- target.OnJoinedRoom();
- }
- }
- public void OnCreateRoomFailed(short returnCode, string message)
- {
- this.client.UpdateCallbackTargets();
- foreach (IMatchmakingCallbacks target in this)
- {
- target.OnCreateRoomFailed(returnCode, message);
- }
- }
- public void OnJoinRandomFailed(short returnCode, string message)
- {
- this.client.UpdateCallbackTargets();
- foreach (IMatchmakingCallbacks target in this)
- {
- target.OnJoinRandomFailed(returnCode, message);
- }
- }
- public void OnJoinRoomFailed(short returnCode, string message)
- {
- this.client.UpdateCallbackTargets();
- foreach (IMatchmakingCallbacks target in this)
- {
- target.OnJoinRoomFailed(returnCode, message);
- }
- }
- public void OnLeftRoom()
- {
- this.client.UpdateCallbackTargets();
- foreach (IMatchmakingCallbacks target in this)
- {
- target.OnLeftRoom();
- }
- }
- public void OnFriendListUpdate(List<FriendInfo> friendList)
- {
- this.client.UpdateCallbackTargets();
- foreach (IMatchmakingCallbacks target in this)
- {
- target.OnFriendListUpdate(friendList);
- }
- }
- }
- /// <summary>
- /// Container type for callbacks defined by IInRoomCallbacks. See InRoomCallbackTargets.
- /// </summary>
- /// <remarks>
- /// While the interfaces of callbacks wrap up the methods that will be called,
- /// the container classes implement a simple way to call a method on all registered objects.
- /// </remarks>
- internal class InRoomCallbacksContainer : List<IInRoomCallbacks>, IInRoomCallbacks
- {
- private readonly LoadBalancingClient client;
- public InRoomCallbacksContainer(LoadBalancingClient client)
- {
- this.client = client;
- }
- public void OnPlayerEnteredRoom(Player newPlayer)
- {
- this.client.UpdateCallbackTargets();
- foreach (IInRoomCallbacks target in this)
- {
- target.OnPlayerEnteredRoom(newPlayer);
- }
- }
- public void OnPlayerLeftRoom(Player otherPlayer)
- {
- this.client.UpdateCallbackTargets();
- foreach (IInRoomCallbacks target in this)
- {
- target.OnPlayerLeftRoom(otherPlayer);
- }
- }
- public void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
- {
- this.client.UpdateCallbackTargets();
- foreach (IInRoomCallbacks target in this)
- {
- target.OnRoomPropertiesUpdate(propertiesThatChanged);
- }
- }
- public void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProp)
- {
- this.client.UpdateCallbackTargets();
- foreach (IInRoomCallbacks target in this)
- {
- target.OnPlayerPropertiesUpdate(targetPlayer, changedProp);
- }
- }
- public void OnMasterClientSwitched(Player newMasterClient)
- {
- this.client.UpdateCallbackTargets();
- foreach (IInRoomCallbacks target in this)
- {
- target.OnMasterClientSwitched(newMasterClient);
- }
- }
- }
- /// <summary>
- /// Container type for callbacks defined by ILobbyCallbacks. See LobbyCallbackTargets.
- /// </summary>
- /// <remarks>
- /// While the interfaces of callbacks wrap up the methods that will be called,
- /// the container classes implement a simple way to call a method on all registered objects.
- /// </remarks>
- internal class LobbyCallbacksContainer : List<ILobbyCallbacks>, ILobbyCallbacks
- {
- private readonly LoadBalancingClient client;
- public LobbyCallbacksContainer(LoadBalancingClient client)
- {
- this.client = client;
- }
- public void OnJoinedLobby()
- {
- this.client.UpdateCallbackTargets();
- foreach (ILobbyCallbacks target in this)
- {
- target.OnJoinedLobby();
- }
- }
- public void OnLeftLobby()
- {
- this.client.UpdateCallbackTargets();
- foreach (ILobbyCallbacks target in this)
- {
- target.OnLeftLobby();
- }
- }
- public void OnRoomListUpdate(List<RoomInfo> roomList)
- {
- this.client.UpdateCallbackTargets();
- foreach (ILobbyCallbacks target in this)
- {
- target.OnRoomListUpdate(roomList);
- }
- }
- public void OnLobbyStatisticsUpdate(List<TypedLobbyInfo> lobbyStatistics)
- {
- this.client.UpdateCallbackTargets();
- foreach (ILobbyCallbacks target in this)
- {
- target.OnLobbyStatisticsUpdate(lobbyStatistics);
- }
- }
- }
- /// <summary>
- /// Container type for callbacks defined by IWebRpcCallback. See WebRpcCallbackTargets.
- /// </summary>
- /// <remarks>
- /// While the interfaces of callbacks wrap up the methods that will be called,
- /// the container classes implement a simple way to call a method on all registered objects.
- /// </remarks>
- internal class WebRpcCallbacksContainer : List<IWebRpcCallback>, IWebRpcCallback
- {
- private LoadBalancingClient client;
- public WebRpcCallbacksContainer(LoadBalancingClient client)
- {
- this.client = client;
- }
- public void OnWebRpcResponse(OperationResponse response)
- {
- this.client.UpdateCallbackTargets();
- foreach (IWebRpcCallback target in this)
- {
- target.OnWebRpcResponse(response);
- }
- }
- }
- /// <summary>
- /// Container type for callbacks defined by <see cref="IErrorInfoCallback"/>. See <see cref="LoadBalancingClient.ErrorInfoCallbackTargets"/>.
- /// </summary>
- /// <remarks>
- /// While the interfaces of callbacks wrap up the methods that will be called,
- /// the container classes implement a simple way to call a method on all registered objects.
- /// </remarks>
- internal class ErrorInfoCallbacksContainer : List<IErrorInfoCallback>, IErrorInfoCallback
- {
- private LoadBalancingClient client;
- public ErrorInfoCallbacksContainer(LoadBalancingClient client)
- {
- this.client = client;
- }
- public void OnErrorInfo(ErrorInfo errorInfo)
- {
- this.client.UpdateCallbackTargets();
- foreach (IErrorInfoCallback target in this)
- {
- target.OnErrorInfo(errorInfo);
- }
- }
- }
- /// <summary>
- /// Class wrapping the received <see cref="EventCode.ErrorInfo"/> event.
- /// </summary>
- /// <remarks>
- /// This is passed inside <see cref="IErrorInfoCallback.OnErrorInfo"/> callback.
- /// If you implement <see cref="IOnEventCallback.OnEvent"/> or <see cref="LoadBalancingClient.EventReceived"/> you will also get <see cref="EventCode.ErrorInfo"/> but not parsed.
- ///
- /// In most cases this could be either:
- /// 1. an error from webhooks plugin (if HasErrorInfo is enabled), read more here:
- /// https://doc.photonengine.com/en-us/realtime/current/gameplay/web-extensions/webhooks#options
- /// 2. an error sent from a custom server plugin via PluginHost.BroadcastErrorInfoEvent, see example here:
- /// https://doc.photonengine.com/en-us/server/current/plugins/manual#handling_http_response
- /// 3. an error sent from the server, for example, when the limit of cached events has been exceeded in the room
- /// (all clients will be disconnected and the room will be closed in this case)
- /// read more here: https://doc.photonengine.com/en-us/realtime/current/gameplay/cached-events#special_considerations
- /// </remarks>
- public class ErrorInfo
- {
- /// <summary>
- /// String containing information about the error.
- /// </summary>
- public readonly string Info;
- public ErrorInfo(EventData eventData)
- {
- this.Info = eventData[ParameterCode.Info] as string;
- }
- public override string ToString()
- {
- return string.Format("ErrorInfo: {0}", this.Info);
- }
- }
- }
|