From 7415b74e8fab5f0236be6450ff1dbaae189630ae Mon Sep 17 00:00:00 2001 From: LongLy Date: Mon, 13 Jan 2025 15:36:08 -0700 Subject: [PATCH] Added CommonUser Plugins. Implemented GameplayStackTags --- Plugins/CommonUser/CommonUser.uplugin | 38 + Plugins/CommonUser/Resources/Icon128.png | Bin 0 -> 12699 bytes .../Source/CommonUser/CommonUser.Build.cs | 71 + .../AsyncAction_CommonUserInitialize.cpp | 105 + .../Private/CommonSessionSubsystem.cpp | 1618 ++++++++++ .../Private/CommonUserBasicPresence.cpp | 120 + .../CommonUser/Private/CommonUserModule.cpp | 22 + .../Private/CommonUserSubsystem.cpp | 2684 +++++++++++++++++ .../CommonUser/Private/CommonUserTypes.cpp | 17 + .../Public/AsyncAction_CommonUserInitialize.h | 64 + .../Public/CommonSessionSubsystem.h | 471 +++ .../Public/CommonUserBasicPresence.h | 59 + .../CommonUser/Public/CommonUserModule.h | 14 + .../CommonUser/Public/CommonUserSubsystem.h | 649 ++++ .../CommonUser/Public/CommonUserTypes.h | 218 ++ .../OLSExperienceActionSetDataAsset.cpp | 2 +- .../OLSExperienceDefinitionDataAsset.cpp | 2 +- .../OLSExperienceManagerComponent.cpp | 6 +- Source/ols/Private/GameModes/OLSGameMode.cpp | 529 ++++ Source/ols/Private/Player/OLSPlayerState.cpp | 50 +- .../ols/Private/Systems/OLSAssetManager.cpp | 4 +- .../Private/Systems/OLSGameplayTagStack.cpp | 104 + .../GameModes/OLSExperienceManagerComponent.h | 14 +- Source/ols/Public/GameModes/OLSGameMode.h | 77 + Source/ols/Public/Player/OLSPlayerState.h | 37 +- Source/ols/Public/Systems/OLSAssetManager.h | 4 +- .../ols/Public/Systems/OLSGameplayTagStack.h | 97 + Source/ols/ols.Build.cs | 2 +- 28 files changed, 7051 insertions(+), 27 deletions(-) create mode 100644 Plugins/CommonUser/CommonUser.uplugin create mode 100644 Plugins/CommonUser/Resources/Icon128.png create mode 100644 Plugins/CommonUser/Source/CommonUser/CommonUser.Build.cs create mode 100644 Plugins/CommonUser/Source/CommonUser/Private/AsyncAction_CommonUserInitialize.cpp create mode 100644 Plugins/CommonUser/Source/CommonUser/Private/CommonSessionSubsystem.cpp create mode 100644 Plugins/CommonUser/Source/CommonUser/Private/CommonUserBasicPresence.cpp create mode 100644 Plugins/CommonUser/Source/CommonUser/Private/CommonUserModule.cpp create mode 100644 Plugins/CommonUser/Source/CommonUser/Private/CommonUserSubsystem.cpp create mode 100644 Plugins/CommonUser/Source/CommonUser/Private/CommonUserTypes.cpp create mode 100644 Plugins/CommonUser/Source/CommonUser/Public/AsyncAction_CommonUserInitialize.h create mode 100644 Plugins/CommonUser/Source/CommonUser/Public/CommonSessionSubsystem.h create mode 100644 Plugins/CommonUser/Source/CommonUser/Public/CommonUserBasicPresence.h create mode 100644 Plugins/CommonUser/Source/CommonUser/Public/CommonUserModule.h create mode 100644 Plugins/CommonUser/Source/CommonUser/Public/CommonUserSubsystem.h create mode 100644 Plugins/CommonUser/Source/CommonUser/Public/CommonUserTypes.h create mode 100644 Source/ols/Private/GameModes/OLSGameMode.cpp create mode 100644 Source/ols/Private/Systems/OLSGameplayTagStack.cpp create mode 100644 Source/ols/Public/GameModes/OLSGameMode.h create mode 100644 Source/ols/Public/Systems/OLSGameplayTagStack.h diff --git a/Plugins/CommonUser/CommonUser.uplugin b/Plugins/CommonUser/CommonUser.uplugin new file mode 100644 index 0000000..889ddbf --- /dev/null +++ b/Plugins/CommonUser/CommonUser.uplugin @@ -0,0 +1,38 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "CommonUser", + "Description": "Provides gameplay code and blueprint wrappers for online and platform operations.", + "Category": "Gameplay", + "CreatedBy": "Epic Games, Inc.", + "CreatedByURL": "https://www.epicgames.com", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": false, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "CommonUser", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "OnlineSubsystem", + "Enabled": true + }, + { + "Name": "OnlineSubsystemUtils", + "Enabled": true + }, + { + "Name": "OnlineServices", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/CommonUser/Resources/Icon128.png b/Plugins/CommonUser/Resources/Icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..1231d4aad4d0d462fb7b178eb5b30aa61a10df0b GIT binary patch literal 12699 zcmbta^;gv0*Zs`U4U&S$&|T8qCEeZ9Eg&T@fV6ZsC`gAiNDN4~NP~2D_b~7C{TtqO z&%XQTd(K(+?0wgb)=*Qx!6e57002ixQC90ehW-!esQ>N1#VtqwBMf&%Lr(y}BK#jf zKz1$}0AQ*+$jE4D*t>bTdD^?VLzHA>AnqUCY#p3!0Kj)CPuosM`+!93ZuMGPISQJp z?50JG4$+d1g%Tw(uux;*zmK9WS|rx&A&`?prWh)WLW+-vekImq!;ZmRK-;GN79aLK zDrV$qBjCH!T*uw+_)F8g_+HgjUc)3B3>`aNkw=pcid`=KmS8<>uy0^vn?o`Llg=H$ zM{oE*?Fpv^0rx?oqO3G9v@QVT`xgrxfT`xdxZXq}@D8Q3OhC{tAedK@pfWm?2$1xT zm;M1r%7dVJnGD)MAu?bwYHhUzXs`nojKRBq0chTRRsaYvPNgOW6(#`?LYpXAz+MEX zn$(Mt0}QwTB3tD?Az*^LOfIcrRh_$YBOLV+R}XG z5igtl_3B*-O|*0}b3gqw;=|?|+Y^%b8Xr*SC=LopVlOkbM!HpI#5eGQZQcREIlI=mKs7Qw4`2&0$Ifv(8i;aW`*BV_b4L2ilu`LM-ge#C@1kLa%;utKy(!; zFU3BBg(6Ml+ml3wfOnzK5giKLsUh{6Vl&uHGHqo74Xr4$WR4Ad4B%OG#)cnOv;1Tc`kX!bJFq?9Q)GPDys^pRP;m~XgrKWNx7u@TiRc8ds6#5huVFwc7lItZ`CrU^ruG;6!tUr zk*J#RIFBD>0arM>Liq#X$RKG>+)!Cm1E4LSL#;eX&h-&Xxo*Gltot9 zmAUCi6bBi?qfrfitNd1%Db_6fX};Al0Ku|;-Qdec?SxYq;T^))$MAD}@$)B^Uzu>q zU$J5p%cZ6(mQGCl5dz0@%Fm`XFQf?`&Q&X_luDSq&(v~k;*I8~%) zq#IN!R%%u%9Ch;7oRsGM=#=|q_!NRGHTa&|JO$|qd zQwc@UFIk^%*V5C>{4O(SzKUDvs$b{cSVVwm+iZXXWGM@xD3?m~7E)xeT}rd}lyqpk`23Jybo- z)>3Wz!Tdu+MMPzAd~E#N_*@oWju`j+yS<#focWx!77HU^Bev$U=2jb}`fZ~hhNsOP zuHi;Ph9w5NMy3t&)p^zQbHA#8l@gS;simk@=Fi#vuDfU+ZZ21 zJEZ6ksSsoE)4l&^>h5?6;boiK`o$BeuZ3+=#8L^N)uB5*)ztPw$BEU{cYB!=NfQpZ z;Tl2vb5m%RyOy!PgRmLHBg6G0B;wtp49Nd*XYl#_S&{KvlYNv;mtD=V<5m}{Wq;4d zB3{AaD7qxj&f6|Az+r1RHfxY)pyaIlMu>x@hTqk>Ywh{uDsnS#6KgAgG?R14)ZMRW zqW3zyl%$;F6`OFnq)L>UVCuOPK1&(NSNcmrANqJqzh25-I~vYE{C}brWK3Azs$D9w zsQM=#Cw1`o(e?9`u+lRGRqDbYi^f?74D+3wJ8 z*Y?wBl}&j4OTTMu3+LN3v|*=)#3~d+cFbn!ANx8+O!F*g^>#M;w%y~=BSPtw`K;q7 zV+|wAi2}K21&EVZy{|Tsn@b{;_1P&6b~~#ah3Z8;{FX7dh*4N0^iZorTVtA8TxQiP zPxLctf;t)eRh>f2dPYKfnm|rRSh|=y;ekgh^Czb22Aqa#O_q-lc@*Nr(J?hd%cL2^ z!3#_)zB?3=ZX?}UE2)j;m3?g=CT*u}4|Z4C^Nn%SD>8O7a9wd0ml|=_^cqiYZsnFa zGsc;ge}y&6w0-XuZSAlr9iA8$k5q;Xj@J*JL?=@A~JIBB0}z_jq>MxZ@5k zKHRme3({4cwVkzjQhI8*lcFmpF z`5f)+Cu1w)cJ(pwKXZqx{?7`_RCu|(qK1C&uXKhTmJUMyrr2Fhe$7kE3k>3TSg~0C z)*P^BJ+bD9=XTbP@3k>4hlt%1=@6MPxoq{itY6+C)Nj?#t`#rTH562#nWzL40z&MSYnyZ*bIHIjcp9~t2jqrVn? z7*DG^)H}?tB~PRlW&TCZN*KSaES#+bJHmVlul}qk+@XetO}-@EB;d)QBxEIwM&Lvo z9&WR1y{D5NpA{df4_o!AuDIho3jvQ>9NSuTxSG$Vi!2&(=Kb z%m3+3h_#}YDggM?|EEL40N?@fA0GgKHx~dLS^$7>CIFDSC7bul0|3K-lB|@D@6vIg zUn1SS;ojNP>S$%fVW z#12W5G<6LP^A;bT0=v(A6_TS0O_j}`0llI>mpYs z_ua-5ci#0whKVQN93R15{6_uVehg4Euk`|D@RU&F{SH*#&b_LN&|;^jR96dZgv#CS zjYCRIa7~W#;;dUp88xc;#T&(d{&lIY9_ZlJxmt|7CR0e4B&^g^68QiSZd#nLHcs>g zS7F~b_R1Py-n&YkeK=^W0qjs;vv1&R%x^N~VhZK7c=%=jX0s9uVM^HrGpp7sx>pcCh@s?Z6#4M;F&Bb4;%rgn!{ zf8A<+pdy3t&4>~BPMQVT8(Bh?!P|%;7E&X5tp9B9S>+`~LOBWI1G-5TE-nD%z|%!fM@p4h zpy&YTiA5jH0fN--j+JLJl&y=>8M^-WBh06Hph_Bmq)hnJ9Jo$W1xY?3<(Td$9y&h@ zLyI>A7Uj)q!1d=o(O$7fGz3a0+e%2USHKaaL{jNM4IxH52p-CTpBMXn{hM`FxrUYq zfiMLrWWupqg8RT3`CNDDXsz!!0J6$t)iGv8(KC;Y9;IUoFD9)7%8!NnY>x{yAOj$1 zl*enoLs=*k$yF<~WO~?@Ex5eZYMd3e_+A1?#9QM&lZ z{nZrIA0_&Pp|6}qo~oG7bYColkn+j;a@zn~8eIv>StN0SNNisxsR^lt9(w$rEY)!& z&Z2=BiV=V?HAm1mUc_EHB;c13EL$Dz1{3s8RYMU_JV>^$-BUCXc}Y~P2(>>_T{=4| zr;;x=Jj&PFZK-Z@$U?TLtCh@0Wk%788QS`a9s^>)&l4_)!jBF!z?x>WdPh@dkfFwE z$D-dbEunIJQvc&JN@-8czeiE74>lv876np#%}Mq?GjP7h>OOr4Y+r)j%aT~v*f78% zs*@*io-x)#JiK~cbg#h@O3Wtj=;wDnJ(9L%q<#@qC;YBR4Uj3M@tAq6h=Nl zj}Kc^k;MMGCvNrIJ`feA2V!Qnu`=(v<({>QRQ)LXxjaqSTb_bM9jQ?}xP3P$4y zdJ&Hguo<4CMguj7`iXA`vv~Dx^NV6Qogq8Kia6rEf<76~-AggQzeYgdoxSM_yH&g) z1tN>@Dsma$cw%#P$cPTQeyniL_StUQkWxS1iqoCuWJx=2rD82ph;1o+f4Q=!6NzR4X;_uw4gVIY4sNl;4oxe8ivoKg;xvUI}qz9 zBn-}O1y^?Fw?vkh{z{7h@49C!w4!g)WjvYOHWe6mDI7aN-{}KP&?JePXlHSDcsuVmZ)WsJIzS%0ly19Px0i8coNv2edS{PU& zD#d8ZR81uNj+uWp{SnNnW@!2&aTmIwpI05o8OInrji(Tih8cjufvgxpM3|ZZsufM# zBXGbg7L~Nw25dZ_5L&aGwoM5IZXDGKUBo-8i7I@JpD{Nu_;+bP z1LeMlFIEBMPZnXbBsSEj_ddcv$5&_Ta)KB^6&mp|!ai=~%E{RiA zRzaI#eU{m?&q_93W_ihh)8d7qiMNtfpb;KW(il!6*g0J)YO%MfmUj1KEGWd_37@gF z0){+%i1gF@z%xkj-3CgSL&kKMNvxSCrX;Iu3`#~}r`c~7(OqZJ0T!>3BP8IqH_p>R z^aW?{c(hNmDy-+7q)H#AEO}PY$6$vt*biXBhDJ5go96o1?rJ*i4luEw z+1@@HhNI{O=?sP`vX&^zm9YAhT-Uw1g?OXC&lnad8Jcw?e*lN8tlO4d+sh(Ald-I#3V~!(cg{ct*V$oRngnx zYRZ4PKeT-UzT_DC6-9Y&YAMSWcXS1rk5M{^UL;2|zO~Y0Oyww{{A#J1Kt5gR44=^? zHUTF_`s;HhfeA$13maC<&?UvjN2M6jg7pmXhgg>N@wfqW3`vqc6_)xKow0U17W#ap z>BWDLE)v2E;UaY5ykrWj2q8brVmpV(9+YE-6}&vm)b0b!2Q( z*2G$j_@XI6^e^fzemCl0O84NV0|z}JTF<#wPFGt(BD@mmnUMIbP7uRMG+9a?VPsYH zi(9=efpI5B@q4JK>iWB%MmTkII@l0{lX7*#0{Axyy5`;2JT0I^@iHyLCkpIKBTq#ymvf- z`F8j3hi6SeV;Vi19lWpHk*91Szt**Tc)UTO4LJ=8s+fsqgdh3!98T_0J$5s{m zLzi>LZbcPD^WZ<)q4l%^>qp5zXbiO&0ouH910(}11ARu&x~!j=O-!?x z_4u*R#x1xB5 z)LGbvSyDfym8ejr&kP42=_huk4v>h%qU#@di>!t`0m_e|V$5X8ZGtMxO%qw+^ce}J zR7Q@X#oE$F%9@Zc38vsts~1x$I*1mjywg@p!T893n;E9M#Oh*0{8hv_kS~t$M~8*| zI5w`3Ic8m^WHP2Al9g<^G7e7x#X{BpK@+^eCH00g2LPxS&*S2pJM-X|gxovU8z5YF8BTe=8|`)T%oTK?=Ax?>g1)*>0XI zh!MNc?f6a1S&^zU^0OmcXatpx+aOD9q_NMBXH zcteYxjadqLLaA*;z=0F%ITwkjWYRvnKSp`_v`zC4|8s8xj);mhFU&%L5p$g z6Gb>2Ck7x^HmYf%_7*9)k55sJdxB*~+HJ#F{Lh7+P0WPqx#-`?N3&Fy zv(XLt+zFVG)fCsEGrbrgfv}J-$dQbX@>(*#-aSkPZB&j}yL)8IJ#W?%NLlrjw2>QR z41!7O)ZUSHkO&M~>ynR`* zC9ixLKm}f!l8y{gra>shS9fuALo`A7dt30lG2M=3CGFEEP-tLRnZjT{`%KEwx*ffw z$0^Z0KU&@)-B3-OB80ui+jl%7qhA){r8W9;KqAU7Q z?VZ3n$;9mHU4cCKsu!D)cv;c8$s!r)k!JsxYs> zjXq?W?icPuYfbp1)gMK0R2nHR&ME_>X0#i=9`X@cogiA`WdOs*GFhiRg-WCukahJZ`Gbvp(q+~_daG~-4x$Vh$qC1YrDguY}qe@6a_T#V=F8@ zaY>$D&|8LQ^vC;Gz8)24=-#MZ&~=YXzL4>m%^BwHM)Y6;jIX1JAWsrV)5wNd)JnD2 zh8ls-SoX-?^oPqd$dWS!f@J)>hn~zys&QRPHT?P6VNWm)dGl5MkK<_NFS?oanE#1%b;-?SB3mE!p#F zN}IYu&H@e6nqFdGirCy(XPhKORot46u<(Dj=kL;y>a?#k<7|pZ)BKetCs~(txpe9P zVTkf550T3!C*tii8ra7}Q1xcmCxM!aE30+VNk)sPpG`Xdh$~bcQIPvjDY`03l!@FA zyWUO=jFjxOBwZqyQ@Tjj2`6-@YD(6g_&wZLvL0xd5i(|iA4{jhLp>cfO+LOkPD?xW zFf~GCUm#eCk-Wga{%ww)xPCPTIvfxgZ`XpFJR6(dK1Tx~H9<{M^oOV5hdsHTk|-O3 z<=Qr{&f6zWf+S^C;lL&(TUTOI37l_cJ2ztM4}pO|5>Hyi!o3`rA&sMz17xm^rFhr? z1PJ|vWnG5|umY3?EFBao56^gD$)ox(G5Wu5iZ3`_G zk=etx_Ld{J%f#-kFSURUKR9(6cOtuLjYFYc#{d}*vB z+MHiwifwGWzj-n1nhk&Hr>s#<Gs|L5YMDC2lcs z=HAVZ*-Cb+T*KEN9M(@hv7?25#+~?6a~Me?m#OF1hO~~G`}I^l>aqqan1Q2ov-6P{Ax`Rtqy`vLw?J{f7zmykPi9Cn zezwzl812$SV`ZB+y% ziUb`Z$y|1Nw2n|mk|@tV-yHer()W_EZ*k7}?Ec})!quU>z$>XfvJ@3{`q_(lPO*WOXZdlKg=>hcgv&E? zIM7vxXb4ydmxVU4V|#bj4}6Z3$Q_orEP?Kycg~AHina%H6&DW|$5amT;|JUY^qhBJ zeorExDe0q+_GBPd!tunf!vsTz7I~}3CRHZr;laFhC#!b4XVrm|RLgBAalcOw^Nb%q z5&h-zf9|(FtC~69aX9414`aSk?OV+D!dDz_b8c+2lKyGXdfNT@z?2s6<(D~E0(>?s z<4eV~@!{IH@iFZ?mpBy(HqwrROVbSVZvhav5_eQU9${|gbW8AN^I8Y)!qrIl58xm6 ziy-T(V~Ks%z5UL__Gdz((Rtw^gu}d5vO|KdSIKn$ug0}yECTL>>r^G%-KxA`x!e#^ z=hnIZ47A}xS5v&*uBPAN`i>N@&v?xr!SR$Wjc~>h@cQ%{$38j)U>yvV5bJw~0?aj(DH01FS4>`1Ud@sWk zO27rtW!x=P`k|0pomO2fwxx2TxmUqS`I^&Ict+ysA|ymQnCwBE+mr84xPsa0%^72X zkS1aN>bFj=^DqtnM^x`}USRSLwm5d{Z1tX>RVZhh0U#`DS!Wj{tJd(p-T8^;)_J`z zpFX~zQAVToCVs+jY;63XTqyQEU(a=JKkMM5W-NRBglo^w5&Da=c0XsnO`sDKQs8jV zN>5P1{g2|yjS>tQNbxycMJ#+gI;(oFXu7KH(Lw|g@3;1ok=_7N;bj8`o%z{U z5;@|<5tPuGwWbT$pS_FY7mPYgE^}3GAqC$+XXGos9xoTb+E(Bzy&xl={&$LC-BQki zFTK}B7+?{U@Dr$;67tdhYDC(Oq)Kq7i+eBI-LsUXG0WyaZnY|RtaecM%`^2?Ww1&K z+-=O9T@7>lSXo41P(R|&GY*(j(V0lDNZw!{tr9TuLk~rlDxw-Q*q>q zeI1rh4W1lAzVC7aH`97^B=bzJ+0b?AX=OsiwITRgc{nXvKm#a@W>Fr&y%;*OO zbgdo-r83usKQ}$}XzkQa)*ZL+3p~A;l@I2Nc5tgX$TH{SO0Ut))OJ5C?a(S%U&@$U zt{lr}afDy`!({8?VehGbf=}M$j_N2eM|{Ff$H=EK_<)sK_LO)s;Xt<+oj% z1(S6*ghH)~3NbGS0`eb^)n5+!=Uz8zeINj?J-ff7%DFp{+;PsRbbXAF+B-n_P92#B z!)+Mdx=#ikd{%?B{p(le?+RYdVF}CI9}r_5Ff37bsgM-sc7S5|uW0BQ!4N^_QK5)| z0vA6c8bK5#FOS#n6%>Gp1WOD1AD>evr-hI}-b5d}%Gi{cRBIisXcT&qTem;z&i-E! zKmTqjiKm}&SIaFfIcv?{-$gHaQ}3qcQ*va}J|*dgE3+t8%O#V$XG{MK)x%~Ar5P?U zmrM=Gsn!W&dpp!%K##oj#w5GESNe{Dz-#KsTK~WML|?D6BY@f#)M(O+zOO(L;EsI# zJh*mu-NT_YTfP?R+IjI23$U`gXbR@)*H0KyCq(Hp!z;Ag=<6*enKP&>U6+;QXmGVg zc~4MgS>OrA0yjv0v~o8isq^DYtUrX@r1idBWL=0`cx(N#dHq``{i!A%z8}Uw)Du7s zmmus~y1r{)ToN!Q(dvxXsSVg|8c}pyxtRk`5p=i%!ux2ubqpcn z=0~h)t)CsG#ccwM5WVee^lT)tL6gU%W8v%Id(qqm+SfluKaxVxlMQhQq*(pzOD4{2 zsXR64_jb+Q6T}|K<8w3HdJS4YbkbEt&q4QpxKhnWLaM@;u(bb}p3YQzKkNxBUBcB! z;xj&XZ$EvP{*%MmwKrH3WI@%LhFLLXW9IvUOFb4{GLa^zK$4oW%YDr=M)ZFe@1SLEkh8^{&#A%dqkOqY-fex;iZXa z0nqWc65+XAhD-XvE8&E#kBPby(!`&@$~XP44Qt#y5fP{yXS+rcaASe4>h8e?slwl@ z-|kN5)zV*{=eurr81-UANu|kKnKVAHO-}xM^Cg@z7NC7Re4oD%C)T*Xt6Q1IPEWv^ zDi-kLv_YzEWv}xyM*!H;j3_yLRbnLIK*^>DLI8`uY#QN_o|$K;MN5)F3JjYM-cNY8 z>pCaI0G?lheHE@R&H_Z(KKG65RZW8y-Am$P15^a8&1b?dTWnA<{KQ7~c2y>v5m^&us34Y|V@ zlqhIsp`f`JEbox|0|`)Z{b+!&&Tz}`qKooBKBXjzG9XK_>T>k38vB+ms4`9`D2ys- z+`r*LRhvsz&pGi=ycyx?w1$#97qree=p(D?WhypXdK_^g_k{c1)e%p5wM><2@jW1) za#&TKUg}lEtEh$?Q%~OY&3T}W7T{>uZfCV;GsU-w)%~!BUMP5lfVjW#K0SV~%|prM zW163_u}&c#Q&B(Cua0~_ZspJ4e>6y>V$?r;fL|NuCYOso@(KO#A(ig1O5n8opA60j zE%(Y#=B6)4i^2qfILZ=r!ninMS9EE=AQ5`%{HG6)~7-;Y@W~m);U^4jBgV* zb&27D7vzTbLrA-?w-QXp93bRQ&wdoh=SZsNh<<4n-^UBPf8=3har!~-j<@$di23L1 zq=dM)7hLu5M^TEQd>J`E^2};oxh#rx75aKDH$BvvT9Is&K)-?znkYrHDH$LwL5@y24vK9_bRCZDHjQmHSo1COORCw6;Nc^>L$B&g=aKa z*P=OiqyAoAi`Sae;Gbbt-(uo?=(U+&uggSUY}(neK>a+PnZx?~inkAAKt2H)Wf9kZ zzd!(O?6__+7e3cxMQ+jxeaeOf=11XH^A0JO_srr!vcxXNs-+zM`c&=^dTsC2TDxEA zl99DxEvAq}V3eo?&TG9r+42yFs;kmQ$g3vq)OagA8NzI}T8RjEfdGgmO(4vpNy zT|dRvqUBD=T5iz50G=F@gX7HP_a>8}44iI)Yost5RB`3np-VL@Gt9;h@C z6GA5$FY4aAkmMz{{{pZ$+&)78X4Z;CvUKN>OT23*zwv-lti-RKXHcYyDJ_^o z6ZO~=1VRoay_R|qBLw_)7bvL2H0g~tLreO@^T!cBJt!fv*D|U>aAfEi@6*$4-7~+y zD(HU3<_>;PMT+yH=W@DGvvj=S-04X1T`z0GD&k%zJu5_gDhRZxRaS^+Hgg6PkFcs8 z*$+vnsQQVi6IQBI1)pj^@teE^;Ym}3=DScs9e;Jj@z48e5{I5T#awr1md>$K6$O!0I8 z{Rk%+=bKF4rYs5675%;e!XLt?(beOfFE>;=YwiX}BQQjKWCQV`2vuU0i{j_^+ zj?S^(#h_6Mygf)o6o3fY{pue!b%#m12af^}56VFfqenmZcXG?~e~wJA&(u^Waw`0A?6P-3` zmGW0Hkq}80#uvKUY8CBr@$X|qdtQ^VU@h{(PwT;WE^If~`g6|alt){+{baJ4&9oe- zK2B|Q^Ivpoe#^#S`H!@MaqCMF`pf5SC&~Qm=rac!B%?GT;%k>{*NeL#NP9K#2_hwO z-iESn_Pf$`!6>O{QBH$G;-CFRTw%_S`2qNJ1li1aS006dZ0K&lUlw-JHIBlzyE74h z!8l|^iJ%=K`F%wITBUr4^6Z4}MEUbtM@r7BHWIWQbT51_4lUg1Tst@YF3p=#C=_OY`xFQL zfnz*<-IavyUEj*^P6JD8W^!1yCScorz&X+8fkTRDOj9TmA79aAEH(f5WCM+dqz_!N(z2Yc$k256D`7 zokD-nLN;IloasUxE|xHTmudJK*|lVNJI{>hCrCl3u3*o1lYsE<%jghb^beRP;wlR7 zpAUOiD@Q)$Vj?dBR;1AV$qu*?!df~1wxi}5!qGU6ksnFloq5F%V@?-4$yNwQs0#{^ykl?EYK&=dPQZ8veX{Vob3^yttw8^cc{bu}|E*TaPekZu$QUxtSLP a;7#~yJh_ha>A&A^fRdb=Y>l)<=>Gxy=2LS3 literal 0 HcmV?d00001 diff --git a/Plugins/CommonUser/Source/CommonUser/CommonUser.Build.cs b/Plugins/CommonUser/Source/CommonUser/CommonUser.Build.cs new file mode 100644 index 0000000..5bad0c0 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/CommonUser.Build.cs @@ -0,0 +1,71 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class CommonUser : ModuleRules +{ + public CommonUser(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + bool bUseOnlineSubsystemV1 = true; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreOnline", + "GameplayTags", + "OnlineSubsystemUtils", + // ... add other public dependencies that you statically link with here ... + } + ); + + if (bUseOnlineSubsystemV1) + { + PublicDependencyModuleNames.Add("OnlineSubsystem"); + } + else + { + PublicDependencyModuleNames.Add("OnlineServicesInterface"); + } + + PublicDefinitions.Add("COMMONUSER_OSSV1=" + (bUseOnlineSubsystemV1 ? "1" : "0")); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreOnline", + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "ApplicationCore", + "InputCore", + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Plugins/CommonUser/Source/CommonUser/Private/AsyncAction_CommonUserInitialize.cpp b/Plugins/CommonUser/Source/CommonUser/Private/AsyncAction_CommonUserInitialize.cpp new file mode 100644 index 0000000..0c3a709 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/AsyncAction_CommonUserInitialize.cpp @@ -0,0 +1,105 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AsyncAction_CommonUserInitialize.h" + +#include "GenericPlatform/GenericPlatformInputDeviceMapper.h" +#include "TimerManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(AsyncAction_CommonUserInitialize) + +UAsyncAction_CommonUserInitialize* UAsyncAction_CommonUserInitialize::InitializeForLocalPlay(UCommonUserSubsystem* Target, int32 LocalPlayerIndex, FInputDeviceId PrimaryInputDevice, bool bCanUseGuestLogin) +{ + if (!PrimaryInputDevice.IsValid()) + { + // Set to default device + PrimaryInputDevice = IPlatformInputDeviceMapper::Get().GetDefaultInputDevice(); + } + + UAsyncAction_CommonUserInitialize* Action = NewObject(); + + Action->RegisterWithGameInstance(Target); + + if (Target && Action->IsRegistered()) + { + Action->Subsystem = Target; + + Action->Params.RequestedPrivilege = ECommonUserPrivilege::CanPlay; + Action->Params.LocalPlayerIndex = LocalPlayerIndex; + Action->Params.PrimaryInputDevice = PrimaryInputDevice; + Action->Params.bCanUseGuestLogin = bCanUseGuestLogin; + Action->Params.bCanCreateNewLocalPlayer = true; + } + else + { + Action->SetReadyToDestroy(); + } + + return Action; +} + +UAsyncAction_CommonUserInitialize* UAsyncAction_CommonUserInitialize::LoginForOnlinePlay(UCommonUserSubsystem* Target, int32 LocalPlayerIndex) +{ + UAsyncAction_CommonUserInitialize* Action = NewObject(); + + Action->RegisterWithGameInstance(Target); + + if (Target && Action->IsRegistered()) + { + Action->Subsystem = Target; + + Action->Params.RequestedPrivilege = ECommonUserPrivilege::CanPlayOnline; + Action->Params.LocalPlayerIndex = LocalPlayerIndex; + Action->Params.bCanCreateNewLocalPlayer = false; + } + else + { + Action->SetReadyToDestroy(); + } + + return Action; +} + +void UAsyncAction_CommonUserInitialize::HandleFailure() +{ + const UCommonUserInfo* UserInfo = nullptr; + if (Subsystem.IsValid()) + { + UserInfo = Subsystem->GetUserInfoForLocalPlayerIndex(Params.LocalPlayerIndex); + } + HandleInitializationComplete(UserInfo, false, NSLOCTEXT("CommonUser", "LoginFailedEarly", "Unable to start login process"), Params.RequestedPrivilege, Params.OnlineContext); +} + +void UAsyncAction_CommonUserInitialize::HandleInitializationComplete(const UCommonUserInfo* UserInfo, bool bSuccess, FText Error, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext OnlineContext) +{ + if (ShouldBroadcastDelegates()) + { + OnInitializationComplete.Broadcast(UserInfo, bSuccess, Error, RequestedPrivilege, OnlineContext); + } + + SetReadyToDestroy(); +} + +void UAsyncAction_CommonUserInitialize::Activate() +{ + if (Subsystem.IsValid()) + { + Params.OnUserInitializeComplete.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(UAsyncAction_CommonUserInitialize, HandleInitializationComplete)); + bool bSuccess = Subsystem->TryToInitializeUser(Params); + + if (!bSuccess) + { + // Call failure next frame + FTimerManager* TimerManager = GetTimerManager(); + + if (TimerManager) + { + TimerManager->SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UAsyncAction_CommonUserInitialize::HandleFailure)); + } + } + } + else + { + SetReadyToDestroy(); + } +} + diff --git a/Plugins/CommonUser/Source/CommonUser/Private/CommonSessionSubsystem.cpp b/Plugins/CommonUser/Source/CommonUser/Private/CommonSessionSubsystem.cpp new file mode 100644 index 0000000..f0988aa --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/CommonSessionSubsystem.cpp @@ -0,0 +1,1618 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonSessionSubsystem.h" +#include "AssetRegistry/AssetData.h" +#include "CommonUserTypes.h" +#include "Engine/AssetManager.h" +#include "Engine/GameInstance.h" +#include "Engine/LocalPlayer.h" +#include "GameFramework/PlayerController.h" +#include "Interfaces/OnlineSessionDelegates.h" +#include "Misc/ConfigCacheIni.h" +#include "Online/OnlineSessionNames.h" +#include "OnlineBeaconHost.h" +#include "OnlineSessionSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonSessionSubsystem) + +#if COMMONUSER_OSSV1 +#include "Engine/World.h" +#include "OnlineSubsystemUtils.h" + +FName SETTING_ONLINESUBSYSTEM_VERSION(TEXT("OSSv1")); +#else +#include "Online/OnlineSessionNames.h" +#include "Interfaces/OnlineSessionDelegates.h" +#include "Online/OnlineServicesEngineUtils.h" + +FName SETTING_ONLINESUBSYSTEM_VERSION(TEXT("OSSv2")); +using namespace UE::Online; +#endif // COMMONUSER_OSSV1 + + +DECLARE_LOG_CATEGORY_EXTERN(LogCommonSession, Log, All); +DEFINE_LOG_CATEGORY(LogCommonSession); + +#define LOCTEXT_NAMESPACE "CommonUser" + +////////////////////////////////////////////////////////////////////// +//UCommonSession_SearchSessionRequest + +void UCommonSession_SearchSessionRequest::NotifySearchFinished(bool bSucceeded, const FText& ErrorMessage) +{ + OnSearchFinished.Broadcast(bSucceeded, ErrorMessage); + K2_OnSearchFinished.Broadcast(bSucceeded, ErrorMessage); +} + + +////////////////////////////////////////////////////////////////////// +//UCommonSession_SearchResult + +#if COMMONUSER_OSSV1 +FString UCommonSession_SearchResult::GetDescription() const +{ + return Result.GetSessionIdStr(); +} + +void UCommonSession_SearchResult::GetStringSetting(FName Key, FString& Value, bool& bFoundValue) const +{ + bFoundValue = Result.Session.SessionSettings.Get(Key, /*out*/ Value); +} + +void UCommonSession_SearchResult::GetIntSetting(FName Key, int32& Value, bool& bFoundValue) const +{ + bFoundValue = Result.Session.SessionSettings.Get(Key, /*out*/ Value); +} + +int32 UCommonSession_SearchResult::GetNumOpenPrivateConnections() const +{ + return Result.Session.NumOpenPrivateConnections; +} + +int32 UCommonSession_SearchResult::GetNumOpenPublicConnections() const +{ + return Result.Session.NumOpenPublicConnections; +} + +int32 UCommonSession_SearchResult::GetMaxPublicConnections() const +{ + return Result.Session.SessionSettings.NumPublicConnections; +} + +int32 UCommonSession_SearchResult::GetPingInMs() const +{ + return Result.PingInMs; +} +#else +FString UCommonSession_SearchResult::GetDescription() const +{ + return ToLogString(Lobby->LobbyId); +} + +void UCommonSession_SearchResult::GetStringSetting(FName Key, FString& Value, bool& bFoundValue) const +{ + if (const FSchemaVariant* VariantValue = Lobby->Attributes.Find(Key)) + { + bFoundValue = true; + Value = VariantValue->GetString(); + } + else + { + bFoundValue = false; + } +} + +void UCommonSession_SearchResult::GetIntSetting(FName Key, int32& Value, bool& bFoundValue) const +{ + if (const FSchemaVariant* VariantValue = Lobby->Attributes.Find(Key)) + { + bFoundValue = true; + Value = (int32)VariantValue->GetInt64(); + } + else + { + bFoundValue = false; + } +} + +int32 UCommonSession_SearchResult::GetNumOpenPrivateConnections() const +{ + // TODO: Private connections + return 0; +} + +int32 UCommonSession_SearchResult::GetNumOpenPublicConnections() const +{ + return Lobby->MaxMembers - Lobby->Members.Num(); +} + +int32 UCommonSession_SearchResult::GetMaxPublicConnections() const +{ + return Lobby->MaxMembers; +} + +int32 UCommonSession_SearchResult::GetPingInMs() const +{ + // TODO: Not a property of lobbies. Need to implement with sessions. + return 0; +} +#endif //COMMONUSER_OSSV1 + + +class FCommonOnlineSearchSettingsBase : public FGCObject +{ +public: + FCommonOnlineSearchSettingsBase(UCommonSession_SearchSessionRequest* InSearchRequest) + { + SearchRequest = InSearchRequest; + } + + virtual ~FCommonOnlineSearchSettingsBase() {} + + virtual void AddReferencedObjects(FReferenceCollector& Collector) override + { + Collector.AddReferencedObject(SearchRequest); + } + + virtual FString GetReferencerName() const override + { + static const FString NameString = TEXT("FCommonOnlineSearchSettings"); + return NameString; + } + +public: + TObjectPtr SearchRequest = nullptr; +}; + +#if COMMONUSER_OSSV1 +////////////////////////////////////////////////////////////////////// +// FCommonSession_OnlineSessionSettings + +class FCommonSession_OnlineSessionSettings : public FOnlineSessionSettings +{ +public: + + FCommonSession_OnlineSessionSettings(bool bIsLAN = false, bool bIsPresence = false, int32 MaxNumPlayers = 4) + { + NumPublicConnections = MaxNumPlayers; + if (NumPublicConnections < 0) + { + NumPublicConnections = 0; + } + NumPrivateConnections = 0; + bIsLANMatch = bIsLAN; + bShouldAdvertise = true; + bAllowJoinInProgress = true; + bAllowInvites = true; + bUsesPresence = bIsPresence; + bAllowJoinViaPresence = true; + bAllowJoinViaPresenceFriendsOnly = false; + } + + virtual ~FCommonSession_OnlineSessionSettings() {} +}; + +////////////////////////////////////////////////////////////////////// +// FCommonOnlineSearchSettingsOSSv1 + +class FCommonOnlineSearchSettingsOSSv1 : public FOnlineSessionSearch, public FCommonOnlineSearchSettingsBase +{ +public: + FCommonOnlineSearchSettingsOSSv1(UCommonSession_SearchSessionRequest* InSearchRequest) + : FCommonOnlineSearchSettingsBase(InSearchRequest) + { + bIsLanQuery = (InSearchRequest->OnlineMode == ECommonSessionOnlineMode::LAN); + MaxSearchResults = 10; + PingBucketSize = 50; + + QuerySettings.Set(SETTING_ONLINESUBSYSTEM_VERSION, true, EOnlineComparisonOp::Equals); + + if (InSearchRequest->bUseLobbies) + { + QuerySettings.Set(SEARCH_LOBBIES, true, EOnlineComparisonOp::Equals); + } + } + + virtual ~FCommonOnlineSearchSettingsOSSv1() {} +}; +#else + +class FCommonOnlineSearchSettingsOSSv2 : public FCommonOnlineSearchSettingsBase +{ +public: + FCommonOnlineSearchSettingsOSSv2(UCommonSession_SearchSessionRequest* InSearchRequest) + : FCommonOnlineSearchSettingsBase(InSearchRequest) + { + FindLobbyParams.MaxResults = 10; + + FindLobbyParams.Filters.Emplace(FFindLobbySearchFilter{ SETTING_ONLINESUBSYSTEM_VERSION, ESchemaAttributeComparisonOp::Equals, true }); + } +public: + FFindLobbies::Params FindLobbyParams; +}; + +#endif // COMMONUSER_OSSV1 + +////////////////////////////////////////////////////////////////////// +// UCommonSession_HostSessionRequest + +FString UCommonSession_HostSessionRequest::GetMapName() const +{ + FAssetData MapAssetData; + if (UAssetManager::Get().GetPrimaryAssetData(MapID, /*out*/ MapAssetData)) + { + return MapAssetData.PackageName.ToString(); + } + else + { + return FString(); + } +} + +FString UCommonSession_HostSessionRequest::ConstructTravelURL() const +{ + FString CombinedExtraArgs; + + if (OnlineMode == ECommonSessionOnlineMode::LAN) + { + CombinedExtraArgs += TEXT("?bIsLanMatch"); + } + + if (OnlineMode != ECommonSessionOnlineMode::Offline) + { + CombinedExtraArgs += TEXT("?listen"); + } + + for (const auto& KVP : ExtraArgs) + { + if (!KVP.Key.IsEmpty()) + { + if (KVP.Value.IsEmpty()) + { + CombinedExtraArgs += FString::Printf(TEXT("?%s"), *KVP.Key); + } + else + { + CombinedExtraArgs += FString::Printf(TEXT("?%s=%s"), *KVP.Key, *KVP.Value); + } + } + } + + //bIsRecordingDemo ? TEXT("?DemoRec") : TEXT("")); + + return FString::Printf(TEXT("%s%s"), + *GetMapName(), + *CombinedExtraArgs); +} + +bool UCommonSession_HostSessionRequest::ValidateAndLogErrors(FText& OutError) const +{ +#if WITH_SERVER_CODE + if (GetMapName().IsEmpty()) + { + OutError = FText::Format(NSLOCTEXT("NetworkErrors", "InvalidMapFormat", "Can't find asset data for MapID {0}, hosting request failed."), FText::FromString(MapID.ToString())); + return false; + } + + return true; +#else + // Client builds are only meant to connect to dedicated servers, they are missing the code to host a session by default + // You can change this behavior in subclasses to handle something like a tutorial + OutError = NSLOCTEXT("NetworkErrors", "ClientBuildCannotHost", "Client builds cannot host game sessions."); + return false; +#endif +} + +int32 UCommonSession_HostSessionRequest::GetMaxPlayers() const +{ + return MaxPlayerCount; +} + +////////////////////////////////////////////////////////////////////// +// UCommonSessionSubsystem + +void UCommonSessionSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + BindOnlineDelegates(); + GEngine->OnTravelFailure().AddUObject(this, &UCommonSessionSubsystem::TravelLocalSessionFailure); + + FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UCommonSessionSubsystem::HandlePostLoadMap); + + UGameInstance* GameInstance = GetGameInstance(); + bIsDedicatedServer = GameInstance->IsDedicatedServerInstance(); +} + +void UCommonSessionSubsystem::BindOnlineDelegates() +{ +#if COMMONUSER_OSSV1 + BindOnlineDelegatesOSSv1(); +#else + BindOnlineDelegatesOSSv2(); +#endif +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::BindOnlineDelegatesOSSv1() +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + + const IOnlineSessionPtr SessionInterface = OnlineSub->GetSessionInterface(); + check(SessionInterface.IsValid()); + + SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)); + SessionInterface->AddOnStartSessionCompleteDelegate_Handle(FOnStartSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnStartSessionComplete)); + SessionInterface->AddOnUpdateSessionCompleteDelegate_Handle(FOnUpdateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnUpdateSessionComplete)); + SessionInterface->AddOnEndSessionCompleteDelegate_Handle(FOnEndSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnEndSessionComplete)); + SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(FOnDestroySessionCompleteDelegate::CreateUObject(this, &ThisClass::OnDestroySessionComplete)); + SessionInterface->AddOnDestroySessionRequestedDelegate_Handle(FOnDestroySessionRequestedDelegate::CreateUObject(this, &ThisClass::OnDestroySessionRequested)); + +// SessionInterface->AddOnMatchmakingCompleteDelegate_Handle(FOnMatchmakingCompleteDelegate::CreateUObject(this, &ThisClass::OnMatchmakingComplete)); +// SessionInterface->AddOnCancelMatchmakingCompleteDelegate_Handle(FOnCancelMatchmakingCompleteDelegate::CreateUObject(this, &ThisClass::OnCancelMatchmakingComplete)); + + SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)); +// SessionInterface->AddOnCancelFindSessionsCompleteDelegate_Handle(FOnCancelFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnCancelFindSessionsComplete)); +// SessionInterface->AddOnPingSearchResultsCompleteDelegate_Handle(FOnPingSearchResultsCompleteDelegate::CreateUObject(this, &ThisClass::OnPingSearchResultsComplete)); + SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete)); + +// TWO_PARAM(OnSessionParticipantJoined, FName, const FUniqueNetId&); +// THREE_PARAM(OnSessionParticipantLeft, FName, const FUniqueNetId&, EOnSessionParticipantLeftReason); +// ONE_PARAM(OnQosDataRequested, FName); +// TWO_PARAM(OnSessionCustomDataChanged, FName, const FOnlineSessionSettings&); +// TWO_PARAM(OnSessionSettingsUpdated, FName, const FOnlineSessionSettings&); +// THREE_PARAM(OnSessionParticipantSettingsUpdated, FName, const FUniqueNetId&, const FOnlineSessionSettings&); +// FOUR_PARAM(OnSessionInviteReceived, const FUniqueNetId& /*UserId*/, const FUniqueNetId& /*FromId*/, const FString& /*AppId*/, const FOnlineSessionSearchResult& /*InviteResult*/); +// THREE_PARAM(OnRegisterPlayersComplete, FName, const TArray< FUniqueNetIdRef >&, bool); +// THREE_PARAM(OnUnregisterPlayersComplete, FName, const TArray< FUniqueNetIdRef >&, bool); + + SessionInterface->AddOnSessionUserInviteAcceptedDelegate_Handle(FOnSessionUserInviteAcceptedDelegate::CreateUObject(this, &ThisClass::HandleSessionUserInviteAccepted)); + SessionInterface->AddOnSessionFailureDelegate_Handle(FOnSessionFailureDelegate::CreateUObject(this, &ThisClass::HandleSessionFailure)); +} + +#else + +void UCommonSessionSubsystem::BindOnlineDelegatesOSSv2() +{ + // TODO: Bind OSSv2 delegates when they are available + // Note that most OSSv1 delegates above are implemented as completion delegates in OSSv2 and don't need to be subscribed to + TSharedPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + + LobbyJoinRequestedHandle = Lobbies->OnUILobbyJoinRequested().Add(this, &UCommonSessionSubsystem::OnSessionJoinRequested); +} +#endif + +void UCommonSessionSubsystem::Deinitialize() +{ +#if COMMONUSER_OSSV1 + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + + if (OnlineSub) + { + // During shutdown this may not be valid + const IOnlineSessionPtr SessionInterface = OnlineSub->GetSessionInterface(); + if (SessionInterface) + { + SessionInterface->ClearOnSessionFailureDelegates(this); + } + } +#endif // COMMONUSER_OSSV1 + + if (GEngine) + { + GEngine->OnTravelFailure().RemoveAll(this); + } + + FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this); + + Super::Deinitialize(); +} + +bool UCommonSessionSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + + // Only create an instance if there is not a game-specific subclass + return ChildClasses.Num() == 0; +} + +UCommonSession_HostSessionRequest* UCommonSessionSubsystem::CreateOnlineHostSessionRequest() +{ + /** Game-specific subsystems can override this or you can modify after creation */ + + UCommonSession_HostSessionRequest* NewRequest = NewObject(this); + NewRequest->OnlineMode = ECommonSessionOnlineMode::Online; + NewRequest->bUseLobbies = bUseLobbiesDefault; + NewRequest->bUseLobbiesVoiceChat = bUseLobbiesVoiceChatDefault; + + // We enable presence by default in the primary session used for matchmaking. For online systems that care about presence, only the primary session should have presence enabled + NewRequest->bUsePresence = !IsRunningDedicatedServer(); + + return NewRequest; +} + +UCommonSession_SearchSessionRequest* UCommonSessionSubsystem::CreateOnlineSearchSessionRequest() +{ + /** Game-specific subsystems can override this or you can modify after creation */ + + UCommonSession_SearchSessionRequest* NewRequest = NewObject(this); + NewRequest->OnlineMode = ECommonSessionOnlineMode::Online; + + NewRequest->bUseLobbies = bUseLobbiesDefault; + + return NewRequest; +} + +void UCommonSessionSubsystem::HostSession(APlayerController* HostingPlayer, UCommonSession_HostSessionRequest* Request) +{ + if (Request == nullptr) + { + SetCreateSessionError(NSLOCTEXT("NetworkErrors", "InvalidRequest", "HostSession passed an invalid request.")); + OnCreateSessionComplete(NAME_None, false); + return; + } + + ULocalPlayer* LocalPlayer = (HostingPlayer != nullptr) ? HostingPlayer->GetLocalPlayer() : nullptr; + if (LocalPlayer == nullptr && !bIsDedicatedServer) + { + SetCreateSessionError(NSLOCTEXT("NetworkErrors", "InvalidHostingPlayer", "HostingPlayer is invalid.")); + OnCreateSessionComplete(NAME_None, false); + return; + } + + FText OutError; + if (!Request->ValidateAndLogErrors(OutError)) + { + SetCreateSessionError(OutError); + OnCreateSessionComplete(NAME_None, false); + return; + } + + if (Request->OnlineMode == ECommonSessionOnlineMode::Offline) + { + if (GetWorld()->GetNetMode() == NM_Client) + { + SetCreateSessionError(NSLOCTEXT("NetworkErrors", "CannotHostAsClient", "Cannot host offline game as client.")); + OnCreateSessionComplete(NAME_None, false); + return; + } + else + { + // Offline so travel to the specified match URL immediately + GetWorld()->ServerTravel(Request->ConstructTravelURL()); + } + } + else + { + CreateOnlineSessionInternal(LocalPlayer, Request); + } + + NotifySessionInformationUpdated(ECommonSessionInformationState::InGame, Request->ModeNameForAdvertisement, Request->GetMapName()); +} + +void UCommonSessionSubsystem::CreateOnlineSessionInternal(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request) +{ + CreateSessionResult = FOnlineResultInformation(); + PendingTravelURL = Request->ConstructTravelURL(); + +#if COMMONUSER_OSSV1 + CreateOnlineSessionInternalOSSv1(LocalPlayer, Request); +#else + CreateOnlineSessionInternalOSSv2(LocalPlayer, Request); +#endif +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::CreateOnlineSessionInternalOSSv1(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request) +{ + const FName SessionName(NAME_GameSession); + const int32 MaxPlayers = Request->GetMaxPlayers(); + + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions); + + FUniqueNetIdPtr UserId; + if (LocalPlayer) + { + UserId = LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(); + } + else if (bIsDedicatedServer) + { + UserId = OnlineSub->GetIdentityInterface()->GetUniquePlayerId(DEDICATED_SERVER_USER_INDEX); + } + + //@TODO: You can get here on some platforms while trying to do a LAN session, does that require a valid user id? + if (ensure(UserId.IsValid())) + { + FCommonSession_OnlineSessionSettings HostSettings(Request->OnlineMode == ECommonSessionOnlineMode::LAN, Request->bUsePresence, MaxPlayers); + HostSettings.bUseLobbiesIfAvailable = Request->bUseLobbies; + HostSettings.bUseLobbiesVoiceChatIfAvailable = Request->bUseLobbiesVoiceChat; + HostSettings.Set(SETTING_GAMEMODE, Request->ModeNameForAdvertisement, EOnlineDataAdvertisementType::ViaOnlineService); + HostSettings.Set(SETTING_MAPNAME, Request->GetMapName(), EOnlineDataAdvertisementType::ViaOnlineService); + //@TODO: HostSettings.Set(SETTING_MATCHING_HOPPER, FString("TeamDeathmatch"), EOnlineDataAdvertisementType::DontAdvertise); + HostSettings.Set(SETTING_MATCHING_TIMEOUT, 120.0f, EOnlineDataAdvertisementType::ViaOnlineService); + HostSettings.Set(SETTING_SESSION_TEMPLATE_NAME, FString(TEXT("GameSession")), EOnlineDataAdvertisementType::ViaOnlineService); + HostSettings.Set(SETTING_ONLINESUBSYSTEM_VERSION, true, EOnlineDataAdvertisementType::ViaOnlineService); + + Sessions->CreateSession(*UserId, SessionName, HostSettings); + NotifySessionInformationUpdated(ECommonSessionInformationState::InGame, Request->ModeNameForAdvertisement, Request->GetMapName()); + } + else + { + OnCreateSessionComplete(SessionName, false); + } +} + +#else + +void UCommonSessionSubsystem::CreateOnlineSessionInternalOSSv2(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request) +{ + // Only lobbies are supported for now + if (!ensureMsgf(Request->bUseLobbies, TEXT("Only Lobbies are supported in this release"))) + { + Request->bUseLobbies = true; + } + + const FName SessionName(NAME_GameSession); + const int32 MaxPlayers = Request->GetMaxPlayers(); + + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + FCreateLobby::Params CreateParams; + + if (LocalPlayer) + { + CreateParams.LocalAccountId = LocalPlayer->GetPreferredUniqueNetId().GetV2(); + } + else if (bIsDedicatedServer) + { + // TODO what should this do for v2? + } + + CreateParams.LocalName = SessionName; + CreateParams.SchemaId = FSchemaId(TEXT("GameLobby")); // TODO: make a parameter + CreateParams.bPresenceEnabled = Request->bUsePresence; + CreateParams.MaxMembers = MaxPlayers; + CreateParams.JoinPolicy = ELobbyJoinPolicy::PublicAdvertised; // TODO: Check parameters + + CreateParams.Attributes.Emplace(SETTING_GAMEMODE, Request->ModeNameForAdvertisement); + CreateParams.Attributes.Emplace(SETTING_MAPNAME, Request->GetMapName()); + //@TODO: CreateParams.Attributes.Emplace(SETTING_MATCHING_HOPPER, FString("TeamDeathmatch")); + CreateParams.Attributes.Emplace(SETTING_MATCHING_TIMEOUT, 120.0f); + CreateParams.Attributes.Emplace(SETTING_SESSION_TEMPLATE_NAME, FString(TEXT("GameSession"))); + CreateParams.Attributes.Emplace(SETTING_ONLINESUBSYSTEM_VERSION, true); + + CreateParams.UserAttributes.Emplace(SETTING_GAMEMODE, FString(TEXT("GameSession"))); + + // TODO: Add splitscreen players + + FString ModeName = Request->ModeNameForAdvertisement; + FString MapName = Request->GetMapName(); + + Lobbies->CreateLobby(MoveTemp(CreateParams)).OnComplete(this, [this, SessionName, ModeName, MapName](const TOnlineResult& CreateResult) + { + OnCreateSessionComplete(SessionName, CreateResult.IsOk()); + NotifySessionInformationUpdated(ECommonSessionInformationState::InGame, ModeName, MapName); + }); +} + +#endif + +void UCommonSessionSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnCreateSessionComplete(SessionName: %s, bWasSuccessful: %d)"), *SessionName.ToString(), bWasSuccessful); + +#if COMMONUSER_OSSV1 // OSSv2 joins splitscreen players as part of the create call + // Add the splitscreen player if one exists +#if 0 //@TODO: + if (bWasSuccessful && LocalPlayers.Num() > 1) + { + IOnlineSessionPtr Sessions = Online::GetSessionInterface(GetWorld()); + if (Sessions.IsValid() && LocalPlayers[1]->GetPreferredUniqueNetId().IsValid()) + { + Sessions->RegisterLocalPlayer(*LocalPlayers[1]->GetPreferredUniqueNetId(), NAME_GameSession, + FOnRegisterLocalPlayerCompleteDelegate::CreateUObject(this, &ThisClass::OnRegisterLocalPlayerComplete_CreateSession)); + } + } + else +#endif +#endif + { + // We either failed or there is only a single local user + FinishSessionCreation(bWasSuccessful); + } +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::OnRegisterLocalPlayerComplete_CreateSession(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result) +{ + FinishSessionCreation(Result == EOnJoinSessionCompleteResult::Success); +} + +void UCommonSessionSubsystem::OnStartSessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnStartSessionComplete(SessionName: %s, bWasSuccessful: %d)"), *SessionName.ToString(), bWasSuccessful); + + if (bWantToDestroyPendingSession) + { + CleanUpSessions(); + } +} +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::FinishSessionCreation(bool bWasSuccessful) +{ + if (bWasSuccessful) + { + //@TODO Synchronize timing of this with join callbacks, modify both places and the comments if plan changes + CreateSessionResult = FOnlineResultInformation(); + CreateSessionResult.bWasSuccessful = true; + + if (bUseBeacons) + { + CreateHostReservationBeacon(); + } + + NotifyCreateSessionComplete(CreateSessionResult); + + // Travel to the specified match URL + GetWorld()->ServerTravel(PendingTravelURL); + } + else + { + if (CreateSessionResult.bWasSuccessful || CreateSessionResult.ErrorText.IsEmpty()) + { + FString ReturnError = TEXT("GenericFailure"); // TODO: No good way to get session error codes out of OSSV1 + FText ReturnReason = NSLOCTEXT("NetworkErrors", "CreateSessionFailed", "Failed to create session."); + + CreateSessionResult.bWasSuccessful = false; + CreateSessionResult.ErrorId = ReturnError; + CreateSessionResult.ErrorText = ReturnReason; + } + + UE_LOG(LogCommonSession, Error, TEXT("FinishSessionCreation(%s): %s"), *CreateSessionResult.ErrorId, *CreateSessionResult.ErrorText.ToString()); + + NotifyCreateSessionComplete(CreateSessionResult); + NotifySessionInformationUpdated(ECommonSessionInformationState::OutOfGame); + } +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::OnUpdateSessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnUpdateSessionComplete(SessionName: %s, bWasSuccessful: %s"), *SessionName.ToString(), bWasSuccessful ? TEXT("true") : TEXT("false")); +} + +void UCommonSessionSubsystem::OnEndSessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnEndSessionComplete(SessionName: %s, bWasSuccessful: %s)"), *SessionName.ToString(), bWasSuccessful ? TEXT("true") : TEXT("false")); + CleanUpSessions(); +} + +void UCommonSessionSubsystem::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnDestroySessionComplete(SessionName: %s, bWasSuccessful: %s)"), *SessionName.ToString(), bWasSuccessful ? TEXT("true") : TEXT("false")); + bWantToDestroyPendingSession = false; +} + +void UCommonSessionSubsystem::OnDestroySessionRequested(int32 LocalUserNum, FName SessionName) +{ + FPlatformUserId PlatformUserId = IPlatformInputDeviceMapper::Get().GetPlatformUserForUserIndex(LocalUserNum); + + NotifyDestroySessionRequested(PlatformUserId, SessionName); +} +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::FindSessions(APlayerController* SearchingPlayer, UCommonSession_SearchSessionRequest* Request) +{ + if (Request == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("FindSessions passed a null request")); + return; + } + +#if COMMONUSER_OSSV1 + FindSessionsInternal(SearchingPlayer, MakeShared(Request)); +#else + FindSessionsInternal(SearchingPlayer, MakeShared(Request)); +#endif // COMMONUSER_OSSV1 +} + +void UCommonSessionSubsystem::FindSessionsInternal(APlayerController* SearchingPlayer, const TSharedRef& InSearchSettings) +{ + if (SearchSettings.IsValid()) + { + //@TODO: This is a poor user experience for the API user, we should let the additional search piggyback and + // just give it the same results as the currently pending one + // (or enqueue the request and service it when the previous one finishes or fails) + UE_LOG(LogCommonSession, Error, TEXT("A previous FindSessions call is still in progress, aborting")); + SearchSettings->SearchRequest->NotifySearchFinished(false, LOCTEXT("Error_FindSessionAlreadyInProgress", "Session search already in progress")); + } + + ULocalPlayer* LocalPlayer = (SearchingPlayer != nullptr) ? SearchingPlayer->GetLocalPlayer() : nullptr; + if (LocalPlayer == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("SearchingPlayer is invalid")); + InSearchSettings->SearchRequest->NotifySearchFinished(false, LOCTEXT("Error_FindSessionBadPlayer", "Session search was not provided a local player")); + return; + } + + SearchSettings = InSearchSettings; +#if COMMONUSER_OSSV1 + FindSessionsInternalOSSv1(LocalPlayer); +#else + FindSessionsInternalOSSv2(LocalPlayer); +#endif +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::FindSessionsInternalOSSv1(ULocalPlayer* LocalPlayer) +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions); + + SearchSettings->QuerySettings.Set(SETTING_SESSION_TEMPLATE_NAME, FString("GameSession"), EOnlineComparisonOp::Equals); + + if (!Sessions->FindSessions(*LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), StaticCastSharedRef(SearchSettings.ToSharedRef()))) + { + // Some session search failures will call this delegate inside the function, others will not + OnFindSessionsComplete(false); + } +} + +#else + +void UCommonSessionSubsystem::FindSessionsInternalOSSv2(ULocalPlayer* LocalPlayer) +{ + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + + FFindLobbies::Params FindLobbyParams = StaticCastSharedPtr(SearchSettings)->FindLobbyParams; + FindLobbyParams.LocalAccountId = LocalPlayer->GetPreferredUniqueNetId().GetV2(); + + Lobbies->FindLobbies(MoveTemp(FindLobbyParams)).OnComplete(this, [this, LocalSearchSettings = SearchSettings](const TOnlineResult& FindResult) + { + if (LocalSearchSettings != SearchSettings) + { + // This was an abandoned search, ignore + return; + } + const bool bWasSuccessful = FindResult.IsOk(); + UE_LOG(LogCommonSession, Log, TEXT("FindLobbies(bWasSuccessful: %s)"), *LexToString(bWasSuccessful)); + check(SearchSettings.IsValid()); + if (bWasSuccessful) + { + const FFindLobbies::Result& FindResults = FindResult.GetOkValue(); + SearchSettings->SearchRequest->Results.Reset(FindResults.Lobbies.Num()); + + for (const TSharedRef& Lobby : FindResults.Lobbies) + { + if (!Lobby->OwnerAccountId.IsValid()) + { + UE_LOG(LogCommonSession, Verbose, TEXT("\tIgnoring Lobby with no owner (LobbyId: %s)"), + *ToLogString(Lobby->LobbyId)); + } + else if (Lobby->Members.Num() == 0) + { + UE_LOG(LogCommonSession, Verbose, TEXT("\tIgnoring Lobby with no members (UserId: %s)"), + *ToLogString(Lobby->OwnerAccountId)); + } + else + { + UCommonSession_SearchResult* Entry = NewObject(SearchSettings->SearchRequest); + Entry->Lobby = Lobby; + SearchSettings->SearchRequest->Results.Add(Entry); + + UE_LOG(LogCommonSession, Log, TEXT("\tFound lobby (UserId: %s, NumOpenConns: %d)"), + *ToLogString(Lobby->OwnerAccountId), Lobby->MaxMembers - Lobby->Members.Num()); + } + } + } + else + { + SearchSettings->SearchRequest->Results.Empty(); + } + + const FText ResultText = bWasSuccessful ? FText() : FindResult.GetErrorValue().GetText(); + + SearchSettings->SearchRequest->NotifySearchFinished(bWasSuccessful, ResultText); + SearchSettings.Reset(); + }); +} +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::QuickPlaySession(APlayerController* JoiningOrHostingPlayer, UCommonSession_HostSessionRequest* HostRequest) +{ + UE_LOG(LogCommonSession, Log, TEXT("QuickPlay Requested")); + + if (HostRequest == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("QuickPlaySession passed a null request")); + return; + } + + TStrongObjectPtr HostRequestPtr = TStrongObjectPtr(HostRequest); + TWeakObjectPtr JoiningOrHostingPlayerPtr = TWeakObjectPtr(JoiningOrHostingPlayer); + + UCommonSession_SearchSessionRequest* QuickPlayRequest = CreateOnlineSearchSessionRequest(); + QuickPlayRequest->OnSearchFinished.AddUObject(this, &UCommonSessionSubsystem::HandleQuickPlaySearchFinished, JoiningOrHostingPlayerPtr, HostRequestPtr); + + // We enable presence by default on the primary session used for matchmaking. For online systems that care about presence, only the primary session should have presence enabled + + HostRequestPtr->bUseLobbies = bUseLobbiesDefault; + HostRequestPtr->bUseLobbiesVoiceChat = bUseLobbiesVoiceChatDefault; + HostRequestPtr->bUsePresence = true; + QuickPlayRequest->bUseLobbies = bUseLobbiesDefault; + + NotifySessionInformationUpdated(ECommonSessionInformationState::Matchmaking); + FindSessionsInternal(JoiningOrHostingPlayer, CreateQuickPlaySearchSettings(HostRequest, QuickPlayRequest)); +} + +TSharedRef UCommonSessionSubsystem::CreateQuickPlaySearchSettings(UCommonSession_HostSessionRequest* HostRequest, UCommonSession_SearchSessionRequest* SearchRequest) +{ +#if COMMONUSER_OSSV1 + return CreateQuickPlaySearchSettingsOSSv1(HostRequest, SearchRequest); +#else + return CreateQuickPlaySearchSettingsOSSv2(HostRequest, SearchRequest); +#endif // COMMONUSER_OSSV1 +} + +#if COMMONUSER_OSSV1 +TSharedRef UCommonSessionSubsystem::CreateQuickPlaySearchSettingsOSSv1(UCommonSession_HostSessionRequest* HostRequest, UCommonSession_SearchSessionRequest* SearchRequest) +{ + TSharedRef QuickPlaySearch = MakeShared(SearchRequest); + + /** By default quick play does not want to include the map or game mode, games can fill this in as desired + if (!HostRequest->ModeNameForAdvertisement.IsEmpty()) + { + QuickPlaySearch->QuerySettings.Set(SETTING_GAMEMODE, HostRequest->ModeNameForAdvertisement, EOnlineComparisonOp::Equals); + } + + if (!HostRequest->GetMapName().IsEmpty()) + { + QuickPlaySearch->QuerySettings.Set(SETTING_MAPNAME, HostRequest->GetMapName(), EOnlineComparisonOp::Equals); + } + */ + + // QuickPlaySearch->QuerySettings.Set(SEARCH_DEDICATED_ONLY, true, EOnlineComparisonOp::Equals); + return QuickPlaySearch; +} + +#else + +TSharedRef UCommonSessionSubsystem::CreateQuickPlaySearchSettingsOSSv2(UCommonSession_HostSessionRequest* HostRequest, UCommonSession_SearchSessionRequest* SearchRequest) +{ + TSharedRef QuickPlaySearch = MakeShared(SearchRequest); + + /** By default quick play does not want to include the map or game mode, games can fill this in as desired + if (!HostRequest->ModeNameForAdvertisement.IsEmpty()) + { + QuickPlaySearch->FindLobbyParams.Filters.Emplace(FFindLobbySearchFilter{SETTING_GAMEMODE, ESchemaAttributeComparisonOp::Equals, HostRequest->ModeNameForAdvertisement}); + } + if (!HostRequest->GetMapName().IsEmpty()) + { + QuickPlaySearch->FindLobbyParams.Filters.Emplace(FFindLobbySearchFilter{SETTING_MAPNAME, ESchemaAttributeComparisonOp::Equals, HostRequest->GetMapName()}); + } + */ + + return QuickPlaySearch; +} + +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::HandleQuickPlaySearchFinished(bool bSucceeded, const FText& ErrorMessage, TWeakObjectPtr JoiningOrHostingPlayer, TStrongObjectPtr HostRequest) +{ + const int32 ResultCount = SearchSettings->SearchRequest->Results.Num(); + UE_LOG(LogCommonSession, Log, TEXT("QuickPlay Search Finished %s (Results %d) (Error: %s)"), bSucceeded ? TEXT("Success") : TEXT("Failed"), ResultCount, *ErrorMessage.ToString()); + + //@TODO: We have to check if the error message is empty because some OSS layers report a failure just because there are no sessions. Please fix with OSS 2.0. + if (bSucceeded || ErrorMessage.IsEmpty()) + { + // Join the best search result. + if (ResultCount > 0) + { + //@TODO: We should probably look at ping? maybe some other factors to find the best. Idk if they come pre-sorted or not. + for (UCommonSession_SearchResult* Result : SearchSettings->SearchRequest->Results) + { + JoinSession(JoiningOrHostingPlayer.Get(), Result); + return; + } + } + else + { + HostSession(JoiningOrHostingPlayer.Get(), HostRequest.Get()); + } + } + else + { + //@TODO: This sucks, need to tell someone. + NotifySessionInformationUpdated(ECommonSessionInformationState::OutOfGame); + } +} + +void UCommonSessionSubsystem::CleanUpSessions() +{ + bWantToDestroyPendingSession = true; + + if (bUseBeacons) + { + DestroyHostReservationBeacon(); + } + + NotifySessionInformationUpdated(ECommonSessionInformationState::OutOfGame); +#if COMMONUSER_OSSV1 + CleanUpSessionsOSSv1(); +#else + CleanUpSessionsOSSv2(); +#endif // COMMONUSER_OSSV1 +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::CleanUpSessionsOSSv1() +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions); + + EOnlineSessionState::Type SessionState = Sessions->GetSessionState(NAME_GameSession); + UE_LOG(LogCommonSession, Log, TEXT("Session state is %s"), EOnlineSessionState::ToString(SessionState)); + + if (EOnlineSessionState::InProgress == SessionState) + { + UE_LOG(LogCommonSession, Log, TEXT("Ending session because of return to front end")); + Sessions->EndSession(NAME_GameSession); + } + else if (EOnlineSessionState::Ending == SessionState) + { + UE_LOG(LogCommonSession, Log, TEXT("Waiting for session to end on return to main menu")); + } + else if (EOnlineSessionState::Ended == SessionState || EOnlineSessionState::Pending == SessionState) + { + UE_LOG(LogCommonSession, Log, TEXT("Destroying session on return to main menu")); + Sessions->DestroySession(NAME_GameSession); + } + else if (EOnlineSessionState::Starting == SessionState || EOnlineSessionState::Creating == SessionState) + { + UE_LOG(LogCommonSession, Log, TEXT("Waiting for session to start, and then we will end it to return to main menu")); + } +} + +#else +void UCommonSessionSubsystem::CleanUpSessionsOSSv2() +{ + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + + FAccountId LocalPlayerId = GetAccountId(GetGameInstance()->GetFirstLocalPlayerController()); + FLobbyId LobbyId = GetLobbyId(NAME_GameSession); + + if (!LocalPlayerId.IsValid() || !LobbyId.IsValid()) + { + return; + } + // TODO: Include all local players leave the lobby + Lobbies->LeaveLobby({LocalPlayerId, LobbyId}); +} + +#endif // COMMONUSER_OSSV1 + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::OnFindSessionsComplete(bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnFindSessionsComplete(bWasSuccessful: %s)"), bWasSuccessful ? TEXT("true") : TEXT("false")); + + if (!SearchSettings.IsValid()) + { + // This could get called twice for failed session searches, or for a search requested by a different system + return; + } + + FCommonOnlineSearchSettingsOSSv1& SearchSettingsV1 = *StaticCastSharedPtr(SearchSettings); + if (SearchSettingsV1.SearchState == EOnlineAsyncTaskState::InProgress) + { + UE_LOG(LogCommonSession, Error, TEXT("OnFindSessionsComplete called when search is still in progress!")); + return; + } + + if (!ensure(SearchSettingsV1.SearchRequest)) + { + UE_LOG(LogCommonSession, Error, TEXT("OnFindSessionsComplete called with invalid search request object!")); + return; + } + + if (bWasSuccessful) + { + SearchSettingsV1.SearchRequest->Results.Reset(SearchSettingsV1.SearchResults.Num()); + + for (const FOnlineSessionSearchResult& Result : SearchSettingsV1.SearchResults) + { + check(Result.IsValid()); + + UCommonSession_SearchResult* Entry = NewObject(SearchSettingsV1.SearchRequest); + Entry->Result = Result; + SearchSettingsV1.SearchRequest->Results.Add(Entry); + FString OwningUserId = TEXT("Unknown"); + if (Result.Session.OwningUserId.IsValid()) + { + OwningUserId = Result.Session.OwningUserId->ToString(); + } + + UE_LOG(LogCommonSession, Log, TEXT("\tFound session (UserId: %s, UserName: %s, NumOpenPrivConns: %d, NumOpenPubConns: %d, Ping: %d ms"), + *OwningUserId, + *Result.Session.OwningUserName, + Result.Session.NumOpenPrivateConnections, + Result.Session.NumOpenPublicConnections, + Result.PingInMs + ); + } + } + else + { + SearchSettingsV1.SearchRequest->Results.Empty(); + } + + if (0) + { + // Fake Sessions OSSV1 + for (int i = 0; i < 10; i++) + { + UCommonSession_SearchResult* Entry = NewObject(SearchSettings->SearchRequest); + FOnlineSessionSearchResult FakeResult; + FakeResult.Session.OwningUserName = TEXT("Fake User"); + FakeResult.Session.SessionSettings.NumPublicConnections = 10; + FakeResult.Session.SessionSettings.bShouldAdvertise = true; + FakeResult.Session.SessionSettings.bAllowJoinInProgress = true; + FakeResult.PingInMs=99; + Entry->Result = FakeResult; + SearchSettingsV1.SearchRequest->Results.Add(Entry); + } + } + + SearchSettingsV1.SearchRequest->NotifySearchFinished(bWasSuccessful, bWasSuccessful ? FText() : LOCTEXT("Error_FindSessionV1Failed", "Find session failed")); + SearchSettings.Reset(); +} +#endif // COMMONUSER_OSSV1 + + +void UCommonSessionSubsystem::JoinSession(APlayerController* JoiningPlayer, UCommonSession_SearchResult* Request) +{ + if (Request == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("JoinSession passed a null request")); + return; + } + + ULocalPlayer* LocalPlayer = (JoiningPlayer != nullptr) ? JoiningPlayer->GetLocalPlayer() : nullptr; + if (LocalPlayer == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("JoiningPlayer is invalid")); + return; + } + + // Update presence here since we won't have the raw game mode and map name keys after client travel. If joining/travel fails, it is reset to main menu + FString SessionGameMode, SessionMapName; + bool bEmpty; + Request->GetStringSetting(SETTING_GAMEMODE, SessionGameMode, bEmpty); + Request->GetStringSetting(SETTING_MAPNAME, SessionMapName, bEmpty); + NotifySessionInformationUpdated(ECommonSessionInformationState::InGame, SessionGameMode, SessionMapName); + + JoinSessionInternal(LocalPlayer, Request); +} + +void UCommonSessionSubsystem::JoinSessionInternal(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request) +{ +#if COMMONUSER_OSSV1 + JoinSessionInternalOSSv1(LocalPlayer, Request); +#else + JoinSessionInternalOSSv2(LocalPlayer, Request); +#endif // COMMONUSER_OSSV1 +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::JoinSessionInternalOSSv1(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request) +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions); + + // We need to manually set that we want this to be our presence session + Request->Result.Session.SessionSettings.bUsesPresence = true; + Request->Result.Session.SessionSettings.bUseLobbiesIfAvailable = true; + + Sessions->JoinSession(*LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), NAME_GameSession, Request->Result); +} + +void UCommonSessionSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) +{ + // Add any splitscreen players if they exist + //@TODO: +// if (Result == EOnJoinSessionCompleteResult::Success && LocalPlayers.Num() > 1) +// { +// IOnlineSessionPtr Sessions = Online::GetSessionInterface(GetWorld()); +// if (Sessions.IsValid() && LocalPlayers[1]->GetPreferredUniqueNetId().IsValid()) +// { +// Sessions->RegisterLocalPlayer(*LocalPlayers[1]->GetPreferredUniqueNetId(), NAME_GameSession, +// FOnRegisterLocalPlayerCompleteDelegate::CreateUObject(this, &UShooterGameInstance::OnRegisterJoiningLocalPlayerComplete)); +// } +// } +// else + { + FinishJoinSession(Result); + } +} + +void UCommonSessionSubsystem::OnRegisterJoiningLocalPlayerComplete(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result) +{ + FinishJoinSession(Result); +} + +void UCommonSessionSubsystem::ConnectToHostReservationBeacon() +{ + UWorld* const World = GetWorld(); + check(World); + ReservationBeaconClient = World->SpawnActor(APartyBeaconClient::StaticClass()); + check(ReservationBeaconClient.IsValid()); + + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(World); + check(OnlineSub); + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions); + FNamedOnlineSession* Session = Sessions->GetNamedSession(NAME_GameSession); + check(Session); + FString SessionIdStr = Session->GetSessionIdStr(); + + FString ConnectInfo; + Sessions->GetResolvedConnectString(NAME_GameSession, ConnectInfo, NAME_BeaconPort); + + IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + check(Identity); + FUniqueNetIdWrapper DefaultNetId = Identity->GetUniquePlayerId(0); + check(DefaultNetId.IsValid()); + + FPlayerReservation PlayerReservation; + PlayerReservation.UniqueId = *DefaultNetId; + PlayerReservation.Platform = OnlineSub->GetLocalPlatformName(); + + ReservationBeaconClient->OnHostConnectionFailure().BindWeakLambda(this, [this]() + { + // We only want to react to failure calls while the connection is active, not when it closes + if(ReservationBeaconClient->GetNetDriver()) + { + FOnlineResultInformation JoinSessionResult; + JoinSessionResult.bWasSuccessful = false; + JoinSessionResult.ErrorId = TEXT("UnknownError"); + + NotifyJoinSessionComplete(JoinSessionResult); + NotifySessionInformationUpdated(ECommonSessionInformationState::OutOfGame); + + CleanUpSessions(); + } + }); + + ReservationBeaconClient->OnReservationRequestComplete().BindWeakLambda(this, [this](EPartyReservationResult::Type ReservationResponse) + { + if (ReservationResponse == EPartyReservationResult::ReservationAccepted) + { + FOnlineResultInformation JoinSessionResult; + JoinSessionResult.bWasSuccessful = true; + NotifyJoinSessionComplete(JoinSessionResult); + + InternalTravelToSession(NAME_GameSession); + } + else + { + FOnlineResultInformation JoinSessionResult; + JoinSessionResult.bWasSuccessful = false; + JoinSessionResult.ErrorId = TEXT("UnknownError"); + + NotifyJoinSessionComplete(JoinSessionResult); + NotifySessionInformationUpdated(ECommonSessionInformationState::OutOfGame); + + CleanUpSessions(); + } + }); + + ReservationBeaconClient->RequestReservation(ConnectInfo, SessionIdStr, *DefaultNetId, { PlayerReservation }); +} + +void UCommonSessionSubsystem::FinishJoinSession(EOnJoinSessionCompleteResult::Type Result) +{ + if (Result == EOnJoinSessionCompleteResult::Success) + { + if (bUseBeacons) + { + // InternalTravelToSession and the notification will be called by the beacon after a successful reservation. The beacon will be destroyed during travel. + ConnectToHostReservationBeacon(); + } + else + { + //@TODO Synchronize timing of this with create callbacks, modify both places and the comments if plan changes + FOnlineResultInformation JoinSessionResult; + JoinSessionResult.bWasSuccessful = true; + NotifyJoinSessionComplete(JoinSessionResult); + + InternalTravelToSession(NAME_GameSession); + } + } + else + { + FText ReturnReason; + switch (Result) + { + case EOnJoinSessionCompleteResult::SessionIsFull: + ReturnReason = NSLOCTEXT("NetworkErrors", "SessionIsFull", "Game is full."); + break; + case EOnJoinSessionCompleteResult::SessionDoesNotExist: + ReturnReason = NSLOCTEXT("NetworkErrors", "SessionDoesNotExist", "Game no longer exists."); + break; + default: + ReturnReason = NSLOCTEXT("NetworkErrors", "JoinSessionFailed", "Join failed."); + break; + } + + //@TODO: Error handling + UE_LOG(LogCommonSession, Error, TEXT("FinishJoinSession(Failed with Result: %s)"), *ReturnReason.ToString()); + + // No FOnlineError to initialize from + FOnlineResultInformation JoinSessionResult; + JoinSessionResult.bWasSuccessful = false; + JoinSessionResult.ErrorId = LexToString(Result); // This is not robust but there is no extended information available + JoinSessionResult.ErrorText = ReturnReason; + NotifyJoinSessionComplete(JoinSessionResult); + NotifySessionInformationUpdated(ECommonSessionInformationState::OutOfGame); + + // If the session join failed, we'll clean up the session + CleanUpSessions(); + } +} + +#else + +void UCommonSessionSubsystem::JoinSessionInternalOSSv2(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request) +{ + const FName SessionName(NAME_GameSession); + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + + FJoinLobby::Params JoinParams; + JoinParams.LocalAccountId = LocalPlayer->GetPreferredUniqueNetId().GetV2(); + JoinParams.LocalName = SessionName; + JoinParams.LobbyId = Request->Lobby->LobbyId; + JoinParams.bPresenceEnabled = true; + + // Add any splitscreen players if they exist //@TODO: See UCommonSessionSubsystem::OnJoinSessionComplete + + Lobbies->JoinLobby(MoveTemp(JoinParams)).OnComplete(this, [this, SessionName](const TOnlineResult& JoinResult) + { + if (JoinResult.IsOk()) + { + InternalTravelToSession(SessionName); + } + else + { + //@TODO: Error handling + UE_LOG(LogCommonSession, Error, TEXT("JoinLobby Failed with Result: %s"), *ToLogString(JoinResult.GetErrorValue())); + } + }); +} + +void UCommonSessionSubsystem::OnSessionJoinRequested(const UE::Online::FUILobbyJoinRequested& EventParams) +{ + TSharedPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + IAuthPtr Auth = OnlineServices->GetAuthInterface(); + check(Auth); + TOnlineResult Account = Auth->GetLocalOnlineUserByOnlineAccountId({ EventParams.LocalAccountId }); + if (Account.IsOk()) + { + FPlatformUserId PlatformUserId = Account.GetOkValue().AccountInfo->PlatformUserId; + UCommonSession_SearchResult* RequestedSession = nullptr; + FOnlineResultInformation ResultInfo; + if (EventParams.Result.IsOk()) + { + RequestedSession = NewObject(this); + RequestedSession->Lobby = EventParams.Result.GetOkValue(); + } + else + { + ResultInfo.FromOnlineError(EventParams.Result.GetErrorValue()); + } + NotifyUserRequestedSession(PlatformUserId, RequestedSession, ResultInfo); + } + else + { + UE_LOG(LogCommonSession, Error, TEXT("OnJoinLobbyRequested: Failed to get account by local user id %s"), *UE::Online::ToLogString(EventParams.LocalAccountId)); + } +} + +UE::Online::FAccountId UCommonSessionSubsystem::GetAccountId(APlayerController* PlayerController) const +{ + if (const ULocalPlayer* const LocalPlayer = PlayerController->GetLocalPlayer()) + { + FUniqueNetIdRepl LocalPlayerIdRepl = LocalPlayer->GetPreferredUniqueNetId(); + if (LocalPlayerIdRepl.IsValid()) + { + return LocalPlayerIdRepl.GetV2(); + } + } + return FAccountId(); +} + +UE::Online::FLobbyId UCommonSessionSubsystem::GetLobbyId(const FName SessionName) const +{ + FAccountId LocalUserId = GetAccountId(GetGameInstance()->GetFirstLocalPlayerController()); + if (LocalUserId.IsValid()) + { + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + TOnlineResult JoinedLobbies = Lobbies->GetJoinedLobbies({ LocalUserId }); + if (JoinedLobbies.IsOk()) + { + for (const TSharedRef& Lobby : JoinedLobbies.GetOkValue().Lobbies) + { + if (Lobby->LocalName == SessionName) + { + return Lobby->LobbyId; + } + } + } + } + return FLobbyId(); +} + +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::InternalTravelToSession(const FName SessionName) +{ + //@TODO: Ideally we'd use triggering player instead of first (they're all gonna go at once so it probably doesn't matter) + APlayerController* const PlayerController = GetGameInstance()->GetFirstLocalPlayerController(); + if (PlayerController == nullptr) + { + FText ReturnReason = NSLOCTEXT("NetworkErrors", "InvalidPlayerController", "Invalid Player Controller"); + UE_LOG(LogCommonSession, Error, TEXT("InternalTravelToSession(Failed due to %s)"), *ReturnReason.ToString()); + return; + } + + FString URL; +#if COMMONUSER_OSSV1 + // travel to session + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions.IsValid()); + + if (!Sessions->GetResolvedConnectString(SessionName, URL)) + { + FText FailReason = NSLOCTEXT("NetworkErrors", "TravelSessionFailed", "Travel to Session failed."); + UE_LOG(LogCommonSession, Error, TEXT("InternalTravelToSession(%s)"), *FailReason.ToString()); + return; + } +#else + TSharedPtr OnlineServices = GetServices(GetWorld(), EOnlineServices::Default); + check(OnlineServices); + + FAccountId LocalUserId = GetAccountId(PlayerController); + if (LocalUserId.IsValid()) + { + TOnlineResult Result = OnlineServices->GetResolvedConnectString({LocalUserId, GetLobbyId(SessionName)}); + if (ensure(Result.IsOk())) + { + URL = Result.GetOkValue().ResolvedConnectString; + } + } +#endif // COMMONUSER_OSSV1 + + // Allow modification of the URL prior to travel + OnPreClientTravelEvent.Broadcast(URL); + + PlayerController->ClientTravel(URL, TRAVEL_Absolute); +} + +void UCommonSessionSubsystem::NotifyUserRequestedSession(const FPlatformUserId& PlatformUserId, UCommonSession_SearchResult* RequestedSession, const FOnlineResultInformation& RequestedSessionResult) +{ + OnUserRequestedSessionEvent.Broadcast(PlatformUserId, RequestedSession, RequestedSessionResult); + K2_OnUserRequestedSessionEvent.Broadcast(PlatformUserId, RequestedSession, RequestedSessionResult); +} + +void UCommonSessionSubsystem::NotifyJoinSessionComplete(const FOnlineResultInformation& Result) +{ + OnJoinSessionCompleteEvent.Broadcast(Result); + K2_OnJoinSessionCompleteEvent.Broadcast(Result); +} + +void UCommonSessionSubsystem::NotifyCreateSessionComplete(const FOnlineResultInformation& Result) +{ + OnCreateSessionCompleteEvent.Broadcast(Result); + K2_OnCreateSessionCompleteEvent.Broadcast(Result); +} + +void UCommonSessionSubsystem::NotifySessionInformationUpdated(ECommonSessionInformationState SessionStatus, const FString& GameMode, const FString& MapName) +{ + OnSessionInformationChangedEvent.Broadcast(SessionStatus, GameMode, MapName); + K2_OnSessionInformationChangedEvent.Broadcast(SessionStatus, GameMode, MapName); +} + +void UCommonSessionSubsystem::NotifyDestroySessionRequested(const FPlatformUserId& PlatformUserId, const FName& SessionName) +{ + OnDestroySessionRequestedEvent.Broadcast(PlatformUserId, SessionName); + K2_OnDestroySessionRequestedEvent.Broadcast(PlatformUserId, SessionName); +} + +void UCommonSessionSubsystem::SetCreateSessionError(const FText& ErrorText) +{ + CreateSessionResult.bWasSuccessful = false; + CreateSessionResult.ErrorId = TEXT("InternalFailure"); + + // TODO May want to replace with a generic error text in shipping builds depending on how much data you want to give users + CreateSessionResult.ErrorText = ErrorText; +} + +void UCommonSessionSubsystem::CreateHostReservationBeacon() +{ + check(!BeaconHostListener.IsValid()); + check(!ReservationBeaconHost.IsValid()); + + UWorld* const World = GetWorld(); + BeaconHostListener = World->SpawnActor(AOnlineBeaconHost::StaticClass()); + check(BeaconHostListener.IsValid()); + verify(BeaconHostListener->InitHost()); + + ReservationBeaconHost = World->SpawnActor(APartyBeaconHost::StaticClass()); + check(ReservationBeaconHost.IsValid()); + + if (ReservationBeaconHostState) + { + ReservationBeaconHost->InitFromBeaconState(&*ReservationBeaconHostState); + } + else + { + // TODO: We are using the default hard-coded values for the parameters for now, but they are configurable + ReservationBeaconHost->InitHostBeacon(BeaconTeamCount, BeaconTeamSize, BeaconMaxReservations, NAME_GameSession); + ReservationBeaconHostState = ReservationBeaconHost->GetState(); + } + + BeaconHostListener->RegisterHost(ReservationBeaconHost.Get()); + BeaconHostListener->PauseBeaconRequests(false); +} + +void UCommonSessionSubsystem::DestroyHostReservationBeacon() +{ + if (BeaconHostListener.IsValid() && ReservationBeaconHost.IsValid()) + { + BeaconHostListener->UnregisterHost(ReservationBeaconHost->GetBeaconType()); + } + if (BeaconHostListener.IsValid()) + { + BeaconHostListener->Destroy(); + BeaconHostListener = nullptr; + } + if (ReservationBeaconHost.IsValid()) + { + ReservationBeaconHost->Destroy(); + ReservationBeaconHost = nullptr; + } +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::HandleSessionFailure(const FUniqueNetId& NetId, ESessionFailure::Type FailureType) +{ + UE_LOG(LogCommonSession, Warning, TEXT("UCommonSessionSubsystem::HandleSessionFailure(NetId: %s, FailureType: %s)"), *NetId.ToDebugString(), LexToString(FailureType)); + + //@TODO: Probably need to do a bit more... +} + +void UCommonSessionSubsystem::HandleSessionUserInviteAccepted(const bool bWasSuccessful, const int32 LocalUserIndex, FUniqueNetIdPtr AcceptingUserId, const FOnlineSessionSearchResult& SearchResult) +{ + FPlatformUserId PlatformUserId = IPlatformInputDeviceMapper::Get().GetPlatformUserForUserIndex(LocalUserIndex); + + UCommonSession_SearchResult* RequestedSession = nullptr; + FOnlineResultInformation ResultInfo; + if (bWasSuccessful) + { + RequestedSession = NewObject(this); + RequestedSession->Result = SearchResult; + } + else + { + // No FOnlineError to initialize from + ResultInfo.bWasSuccessful = false; + ResultInfo.ErrorId = TEXT("failed"); // This is not robust but there is no extended information available + ResultInfo.ErrorText = LOCTEXT("Error_SessionUserInviteAcceptedFailed", "Failed to handle the join request"); + } + NotifyUserRequestedSession(PlatformUserId, RequestedSession, ResultInfo); +} + +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::TravelLocalSessionFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ReasonString) +{ + // The delegate for this is global, but PIE can have more than one game instance, so make + // sure it's being raised for the same world this game instance subsystem is associated with + if (World != GetWorld()) + { + return; + } + + UE_LOG(LogCommonSession, Warning, TEXT("TravelLocalSessionFailure(World: %s, FailureType: %s, ReasonString: %s)"), + *GetPathNameSafe(World), + ETravelFailure::ToString(FailureType), + *ReasonString); + + // TODO: Broadcast this failure when we are also able to broadcast a success. Presently we broadcast a success before starting the travel, so a failure after a success is confusing. + //FOnlineResultInformation JoinSessionResult; + //JoinSessionResult.bWasSuccessful = false; + //JoinSessionResult.ErrorId = ReasonString; // TODO: Is this an adequate ErrorId? + //JoinSessionResult.ErrorText = FText::FromString(ReasonString); + //NotifyJoinSessionComplete(JoinSessionResult); + NotifySessionInformationUpdated(ECommonSessionInformationState::OutOfGame); +} + +void UCommonSessionSubsystem::HandlePostLoadMap(UWorld* World) +{ + // Ignore null worlds. + if (!World) + { + return; + } + + // Ignore any world that isn't part of this game instance, which can be the case in the editor. + if (World->GetGameInstance() != GetGameInstance()) + { + return; + } + + // We don't care about updating the session unless the world type is game/pie. + if (!(World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE)) + { + return; + } + +#if COMMONUSER_OSSV1 + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + + const IOnlineSessionPtr SessionInterface = OnlineSub->GetSessionInterface(); + check(SessionInterface.IsValid()); + + const FName SessionName(NAME_GameSession); + FNamedOnlineSession* CurrentSession = SessionInterface->GetNamedSession(SessionName); + + // If we're hosting a session, update the advertised map name. + if (CurrentSession != nullptr && CurrentSession->bHosting) + { + // This needs to be the full package path to match the host GetMapName function, World->GetMapName is currently the short name - update host settings + CurrentSession->SessionSettings.Set(SETTING_MAPNAME, UWorld::RemovePIEPrefix(World->GetOutermost()->GetName()), EOnlineDataAdvertisementType::ViaOnlineService); + + SessionInterface->UpdateSession(SessionName, CurrentSession->SessionSettings, true); + + if (bUseBeacons) + { + CreateHostReservationBeacon(); + } + } +#endif // COMMONUSER_OSSV1 +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/CommonUser/Source/CommonUser/Private/CommonUserBasicPresence.cpp b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserBasicPresence.cpp new file mode 100644 index 0000000..067fc82 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserBasicPresence.cpp @@ -0,0 +1,120 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#include "CommonUserBasicPresence.h" +#include "CommonSessionSubsystem.h" +#include "Engine/GameInstance.h" +#include "Engine/LocalPlayer.h" +#include "CommonUserTypes.h" + + +#if COMMONUSER_OSSV1 +#include "OnlineSubsystemUtils.h" +#include "Interfaces/OnlinePresenceInterface.h" +#else +#include "Online/OnlineServicesEngineUtils.h" +#include "Online/Presence.h" +#endif + +DECLARE_LOG_CATEGORY_EXTERN(LogUserBasicPresence, Log, All); +DEFINE_LOG_CATEGORY(LogUserBasicPresence); + +UCommonUserBasicPresence::UCommonUserBasicPresence() +{ + +} + +void UCommonUserBasicPresence::Initialize(FSubsystemCollectionBase& Collection) +{ + UCommonSessionSubsystem* CommonSession = Collection.InitializeDependency(); + if(ensure(CommonSession)) + { + CommonSession->OnSessionInformationChangedEvent.AddUObject(this, &UCommonUserBasicPresence::OnNotifySessionInformationChanged); + } +} + +void UCommonUserBasicPresence::Deinitialize() +{ + +} + +FString UCommonUserBasicPresence::SessionStateToBackendKey(ECommonSessionInformationState SessionStatus) +{ + switch (SessionStatus) + { + case ECommonSessionInformationState::OutOfGame: + return PresenceStatusMainMenu; + break; + case ECommonSessionInformationState::Matchmaking: + return PresenceStatusMatchmaking; + break; + case ECommonSessionInformationState::InGame: + return PresenceStatusInGame; + break; + default: + UE_LOG(LogUserBasicPresence, Error, TEXT("UCommonUserBasicPresence::SessionStateToBackendKey: Found unknown enum value %d"), (uint8)SessionStatus); + return TEXT("Unknown"); + break; + + } +} + +void UCommonUserBasicPresence::OnNotifySessionInformationChanged(ECommonSessionInformationState SessionStatus, const FString& GameMode, const FString& MapName) +{ + if (bEnableSessionsBasedPresence && !GetGameInstance()->IsDedicatedServerInstance()) + { + // trim the map name since its a URL + FString MapNameTruncated = MapName; + if (!MapNameTruncated.IsEmpty()) + { + int LastIndexOfSlash = 0; + MapNameTruncated.FindLastChar('/', LastIndexOfSlash); + MapNameTruncated = MapNameTruncated.RightChop(LastIndexOfSlash + 1); + } + +#if COMMONUSER_OSSV1 + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if(OnlineSub) + { + IOnlinePresencePtr Presence = OnlineSub->GetPresenceInterface(); + if(Presence) + { + FOnlineUserPresenceStatus UpdatedPresence; + UpdatedPresence.State = EOnlinePresenceState::Online; // We'll only send the presence update if the user has a valid UniqueNetId, so we can assume they are Online + UpdatedPresence.StatusStr = *SessionStateToBackendKey(SessionStatus); + UpdatedPresence.Properties.Emplace(PresenceKeyGameMode, GameMode); + UpdatedPresence.Properties.Emplace(PresenceKeyMapName, MapNameTruncated); + + for (const ULocalPlayer* LocalPlayer : GetGameInstance()->GetLocalPlayers()) + { + if (LocalPlayer && LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId() != nullptr) + { + Presence->SetPresence(*LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), UpdatedPresence); + } + } + } + } + +#else + + UE::Online::IOnlineServicesPtr OnlineServices = UE::Online::GetServices(GetWorld()); + check(OnlineServices); + UE::Online::IPresencePtr Presence = OnlineServices->GetPresenceInterface(); + if(Presence) + { + for (const ULocalPlayer* LocalPlayer : GetGameInstance()->GetLocalPlayers()) + { + if (LocalPlayer && LocalPlayer->GetPreferredUniqueNetId().IsV2()) + { + UE::Online::FPartialUpdatePresence::Params UpdateParams; + UpdateParams.LocalAccountId = LocalPlayer->GetPreferredUniqueNetId().GetV2(); + UpdateParams.Mutations.StatusString.Emplace(*SessionStateToBackendKey(SessionStatus)); + UpdateParams.Mutations.UpdatedProperties.Emplace(PresenceKeyGameMode, GameMode); + UpdateParams.Mutations.UpdatedProperties.Emplace(PresenceKeyMapName, MapNameTruncated); + + Presence->PartialUpdatePresence(MoveTemp(UpdateParams)); + } + } + + } +#endif + } +} \ No newline at end of file diff --git a/Plugins/CommonUser/Source/CommonUser/Private/CommonUserModule.cpp b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserModule.cpp new file mode 100644 index 0000000..c738a9b --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserModule.cpp @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonUserModule.h" + +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE "FCommonUserModule" + +void FCommonUserModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FCommonUserModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FCommonUserModule, CommonUser) \ No newline at end of file diff --git a/Plugins/CommonUser/Source/CommonUser/Private/CommonUserSubsystem.cpp b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserSubsystem.cpp new file mode 100644 index 0000000..1a998f8 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserSubsystem.cpp @@ -0,0 +1,2684 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonUserSubsystem.h" +#include "Engine/GameInstance.h" +#include "Engine/LocalPlayer.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerState.h" +#include "InputKeyEventArgs.h" +#include "NativeGameplayTags.h" +#include "TimerManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonUserSubsystem) + +#if COMMONUSER_OSSV1 +#include "OnlineSubsystemNames.h" +#include "OnlineSubsystemUtils.h" +#else +#include "Online/Auth.h" +#include "Online/ExternalUI.h" +#include "Online/OnlineResult.h" +#include "Online/OnlineServices.h" +#include "Online/OnlineServicesEngineUtils.h" +#include "Online/Privileges.h" + +using namespace UE::Online; +#endif + +DECLARE_LOG_CATEGORY_EXTERN(LogCommonUser, Log, All); +DEFINE_LOG_CATEGORY(LogCommonUser); + +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::SystemMessage_Error, "SystemMessage.Error"); +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::SystemMessage_Warning, "SystemMessage.Warning"); +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::SystemMessage_Display, "SystemMessage.Display"); +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::SystemMessage_Error_InitializeLocalPlayerFailed, "SystemMessage.Error.InitializeLocalPlayerFailed"); + +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::Platform_Trait_RequiresStrictControllerMapping, "Platform.Trait.RequiresStrictControllerMapping"); +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::Platform_Trait_SingleOnlineUser, "Platform.Trait.SingleOnlineUser"); + + +////////////////////////////////////////////////////////////////////// +// UCommonUserInfo + +UCommonUserInfo::FCachedData* UCommonUserInfo::GetCachedData(ECommonUserOnlineContext Context) +{ + // Look up directly, game has a separate cache than default + FCachedData* FoundData = CachedDataMap.Find(Context); + if (FoundData) + { + return FoundData; + } + + // Now try system resolution + UCommonUserSubsystem* Subsystem = GetSubsystem(); + + ECommonUserOnlineContext ResolvedContext = Subsystem->ResolveOnlineContext(Context); + return CachedDataMap.Find(ResolvedContext); +} + +const UCommonUserInfo::FCachedData* UCommonUserInfo::GetCachedData(ECommonUserOnlineContext Context) const +{ + return const_cast(this)->GetCachedData(Context); +} + +void UCommonUserInfo::UpdateCachedPrivilegeResult(ECommonUserPrivilege Privilege, ECommonUserPrivilegeResult Result, ECommonUserOnlineContext Context) +{ + // This should only be called with a resolved and valid type + FCachedData* GameCache = GetCachedData(ECommonUserOnlineContext::Game); + FCachedData* ContextCache = GetCachedData(Context); + + if (!ensure(GameCache && ContextCache)) + { + // Should always be valid + return; + } + + // Update direct cache first + ContextCache->CachedPrivileges.Add(Privilege, Result); + + if (GameCache != ContextCache) + { + // Look for another context to merge into game + ECommonUserPrivilegeResult GameContextResult = Result; + ECommonUserPrivilegeResult OtherContextResult = ECommonUserPrivilegeResult::Available; + for (TPair& Pair : CachedDataMap) + { + if (&Pair.Value != ContextCache && &Pair.Value != GameCache) + { + ECommonUserPrivilegeResult* FoundResult = Pair.Value.CachedPrivileges.Find(Privilege); + if (FoundResult) + { + OtherContextResult = *FoundResult; + } + else + { + OtherContextResult = ECommonUserPrivilegeResult::Unknown; + } + break; + } + } + + if (GameContextResult == ECommonUserPrivilegeResult::Available && OtherContextResult != ECommonUserPrivilegeResult::Available) + { + // Other context is worse, use that + GameContextResult = OtherContextResult; + } + + GameCache->CachedPrivileges.Add(Privilege, GameContextResult); + } +} + +void UCommonUserInfo::UpdateCachedNetId(const FUniqueNetIdRepl& NewId, ECommonUserOnlineContext Context) +{ + FCachedData* ContextCache = GetCachedData(Context); + + if (ensure(ContextCache)) + { + ContextCache->CachedNetId = NewId; + + // Update the nickname + const UCommonUserSubsystem* Subsystem = GetSubsystem(); + if (ensure(Subsystem)) + { + if (bIsGuest) + { + if (ContextCache->CachedNickname.IsEmpty()) + { + // Set a default guest name if it is empty, can be changed with SetNickname + ContextCache->CachedNickname = NSLOCTEXT("CommonUser", "GuestNickname", "Guest").ToString(); + } + } + else + { + // Refresh with the system nickname, overrides SetNickname + ContextCache->CachedNickname = Subsystem->GetLocalUserNickname(GetPlatformUserId(), Context); + } + } + } + + // We don't merge the ids because of how guests work +} + +class UCommonUserSubsystem* UCommonUserInfo::GetSubsystem() const +{ + return Cast(GetOuter()); +} + +bool UCommonUserInfo::IsLoggedIn() const +{ + return (InitializationState == ECommonUserInitializationState::LoggedInLocalOnly || InitializationState == ECommonUserInitializationState::LoggedInOnline); +} + +bool UCommonUserInfo::IsDoingLogin() const +{ + return (InitializationState == ECommonUserInitializationState::DoingInitialLogin || InitializationState == ECommonUserInitializationState::DoingNetworkLogin); +} + +ECommonUserPrivilegeResult UCommonUserInfo::GetCachedPrivilegeResult(ECommonUserPrivilege Privilege, ECommonUserOnlineContext Context) const +{ + const FCachedData* FoundCached = GetCachedData(Context); + + if (FoundCached) + { + const ECommonUserPrivilegeResult* FoundResult = FoundCached->CachedPrivileges.Find(Privilege); + if (FoundResult) + { + return *FoundResult; + } + } + return ECommonUserPrivilegeResult::Unknown; +} + +ECommonUserAvailability UCommonUserInfo::GetPrivilegeAvailability(ECommonUserPrivilege Privilege) const +{ + // Bad feature or user + if ((int32)Privilege < 0 || (int32)Privilege >= (int32)ECommonUserPrivilege::Invalid_Count || InitializationState == ECommonUserInitializationState::Invalid) + { + return ECommonUserAvailability::Invalid; + } + + ECommonUserPrivilegeResult CachedResult = GetCachedPrivilegeResult(Privilege, ECommonUserOnlineContext::Game); + + // First handle explicit failures + switch (CachedResult) + { + case ECommonUserPrivilegeResult::LicenseInvalid: + case ECommonUserPrivilegeResult::VersionOutdated: + case ECommonUserPrivilegeResult::AgeRestricted: + return ECommonUserAvailability::AlwaysUnavailable; + + case ECommonUserPrivilegeResult::NetworkConnectionUnavailable: + case ECommonUserPrivilegeResult::AccountTypeRestricted: + case ECommonUserPrivilegeResult::AccountUseRestricted: + case ECommonUserPrivilegeResult::PlatformFailure: + return ECommonUserAvailability::CurrentlyUnavailable; + + default: + break; + } + + if (bIsGuest) + { + // Guests can only play, cannot use online features + if (Privilege == ECommonUserPrivilege::CanPlay) + { + return ECommonUserAvailability::NowAvailable; + } + else + { + return ECommonUserAvailability::AlwaysUnavailable; + } + } + + // Check network status + if (Privilege == ECommonUserPrivilege::CanPlayOnline || + Privilege == ECommonUserPrivilege::CanUseCrossPlay || + Privilege == ECommonUserPrivilege::CanCommunicateViaTextOnline || + Privilege == ECommonUserPrivilege::CanCommunicateViaVoiceOnline) + { + UCommonUserSubsystem* Subsystem = GetSubsystem(); + if (ensure(Subsystem) && !Subsystem->HasOnlineConnection(ECommonUserOnlineContext::Game)) + { + return ECommonUserAvailability::CurrentlyUnavailable; + } + } + + if (InitializationState == ECommonUserInitializationState::FailedtoLogin) + { + // Failed a prior login attempt + return ECommonUserAvailability::CurrentlyUnavailable; + } + else if (InitializationState == ECommonUserInitializationState::Unknown || InitializationState == ECommonUserInitializationState::DoingInitialLogin) + { + // Haven't logged in yet + return ECommonUserAvailability::PossiblyAvailable; + } + else if (InitializationState == ECommonUserInitializationState::LoggedInLocalOnly || InitializationState == ECommonUserInitializationState::DoingNetworkLogin) + { + // Local login succeeded so play checks are valid + if (Privilege == ECommonUserPrivilege::CanPlay && CachedResult == ECommonUserPrivilegeResult::Available) + { + return ECommonUserAvailability::NowAvailable; + } + + // Haven't logged in online yet + return ECommonUserAvailability::PossiblyAvailable; + } + else if (InitializationState == ECommonUserInitializationState::LoggedInOnline) + { + // Fully logged in + if (CachedResult == ECommonUserPrivilegeResult::Available) + { + return ECommonUserAvailability::NowAvailable; + } + + // Failed for other reason + return ECommonUserAvailability::CurrentlyUnavailable; + } + + return ECommonUserAvailability::Unknown; +} + +FUniqueNetIdRepl UCommonUserInfo::GetNetId(ECommonUserOnlineContext Context) const +{ + const FCachedData* FoundCached = GetCachedData(Context); + + if (FoundCached) + { + return FoundCached->CachedNetId; + } + + return FUniqueNetIdRepl(); +} + +FString UCommonUserInfo::GetNickname(ECommonUserOnlineContext Context) const +{ + const FCachedData* FoundCached = GetCachedData(Context); + + if (FoundCached) + { + return FoundCached->CachedNickname; + } + + // TODO maybe return unknown user here? + return FString(); +} + +void UCommonUserInfo::SetNickname(const FString& NewNickname, ECommonUserOnlineContext Context) +{ + FCachedData* ContextCache = GetCachedData(Context); + + if (ensure(ContextCache)) + { + ContextCache->CachedNickname = NewNickname; + } +} + +FString UCommonUserInfo::GetDebugString() const +{ + FUniqueNetIdRepl NetId = GetNetId(); + return NetId.ToDebugString(); +} + +FPlatformUserId UCommonUserInfo::GetPlatformUserId() const +{ + return PlatformUser; +} + +int32 UCommonUserInfo::GetPlatformUserIndex() const +{ + // Convert our platform id to index + const UCommonUserSubsystem* Subsystem = GetSubsystem(); + + if (ensure(Subsystem)) + { + return Subsystem->GetPlatformUserIndexForId(PlatformUser); + } + + return INDEX_NONE; +} + + +////////////////////////////////////////////////////////////////////// +// UCommonUserSubsystem + +void UCommonUserSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + // Create our OSS wrappers + CreateOnlineContexts(); + + BindOnlineDelegates(); + + IPlatformInputDeviceMapper& DeviceMapper = IPlatformInputDeviceMapper::Get(); + DeviceMapper.GetOnInputDeviceConnectionChange().AddUObject(this, &ThisClass::HandleInputDeviceConnectionChanged); + + // Matches the engine default + SetMaxLocalPlayers(4); + + ResetUserState(); + + UGameInstance* GameInstance = GetGameInstance(); + bIsDedicatedServer = GameInstance->IsDedicatedServerInstance(); +} + +void UCommonUserSubsystem::CreateOnlineContexts() +{ + // First initialize default + DefaultContextInternal = new FOnlineContextCache(); +#if COMMONUSER_OSSV1 + DefaultContextInternal->OnlineSubsystem = Online::GetSubsystem(GetWorld()); + check(DefaultContextInternal->OnlineSubsystem); + DefaultContextInternal->IdentityInterface = DefaultContextInternal->OnlineSubsystem->GetIdentityInterface(); + check(DefaultContextInternal->IdentityInterface.IsValid()); + + IOnlineSubsystem* PlatformSub = IOnlineSubsystem::GetByPlatform(); + + if (PlatformSub && DefaultContextInternal->OnlineSubsystem != PlatformSub) + { + // Set up the optional platform service if it exists + PlatformContextInternal = new FOnlineContextCache(); + PlatformContextInternal->OnlineSubsystem = PlatformSub; + PlatformContextInternal->IdentityInterface = PlatformSub->GetIdentityInterface(); + check(PlatformContextInternal->IdentityInterface.IsValid()); + } +#else + DefaultContextInternal->OnlineServices = GetServices(GetWorld(), EOnlineServices::Default); + check(DefaultContextInternal->OnlineServices); + DefaultContextInternal->AuthService = DefaultContextInternal->OnlineServices->GetAuthInterface(); + check(DefaultContextInternal->AuthService); + + UE::Online::IOnlineServicesPtr PlatformServices = GetServices(GetWorld(), EOnlineServices::Platform); + if (PlatformServices && DefaultContextInternal->OnlineServices != PlatformServices) + { + PlatformContextInternal = new FOnlineContextCache(); + PlatformContextInternal->OnlineServices = PlatformServices; + PlatformContextInternal->AuthService = PlatformContextInternal->OnlineServices->GetAuthInterface(); + check(PlatformContextInternal->AuthService); + } +#endif + + // Explicit external services can be set up after if needed +} + +void UCommonUserSubsystem::Deinitialize() +{ + DestroyOnlineContexts(); + + IPlatformInputDeviceMapper& DeviceMapper = IPlatformInputDeviceMapper::Get(); + DeviceMapper.GetOnInputDeviceConnectionChange().RemoveAll(this); + + LocalUserInfos.Reset(); + ActiveLoginRequests.Reset(); + + Super::Deinitialize(); +} + +void UCommonUserSubsystem::DestroyOnlineContexts() +{ + // All cached shared ptrs must be cleared here + if (ServiceContextInternal && ServiceContextInternal != DefaultContextInternal) + { + delete ServiceContextInternal; + } + if (PlatformContextInternal && PlatformContextInternal != DefaultContextInternal) + { + delete PlatformContextInternal; + } + if (DefaultContextInternal) + { + delete DefaultContextInternal; + } + + ServiceContextInternal = PlatformContextInternal = DefaultContextInternal = nullptr; +} + +UCommonUserInfo* UCommonUserSubsystem::CreateLocalUserInfo(int32 LocalPlayerIndex) +{ + UCommonUserInfo* NewUser = nullptr; + if (ensure(!LocalUserInfos.Contains(LocalPlayerIndex))) + { + NewUser = NewObject(this); + NewUser->LocalPlayerIndex = LocalPlayerIndex; + NewUser->InitializationState = ECommonUserInitializationState::Unknown; + + // Always create game and default cache + NewUser->CachedDataMap.Add(ECommonUserOnlineContext::Game, UCommonUserInfo::FCachedData()); + NewUser->CachedDataMap.Add(ECommonUserOnlineContext::Default, UCommonUserInfo::FCachedData()); + + // Add platform if needed + if (HasSeparatePlatformContext()) + { + NewUser->CachedDataMap.Add(ECommonUserOnlineContext::Platform, UCommonUserInfo::FCachedData()); + } + + LocalUserInfos.Add(LocalPlayerIndex, NewUser); + } + return NewUser; +} + +bool UCommonUserSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + + // Only create an instance if there is not a game-specific subclass + return ChildClasses.Num() == 0; +} + +void UCommonUserSubsystem::BindOnlineDelegates() +{ +#if COMMONUSER_OSSV1 + return BindOnlineDelegatesOSSv1(); +#else + return BindOnlineDelegatesOSSv2(); +#endif +} + +void UCommonUserSubsystem::LogOutLocalUser(FPlatformUserId PlatformUser) +{ + UCommonUserInfo* UserInfo = ModifyInfo(GetUserInfoForPlatformUser(PlatformUser)); + + // Don't need to do anything if the user has never logged in fully or is in the process of logging in + if (UserInfo && (UserInfo->InitializationState == ECommonUserInitializationState::LoggedInLocalOnly || UserInfo->InitializationState == ECommonUserInitializationState::LoggedInOnline)) + { + ECommonUserAvailability OldAvailablity = UserInfo->GetPrivilegeAvailability(ECommonUserPrivilege::CanPlay); + + UserInfo->InitializationState = ECommonUserInitializationState::FailedtoLogin; + + // This will broadcast the game delegate + HandleChangedAvailability(UserInfo, ECommonUserPrivilege::CanPlay, OldAvailablity); + } +} + +bool UCommonUserSubsystem::TransferPlatformAuth(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ +#if COMMONUSER_OSSV1 + // Not supported in V1 path + return false; +#else + return TransferPlatformAuthOSSv2(System, Request, PlatformUser); +#endif +} + +bool UCommonUserSubsystem::AutoLogin(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player AutoLogin requested - UserIdx:%d, Privilege:%d, Context:%d"), + PlatformUser.GetInternalId(), + (int32)Request->DesiredPrivilege, + (int32)Request->DesiredContext); + +#if COMMONUSER_OSSV1 + return AutoLoginOSSv1(System, Request, PlatformUser); +#else + return AutoLoginOSSv2(System, Request, PlatformUser); +#endif +} + +bool UCommonUserSubsystem::ShowLoginUI(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player LoginUI requested - UserIdx:%d, Privilege:%d, Context:%d"), + PlatformUser.GetInternalId(), + (int32)Request->DesiredPrivilege, + (int32)Request->DesiredContext); + +#if COMMONUSER_OSSV1 + return ShowLoginUIOSSv1(System, Request, PlatformUser); +#else + return ShowLoginUIOSSv2(System, Request, PlatformUser); +#endif +} + +bool UCommonUserSubsystem::QueryUserPrivilege(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ +#if COMMONUSER_OSSV1 + return QueryUserPrivilegeOSSv1(System, Request, PlatformUser); +#else + return QueryUserPrivilegeOSSv2(System, Request, PlatformUser); +#endif +} + + +#if COMMONUSER_OSSV1 +IOnlineSubsystem* UCommonUserSubsystem::GetOnlineSubsystem(ECommonUserOnlineContext Context) const +{ + const FOnlineContextCache* System = GetContextCache(Context); + + if (System) + { + return System->OnlineSubsystem; + } + + return nullptr; +} + +IOnlineIdentity* UCommonUserSubsystem::GetOnlineIdentity(ECommonUserOnlineContext Context) const +{ + const FOnlineContextCache* System = GetContextCache(Context); + if (System) + { + return System->IdentityInterface.Get(); + } + + return nullptr; +} + +FName UCommonUserSubsystem::GetOnlineSubsystemName(ECommonUserOnlineContext Context) const +{ + IOnlineSubsystem* SubSystem = GetOnlineSubsystem(Context); + if (SubSystem) + { + return SubSystem->GetSubsystemName(); + } + + return NAME_None; +} + +EOnlineServerConnectionStatus::Type UCommonUserSubsystem::GetConnectionStatus(ECommonUserOnlineContext Context) const +{ + const FOnlineContextCache* System = GetContextCache(Context); + if (System) + { + return System->CurrentConnectionStatus; + } + + return EOnlineServerConnectionStatus::ServiceUnavailable; +} + +void UCommonUserSubsystem::BindOnlineDelegatesOSSv1() +{ + ECommonUserOnlineContext ServiceType = ResolveOnlineContext(ECommonUserOnlineContext::ServiceOrDefault); + ECommonUserOnlineContext PlatformType = ResolveOnlineContext(ECommonUserOnlineContext::PlatformOrDefault); + FOnlineContextCache* ServiceContext = GetContextCache(ServiceType); + FOnlineContextCache* PlatformContext = GetContextCache(PlatformType); + check(ServiceContext && ServiceContext->OnlineSubsystem && PlatformContext && PlatformContext->OnlineSubsystem); + // Connection delegates need to listen for both systems + + ServiceContext->OnlineSubsystem->AddOnConnectionStatusChangedDelegate_Handle(FOnConnectionStatusChangedDelegate::CreateUObject(this, &ThisClass::HandleNetworkConnectionStatusChanged, ServiceType)); + ServiceContext->CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; + + for (int32 PlayerIdx = 0; PlayerIdx < MAX_LOCAL_PLAYERS; PlayerIdx++) + { + ServiceContext->IdentityInterface->AddOnLoginStatusChangedDelegate_Handle(PlayerIdx, FOnLoginStatusChangedDelegate::CreateUObject(this, &ThisClass::HandleIdentityLoginStatusChanged, ServiceType)); + ServiceContext->IdentityInterface->AddOnLoginCompleteDelegate_Handle(PlayerIdx, FOnLoginCompleteDelegate::CreateUObject(this, &ThisClass::HandleUserLoginCompleted, ServiceType)); + } + + if (ServiceType != PlatformType) + { + PlatformContext->OnlineSubsystem->AddOnConnectionStatusChangedDelegate_Handle(FOnConnectionStatusChangedDelegate::CreateUObject(this, &ThisClass::HandleNetworkConnectionStatusChanged, PlatformType)); + PlatformContext->CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; + + for (int32 PlayerIdx = 0; PlayerIdx < MAX_LOCAL_PLAYERS; PlayerIdx++) + { + PlatformContext->IdentityInterface->AddOnLoginStatusChangedDelegate_Handle(PlayerIdx, FOnLoginStatusChangedDelegate::CreateUObject(this, &ThisClass::HandleIdentityLoginStatusChanged, PlatformType)); + PlatformContext->IdentityInterface->AddOnLoginCompleteDelegate_Handle(PlayerIdx, FOnLoginCompleteDelegate::CreateUObject(this, &ThisClass::HandleUserLoginCompleted, PlatformType)); + } + } + + // Hardware change delegates only listen to platform + PlatformContext->IdentityInterface->AddOnControllerPairingChangedDelegate_Handle(FOnControllerPairingChangedDelegate::CreateUObject(this, &ThisClass::HandleControllerPairingChanged)); +} + +bool UCommonUserSubsystem::AutoLoginOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + return System->IdentityInterface->AutoLogin(GetPlatformUserIndexForId(PlatformUser)); +} + +bool UCommonUserSubsystem::ShowLoginUIOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + IOnlineExternalUIPtr ExternalUI = System->OnlineSubsystem->GetExternalUIInterface(); + if (ExternalUI.IsValid()) + { + // TODO Unclear which flags should be set + return ExternalUI->ShowLoginUI(GetPlatformUserIndexForId(PlatformUser), false, false, FOnLoginUIClosedDelegate::CreateUObject(this, &ThisClass::HandleOnLoginUIClosed, Request->CurrentContext)); + } + return false; +} + +bool UCommonUserSubsystem::QueryUserPrivilegeOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + // Start query on unknown or failure + EUserPrivileges::Type OSSPrivilege = ConvertOSSPrivilege(Request->DesiredPrivilege); + + FUniqueNetIdRepl CurrentId = GetLocalUserNetId(PlatformUser, Request->CurrentContext); + check(CurrentId.IsValid()); + IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate Delegate = IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateUObject(this, &UCommonUserSubsystem::HandleCheckPrivilegesComplete, Request->DesiredPrivilege, Request->UserInfo, Request->CurrentContext); + System->IdentityInterface->GetUserPrivilege(*CurrentId, OSSPrivilege, Delegate); + + // This may immediately succeed and reenter this function, so we have to return + return true; +} + +#else + +UE::Online::EOnlineServices UCommonUserSubsystem::GetOnlineServicesProvider(ECommonUserOnlineContext Context) const +{ + if (const FOnlineContextCache* System = GetContextCache(Context)) + { + return System->OnlineServices->GetServicesProvider(); + } + return UE::Online::EOnlineServices::None; +} + +UE::Online::IAuthPtr UCommonUserSubsystem::GetOnlineAuth(ECommonUserOnlineContext Context) const +{ + if (const FOnlineContextCache* System = GetContextCache(Context)) + { + return System->AuthService; + } + return nullptr; +} + +UE::Online::EOnlineServicesConnectionStatus UCommonUserSubsystem::GetConnectionStatus(ECommonUserOnlineContext Context) const +{ + if (const FOnlineContextCache* System = GetContextCache(Context)) + { + return System->CurrentConnectionStatus; + } + return UE::Online::EOnlineServicesConnectionStatus::NotConnected; +} + +void UCommonUserSubsystem::BindOnlineDelegatesOSSv2() +{ + ECommonUserOnlineContext ServiceType = ResolveOnlineContext(ECommonUserOnlineContext::ServiceOrDefault); + ECommonUserOnlineContext PlatformType = ResolveOnlineContext(ECommonUserOnlineContext::PlatformOrDefault); + FOnlineContextCache* ServiceContext = GetContextCache(ServiceType); + FOnlineContextCache* PlatformContext = GetContextCache(PlatformType); + check(ServiceContext && ServiceContext->OnlineServices && PlatformContext && PlatformContext->OnlineServices); + + ServiceContext->LoginStatusChangedHandle = ServiceContext->AuthService->OnLoginStatusChanged().Add(this, &ThisClass::HandleAuthLoginStatusChanged, ServiceType); + if (IConnectivityPtr ConnectivityInterface = ServiceContext->OnlineServices->GetConnectivityInterface()) + { + ServiceContext->ConnectionStatusChangedHandle = ConnectivityInterface->OnConnectionStatusChanged().Add(this, &ThisClass::HandleNetworkConnectionStatusChanged, ServiceType); + } + CacheConnectionStatus(ServiceType); + + if (ServiceType != PlatformType) + { + PlatformContext->LoginStatusChangedHandle = PlatformContext->AuthService->OnLoginStatusChanged().Add(this, &ThisClass::HandleAuthLoginStatusChanged, PlatformType); + if (IConnectivityPtr ConnectivityInterface = PlatformContext->OnlineServices->GetConnectivityInterface()) + { + PlatformContext->ConnectionStatusChangedHandle = ConnectivityInterface->OnConnectionStatusChanged().Add(this, &ThisClass::HandleNetworkConnectionStatusChanged, PlatformType); + } + CacheConnectionStatus(PlatformType); + } + // TODO: Controller Pairing Changed - move out of OSS and listen to CoreDelegate directly? +} + +void UCommonUserSubsystem::CacheConnectionStatus(ECommonUserOnlineContext Context) +{ + FOnlineContextCache* ContextCache = GetContextCache(Context); + check(ContextCache); + + EOnlineServicesConnectionStatus ConnectionStatus = EOnlineServicesConnectionStatus::NotConnected; + if (IConnectivityPtr ConnectivityInterface = ContextCache->OnlineServices->GetConnectivityInterface()) + { + const TOnlineResult Result = ConnectivityInterface->GetConnectionStatus(FGetConnectionStatus::Params()); + if (Result.IsOk()) + { + ConnectionStatus = Result.GetOkValue().Status; + } + } + else + { + ConnectionStatus = EOnlineServicesConnectionStatus::Connected; + } + + UE::Online::FConnectionStatusChanged EventParams; + EventParams.PreviousStatus = ContextCache->CurrentConnectionStatus; + EventParams.CurrentStatus = ConnectionStatus; + HandleNetworkConnectionStatusChanged(EventParams, Context); +} + +bool UCommonUserSubsystem::TransferPlatformAuthOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + IAuthPtr PlatformAuthInterface = GetOnlineAuth(ECommonUserOnlineContext::Platform); + if (Request->CurrentContext != ECommonUserOnlineContext::Platform + && PlatformAuthInterface) + { + FAuthQueryExternalAuthToken::Params Params; + Params.LocalAccountId = GetLocalUserNetId(PlatformUser, ECommonUserOnlineContext::Platform).GetV2(); + + PlatformAuthInterface->QueryExternalAuthToken(MoveTemp(Params)) + .OnComplete(this, [this, Request](const TOnlineResult& Result) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + return; + } + + if (Result.IsOk()) + { + const FAuthQueryExternalAuthToken::Result& GenerateAuthTokenResult = Result.GetOkValue(); + FAuthLogin::Params Params; + Params.PlatformUserId = UserInfo->GetPlatformUserId(); + Params.CredentialsType = LoginCredentialsType::ExternalAuth; + Params.CredentialsToken.Emplace(GenerateAuthTokenResult.ExternalAuthToken); + + IAuthPtr PrimaryAuthInterface = GetOnlineAuth(Request->CurrentContext); + PrimaryAuthInterface->Login(MoveTemp(Params)) + .OnComplete(this, [this, Request](const TOnlineResult& Result) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + return; + } + + if (Result.IsOk()) + { + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::Done; + Request->Error.Reset(); + } + else + { + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::Failed; + Request->Error = Result.GetErrorValue(); + } + ProcessLoginRequest(Request); + }); + } + else + { + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::Failed; + Request->Error = Result.GetErrorValue(); + ProcessLoginRequest(Request); + } + }); + return true; + } + return false; +} + +bool UCommonUserSubsystem::AutoLoginOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + FAuthLogin::Params LoginParameters; + LoginParameters.PlatformUserId = PlatformUser; + LoginParameters.CredentialsType = LoginCredentialsType::Auto; + // Leave other LoginParameters as default to allow the online service to determine how to try to automatically log in the user + TOnlineAsyncOpHandle LoginHandle = System->AuthService->Login(MoveTemp(LoginParameters)); + LoginHandle.OnComplete(this, &ThisClass::HandleUserLoginCompletedV2, PlatformUser, Request->CurrentContext); + return true; +} + +bool UCommonUserSubsystem::ShowLoginUIOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + IExternalUIPtr ExternalUI = System->OnlineServices->GetExternalUIInterface(); + if (ExternalUI.IsValid()) + { + FExternalUIShowLoginUI::Params ShowLoginUIParameters; + ShowLoginUIParameters.PlatformUserId = PlatformUser; + TOnlineAsyncOpHandle LoginHandle = ExternalUI->ShowLoginUI(MoveTemp(ShowLoginUIParameters)); + LoginHandle.OnComplete(this, &ThisClass::HandleOnLoginUIClosedV2, PlatformUser, Request->CurrentContext); + return true; + } + return false; +} + +bool UCommonUserSubsystem::QueryUserPrivilegeOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (IPrivilegesPtr PrivilegesInterface = System->OnlineServices->GetPrivilegesInterface()) + { + const EUserPrivileges DesiredPrivilege = ConvertOnlineServicesPrivilege(Request->DesiredPrivilege); + + FQueryUserPrivilege::Params Params; + Params.LocalAccountId = GetLocalUserNetId(PlatformUser, Request->CurrentContext).GetV2(); + Params.Privilege = DesiredPrivilege; + TOnlineAsyncOpHandle QueryHandle = PrivilegesInterface->QueryUserPrivilege(MoveTemp(Params)); + QueryHandle.OnComplete(this, &ThisClass::HandleCheckPrivilegesComplete, Request->UserInfo, DesiredPrivilege, Request->CurrentContext); + return true; + } + else + { + UpdateUserPrivilegeResult(UserInfo, Request->DesiredPrivilege, ECommonUserPrivilegeResult::Available, Request->CurrentContext); + } + return false; +} + +TSharedPtr UCommonUserSubsystem::GetOnlineServiceAccountInfo(IAuthPtr AuthService, FPlatformUserId InUserId) const +{ + TSharedPtr AccountInfo; + FAuthGetLocalOnlineUserByPlatformUserId::Params GetAccountParams = { InUserId }; + TOnlineResult GetAccountResult = AuthService->GetLocalOnlineUserByPlatformUserId(MoveTemp(GetAccountParams)); + if (GetAccountResult.IsOk()) + { + AccountInfo = GetAccountResult.GetOkValue().AccountInfo; + } + return AccountInfo; +} + +#endif + +bool UCommonUserSubsystem::HasOnlineConnection(ECommonUserOnlineContext Context) const +{ +#if COMMONUSER_OSSV1 + EOnlineServerConnectionStatus::Type ConnectionType = GetConnectionStatus(Context); + + if (ConnectionType == EOnlineServerConnectionStatus::Normal || ConnectionType == EOnlineServerConnectionStatus::Connected) + { + return true; + } + + return false; +#else + return GetConnectionStatus(Context) == UE::Online::EOnlineServicesConnectionStatus::Connected; +#endif +} + +ELoginStatusType UCommonUserSubsystem::GetLocalUserLoginStatus(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context) const +{ + if (!IsRealPlatformUser(PlatformUser)) + { + return ELoginStatusType::NotLoggedIn; + } + + const FOnlineContextCache* System = GetContextCache(Context); + if (System) + { +#if COMMONUSER_OSSV1 + return System->IdentityInterface->GetLoginStatus(GetPlatformUserIndexForId(PlatformUser)); +#else + if (TSharedPtr AccountInfo = GetOnlineServiceAccountInfo(System->AuthService, PlatformUser)) + { + return AccountInfo->LoginStatus; + } +#endif + } + return ELoginStatusType::NotLoggedIn; +} + +FUniqueNetIdRepl UCommonUserSubsystem::GetLocalUserNetId(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context) const +{ + if (!IsRealPlatformUser(PlatformUser)) + { + return FUniqueNetIdRepl(); + } + + const FOnlineContextCache* System = GetContextCache(Context); + if (System) + { +#if COMMONUSER_OSSV1 + return FUniqueNetIdRepl(System->IdentityInterface->GetUniquePlayerId(GetPlatformUserIndexForId(PlatformUser))); +#else + // TODO: OSSv2 FUniqueNetIdRepl wrapping FAccountId is in progress + if (TSharedPtr AccountInfo = GetOnlineServiceAccountInfo(System->AuthService, PlatformUser)) + { + return FUniqueNetIdRepl(AccountInfo->AccountId); + } +#endif + } + + return FUniqueNetIdRepl(); +} + +FString UCommonUserSubsystem::GetLocalUserNickname(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context) const +{ +#if COMMONUSER_OSSV1 + IOnlineIdentity* Identity = GetOnlineIdentity(Context); + if (ensure(Identity)) + { + return Identity->GetPlayerNickname(GetPlatformUserIndexForId(PlatformUser)); + } +#else + if (IAuthPtr AuthService = GetOnlineAuth(Context)) + { + if (TSharedPtr AccountInfo = GetOnlineServiceAccountInfo(AuthService, PlatformUser)) + { + if (const FSchemaVariant* DisplayName = AccountInfo->Attributes.Find(AccountAttributeData::DisplayName)) + { + return DisplayName->GetString(); + } + } + } +#endif // COMMONUSER_OSSV1 + + return FString(); +} + +void UCommonUserSubsystem::SendSystemMessage(FGameplayTag MessageType, FText TitleText, FText BodyText) +{ + OnHandleSystemMessage.Broadcast(MessageType, TitleText, BodyText); +} + +void UCommonUserSubsystem::SetMaxLocalPlayers(int32 InMaxLocalPlayers) +{ + if (ensure(InMaxLocalPlayers >= 1)) + { + // We can have more local players than MAX_LOCAL_PLAYERS, the rest are treated as guests + MaxNumberOfLocalPlayers = InMaxLocalPlayers; + + UGameInstance* GameInstance = GetGameInstance(); + UGameViewportClient* ViewportClient = GameInstance ? GameInstance->GetGameViewportClient() : nullptr; + + if (ViewportClient) + { + ViewportClient->MaxSplitscreenPlayers = MaxNumberOfLocalPlayers; + } + } +} + +int32 UCommonUserSubsystem::GetMaxLocalPlayers() const +{ + return MaxNumberOfLocalPlayers; +} + +int32 UCommonUserSubsystem::GetNumLocalPlayers() const +{ + UGameInstance* GameInstance = GetGameInstance(); + if (ensure(GameInstance)) + { + return GameInstance->GetNumLocalPlayers(); + } + return 1; +} + +ECommonUserInitializationState UCommonUserSubsystem::GetLocalPlayerInitializationState(int32 LocalPlayerIndex) const +{ + const UCommonUserInfo* UserInfo = GetUserInfoForLocalPlayerIndex(LocalPlayerIndex); + if (UserInfo) + { + return UserInfo->InitializationState; + } + + if (LocalPlayerIndex < 0 || LocalPlayerIndex >= GetMaxLocalPlayers()) + { + return ECommonUserInitializationState::Invalid; + } + + return ECommonUserInitializationState::Unknown; +} + +bool UCommonUserSubsystem::TryToInitializeForLocalPlay(int32 LocalPlayerIndex, FInputDeviceId PrimaryInputDevice, bool bCanUseGuestLogin) +{ + if (!PrimaryInputDevice.IsValid()) + { + // Set to default device + PrimaryInputDevice = IPlatformInputDeviceMapper::Get().GetDefaultInputDevice(); + } + + FCommonUserInitializeParams Params; + Params.LocalPlayerIndex = LocalPlayerIndex; + Params.PrimaryInputDevice = PrimaryInputDevice; + Params.bCanUseGuestLogin = bCanUseGuestLogin; + Params.bCanCreateNewLocalPlayer = true; + Params.RequestedPrivilege = ECommonUserPrivilege::CanPlay; + + return TryToInitializeUser(Params); +} + +bool UCommonUserSubsystem::TryToLoginForOnlinePlay(int32 LocalPlayerIndex) +{ + FCommonUserInitializeParams Params; + Params.LocalPlayerIndex = LocalPlayerIndex; + Params.bCanCreateNewLocalPlayer = false; + Params.RequestedPrivilege = ECommonUserPrivilege::CanPlayOnline; + + return TryToInitializeUser(Params); +} + +bool UCommonUserSubsystem::TryToInitializeUser(FCommonUserInitializeParams Params) +{ + if (Params.LocalPlayerIndex < 0 || (!Params.bCanCreateNewLocalPlayer && Params.LocalPlayerIndex >= GetNumLocalPlayers())) + { + if (!bIsDedicatedServer) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser %d failed with current %d and max %d, invalid index"), + Params.LocalPlayerIndex, GetNumLocalPlayers(), GetMaxLocalPlayers()); + return false; + } + } + + if (Params.LocalPlayerIndex > GetNumLocalPlayers() || Params.LocalPlayerIndex >= GetMaxLocalPlayers()) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser %d failed with current %d and max %d, can only create in order up to max players"), + Params.LocalPlayerIndex, GetNumLocalPlayers(), GetMaxLocalPlayers()); + return false; + } + + // Fill in platform user and input device if needed + if (Params.ControllerId != INDEX_NONE && (!Params.PrimaryInputDevice.IsValid() || !Params.PlatformUser.IsValid())) + { + IPlatformInputDeviceMapper::Get().RemapControllerIdToPlatformUserAndDevice(Params.ControllerId, Params.PlatformUser, Params.PrimaryInputDevice); + } + + if (Params.PrimaryInputDevice.IsValid() && !Params.PlatformUser.IsValid()) + { + Params.PlatformUser = GetPlatformUserIdForInputDevice(Params.PrimaryInputDevice); + } + else if (Params.PlatformUser.IsValid() && !Params.PrimaryInputDevice.IsValid()) + { + Params.PrimaryInputDevice = GetPrimaryInputDeviceForPlatformUser(Params.PlatformUser); + } + + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(Params.LocalPlayerIndex)); + UCommonUserInfo* LocalUserInfoForController = ModifyInfo(GetUserInfoForInputDevice(Params.PrimaryInputDevice)); + + if (LocalUserInfoForController && LocalUserInfo && LocalUserInfoForController != LocalUserInfo) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser %d failed because controller %d is already assigned to player %d"), + Params.LocalPlayerIndex, Params.PrimaryInputDevice.GetId(), LocalUserInfoForController->LocalPlayerIndex); + return false; + } + + if (Params.LocalPlayerIndex == 0 && Params.bCanUseGuestLogin) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser failed because player 0 cannot be a guest")); + return false; + } + + if (!LocalUserInfo) + { + LocalUserInfo = CreateLocalUserInfo(Params.LocalPlayerIndex); + } + else + { + // Copy from existing user info + if (!Params.PrimaryInputDevice.IsValid()) + { + Params.PrimaryInputDevice = LocalUserInfo->PrimaryInputDevice; + } + + if (!Params.PlatformUser.IsValid()) + { + Params.PlatformUser = LocalUserInfo->PlatformUser; + } + } + + if (LocalUserInfo->InitializationState != ECommonUserInitializationState::Unknown && LocalUserInfo->InitializationState != ECommonUserInitializationState::FailedtoLogin) + { + // Not allowed to change parameters during login + if (LocalUserInfo->PrimaryInputDevice != Params.PrimaryInputDevice || LocalUserInfo->PlatformUser != Params.PlatformUser || LocalUserInfo->bCanBeGuest != Params.bCanUseGuestLogin) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser failed because player %d has already started the login process with diffrent settings!"), Params.LocalPlayerIndex); + return false; + } + } + + // Set desired index now so if it creates a player it knows what controller to use + LocalUserInfo->PrimaryInputDevice = Params.PrimaryInputDevice; + LocalUserInfo->PlatformUser = Params.PlatformUser; + LocalUserInfo->bCanBeGuest = Params.bCanUseGuestLogin; + RefreshLocalUserInfo(LocalUserInfo); + + // Either doing an initial or network login + if (LocalUserInfo->GetPrivilegeAvailability(ECommonUserPrivilege::CanPlay) == ECommonUserAvailability::NowAvailable && Params.RequestedPrivilege == ECommonUserPrivilege::CanPlayOnline) + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::DoingNetworkLogin; + } + else + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::DoingInitialLogin; + } + + LoginLocalUser(LocalUserInfo, Params.RequestedPrivilege, Params.OnlineContext, FOnLocalUserLoginCompleteDelegate::CreateUObject(this, &ThisClass::HandleLoginForUserInitialize, Params)); + + return true; +} + +void UCommonUserSubsystem::ListenForLoginKeyInput(TArray AnyUserKeys, TArray NewUserKeys, FCommonUserInitializeParams Params) +{ + UGameViewportClient* ViewportClient = GetGameInstance()->GetGameViewportClient(); + if (ensure(ViewportClient)) + { + const bool bIsMapped = LoginKeysForAnyUser.Num() > 0 || LoginKeysForNewUser.Num() > 0; + const bool bShouldBeMapped = AnyUserKeys.Num() > 0 || NewUserKeys.Num() > 0; + + if (bIsMapped && !bShouldBeMapped) + { + // Set it back to wrapped handler + ViewportClient->OnOverrideInputKey() = WrappedInputKeyHandler; + WrappedInputKeyHandler.Unbind(); + } + else if (!bIsMapped && bShouldBeMapped) + { + // Set up a wrapped handler + WrappedInputKeyHandler = ViewportClient->OnOverrideInputKey(); + ViewportClient->OnOverrideInputKey().BindUObject(this, &UCommonUserSubsystem::OverrideInputKeyForLogin); + } + + LoginKeysForAnyUser = AnyUserKeys; + LoginKeysForNewUser = NewUserKeys; + + if (bShouldBeMapped) + { + ParamsForLoginKey = Params; + } + else + { + ParamsForLoginKey = FCommonUserInitializeParams(); + } + } +} + +bool UCommonUserSubsystem::CancelUserInitialization(int32 LocalPlayerIndex) +{ + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(LocalPlayerIndex)); + if (!LocalUserInfo) + { + return false; + } + + if (!LocalUserInfo->IsDoingLogin()) + { + return false; + } + + // Remove from login queue + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + if (Request->UserInfo.IsValid() && Request->UserInfo->LocalPlayerIndex == LocalPlayerIndex) + { + ActiveLoginRequests.Remove(Request); + } + } + + // Set state with best guess + if (LocalUserInfo->InitializationState == ECommonUserInitializationState::DoingNetworkLogin) + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::LoggedInLocalOnly; + } + else + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::FailedtoLogin; + } + + return true; +} + +bool UCommonUserSubsystem::TryToLogOutUser(int32 LocalPlayerIndex, bool bDestroyPlayer) +{ + UGameInstance* GameInstance = GetGameInstance(); + + if (!ensure(GameInstance)) + { + return false; + } + + if (LocalPlayerIndex == 0 && bDestroyPlayer) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToLogOutUser cannot destroy player 0")); + return false; + } + + CancelUserInitialization(LocalPlayerIndex); + + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(LocalPlayerIndex)); + if (!LocalUserInfo) + { + UE_LOG(LogCommonUser, Warning, TEXT("TryToLogOutUser failed to log out user %i because they are not logged in"), LocalPlayerIndex); + return false; + } + + FPlatformUserId UserId = LocalUserInfo->PlatformUser; + if (IsRealPlatformUser(UserId)) + { + // Currently this does not do platform logout in case they want to log back in immediately after + UE_LOG(LogCommonUser, Log, TEXT("TryToLogOutUser succeeded for real platform user %d"), UserId.GetInternalId()); + + LogOutLocalUser(UserId); + } + else if (ensure(LocalUserInfo->bIsGuest)) + { + // For guest users just delete it + UE_LOG(LogCommonUser, Log, TEXT("TryToLogOutUser succeeded for guest player index %d"), LocalPlayerIndex); + + LocalUserInfos.Remove(LocalPlayerIndex); + } + + if (bDestroyPlayer) + { + ULocalPlayer* ExistingPlayer = GameInstance->FindLocalPlayerFromPlatformUserId(UserId); + + if (ExistingPlayer) + { + GameInstance->RemoveLocalPlayer(ExistingPlayer); + } + } + + return true; +} + +void UCommonUserSubsystem::ResetUserState() +{ + // Manually purge existing info objects + for (TPair Pair : LocalUserInfos) + { + if (Pair.Value) + { + Pair.Value->MarkAsGarbage(); + } + } + + LocalUserInfos.Reset(); + + // Cancel in-progress logins + ActiveLoginRequests.Reset(); + + // Create player info for id 0 + UCommonUserInfo* FirstUser = CreateLocalUserInfo(0); + + FirstUser->PlatformUser = IPlatformInputDeviceMapper::Get().GetPrimaryPlatformUser(); + FirstUser->PrimaryInputDevice = IPlatformInputDeviceMapper::Get().GetPrimaryInputDeviceForUser(FirstUser->PlatformUser); + + // TODO: Schedule a refresh of player 0 for next frame? + RefreshLocalUserInfo(FirstUser); +} + +bool UCommonUserSubsystem::OverrideInputKeyForLogin(FInputKeyEventArgs& EventArgs) +{ + int32 NextLocalPlayerIndex = INDEX_NONE; + + const UCommonUserInfo* MappedUser = GetUserInfoForInputDevice(EventArgs.InputDevice); + if (EventArgs.Event == IE_Pressed) + { + if (MappedUser == nullptr || !MappedUser->IsLoggedIn()) + { + if (MappedUser) + { + NextLocalPlayerIndex = MappedUser->LocalPlayerIndex; + } + else + { + // Find next player + for (int32 i = 0; i < MaxNumberOfLocalPlayers; i++) + { + if (GetLocalPlayerInitializationState(i) == ECommonUserInitializationState::Unknown) + { + NextLocalPlayerIndex = i; + break; + } + } + } + + if (NextLocalPlayerIndex != INDEX_NONE) + { + if (LoginKeysForAnyUser.Contains(EventArgs.Key)) + { + // If we're in the middle of logging in just return true to ignore platform-specific input + if (MappedUser && MappedUser->IsDoingLogin()) + { + return true; + } + + // Press start screen + FCommonUserInitializeParams NewParams = ParamsForLoginKey; + NewParams.LocalPlayerIndex = NextLocalPlayerIndex; + NewParams.PrimaryInputDevice = EventArgs.InputDevice; + + return TryToInitializeUser(NewParams); + } + + // See if this controller id is mapped + MappedUser = GetUserInfoForInputDevice(EventArgs.InputDevice); + + if (!MappedUser || MappedUser->LocalPlayerIndex == INDEX_NONE) + { + if (LoginKeysForNewUser.Contains(EventArgs.Key)) + { + // If we're in the middle of logging in just return true to ignore platform-specific input + if (MappedUser && MappedUser->IsDoingLogin()) + { + return true; + } + + // Local multiplayer + FCommonUserInitializeParams NewParams = ParamsForLoginKey; + NewParams.LocalPlayerIndex = NextLocalPlayerIndex; + NewParams.PrimaryInputDevice = EventArgs.InputDevice; + + return TryToInitializeUser(NewParams); + } + } + } + } + } + + if (WrappedInputKeyHandler.IsBound()) + { + return WrappedInputKeyHandler.Execute(EventArgs); + } + + return false; +} + +static inline FText GetErrorText(const FOnlineErrorType& InOnlineError) +{ +#if COMMONUSER_OSSV1 + return InOnlineError.GetErrorMessage(); +#else + return InOnlineError.GetText(); +#endif +} + +void UCommonUserSubsystem::HandleLoginForUserInitialize(const UCommonUserInfo* UserInfo, ELoginStatusType NewStatus, FUniqueNetIdRepl NetId, const TOptional& InError, ECommonUserOnlineContext Context, FCommonUserInitializeParams Params) +{ + UGameInstance* GameInstance = GetGameInstance(); + check(GameInstance); + FTimerManager& TimerManager = GameInstance->GetTimerManager(); + TOptional Error = InError; // Copy so we can reset on handled errors + + UCommonUserInfo* LocalUserInfo = ModifyInfo(UserInfo); + UCommonUserInfo* FirstUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(0)); + + if (!ensure(LocalUserInfo && FirstUserInfo)) + { + return; + } + + // Check the hard platform/service ids + RefreshLocalUserInfo(LocalUserInfo); + + FUniqueNetIdRepl FirstPlayerId = FirstUserInfo->GetNetId(ECommonUserOnlineContext::PlatformOrDefault); + + // Check to see if we should make a guest after a login failure. Some platforms return success but reuse the first player's id, count this as a failure + if (LocalUserInfo != FirstUserInfo && LocalUserInfo->bCanBeGuest && (NewStatus == ELoginStatusType::NotLoggedIn || NetId == FirstPlayerId)) + { +#if COMMONUSER_OSSV1 + NetId = (FUniqueNetIdRef)FUniqueNetIdString::Create(FString::Printf(TEXT("GuestPlayer%d"), LocalUserInfo->LocalPlayerIndex), NULL_SUBSYSTEM); +#else + // TODO: OSSv2 FUniqueNetIdRepl wrapping FAccountId is in progress + // TODO: OSSv2 - How to handle guest accounts? +#endif + LocalUserInfo->bIsGuest = true; + NewStatus = ELoginStatusType::UsingLocalProfile; + Error.Reset(); + UE_LOG(LogCommonUser, Log, TEXT("HandleLoginForUserInitialize created guest id %s for local player %d"), *NetId.ToString(), LocalUserInfo->LocalPlayerIndex); + } + else + { + LocalUserInfo->bIsGuest = false; + } + + ensure(LocalUserInfo->IsDoingLogin()); + + if (Error.IsSet()) + { + FText ErrorText = GetErrorText(Error.GetValue()); + TimerManager.SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UCommonUserSubsystem::HandleUserInitializeFailed, Params, ErrorText)); + return; + } + + if (Context == ECommonUserOnlineContext::Game) + { + LocalUserInfo->UpdateCachedNetId(NetId, ECommonUserOnlineContext::Game); + } + + ULocalPlayer* CurrentPlayer = GameInstance->GetLocalPlayerByIndex(LocalUserInfo->LocalPlayerIndex); + if (!CurrentPlayer && Params.bCanCreateNewLocalPlayer) + { + FString ErrorString; + CurrentPlayer = GameInstance->CreateLocalPlayer(LocalUserInfo->PlatformUser, ErrorString, true); + + if (!CurrentPlayer) + { + TimerManager.SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UCommonUserSubsystem::HandleUserInitializeFailed, Params, FText::AsCultureInvariant(ErrorString))); + return; + } + ensure(GameInstance->GetLocalPlayerByIndex(LocalUserInfo->LocalPlayerIndex) == CurrentPlayer); + } + + // Updates controller and net id if needed + SetLocalPlayerUserInfo(CurrentPlayer, LocalUserInfo); + + // Set a delayed callback + TimerManager.SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UCommonUserSubsystem::HandleUserInitializeSucceeded, Params)); +} + +void UCommonUserSubsystem::HandleUserInitializeFailed(FCommonUserInitializeParams Params, FText Error) +{ + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(Params.LocalPlayerIndex)); + + if (!LocalUserInfo) + { + // The user info was reset since this was scheduled + return; + } + + UE_LOG(LogCommonUser, Warning, TEXT("TryToInitializeUser %d failed with error %s"), LocalUserInfo->LocalPlayerIndex, *Error.ToString()); + + // If state is wrong, abort as we might have gotten canceled + if (!ensure(LocalUserInfo->IsDoingLogin())) + { + return; + } + + // If initial login failed or we ended up totally logged out, set to complete failure + ELoginStatusType NewStatus = GetLocalUserLoginStatus(Params.PlatformUser, Params.OnlineContext); + if (NewStatus == ELoginStatusType::NotLoggedIn || LocalUserInfo->InitializationState == ECommonUserInitializationState::DoingInitialLogin) + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::FailedtoLogin; + } + else + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::LoggedInLocalOnly; + } + + FText TitleText = NSLOCTEXT("CommonUser", "LoginFailedTitle", "Login Failure"); + + if (!Params.bSuppressLoginErrors) + { + SendSystemMessage(FCommonUserTags::SystemMessage_Error_InitializeLocalPlayerFailed, TitleText, Error); + } + + // Call callbacks + Params.OnUserInitializeComplete.ExecuteIfBound(LocalUserInfo, false, Error, Params.RequestedPrivilege, Params.OnlineContext); + OnUserInitializeComplete.Broadcast(LocalUserInfo, false, Error, Params.RequestedPrivilege, Params.OnlineContext); +} + +void UCommonUserSubsystem::HandleUserInitializeSucceeded(FCommonUserInitializeParams Params) +{ + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(Params.LocalPlayerIndex)); + + if (!LocalUserInfo) + { + // The user info was reset since this was scheduled + return; + } + + // If state is wrong, abort as we might have gotten cancelled + if (!ensure(LocalUserInfo->IsDoingLogin())) + { + return; + } + + // Fix up state + if (Params.RequestedPrivilege == ECommonUserPrivilege::CanPlayOnline) + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::LoggedInOnline; + } + else + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::LoggedInLocalOnly; + } + + ensure(LocalUserInfo->GetPrivilegeAvailability(Params.RequestedPrivilege) == ECommonUserAvailability::NowAvailable); + + // Call callbacks + Params.OnUserInitializeComplete.ExecuteIfBound(LocalUserInfo, true, FText(), Params.RequestedPrivilege, Params.OnlineContext); + OnUserInitializeComplete.Broadcast(LocalUserInfo, true, FText(), Params.RequestedPrivilege, Params.OnlineContext); +} + +bool UCommonUserSubsystem::LoginLocalUser(const UCommonUserInfo* UserInfo, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext Context, FOnLocalUserLoginCompleteDelegate OnComplete) +{ + UCommonUserInfo* LocalUserInfo = ModifyInfo(UserInfo); + if (!ensure(UserInfo)) + { + return false; + } + + TSharedRef NewRequest = MakeShared(LocalUserInfo, RequestedPrivilege, Context, MoveTemp(OnComplete)); + ActiveLoginRequests.Add(NewRequest); + + // This will execute callback or start login process + ProcessLoginRequest(NewRequest); + + return true; +} + +void UCommonUserSubsystem::ProcessLoginRequest(TSharedRef Request) +{ + // First, see if we've fully logged in + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + return; + } + + const FPlatformUserId PlatformUser = UserInfo->GetPlatformUserId(); + + // If the platform user id is invalid because this is a guest, skip right to failure + if (!IsRealPlatformUser(PlatformUser)) + { +#if COMMONUSER_OSSV1 + Request->Error = FOnlineError(NSLOCTEXT("CommonUser", "InvalidPlatformUser", "Invalid Platform User")); +#else + Request->Error = UE::Online::Errors::InvalidUser(); +#endif + // Remove from active array + ActiveLoginRequests.Remove(Request); + + // Execute delegate if bound + Request->Delegate.ExecuteIfBound(UserInfo, ELoginStatusType::NotLoggedIn, FUniqueNetIdRepl(), Request->Error, Request->DesiredContext); + + return; + } + + // Figure out what context to process first + if (Request->CurrentContext == ECommonUserOnlineContext::Invalid) + { + // First start with platform context if this is a game login + if (Request->DesiredContext == ECommonUserOnlineContext::Game) + { + Request->CurrentContext = ResolveOnlineContext(ECommonUserOnlineContext::PlatformOrDefault); + } + else + { + Request->CurrentContext = ResolveOnlineContext(Request->DesiredContext); + } + } + + ELoginStatusType CurrentStatus = GetLocalUserLoginStatus(PlatformUser, Request->CurrentContext); + FUniqueNetIdRepl CurrentId = GetLocalUserNetId(PlatformUser, Request->CurrentContext); + FOnlineContextCache* System = GetContextCache(Request->CurrentContext); + + if (!ensure(System)) + { + return; + } + + // Starting a new request + if (Request->OverallLoginState == ECommonUserAsyncTaskState::NotStarted) + { + Request->OverallLoginState = ECommonUserAsyncTaskState::InProgress; + } + + bool bHasRequiredStatus = (CurrentStatus == ELoginStatusType::LoggedIn); + if (Request->DesiredPrivilege == ECommonUserPrivilege::CanPlay) + { + // If this is not an online required login, allow local profile to count as fully logged in + bHasRequiredStatus |= (CurrentStatus == ELoginStatusType::UsingLocalProfile); + } + + // Check for overall success + if (bHasRequiredStatus && CurrentId.IsValid()) + { + // Stall if we're waiting for the login UI to close + if (Request->LoginUIState == ECommonUserAsyncTaskState::InProgress) + { + return; + } + + Request->OverallLoginState = ECommonUserAsyncTaskState::Done; + } + else + { + // Try using platform auth to login + if (Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::NotStarted) + { + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::InProgress; + + if (TransferPlatformAuth(System, Request, PlatformUser)) + { + return; + } + // We didn't start a login attempt, so set failure + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::Failed; + } + + // Next check AutoLogin + if (Request->AutoLoginState == ECommonUserAsyncTaskState::NotStarted) + { + if (Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Done || Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Failed) + { + Request->AutoLoginState = ECommonUserAsyncTaskState::InProgress; + + // Try an auto login with default credentials, this will work on many platforms + if (AutoLogin(System, Request, PlatformUser)) + { + return; + } + // We didn't start an autologin attempt, so set failure + Request->AutoLoginState = ECommonUserAsyncTaskState::Failed; + } + } + + // Next check login UI + if (Request->LoginUIState == ECommonUserAsyncTaskState::NotStarted) + { + if ((Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Done || Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Failed) + && (Request->AutoLoginState == ECommonUserAsyncTaskState::Done || Request->AutoLoginState == ECommonUserAsyncTaskState::Failed)) + { + Request->LoginUIState = ECommonUserAsyncTaskState::InProgress; + + if (ShowLoginUI(System, Request, PlatformUser)) + { + return; + } + // We didn't show a UI, so set failure + Request->LoginUIState = ECommonUserAsyncTaskState::Failed; + } + } + } + + // Check for overall failure + if (Request->LoginUIState == ECommonUserAsyncTaskState::Failed && + Request->AutoLoginState == ECommonUserAsyncTaskState::Failed && + Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Failed) + { + Request->OverallLoginState = ECommonUserAsyncTaskState::Failed; + } + else if (Request->OverallLoginState == ECommonUserAsyncTaskState::InProgress && + Request->LoginUIState != ECommonUserAsyncTaskState::InProgress && + Request->AutoLoginState != ECommonUserAsyncTaskState::InProgress && + Request->TransferPlatformAuthState != ECommonUserAsyncTaskState::InProgress) + { + // If none of the substates are still in progress but we haven't successfully logged in, mark this as a failure to avoid stalling forever + Request->OverallLoginState = ECommonUserAsyncTaskState::Failed; + } + + if (Request->OverallLoginState == ECommonUserAsyncTaskState::Done) + { + // Do the permissions check if needed + if (Request->PrivilegeCheckState == ECommonUserAsyncTaskState::NotStarted) + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::InProgress; + + ECommonUserPrivilegeResult CachedResult = UserInfo->GetCachedPrivilegeResult(Request->DesiredPrivilege, Request->CurrentContext); + if (CachedResult == ECommonUserPrivilegeResult::Available) + { + // Use cached success value + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Done; + } + else + { + if (QueryUserPrivilege(System, Request, PlatformUser)) + { + return; + } + else + { +#if !COMMONUSER_OSSV1 + // Temp while OSSv2 gets privileges implemented + CachedResult = ECommonUserPrivilegeResult::Available; + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Done; +#endif + } + } + } + + if (Request->PrivilegeCheckState == ECommonUserAsyncTaskState::Failed) + { + // Count a privilege failure as a login failure + Request->OverallLoginState = ECommonUserAsyncTaskState::Failed; + } + else if (Request->PrivilegeCheckState == ECommonUserAsyncTaskState::Done) + { + // If platform context done but still need to do service context, do that next + ECommonUserOnlineContext ResolvedDesiredContext = ResolveOnlineContext(Request->DesiredContext); + + if (Request->OverallLoginState == ECommonUserAsyncTaskState::Done && Request->CurrentContext != ResolvedDesiredContext) + { + Request->CurrentContext = ResolvedDesiredContext; + Request->OverallLoginState = ECommonUserAsyncTaskState::NotStarted; + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::NotStarted; + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::NotStarted; + Request->AutoLoginState = ECommonUserAsyncTaskState::NotStarted; + Request->LoginUIState = ECommonUserAsyncTaskState::NotStarted; + + // Reprocess and immediately return + ProcessLoginRequest(Request); + return; + } + } + } + + if (Request->PrivilegeCheckState == ECommonUserAsyncTaskState::InProgress) + { + // Stall to wait for it to finish + return; + } + + // If done, remove and do callback + if (Request->OverallLoginState == ECommonUserAsyncTaskState::Done || Request->OverallLoginState == ECommonUserAsyncTaskState::Failed) + { + // Skip if this already happened in a nested function + if (ActiveLoginRequests.Contains(Request)) + { + // Add a generic error if none is set + if (Request->OverallLoginState == ECommonUserAsyncTaskState::Failed && !Request->Error.IsSet()) + { + #if COMMONUSER_OSSV1 + Request->Error = FOnlineError(NSLOCTEXT("CommonUser", "FailedToRequest", "Failed to Request Login")); + #else + Request->Error = UE::Online::Errors::RequestFailure(); + #endif + } + + // Remove from active array + ActiveLoginRequests.Remove(Request); + + // Execute delegate if bound + Request->Delegate.ExecuteIfBound(UserInfo, CurrentStatus, CurrentId, Request->Error, Request->DesiredContext); + } + } +} + +#if COMMONUSER_OSSV1 +void UCommonUserSubsystem::HandleUserLoginCompleted(int32 PlatformUserIndex, bool bWasSuccessful, const FUniqueNetId& NetId, const FString& ErrorString, ECommonUserOnlineContext Context) +{ + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + ELoginStatusType NewStatus = GetLocalUserLoginStatus(PlatformUser, Context); + FUniqueNetIdRepl NewId = FUniqueNetIdRepl(NetId); + UE_LOG(LogCommonUser, Log, TEXT("Player login Completed - System:%s, UserIdx:%d, Successful:%d, NewStatus:%s, NewId:%s, ErrorIfAny:%s"), + *GetOnlineSubsystemName(Context).ToString(), + PlatformUserIndex, + (int32)bWasSuccessful, + ELoginStatus::ToString(NewStatus), + *NewId.ToString(), + *ErrorString); + + // Update any waiting login requests + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + continue; + } + + if (UserInfo->PlatformUser == PlatformUser && Request->CurrentContext == Context) + { + // On some platforms this gets called from the login UI with a failure + if (Request->AutoLoginState == ECommonUserAsyncTaskState::InProgress) + { + Request->AutoLoginState = bWasSuccessful ? ECommonUserAsyncTaskState::Done : ECommonUserAsyncTaskState::Failed; + } + + if (!bWasSuccessful) + { + Request->Error = FOnlineError(FText::FromString(ErrorString)); + } + + ProcessLoginRequest(Request); + } + } +} + +void UCommonUserSubsystem::HandleOnLoginUIClosed(TSharedPtr LoggedInNetId, const int PlatformUserIndex, const FOnlineError& Error, ECommonUserOnlineContext Context) +{ + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + + // Update any waiting login requests + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + continue; + } + + // Look for first user trying to log in on this context + if (Request->CurrentContext == Context && Request->LoginUIState == ECommonUserAsyncTaskState::InProgress) + { + if (LoggedInNetId.IsValid() && LoggedInNetId->IsValid() && Error.WasSuccessful()) + { + // The platform user id that actually logged in may not be the same one who requested the UI, + // so swap it if the returned id is actually valid + if (UserInfo->PlatformUser != PlatformUser && PlatformUser != PLATFORMUSERID_NONE) + { + UserInfo->PlatformUser = PlatformUser; + } + + Request->LoginUIState = ECommonUserAsyncTaskState::Done; + Request->Error.Reset(); + } + else + { + Request->LoginUIState = ECommonUserAsyncTaskState::Failed; + Request->Error = Error; + } + + ProcessLoginRequest(Request); + } + } +} + +void UCommonUserSubsystem::HandleCheckPrivilegesComplete(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults, ECommonUserPrivilege UserPrivilege, TWeakObjectPtr CommonUserInfo, ECommonUserOnlineContext Context) +{ + // Only handle if user still exists + UCommonUserInfo* UserInfo = CommonUserInfo.Get(); + + if (!UserInfo) + { + return; + } + + ECommonUserPrivilegeResult UserResult = ConvertOSSPrivilegeResult(Privilege, PrivilegeResults); + + // Update the user cached value + UpdateUserPrivilegeResult(UserInfo, UserPrivilege, UserResult, Context); + + FOnlineContextCache* ContextCache = GetContextCache(Context); + check(ContextCache); + + // If this returns disconnected, update the connection status + if (UserResult == ECommonUserPrivilegeResult::NetworkConnectionUnavailable) + { + ContextCache->CurrentConnectionStatus = EOnlineServerConnectionStatus::NoNetworkConnection; + } + else if (UserResult == ECommonUserPrivilegeResult::Available && UserPrivilege == ECommonUserPrivilege::CanPlayOnline) + { + if (ContextCache->CurrentConnectionStatus == EOnlineServerConnectionStatus::NoNetworkConnection) + { + ContextCache->CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; + } + } + + // See if a login request is waiting on this + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + if (Request->UserInfo.Get() == UserInfo && Request->CurrentContext == Context && Request->DesiredPrivilege == UserPrivilege && Request->PrivilegeCheckState == ECommonUserAsyncTaskState::InProgress) + { + if (UserResult == ECommonUserPrivilegeResult::Available) + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Done; + } + else + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Failed; + + // Forms strings in english like "(The user is not allowed) to (play the game)" + Request->Error = FOnlineError(FText::Format(NSLOCTEXT("CommonUser", "PrivilegeFailureFormat", "{0} to {1}"), GetPrivilegeResultDescription(UserResult), GetPrivilegeDescription(UserPrivilege))); + } + + ProcessLoginRequest(Request); + } + } +} +#else + +void UCommonUserSubsystem::HandleUserLoginCompletedV2(const UE::Online::TOnlineResult& Result, FPlatformUserId PlatformUser, ECommonUserOnlineContext Context) +{ + const bool bWasSuccessful = Result.IsOk(); + FAccountId NewId; + if (bWasSuccessful) + { + NewId = Result.GetOkValue().AccountInfo->AccountId; + } + + ELoginStatusType NewStatus = GetLocalUserLoginStatus(PlatformUser, Context); + UE_LOG(LogCommonUser, Log, TEXT("Player login Completed - System:%d, UserIdx:%d, Successful:%d, NewId:%s, ErrorIfAny:%s"), + (int32)Context, + PlatformUser.GetInternalId(), + (int32)Result.IsOk(), + *ToLogString(NewId), + Result.IsError() ? *Result.GetErrorValue().GetLogString() : TEXT("")); + + // Update any waiting login requests + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + continue; + } + + if (UserInfo->PlatformUser == PlatformUser && Request->CurrentContext == Context) + { + // On some platforms this gets called from the login UI with a failure + if (Request->AutoLoginState == ECommonUserAsyncTaskState::InProgress) + { + Request->AutoLoginState = bWasSuccessful ? ECommonUserAsyncTaskState::Done : ECommonUserAsyncTaskState::Failed; + } + + if (bWasSuccessful) + { + Request->Error.Reset(); + } + else + { + Request->Error = Result.GetErrorValue(); + } + + ProcessLoginRequest(Request); + } + } +} + +void UCommonUserSubsystem::HandleOnLoginUIClosedV2(const UE::Online::TOnlineResult& Result, FPlatformUserId PlatformUser, ECommonUserOnlineContext Context) +{ + // Update any waiting login requests + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + continue; + } + + // Look for first user trying to log in on this context + if (Request->CurrentContext == Context && Request->LoginUIState == ECommonUserAsyncTaskState::InProgress) + { + if (Result.IsOk()) + { + // The platform user id that actually logged in may not be the same one who requested the UI, + // so swap it if the returned id is actually valid + if (UserInfo->PlatformUser != PlatformUser && PlatformUser != PLATFORMUSERID_NONE) + { + UserInfo->PlatformUser = PlatformUser; + } + + Request->LoginUIState = ECommonUserAsyncTaskState::Done; + Request->Error.Reset(); + } + else + { + Request->LoginUIState = ECommonUserAsyncTaskState::Failed; + Request->Error = Result.GetErrorValue(); + } + + ProcessLoginRequest(Request); + } + } +} + +void UCommonUserSubsystem::HandleCheckPrivilegesComplete(const UE::Online::TOnlineResult& Result, TWeakObjectPtr CommonUserInfo, EUserPrivileges DesiredPrivilege, ECommonUserOnlineContext Context) +{ + // Only handle if user still exists + UCommonUserInfo* UserInfo = CommonUserInfo.Get(); + if (!UserInfo) + { + return; + } + + ECommonUserPrivilege UserPrivilege = ConvertOnlineServicesPrivilege(DesiredPrivilege); + ECommonUserPrivilegeResult UserResult = ECommonUserPrivilegeResult::PlatformFailure; + if (const FQueryUserPrivilege::Result* OkResult = Result.TryGetOkValue()) + { + UserResult = ConvertOnlineServicesPrivilegeResult(DesiredPrivilege, OkResult->PrivilegeResult); + } + else + { + UE_LOG(LogCommonUser, Warning, TEXT("QueryUserPrivilege failed: %s"), *Result.GetErrorValue().GetLogString()); + } + + // Update the user cached value + UserInfo->UpdateCachedPrivilegeResult(UserPrivilege, UserResult, Context); + + // See if a login request is waiting on this + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + if (Request->UserInfo.Get() == UserInfo && Request->CurrentContext == Context && Request->DesiredPrivilege == UserPrivilege && Request->PrivilegeCheckState == ECommonUserAsyncTaskState::InProgress) + { + if (UserResult == ECommonUserPrivilegeResult::Available) + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Done; + } + else + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Failed; + Request->Error = Result.IsError() ? Result.GetErrorValue() : UE::Online::Errors::Unknown(); + } + + ProcessLoginRequest(Request); + } + } +} +#endif // COMMONUSER_OSSV1 + +void UCommonUserSubsystem::RefreshLocalUserInfo(UCommonUserInfo* UserInfo) +{ + if (ensure(UserInfo)) + { + // Always update default + UserInfo->UpdateCachedNetId(GetLocalUserNetId(UserInfo->PlatformUser, ECommonUserOnlineContext::Default), ECommonUserOnlineContext::Default); + + if (HasSeparatePlatformContext()) + { + // Also update platform + UserInfo->UpdateCachedNetId(GetLocalUserNetId(UserInfo->PlatformUser, ECommonUserOnlineContext::Platform), ECommonUserOnlineContext::Platform); + } + } +} + +void UCommonUserSubsystem::HandleChangedAvailability(UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserAvailability OldAvailability) +{ + ECommonUserAvailability NewAvailability = UserInfo->GetPrivilegeAvailability(Privilege); + + if (OldAvailability != NewAvailability) + { + OnUserPrivilegeChanged.Broadcast(UserInfo, Privilege, OldAvailability, NewAvailability); + } +} + +void UCommonUserSubsystem::UpdateUserPrivilegeResult(UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserPrivilegeResult Result, ECommonUserOnlineContext Context) +{ + check(UserInfo); + + ECommonUserAvailability OldAvailability = UserInfo->GetPrivilegeAvailability(Privilege); + + UserInfo->UpdateCachedPrivilegeResult(Privilege, Result, Context); + + HandleChangedAvailability(UserInfo, Privilege, OldAvailability); +} + +#if COMMONUSER_OSSV1 +ECommonUserPrivilege UCommonUserSubsystem::ConvertOSSPrivilege(EUserPrivileges::Type Privilege) const +{ + switch (Privilege) + { + case EUserPrivileges::CanPlay: + return ECommonUserPrivilege::CanPlay; + case EUserPrivileges::CanPlayOnline: + return ECommonUserPrivilege::CanPlayOnline; + case EUserPrivileges::CanCommunicateOnline: + return ECommonUserPrivilege::CanCommunicateViaTextOnline; // No good thing to do here, just mapping to text. + case EUserPrivileges::CanUseUserGeneratedContent: + return ECommonUserPrivilege::CanUseUserGeneratedContent; + case EUserPrivileges::CanUserCrossPlay: + return ECommonUserPrivilege::CanUseCrossPlay; + default: + return ECommonUserPrivilege::Invalid_Count; + } +} + +EUserPrivileges::Type UCommonUserSubsystem::ConvertOSSPrivilege(ECommonUserPrivilege Privilege) const +{ + switch (Privilege) + { + case ECommonUserPrivilege::CanPlay: + return EUserPrivileges::CanPlay; + case ECommonUserPrivilege::CanPlayOnline: + return EUserPrivileges::CanPlayOnline; + case ECommonUserPrivilege::CanCommunicateViaTextOnline: + case ECommonUserPrivilege::CanCommunicateViaVoiceOnline: + return EUserPrivileges::CanCommunicateOnline; + case ECommonUserPrivilege::CanUseUserGeneratedContent: + return EUserPrivileges::CanUseUserGeneratedContent; + case ECommonUserPrivilege::CanUseCrossPlay: + return EUserPrivileges::CanUserCrossPlay; + default: + // No failure type, return CanPlay + return EUserPrivileges::CanPlay; + } +} + +ECommonUserPrivilegeResult UCommonUserSubsystem::ConvertOSSPrivilegeResult(EUserPrivileges::Type Privilege, uint32 Results) const +{ + // The V1 results enum is a bitfield where each platform behaves a bit differently + if (Results == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + return ECommonUserPrivilegeResult::Available; + } + if ((Results & (uint32)IOnlineIdentity::EPrivilegeResults::UserNotFound) || (Results & (uint32)IOnlineIdentity::EPrivilegeResults::UserNotLoggedIn)) + { + return ECommonUserPrivilegeResult::UserNotLoggedIn; + } + if ((Results & (uint32)IOnlineIdentity::EPrivilegeResults::RequiredPatchAvailable) || (Results & (uint32)IOnlineIdentity::EPrivilegeResults::RequiredSystemUpdate)) + { + return ECommonUserPrivilegeResult::VersionOutdated; + } + if (Results & (uint32)IOnlineIdentity::EPrivilegeResults::AgeRestrictionFailure) + { + return ECommonUserPrivilegeResult::AgeRestricted; + } + if (Results & (uint32)IOnlineIdentity::EPrivilegeResults::AccountTypeFailure) + { + return ECommonUserPrivilegeResult::AccountTypeRestricted; + } + if (Results & (uint32)IOnlineIdentity::EPrivilegeResults::NetworkConnectionUnavailable) + { + return ECommonUserPrivilegeResult::NetworkConnectionUnavailable; + } + + // Bucket other account failures together + uint32 AccountUseFailures = (uint32)IOnlineIdentity::EPrivilegeResults::OnlinePlayRestricted + | (uint32)IOnlineIdentity::EPrivilegeResults::UGCRestriction + | (uint32)IOnlineIdentity::EPrivilegeResults::ChatRestriction; + + if (Results & AccountUseFailures) + { + return ECommonUserPrivilegeResult::AccountUseRestricted; + } + + // If you can't play at all, this is a license failure + if (Privilege == EUserPrivileges::CanPlay) + { + return ECommonUserPrivilegeResult::LicenseInvalid; + } + + // Unknown reason + return ECommonUserPrivilegeResult::PlatformFailure; +} +#else +ECommonUserPrivilege UCommonUserSubsystem::ConvertOnlineServicesPrivilege(EUserPrivileges Privilege) const +{ + switch (Privilege) + { + case EUserPrivileges::CanPlay: + return ECommonUserPrivilege::CanPlay; + case EUserPrivileges::CanPlayOnline: + return ECommonUserPrivilege::CanPlayOnline; + case EUserPrivileges::CanCommunicateViaTextOnline: + return ECommonUserPrivilege::CanCommunicateViaTextOnline; + case EUserPrivileges::CanCommunicateViaVoiceOnline: + return ECommonUserPrivilege::CanCommunicateViaVoiceOnline; + case EUserPrivileges::CanUseUserGeneratedContent: + return ECommonUserPrivilege::CanUseUserGeneratedContent; + case EUserPrivileges::CanCrossPlay: + return ECommonUserPrivilege::CanUseCrossPlay; + default: + return ECommonUserPrivilege::Invalid_Count; + } +} + +EUserPrivileges UCommonUserSubsystem::ConvertOnlineServicesPrivilege(ECommonUserPrivilege Privilege) const +{ + switch (Privilege) + { + case ECommonUserPrivilege::CanPlay: + return EUserPrivileges::CanPlay; + case ECommonUserPrivilege::CanPlayOnline: + return EUserPrivileges::CanPlayOnline; + case ECommonUserPrivilege::CanCommunicateViaTextOnline: + return EUserPrivileges::CanCommunicateViaTextOnline; + case ECommonUserPrivilege::CanCommunicateViaVoiceOnline: + return EUserPrivileges::CanCommunicateViaVoiceOnline; + case ECommonUserPrivilege::CanUseUserGeneratedContent: + return EUserPrivileges::CanUseUserGeneratedContent; + case ECommonUserPrivilege::CanUseCrossPlay: + return EUserPrivileges::CanCrossPlay; + default: + // No failure type, return CanPlay + return EUserPrivileges::CanPlay; + } +} + +ECommonUserPrivilegeResult UCommonUserSubsystem::ConvertOnlineServicesPrivilegeResult(EUserPrivileges Privilege, EPrivilegeResults Results) const +{ + // The V1 results enum is a bitfield where each platform behaves a bit differently + if (Results == EPrivilegeResults::NoFailures) + { + return ECommonUserPrivilegeResult::Available; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::UserNotFound | EPrivilegeResults::UserNotLoggedIn)) + { + return ECommonUserPrivilegeResult::UserNotLoggedIn; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::RequiredPatchAvailable | EPrivilegeResults::RequiredSystemUpdate)) + { + return ECommonUserPrivilegeResult::VersionOutdated; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::AgeRestrictionFailure)) + { + return ECommonUserPrivilegeResult::AgeRestricted; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::AccountTypeFailure)) + { + return ECommonUserPrivilegeResult::AccountTypeRestricted; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::NetworkConnectionUnavailable)) + { + return ECommonUserPrivilegeResult::NetworkConnectionUnavailable; + } + + // Bucket other account failures together + const EPrivilegeResults AccountUseFailures = EPrivilegeResults::OnlinePlayRestricted + | EPrivilegeResults::UGCRestriction + | EPrivilegeResults::ChatRestriction; + + if (EnumHasAnyFlags(Results, AccountUseFailures)) + { + return ECommonUserPrivilegeResult::AccountUseRestricted; + } + + // If you can't play at all, this is a license failure + if (Privilege == EUserPrivileges::CanPlay) + { + return ECommonUserPrivilegeResult::LicenseInvalid; + } + + // Unknown reason + return ECommonUserPrivilegeResult::PlatformFailure; +} +#endif // COMMONUSER_OSSV1 + +FString UCommonUserSubsystem::PlatformUserIdToString(FPlatformUserId UserId) +{ + if (UserId == PLATFORMUSERID_NONE) + { + return TEXT("None"); + } + else + { + return FString::Printf(TEXT("%d"), UserId.GetInternalId()); + } +} + +FString UCommonUserSubsystem::ECommonUserOnlineContextToString(ECommonUserOnlineContext Context) +{ + switch (Context) + { + case ECommonUserOnlineContext::Game: + return TEXT("Game"); + case ECommonUserOnlineContext::Default: + return TEXT("Default"); + case ECommonUserOnlineContext::Service: + return TEXT("Service"); + case ECommonUserOnlineContext::ServiceOrDefault: + return TEXT("Service/Default"); + case ECommonUserOnlineContext::Platform: + return TEXT("Platform"); + case ECommonUserOnlineContext::PlatformOrDefault: + return TEXT("Platform/Default"); + default: + return TEXT("Invalid"); + } +} + +FText UCommonUserSubsystem::GetPrivilegeDescription(ECommonUserPrivilege Privilege) const +{ + switch (Privilege) + { + case ECommonUserPrivilege::CanPlay: + return NSLOCTEXT("CommonUser", "PrivilegeCanPlay", "play the game"); + case ECommonUserPrivilege::CanPlayOnline: + return NSLOCTEXT("CommonUser", "PrivilegeCanPlayOnline", "play online"); + case ECommonUserPrivilege::CanCommunicateViaTextOnline: + return NSLOCTEXT("CommonUser", "PrivilegeCanCommunicateViaTextOnline", "communicate with text"); + case ECommonUserPrivilege::CanCommunicateViaVoiceOnline: + return NSLOCTEXT("CommonUser", "PrivilegeCanCommunicateViaVoiceOnline", "communicate with voice"); + case ECommonUserPrivilege::CanUseUserGeneratedContent: + return NSLOCTEXT("CommonUser", "PrivilegeCanUseUserGeneratedContent", "access user content"); + case ECommonUserPrivilege::CanUseCrossPlay: + return NSLOCTEXT("CommonUser", "PrivilegeCanUseCrossPlay", "play with other platforms"); + default: + return NSLOCTEXT("CommonUser", "PrivilegeInvalid", ""); + } +} + +FText UCommonUserSubsystem::GetPrivilegeResultDescription(ECommonUserPrivilegeResult Result) const +{ + // TODO these strings might have cert requirements we need to override per console + switch (Result) + { + case ECommonUserPrivilegeResult::Unknown: + return NSLOCTEXT("CommonUser", "ResultUnknown", "Unknown if the user is allowed"); + case ECommonUserPrivilegeResult::Available: + return NSLOCTEXT("CommonUser", "ResultAvailable", "The user is allowed"); + case ECommonUserPrivilegeResult::UserNotLoggedIn: + return NSLOCTEXT("CommonUser", "ResultUserNotLoggedIn", "The user must login"); + case ECommonUserPrivilegeResult::LicenseInvalid: + return NSLOCTEXT("CommonUser", "ResultLicenseInvalid", "A valid game license is required"); + case ECommonUserPrivilegeResult::VersionOutdated: + return NSLOCTEXT("CommonUser", "VersionOutdated", "The game or hardware needs to be updated"); + case ECommonUserPrivilegeResult::NetworkConnectionUnavailable: + return NSLOCTEXT("CommonUser", "ResultNetworkConnectionUnavailable", "A network connection is required"); + case ECommonUserPrivilegeResult::AgeRestricted: + return NSLOCTEXT("CommonUser", "ResultAgeRestricted", "This age restricted account is not allowed"); + case ECommonUserPrivilegeResult::AccountTypeRestricted: + return NSLOCTEXT("CommonUser", "ResultAccountTypeRestricted", "This account type does not have access"); + case ECommonUserPrivilegeResult::AccountUseRestricted: + return NSLOCTEXT("CommonUser", "ResultAccountUseRestricted", "This account is not allowed"); + case ECommonUserPrivilegeResult::PlatformFailure: + return NSLOCTEXT("CommonUser", "ResultPlatformFailure", "Not allowed"); + default: + return NSLOCTEXT("CommonUser", "ResultInvalid", ""); + + } +} + +const UCommonUserSubsystem::FOnlineContextCache* UCommonUserSubsystem::GetContextCache(ECommonUserOnlineContext Context) const +{ + return const_cast(this)->GetContextCache(Context); +} + +UCommonUserSubsystem::FOnlineContextCache* UCommonUserSubsystem::GetContextCache(ECommonUserOnlineContext Context) +{ + switch (Context) + { + case ECommonUserOnlineContext::Game: + case ECommonUserOnlineContext::Default: + return DefaultContextInternal; + + case ECommonUserOnlineContext::Service: + return ServiceContextInternal; + case ECommonUserOnlineContext::ServiceOrDefault: + return ServiceContextInternal ? ServiceContextInternal : DefaultContextInternal; + + case ECommonUserOnlineContext::Platform: + return PlatformContextInternal; + case ECommonUserOnlineContext::PlatformOrDefault: + return PlatformContextInternal ? PlatformContextInternal : DefaultContextInternal; + } + + return nullptr; +} + +ECommonUserOnlineContext UCommonUserSubsystem::ResolveOnlineContext(ECommonUserOnlineContext Context) const +{ + switch (Context) + { + case ECommonUserOnlineContext::Game: + case ECommonUserOnlineContext::Default: + return ECommonUserOnlineContext::Default; + + case ECommonUserOnlineContext::Service: + return ServiceContextInternal ? ECommonUserOnlineContext::Service : ECommonUserOnlineContext::Invalid; + case ECommonUserOnlineContext::ServiceOrDefault: + return ServiceContextInternal ? ECommonUserOnlineContext::Service : ECommonUserOnlineContext::Default; + + case ECommonUserOnlineContext::Platform: + return PlatformContextInternal ? ECommonUserOnlineContext::Platform : ECommonUserOnlineContext::Invalid; + case ECommonUserOnlineContext::PlatformOrDefault: + return PlatformContextInternal ? ECommonUserOnlineContext::Platform : ECommonUserOnlineContext::Default; + } + + return ECommonUserOnlineContext::Invalid; +} + +bool UCommonUserSubsystem::HasSeparatePlatformContext() const +{ + ECommonUserOnlineContext ServiceType = ResolveOnlineContext(ECommonUserOnlineContext::ServiceOrDefault); + ECommonUserOnlineContext PlatformType = ResolveOnlineContext(ECommonUserOnlineContext::PlatformOrDefault); + + if (ServiceType != PlatformType) + { + return true; + } + return false; +} + +void UCommonUserSubsystem::SetLocalPlayerUserInfo(ULocalPlayer* LocalPlayer, const UCommonUserInfo* UserInfo) +{ + if (!bIsDedicatedServer && ensure(LocalPlayer && UserInfo)) + { + LocalPlayer->SetPlatformUserId(UserInfo->GetPlatformUserId()); + + FUniqueNetIdRepl NetId = UserInfo->GetNetId(ECommonUserOnlineContext::Game); + LocalPlayer->SetCachedUniqueNetId(NetId); + + // Also update player state if possible + APlayerController* PlayerController = LocalPlayer->GetPlayerController(nullptr); + if (PlayerController && PlayerController->PlayerState) + { + PlayerController->PlayerState->SetUniqueId(NetId); + } + } +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForLocalPlayerIndex(int32 LocalPlayerIndex) const +{ + TObjectPtr const* Found = LocalUserInfos.Find(LocalPlayerIndex); + if (Found) + { + return *Found; + } + return nullptr; +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForPlatformUserIndex(int32 PlatformUserIndex) const +{ + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + return GetUserInfoForPlatformUser(PlatformUser); +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForPlatformUser(FPlatformUserId PlatformUser) const +{ + if (!IsRealPlatformUser(PlatformUser)) + { + return nullptr; + } + + for (TPair Pair : LocalUserInfos) + { + // Don't include guest users in this check + if (ensure(Pair.Value) && Pair.Value->PlatformUser == PlatformUser && !Pair.Value->bIsGuest) + { + return Pair.Value; + } + } + + return nullptr; +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForUniqueNetId(const FUniqueNetIdRepl& NetId) const +{ + if (!NetId.IsValid()) + { + // TODO do we need to handle pre-login case on mobile platforms where netID is invalid? + return nullptr; + } + + for (TPair UserPair : LocalUserInfos) + { + if (ensure(UserPair.Value)) + { + for (const TPair& CachedPair : UserPair.Value->CachedDataMap) + { + if (NetId == CachedPair.Value.CachedNetId) + { + return UserPair.Value; + } + } + } + } + + return nullptr; +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForControllerId(int32 ControllerId) const +{ + FPlatformUserId PlatformUser; + FInputDeviceId IgnoreDevice; + + IPlatformInputDeviceMapper::Get().RemapControllerIdToPlatformUserAndDevice(ControllerId, PlatformUser, IgnoreDevice); + + return GetUserInfoForPlatformUser(PlatformUser); +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForInputDevice(FInputDeviceId InputDevice) const +{ + FPlatformUserId PlatformUser = GetPlatformUserIdForInputDevice(InputDevice); + return GetUserInfoForPlatformUser(PlatformUser); +} + +bool UCommonUserSubsystem::IsRealPlatformUserIndex(int32 PlatformUserIndex) const +{ + if (PlatformUserIndex < 0) + { + return false; + } + +#if COMMONUSER_OSSV1 + if (PlatformUserIndex >= MAX_LOCAL_PLAYERS) + { + // Check against OSS count + return false; + } +#else + // TODO: OSSv2 define MAX_LOCAL_PLAYERS? +#endif + + if (PlatformUserIndex > 0 && GetTraitTags().HasTag(FCommonUserTags::Platform_Trait_SingleOnlineUser)) + { + return false; + } + + return true; +} + +bool UCommonUserSubsystem::IsRealPlatformUser(FPlatformUserId PlatformUser) const +{ + // Validation is done at conversion/allocation time so trust the type + if (!PlatformUser.IsValid()) + { + return false; + } + + // TODO: Validate against OSS or input mapper somehow + + if (GetTraitTags().HasTag(FCommonUserTags::Platform_Trait_SingleOnlineUser)) + { + // Only the default user is supports online functionality + if (PlatformUser != IPlatformInputDeviceMapper::Get().GetPrimaryPlatformUser()) + { + return false; + } + } + + return true; +} + +FPlatformUserId UCommonUserSubsystem::GetPlatformUserIdForIndex(int32 PlatformUserIndex) const +{ + return IPlatformInputDeviceMapper::Get().GetPlatformUserForUserIndex(PlatformUserIndex); +} + +int32 UCommonUserSubsystem::GetPlatformUserIndexForId(FPlatformUserId PlatformUser) const +{ + return IPlatformInputDeviceMapper::Get().GetUserIndexForPlatformUser(PlatformUser); +} + +FPlatformUserId UCommonUserSubsystem::GetPlatformUserIdForInputDevice(FInputDeviceId InputDevice) const +{ + return IPlatformInputDeviceMapper::Get().GetUserForInputDevice(InputDevice); +} + +FInputDeviceId UCommonUserSubsystem::GetPrimaryInputDeviceForPlatformUser(FPlatformUserId PlatformUser) const +{ + return IPlatformInputDeviceMapper::Get().GetPrimaryInputDeviceForUser(PlatformUser); +} + +void UCommonUserSubsystem::SetTraitTags(const FGameplayTagContainer& InTags) +{ + CachedTraitTags = InTags; +} + +bool UCommonUserSubsystem::ShouldWaitForStartInput() const +{ + // By default, don't wait for input if this is a single user platform + return !HasTraitTag(FCommonUserTags::Platform_Trait_SingleOnlineUser.GetTag()); +} + +#if COMMONUSER_OSSV1 +void UCommonUserSubsystem::HandleIdentityLoginStatusChanged(int32 PlatformUserIndex, ELoginStatus::Type OldStatus, ELoginStatus::Type NewStatus, const FUniqueNetId& NewId, ECommonUserOnlineContext Context) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player login status changed - System:%s, UserIdx:%d, OldStatus:%s, NewStatus:%s, NewId:%s"), + *GetOnlineSubsystemName(Context).ToString(), + PlatformUserIndex, + ELoginStatus::ToString(OldStatus), + ELoginStatus::ToString(NewStatus), + *NewId.ToString()); + + if (NewStatus == ELoginStatus::NotLoggedIn && OldStatus != ELoginStatus::NotLoggedIn) + { + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + LogOutLocalUser(PlatformUser); + } +} + +void UCommonUserSubsystem::HandleControllerPairingChanged(int32 PlatformUserIndex, FControllerPairingChangedUserInfo PreviousUser, FControllerPairingChangedUserInfo NewUser) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player controller pairing changed - UserIdx:%d, PreviousUser:%s, NewUser:%s"), + PlatformUserIndex, + *ToDebugString(PreviousUser), + *ToDebugString(NewUser)); + + UGameInstance* GameInstance = GetGameInstance(); + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + ULocalPlayer* ControlledLocalPlayer = GameInstance->FindLocalPlayerFromPlatformUserId(PlatformUser); + ULocalPlayer* NewLocalPlayer = GameInstance->FindLocalPlayerFromUniqueNetId(NewUser.User); + const UCommonUserInfo* NewUserInfo = GetUserInfoForUniqueNetId(FUniqueNetIdRepl(NewUser.User)); + const UCommonUserInfo* PreviousUserInfo = GetUserInfoForUniqueNetId(FUniqueNetIdRepl(PreviousUser.User)); + + // See if we think this is already bound to an existing player + if (PreviousUser.ControllersRemaining == 0 && PreviousUserInfo && PreviousUserInfo != NewUserInfo) + { + // This means that the user deliberately logged out using a platform interface + if (IsRealPlatformUser(PlatformUser)) + { + LogOutLocalUser(PlatformUser); + } + } + + if (ControlledLocalPlayer && ControlledLocalPlayer != NewLocalPlayer) + { + // TODO Currently the platforms that call this delegate do not really handle swapping controller IDs + // SetLocalPlayerUserIndex(ControlledLocalPlayer, -1); + } +} + +void UCommonUserSubsystem::HandleNetworkConnectionStatusChanged(const FString& ServiceName, EOnlineServerConnectionStatus::Type LastConnectionStatus, EOnlineServerConnectionStatus::Type ConnectionStatus, ECommonUserOnlineContext Context) +{ + UE_LOG(LogCommonUser, Log, TEXT("HandleNetworkConnectionStatusChanged(ServiceName: %s, LastStatus: %s, ConnectionStatus: %s)"), + *ServiceName, + EOnlineServerConnectionStatus::ToString(LastConnectionStatus), + EOnlineServerConnectionStatus::ToString(ConnectionStatus)); + + // Cache old availablity for current users + TMap AvailabilityMap; + + for (TPair Pair : LocalUserInfos) + { + AvailabilityMap.Add(Pair.Value, Pair.Value->GetPrivilegeAvailability(ECommonUserPrivilege::CanPlayOnline)); + } + + FOnlineContextCache* System = GetContextCache(Context); + if (ensure(System)) + { + // Service name is normally the same as the OSS name, but not necessarily on all platforms + System->CurrentConnectionStatus = ConnectionStatus; + } + + for (TPair Pair : AvailabilityMap) + { + // Notify other systems when someone goes online/offline + HandleChangedAvailability(Pair.Key, ECommonUserPrivilege::CanPlayOnline, Pair.Value); + } + +} +#else +void UCommonUserSubsystem::HandleAuthLoginStatusChanged(const UE::Online::FAuthLoginStatusChanged& EventParameters, ECommonUserOnlineContext Context) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player login status changed - System:%d, UserId:%s, NewStatus:%s"), + (int)Context, + *ToLogString(EventParameters.AccountInfo->AccountId), + LexToString(EventParameters.LoginStatus)); +} + +void UCommonUserSubsystem::HandleNetworkConnectionStatusChanged(const UE::Online::FConnectionStatusChanged& EventParameters, ECommonUserOnlineContext Context) +{ + UE_LOG(LogCommonUser, Log, TEXT("HandleNetworkConnectionStatusChanged(Context:%d, ServiceName:%s, OldStatus:%s, NewStatus:%s)"), + (int)Context, + *EventParameters.ServiceName, + LexToString(EventParameters.PreviousStatus), + LexToString(EventParameters.CurrentStatus)); + + // Cache old availablity for current users + TMap AvailabilityMap; + + for (TPair Pair : LocalUserInfos) + { + AvailabilityMap.Add(Pair.Value, Pair.Value->GetPrivilegeAvailability(ECommonUserPrivilege::CanPlayOnline)); + } + + FOnlineContextCache* System = GetContextCache(Context); + if (ensure(System)) + { + // Service name is normally the same as the OSS name, but not necessarily on all platforms + System->CurrentConnectionStatus = EventParameters.CurrentStatus; + } + + for (TPair Pair : AvailabilityMap) + { + // Notify other systems when someone goes online/offline + HandleChangedAvailability(Pair.Key, ECommonUserPrivilege::CanPlayOnline, Pair.Value); + } +} +#endif // COMMONUSER_OSSV1 + +void UCommonUserSubsystem::HandleInputDeviceConnectionChanged(EInputDeviceConnectionState NewConnectionState, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId) +{ + FString InputDeviceIDString = FString::Printf(TEXT("%d"), InputDeviceId.GetId()); + const bool bIsConnected = NewConnectionState == EInputDeviceConnectionState::Connected; + UE_LOG(LogCommonUser, Log, TEXT("Controller connection changed - UserIdx:%d, UserID:%s, Connected:%d"), *InputDeviceIDString, *PlatformUserIdToString(PlatformUserId), bIsConnected ? 1 : 0); + + // TODO Implement for platforms that support this +} + diff --git a/Plugins/CommonUser/Source/CommonUser/Private/CommonUserTypes.cpp b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserTypes.cpp new file mode 100644 index 0000000..4c9dc6d --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserTypes.cpp @@ -0,0 +1,17 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonUserTypes.h" +#include "OnlineError.h" + +void FOnlineResultInformation::FromOnlineError(const FOnlineErrorType& InOnlineError) +{ +#if COMMONUSER_OSSV1 + bWasSuccessful = InOnlineError.WasSuccessful(); + ErrorId = InOnlineError.GetErrorCode(); + ErrorText = InOnlineError.GetErrorMessage(); +#else + bWasSuccessful = InOnlineError != UE::Online::Errors::Success(); + ErrorId = InOnlineError.GetErrorId(); + ErrorText = InOnlineError.GetText(); +#endif +} diff --git a/Plugins/CommonUser/Source/CommonUser/Public/AsyncAction_CommonUserInitialize.h b/Plugins/CommonUser/Source/CommonUser/Public/AsyncAction_CommonUserInitialize.h new file mode 100644 index 0000000..aeb79c2 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/AsyncAction_CommonUserInitialize.h @@ -0,0 +1,64 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CommonUserSubsystem.h" +#include "Engine/CancellableAsyncAction.h" + +#include "AsyncAction_CommonUserInitialize.generated.h" + +enum class ECommonUserOnlineContext : uint8; +enum class ECommonUserPrivilege : uint8; +struct FInputDeviceId; + +class FText; +class UObject; +struct FFrame; + +/** + * Async action to handle different functions for initializing users + */ +UCLASS() +class COMMONUSER_API UAsyncAction_CommonUserInitialize : public UCancellableAsyncAction +{ + GENERATED_BODY() + +public: + /** + * Initializes a local player with the common user system, which includes doing platform-specific login and privilege checks. + * When the process has succeeded or failed, it will broadcast the OnInitializationComplete delegate. + * + * @param LocalPlayerIndex Desired index of ULocalPlayer in Game Instance, 0 will be primary player and 1+ for local multiplayer + * @param PrimaryInputDevice Primary input device for the user, if invalid will use the system default + * @param bCanUseGuestLogin If true, this player can be a guest without a real system net id + */ + UFUNCTION(BlueprintCallable, Category = CommonUser, meta = (BlueprintInternalUseOnly = "true")) + static UAsyncAction_CommonUserInitialize* InitializeForLocalPlay(UCommonUserSubsystem* Target, int32 LocalPlayerIndex, FInputDeviceId PrimaryInputDevice, bool bCanUseGuestLogin); + + /** + * Attempts to log an existing user into the platform-specific online backend to enable full online play + * When the process has succeeded or failed, it will broadcast the OnInitializationComplete delegate. + * + * @param LocalPlayerIndex Index of existing LocalPlayer in Game Instance + */ + UFUNCTION(BlueprintCallable, Category = CommonUser, meta = (BlueprintInternalUseOnly = "true")) + static UAsyncAction_CommonUserInitialize* LoginForOnlinePlay(UCommonUserSubsystem* Target, int32 LocalPlayerIndex); + + /** Call when initialization succeeds or fails */ + UPROPERTY(BlueprintAssignable) + FCommonUserOnInitializeCompleteMulticast OnInitializationComplete; + + /** Fail and send callbacks if needed */ + void HandleFailure(); + + /** Wrapper delegate, will pass on to OnInitializationComplete if appropriate */ + UFUNCTION() + virtual void HandleInitializationComplete(const UCommonUserInfo* UserInfo, bool bSuccess, FText Error, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext OnlineContext); + +protected: + /** Actually start the initialization */ + virtual void Activate() override; + + TWeakObjectPtr Subsystem; + FCommonUserInitializeParams Params; +}; diff --git a/Plugins/CommonUser/Source/CommonUser/Public/CommonSessionSubsystem.h b/Plugins/CommonUser/Source/CommonUser/Public/CommonSessionSubsystem.h new file mode 100644 index 0000000..56b6fd8 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/CommonSessionSubsystem.h @@ -0,0 +1,471 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CommonUserTypes.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "UObject/ObjectPtr.h" +#include "UObject/StrongObjectPtr.h" +#include "UObject/PrimaryAssetId.h" +#include "UObject/WeakObjectPtr.h" +#include "PartyBeaconClient.h" +#include "PartyBeaconHost.h" +#include "PartyBeaconState.h" + +class APlayerController; +class AOnlineBeaconHost; +class ULocalPlayer; +namespace ETravelFailure { enum Type : int; } +struct FOnlineResultInformation; + +#if COMMONUSER_OSSV1 +#include "Interfaces/OnlineSessionInterface.h" +#include "OnlineSessionSettings.h" +#else +#include "Online/Lobbies.h" +#include "Online/OnlineAsyncOpHandle.h" +#endif // COMMONUSER_OSSV1 + +#include "CommonSessionSubsystem.generated.h" + +class UWorld; +class FCommonSession_OnlineSessionSettings; + +#if COMMONUSER_OSSV1 +class FCommonOnlineSearchSettingsOSSv1; +using FCommonOnlineSearchSettings = FCommonOnlineSearchSettingsOSSv1; +#else +class FCommonOnlineSearchSettingsOSSv2; +using FCommonOnlineSearchSettings = FCommonOnlineSearchSettingsOSSv2; +#endif // COMMONUSER_OSSV1 + + +////////////////////////////////////////////////////////////////////// +// UCommonSession_HostSessionRequest + +/** Specifies the online features and connectivity that should be used for a game session */ +UENUM(BlueprintType) +enum class ECommonSessionOnlineMode : uint8 +{ + Offline, + LAN, + Online +}; + +/** A request object that stores the parameters used when hosting a gameplay session */ +UCLASS(BlueprintType) +class COMMONUSER_API UCommonSession_HostSessionRequest : public UObject +{ + GENERATED_BODY() + +public: + /** Indicates if the session is a full online session or a different type */ + UPROPERTY(BlueprintReadWrite, Category=Session) + ECommonSessionOnlineMode OnlineMode; + + /** True if this request should create a player-hosted lobbies if available */ + UPROPERTY(BlueprintReadWrite, Category = Session) + bool bUseLobbies; + + /** True if this request should create a lobby with enabled voice chat in available */ + UPROPERTY(BlueprintReadWrite, Category = Session) + bool bUseLobbiesVoiceChat; + + /** True if this request should create a session that will appear in the user's presence information */ + UPROPERTY(BlueprintReadWrite, Category = Session) + bool bUsePresence; + + /** String used during matchmaking to specify what type of game mode this is */ + UPROPERTY(BlueprintReadWrite, Category=Session) + FString ModeNameForAdvertisement; + + /** The map that will be loaded at the start of gameplay, this needs to be a valid Primary Asset top-level map */ + UPROPERTY(BlueprintReadWrite, Category=Session, meta=(AllowedTypes="World")) + FPrimaryAssetId MapID; + + /** Extra arguments passed as URL options to the game */ + UPROPERTY(BlueprintReadWrite, Category=Session) + TMap ExtraArgs; + + /** Maximum players allowed per gameplay session */ + UPROPERTY(BlueprintReadWrite, Category=Session) + int32 MaxPlayerCount = 16; + +public: + /** Returns the maximum players that should actually be used, could be overridden in child classes */ + virtual int32 GetMaxPlayers() const; + + /** Returns the full map name that will be used during gameplay */ + virtual FString GetMapName() const; + + /** Constructs the full URL that will be passed to ServerTravel */ + virtual FString ConstructTravelURL() const; + + /** Returns true if this request is valid, returns false and logs errors if it is not */ + virtual bool ValidateAndLogErrors(FText& OutError) const; +}; + + +////////////////////////////////////////////////////////////////////// +// UCommonSession_SearchResult + +/** A result object returned from the online system that describes a joinable game session */ +UCLASS(BlueprintType) +class COMMONUSER_API UCommonSession_SearchResult : public UObject +{ + GENERATED_BODY() + +public: + /** Returns an internal description of the session, not meant to be human readable */ + UFUNCTION(BlueprintCallable, Category=Session) + FString GetDescription() const; + + /** Gets an arbitrary string setting, bFoundValue will be false if the setting does not exist */ + UFUNCTION(BlueprintPure, Category=Sessions) + void GetStringSetting(FName Key, FString& Value, bool& bFoundValue) const; + + /** Gets an arbitrary integer setting, bFoundValue will be false if the setting does not exist */ + UFUNCTION(BlueprintPure, Category = Sessions) + void GetIntSetting(FName Key, int32& Value, bool& bFoundValue) const; + + /** The number of private connections that are available */ + UFUNCTION(BlueprintPure, Category=Sessions) + int32 GetNumOpenPrivateConnections() const; + + /** The number of publicly available connections that are available */ + UFUNCTION(BlueprintPure, Category=Sessions) + int32 GetNumOpenPublicConnections() const; + + /** The maximum number of publicly available connections that could be available, including already filled connections */ + UFUNCTION(BlueprintPure, Category = Sessions) + int32 GetMaxPublicConnections() const; + + /** Ping to the search result, MAX_QUERY_PING is unreachable */ + UFUNCTION(BlueprintPure, Category=Sessions) + int32 GetPingInMs() const; + +public: + /** Pointer to the platform-specific implementation */ +#if COMMONUSER_OSSV1 + FOnlineSessionSearchResult Result; +#else + TSharedPtr Lobby; +#endif // COMMONUSER_OSSV1 + +}; + + +////////////////////////////////////////////////////////////////////// +// UCommonSession_SearchSessionRequest + +/** Delegates called when a session search completes */ +DECLARE_MULTICAST_DELEGATE_TwoParams(FCommonSession_FindSessionsFinished, bool bSucceeded, const FText& ErrorMessage); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FCommonSession_FindSessionsFinishedDynamic, bool, bSucceeded, FText, ErrorMessage); + +/** Request object describing a session search, this object will be updated once the search has completed */ +UCLASS(BlueprintType) +class COMMONUSER_API UCommonSession_SearchSessionRequest : public UObject +{ + GENERATED_BODY() + +public: + /** Indicates if the this is looking for full online games or a different type like LAN */ + UPROPERTY(BlueprintReadWrite, Category = Session) + ECommonSessionOnlineMode OnlineMode; + + /** True if this request should look for player-hosted lobbies if they are available, false will only search for registered server sessions */ + UPROPERTY(BlueprintReadWrite, Category = Session) + bool bUseLobbies; + + /** List of all found sessions, will be valid when OnSearchFinished is called */ + UPROPERTY(BlueprintReadOnly, Category=Session) + TArray> Results; + + /** Native Delegate called when a session search completes */ + FCommonSession_FindSessionsFinished OnSearchFinished; + + /** Called by subsystem to execute finished delegates */ + void NotifySearchFinished(bool bSucceeded, const FText& ErrorMessage); + +private: + /** Delegate called when a session search completes */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On Search Finished", AllowPrivateAccess = true)) + FCommonSession_FindSessionsFinishedDynamic K2_OnSearchFinished; +}; + + +////////////////////////////////////////////////////////////////////// +// CommonSessionSubsystem Events + +/** + * Event triggered when the local user has requested to join a session from an external source, for example from a platform overlay. + * Generally, the game should transition the player into the session. + * @param LocalPlatformUserId the local user id that accepted the invitation. This is a platform user id because the user might not be signed in yet. + * @param RequestedSession the requested session. Can be null if there was an error processing the request. + * @param RequestedSessionResult result of the requested session processing + */ +DECLARE_MULTICAST_DELEGATE_ThreeParams(FCommonSessionOnUserRequestedSession, const FPlatformUserId& /*LocalPlatformUserId*/, UCommonSession_SearchResult* /*RequestedSession*/, const FOnlineResultInformation& /*RequestedSessionResult*/); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FCommonSessionOnUserRequestedSession_Dynamic, const FPlatformUserId&, LocalPlatformUserId, UCommonSession_SearchResult*, RequestedSession, const FOnlineResultInformation&, RequestedSessionResult); + +/** + * Event triggered when a session join has completed, after joining the underlying session and before traveling to the server if it was successful. + * The event parameters indicate if this was successful, or if there was an error that will stop it from traveling. + * @param Result result of the session join + */ +DECLARE_MULTICAST_DELEGATE_OneParam(FCommonSessionOnJoinSessionComplete, const FOnlineResultInformation& /*Result*/); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCommonSessionOnJoinSessionComplete_Dynamic, const FOnlineResultInformation&, Result); + +/** + * Event triggered when a session creation for hosting has completed, right before it travels to the map. + * The event parameters indicate if this was successful, or if there was an error that will stop it from traveling. + * @param Result result of the session join + */ +DECLARE_MULTICAST_DELEGATE_OneParam(FCommonSessionOnCreateSessionComplete, const FOnlineResultInformation& /*Result*/); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCommonSessionOnCreateSessionComplete_Dynamic, const FOnlineResultInformation&, Result); + +/** + * Event triggered when the local user has requested to destroy a session from an external source, for example from a platform overlay. + * The game should transition the player out of the session. + * @param LocalPlatformUserId the local user id that made the destroy request. This is a platform user id because the user might not be signed in yet. + * @param SessionName the name identifier for the session. + */ +DECLARE_MULTICAST_DELEGATE_TwoParams(FCommonSessionOnDestroySessionRequested, const FPlatformUserId& /*LocalPlatformUserId*/, const FName& /*SessionName*/); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FCommonSessionOnDestroySessionRequested_Dynamic, const FPlatformUserId&, LocalPlatformUserId, const FName&, SessionName); + +/** + * Event triggered when a session join has completed, after resolving the connect string and prior to the client traveling. + * @param URL resolved connection string for the session with any additional arguments + */ +DECLARE_MULTICAST_DELEGATE_OneParam(FCommonSessionOnPreClientTravel, FString& /*URL*/); + +/** + * Event triggered at different points in the session ecosystem that represent a user-presentable state of the session. + * This should not be used for online functionality (use OnCreateSessionComplete or OnJoinSessionComplete for those) but for features such as rich presence + */ +UENUM(BlueprintType) +enum class ECommonSessionInformationState : uint8 +{ + OutOfGame, + Matchmaking, + InGame +}; +DECLARE_MULTICAST_DELEGATE_ThreeParams(FCommonSessionOnSessionInformationChanged, ECommonSessionInformationState /*SessionStatus*/, const FString& /*GameMode*/, const FString& /*MapName*/); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FCommonSessionOnSessionInformationChanged_Dynamic, ECommonSessionInformationState, SessionStatus, const FString&, GameMode, const FString&, MapName); + +////////////////////////////////////////////////////////////////////// +// UCommonSessionSubsystem + +/** + * Game subsystem that handles requests for hosting and joining online games. + * One subsystem is created for each game instance and can be accessed from blueprints or C++ code. + * If a game-specific subclass exists, this base subsystem will not be created. + */ +UCLASS(BlueprintType, Config=Engine) +class COMMONUSER_API UCommonSessionSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + UCommonSessionSubsystem() { } + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + /** Creates a host session request with default options for online games, this can be modified after creation */ + UFUNCTION(BlueprintCallable, Category = Session) + virtual UCommonSession_HostSessionRequest* CreateOnlineHostSessionRequest(); + + /** Creates a session search object with default options to look for default online games, this can be modified after creation */ + UFUNCTION(BlueprintCallable, Category = Session) + virtual UCommonSession_SearchSessionRequest* CreateOnlineSearchSessionRequest(); + + /** Creates a new online game using the session request information, if successful this will start a hard map transfer */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void HostSession(APlayerController* HostingPlayer, UCommonSession_HostSessionRequest* Request); + + /** Starts a process to look for existing sessions or create a new one if no viable sessions are found */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void QuickPlaySession(APlayerController* JoiningOrHostingPlayer, UCommonSession_HostSessionRequest* Request); + + /** Starts process to join an existing session, if successful this will connect to the specified server */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void JoinSession(APlayerController* JoiningPlayer, UCommonSession_SearchResult* Request); + + /** Queries online system for the list of joinable sessions matching the search request */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void FindSessions(APlayerController* SearchingPlayer, UCommonSession_SearchSessionRequest* Request); + + /** Clean up any active sessions, called from cases like returning to the main menu */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void CleanUpSessions(); + + ////////////////////////////////////////////////////////////////////// + // Events + + /** Native Delegate when a local user has accepted an invite */ + FCommonSessionOnUserRequestedSession OnUserRequestedSessionEvent; + /** Event broadcast when a local user has accepted an invite */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On User Requested Session")) + FCommonSessionOnUserRequestedSession_Dynamic K2_OnUserRequestedSessionEvent; + + /** Native Delegate when a JoinSession call has completed */ + FCommonSessionOnJoinSessionComplete OnJoinSessionCompleteEvent; + /** Event broadcast when a JoinSession call has completed */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On Join Session Complete")) + FCommonSessionOnJoinSessionComplete_Dynamic K2_OnJoinSessionCompleteEvent; + + /** Native Delegate when a CreateSession call has completed */ + FCommonSessionOnCreateSessionComplete OnCreateSessionCompleteEvent; + /** Event broadcast when a CreateSession call has completed */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On Create Session Complete")) + FCommonSessionOnCreateSessionComplete_Dynamic K2_OnCreateSessionCompleteEvent; + + /** Native Delegate when the presentable session information has changed */ + FCommonSessionOnSessionInformationChanged OnSessionInformationChangedEvent; + /** Event broadcast when the presentable session information has changed */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On Session Information Changed")) + FCommonSessionOnSessionInformationChanged_Dynamic K2_OnSessionInformationChangedEvent; + + /** Native Delegate when a platform session destroy has been requested */ + FCommonSessionOnDestroySessionRequested OnDestroySessionRequestedEvent; + /** Event broadcast when a platform session destroy has been requested */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On Leave Session Requested")) + FCommonSessionOnDestroySessionRequested_Dynamic K2_OnDestroySessionRequestedEvent; + + /** Native Delegate for modifying the connect URL prior to a client travel */ + FCommonSessionOnPreClientTravel OnPreClientTravelEvent; + + // Config settings, these can overridden in child classes or config files + + /** Sets the default value of bUseLobbies for session search and host requests */ + UPROPERTY(Config) + bool bUseLobbiesDefault = true; + + /** Sets the default value of bUseLobbiesVoiceChat for session host requests */ + UPROPERTY(Config) + bool bUseLobbiesVoiceChatDefault = false; + + /** Enables reservation beacon flow prior to server travel when creating or joining a game session */ + UPROPERTY(Config) + bool bUseBeacons = true; + +protected: + // Functions called during the process of creating or joining a session, these can be overidden for game-specific behavior + + /** Called to fill in a session request from quick play host settings, can be overridden for game-specific behavior */ + virtual TSharedRef CreateQuickPlaySearchSettings(UCommonSession_HostSessionRequest* Request, UCommonSession_SearchSessionRequest* QuickPlayRequest); + + /** Called when a quick play search finishes, can be overridden for game-specific behavior */ + virtual void HandleQuickPlaySearchFinished(bool bSucceeded, const FText& ErrorMessage, TWeakObjectPtr JoiningOrHostingPlayer, TStrongObjectPtr HostRequest); + + /** Called when traveling to a session fails */ + virtual void TravelLocalSessionFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ReasonString); + + /** Called when a new session is either created or fails to be created */ + virtual void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful); + + /** Called to finalize session creation */ + virtual void FinishSessionCreation(bool bWasSuccessful); + + /** Called after traveling to the new hosted session map */ + virtual void HandlePostLoadMap(UWorld* World); + +protected: + // Internal functions for initializing and handling results from the online systems + + void BindOnlineDelegates(); + void CreateOnlineSessionInternal(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request); + void FindSessionsInternal(APlayerController* SearchingPlayer, const TSharedRef& InSearchSettings); + void JoinSessionInternal(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request); + void InternalTravelToSession(const FName SessionName); + void NotifyUserRequestedSession(const FPlatformUserId& PlatformUserId, UCommonSession_SearchResult* RequestedSession, const FOnlineResultInformation& RequestedSessionResult); + void NotifyJoinSessionComplete(const FOnlineResultInformation& Result); + void NotifyCreateSessionComplete(const FOnlineResultInformation& Result); + void NotifySessionInformationUpdated(ECommonSessionInformationState SessionStatusStr, const FString& GameMode = FString(), const FString& MapName = FString()); + void NotifyDestroySessionRequested(const FPlatformUserId& PlatformUserId, const FName& SessionName); + void SetCreateSessionError(const FText& ErrorText); + +#if COMMONUSER_OSSV1 + void BindOnlineDelegatesOSSv1(); + void CreateOnlineSessionInternalOSSv1(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request); + void FindSessionsInternalOSSv1(ULocalPlayer* LocalPlayer); + void JoinSessionInternalOSSv1(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request); + TSharedRef CreateQuickPlaySearchSettingsOSSv1(UCommonSession_HostSessionRequest* Request, UCommonSession_SearchSessionRequest* QuickPlayRequest); + void CleanUpSessionsOSSv1(); + + void HandleSessionFailure(const FUniqueNetId& NetId, ESessionFailure::Type FailureType); + void HandleSessionUserInviteAccepted(const bool bWasSuccessful, const int32 LocalUserIndex, FUniqueNetIdPtr AcceptingUserId, const FOnlineSessionSearchResult& SearchResult); + void OnStartSessionComplete(FName SessionName, bool bWasSuccessful); + void OnRegisterLocalPlayerComplete_CreateSession(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result); + void OnUpdateSessionComplete(FName SessionName, bool bWasSuccessful); + void OnEndSessionComplete(FName SessionName, bool bWasSuccessful); + void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful); + void OnDestroySessionRequested(int32 LocalUserNum, FName SessionName); + void OnFindSessionsComplete(bool bWasSuccessful); + void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); + void OnRegisterJoiningLocalPlayerComplete(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result); + void FinishJoinSession(EOnJoinSessionCompleteResult::Type Result); + +#else + void BindOnlineDelegatesOSSv2(); + void CreateOnlineSessionInternalOSSv2(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request); + void FindSessionsInternalOSSv2(ULocalPlayer* LocalPlayer); + void JoinSessionInternalOSSv2(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request); + TSharedRef CreateQuickPlaySearchSettingsOSSv2(UCommonSession_HostSessionRequest* HostRequest, UCommonSession_SearchSessionRequest* SearchRequest); + void CleanUpSessionsOSSv2(); + + /** Process a join request originating from the online service */ + void OnSessionJoinRequested(const UE::Online::FUILobbyJoinRequested& EventParams); + + /** Get the local user id for a given controller */ + UE::Online::FAccountId GetAccountId(APlayerController* PlayerController) const; + /** Get the lobby id for a given session name */ + UE::Online::FLobbyId GetLobbyId(const FName SessionName) const; + /** Event handle for UI lobby join requested */ + UE::Online::FOnlineEventDelegateHandle LobbyJoinRequestedHandle; +#endif // COMMONUSER_OSSV1 + + void CreateHostReservationBeacon(); + void ConnectToHostReservationBeacon(); + void DestroyHostReservationBeacon(); + +protected: + /** The travel URL that will be used after session operations are complete */ + FString PendingTravelURL; + + /** Most recent result information for a session creation attempt, stored here to allow storing error codes for later */ + FOnlineResultInformation CreateSessionResult; + + /** True if we want to cancel the session after it is created */ + bool bWantToDestroyPendingSession = false; + + /** True if this is a dedicated server, which doesn't require a LocalPlayer to create a session */ + bool bIsDedicatedServer = false; + + /** Settings for the current search */ + TSharedPtr SearchSettings; + + /** General beacon listener for registering beacons with */ + UPROPERTY(Transient) + TWeakObjectPtr BeaconHostListener; + /** State of the beacon host */ + UPROPERTY(Transient) + TObjectPtr ReservationBeaconHostState; + /** Beacon controlling access to this game. */ + UPROPERTY(Transient) + TWeakObjectPtr ReservationBeaconHost; + /** Common class object for beacon communication */ + UPROPERTY(Transient) + TWeakObjectPtr ReservationBeaconClient; + + /** Number of teams for beacon reservation */ + UPROPERTY(Config) + int32 BeaconTeamCount = 2; + /** Size of a team for beacon reservation */ + UPROPERTY(Config) + int32 BeaconTeamSize = 8; + /** Max number of beacon reservations */ + UPROPERTY(Config) + int32 BeaconMaxReservations = 16; +}; diff --git a/Plugins/CommonUser/Source/CommonUser/Public/CommonUserBasicPresence.h b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserBasicPresence.h new file mode 100644 index 0000000..66ff24a --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserBasicPresence.h @@ -0,0 +1,59 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once + +#include "Subsystems/GameInstanceSubsystem.h" +#include "CommonUserBasicPresence.generated.h" + +class UCommonSessionSubsystem; +enum class ECommonSessionInformationState : uint8; + +////////////////////////////////////////////////////////////////////// +// UCommonUserBasicPresence + +/** + * This subsystem plugs into the session subsystem and pushes its information to the presence interface. + * It is not intended to be a full featured rich presence implementation, but can be used as a proof-of-concept + * for pushing information from the session subsystem to the presence system + */ +UCLASS(BlueprintType, Config = Engine) +class COMMONUSER_API UCommonUserBasicPresence : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + UCommonUserBasicPresence(); + + + /** Implement this for initialization of instances of the system */ + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + + /** Implement this for deinitialization of instances of the system */ + virtual void Deinitialize() override; + + /** False is a general purpose killswitch to stop this class from pushing presence*/ + UPROPERTY(Config) + bool bEnableSessionsBasedPresence = false; + + /** Maps the presence status "In-game" to a backend key*/ + UPROPERTY(Config) + FString PresenceStatusInGame; + + /** Maps the presence status "Main Menu" to a backend key*/ + UPROPERTY(Config) + FString PresenceStatusMainMenu; + + /** Maps the presence status "Matchmaking" to a backend key*/ + UPROPERTY(Config) + FString PresenceStatusMatchmaking; + + /** Maps the "Game Mode" rich presence entry to a backend key*/ + UPROPERTY(Config) + FString PresenceKeyGameMode; + + /** Maps the "Map Name" rich presence entry to a backend key*/ + UPROPERTY(Config) + FString PresenceKeyMapName; + + void OnNotifySessionInformationChanged(ECommonSessionInformationState SessionStatus, const FString& GameMode, const FString& MapName); + FString SessionStateToBackendKey(ECommonSessionInformationState SessionStatus); +}; \ No newline at end of file diff --git a/Plugins/CommonUser/Source/CommonUser/Public/CommonUserModule.h b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserModule.h new file mode 100644 index 0000000..da3fb20 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserModule.h @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Modules/ModuleInterface.h" + +class FCommonUserModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/CommonUser/Source/CommonUser/Public/CommonUserSubsystem.h b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserSubsystem.h new file mode 100644 index 0000000..8ad4b8e --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserSubsystem.h @@ -0,0 +1,649 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CommonUserTypes.h" +#include "Engine/GameViewportClient.h" +#include "GameFramework/OnlineReplStructs.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "UObject/WeakObjectPtr.h" +#include "GameplayTagContainer.h" +#include "CommonUserSubsystem.generated.h" + +#if COMMONUSER_OSSV1 +#include "Interfaces/OnlineIdentityInterface.h" +#include "OnlineError.h" +#else +#include "Online/OnlineAsyncOpHandle.h" +#endif + +class FNativeGameplayTag; +class IOnlineSubsystem; + +/** List of tags used by the common user subsystem */ +struct COMMONUSER_API FCommonUserTags +{ + // General severity levels and specific system messages + + static FNativeGameplayTag SystemMessage_Error; // SystemMessage.Error + static FNativeGameplayTag SystemMessage_Warning; // SystemMessage.Warning + static FNativeGameplayTag SystemMessage_Display; // SystemMessage.Display + + /** All attempts to initialize a player failed, user has to do something before trying again */ + static FNativeGameplayTag SystemMessage_Error_InitializeLocalPlayerFailed; // SystemMessage.Error.InitializeLocalPlayerFailed + + + // Platform trait tags, it is expected that the game instance or other system calls SetTraitTags with these tags for the appropriate platform + + /** This tag means it is a console platform that directly maps controller IDs to different system users. If false, the same user can have multiple controllers */ + static FNativeGameplayTag Platform_Trait_RequiresStrictControllerMapping; // Platform.Trait.RequiresStrictControllerMapping + + /** This tag means the platform has a single online user and all players use index 0 */ + static FNativeGameplayTag Platform_Trait_SingleOnlineUser; // Platform.Trait.SingleOnlineUser +}; + +/** Logical representation of an individual user, one of these will exist for all initialized local players */ +UCLASS(BlueprintType) +class COMMONUSER_API UCommonUserInfo : public UObject +{ + GENERATED_BODY() + +public: + /** Primary controller input device for this user, they could also have additional secondary devices */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + FInputDeviceId PrimaryInputDevice; + + /** Specifies the logical user on the local platform, guest users will point to the primary user */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + FPlatformUserId PlatformUser; + + /** If this user is assigned a LocalPlayer, this will match the index in the GameInstance localplayers array once it is fully created */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + int32 LocalPlayerIndex = -1; + + /** If true, this user is allowed to be a guest */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + bool bCanBeGuest = false; + + /** If true, this is a guest user attached to primary user 0 */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + bool bIsGuest = false; + + /** Overall state of the user's initialization process */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + ECommonUserInitializationState InitializationState = ECommonUserInitializationState::Invalid; + + /** Returns true if this user has successfully logged in */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + bool IsLoggedIn() const; + + /** Returns true if this user is in the middle of logging in */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + bool IsDoingLogin() const; + + /** Returns the most recently queries result for a specific privilege, will return unknown if never queried */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + ECommonUserPrivilegeResult GetCachedPrivilegeResult(ECommonUserPrivilege Privilege, ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Ask about the general availability of a feature, this combines cached results with state */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + ECommonUserAvailability GetPrivilegeAvailability(ECommonUserPrivilege Privilege) const; + + /** Returns the net id for the given context */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + FUniqueNetIdRepl GetNetId(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the user's human readable nickname, this will return the value that was cached during UpdateCachedNetId or SetNickname */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + FString GetNickname(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Modify the user's human readable nickname, this can be used when setting up multiple guests but will get overwritten with the platform nickname for real users */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + void SetNickname(const FString& NewNickname, ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game); + + /** Returns an internal debug string for this player */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + FString GetDebugString() const; + + /** Accessor for platform user id */ + FPlatformUserId GetPlatformUserId() const; + + /** Gets the platform user index for older functions expecting an integer */ + int32 GetPlatformUserIndex() const; + + // Internal data, only intended to be accessed by online subsystems + + /** Cached data for each online system */ + struct FCachedData + { + /** Cached net id per system */ + FUniqueNetIdRepl CachedNetId; + + /** Cached nickanem, updated whenever net ID might change */ + FString CachedNickname; + + /** Cached values of various user privileges */ + TMap CachedPrivileges; + }; + + /** Per context cache, game will always exist but others may not */ + TMap CachedDataMap; + + /** Looks up cached data using resolution rules */ + FCachedData* GetCachedData(ECommonUserOnlineContext Context); + const FCachedData* GetCachedData(ECommonUserOnlineContext Context) const; + + /** Updates cached privilege results, will propagate to game if needed */ + void UpdateCachedPrivilegeResult(ECommonUserPrivilege Privilege, ECommonUserPrivilegeResult Result, ECommonUserOnlineContext Context); + + /** Updates cached privilege results, will propagate to game if needed */ + void UpdateCachedNetId(const FUniqueNetIdRepl& NewId, ECommonUserOnlineContext Context); + + /** Return the subsystem this is owned by */ + class UCommonUserSubsystem* GetSubsystem() const; +}; + + +/** Delegates when initialization processes succeed or fail */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FCommonUserOnInitializeCompleteMulticast, const UCommonUserInfo*, UserInfo, bool, bSuccess, FText, Error, ECommonUserPrivilege, RequestedPrivilege, ECommonUserOnlineContext, OnlineContext); +DECLARE_DYNAMIC_DELEGATE_FiveParams(FCommonUserOnInitializeComplete, const UCommonUserInfo*, UserInfo, bool, bSuccess, FText, Error, ECommonUserPrivilege, RequestedPrivilege, ECommonUserOnlineContext, OnlineContext); + +/** Delegate when a system error message is sent, the game can choose to display it to the user using the type tag */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FCommonUserHandleSystemMessageDelegate, FGameplayTag, MessageType, FText, TitleText, FText, BodyText); + +/** Delegate when a privilege changes, this can be bound to see if online status/etc changes during gameplay */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FCommonUserAvailabilityChangedDelegate, const UCommonUserInfo*, UserInfo, ECommonUserPrivilege, Privilege, ECommonUserAvailability, OldAvailability, ECommonUserAvailability, NewAvailability); + + +/** Parameter struct for initialize functions, this would normally be filled in by wrapper functions like async nodes */ +USTRUCT(BlueprintType) +struct COMMONUSER_API FCommonUserInitializeParams +{ + GENERATED_BODY() + + /** What local player index to use, can specify one above current if can create player is enabled */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + int32 LocalPlayerIndex = 0; + + /** Deprecated method of selecting platform user and input device */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + int32 ControllerId = -1; + + /** Primary controller input device for this user, they could also have additional secondary devices */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + FInputDeviceId PrimaryInputDevice; + + /** Specifies the logical user on the local platform */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + FPlatformUserId PlatformUser; + + /** Generally either CanPlay or CanPlayOnline, specifies what level of privilege is required */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + ECommonUserPrivilege RequestedPrivilege = ECommonUserPrivilege::CanPlay; + + /** What specific online context to log in to, game means to login to all relevant ones */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + ECommonUserOnlineContext OnlineContext = ECommonUserOnlineContext::Game; + + /** True if this is allowed to create a new local player for initial login */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + bool bCanCreateNewLocalPlayer = false; + + /** True if this player can be a guest user without an actual online presence */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + bool bCanUseGuestLogin = false; + + /** True if we should not show login errors, the game will be responsible for displaying them */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + bool bSuppressLoginErrors = false; + + /** If bound, call this dynamic delegate at completion of login */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + FCommonUserOnInitializeComplete OnUserInitializeComplete; +}; + +/** + * Game subsystem that handles queries and changes to user identity and login status. + * One subsystem is created for each game instance and can be accessed from blueprints or C++ code. + * If a game-specific subclass exists, this base subsystem will not be created. + */ +UCLASS(BlueprintType, Config=Engine) +class COMMONUSER_API UCommonUserSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + UCommonUserSubsystem() { } + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + + /** BP delegate called when any requested initialization request completes */ + UPROPERTY(BlueprintAssignable, Category = CommonUser) + FCommonUserOnInitializeCompleteMulticast OnUserInitializeComplete; + + /** BP delegate called when the system sends an error/warning message */ + UPROPERTY(BlueprintAssignable, Category = CommonUser) + FCommonUserHandleSystemMessageDelegate OnHandleSystemMessage; + + /** BP delegate called when privilege availability changes for a user */ + UPROPERTY(BlueprintAssignable, Category = CommonUser) + FCommonUserAvailabilityChangedDelegate OnUserPrivilegeChanged; + + /** Send a system message via OnHandleSystemMessage */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual void SendSystemMessage(FGameplayTag MessageType, FText TitleText, FText BodyText); + + /** Sets the maximum number of local players, will not destroy existing ones */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual void SetMaxLocalPlayers(int32 InMaxLocalPLayers); + + /** Gets the maximum number of local players */ + UFUNCTION(BlueprintPure, Category = CommonUser) + int32 GetMaxLocalPlayers() const; + + /** Gets the current number of local players, will always be at least 1 */ + UFUNCTION(BlueprintPure, Category = CommonUser) + int32 GetNumLocalPlayers() const; + + /** Returns the state of initializing the specified local player */ + UFUNCTION(BlueprintPure, Category = CommonUser) + ECommonUserInitializationState GetLocalPlayerInitializationState(int32 LocalPlayerIndex) const; + + /** Returns the user info for a given local player index in game instance, 0 is always valid in a running game */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForLocalPlayerIndex(int32 LocalPlayerIndex) const; + + /** Deprecated, use PlatformUserId when available */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForPlatformUserIndex(int32 PlatformUserIndex) const; + + /** Returns the primary user info for a given platform user index. Can return null */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForPlatformUser(FPlatformUserId PlatformUser) const; + + /** Returns the user info for a unique net id. Can return null */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForUniqueNetId(const FUniqueNetIdRepl& NetId) const; + + /** Deprecated, use InputDeviceId when available */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForControllerId(int32 ControllerId) const; + + /** Returns the user info for a given input device. Can return null */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForInputDevice(FInputDeviceId InputDevice) const; + + /** + * Tries to start the process of creating or updating a local player, including logging in and creating a player controller. + * When the process has succeeded or failed, it will broadcast the OnUserInitializeComplete delegate. + * + * @param LocalPlayerIndex Desired index of LocalPlayer in Game Instance, 0 will be primary player and 1+ for local multiplayer + * @param PrimaryInputDevice The physical controller that should be mapped to this user, will use the default device if invalid + * @param bCanUseGuestLogin If true, this player can be a guest without a real Unique Net Id + * + * @returns true if the process was started, false if it failed before properly starting + */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool TryToInitializeForLocalPlay(int32 LocalPlayerIndex, FInputDeviceId PrimaryInputDevice, bool bCanUseGuestLogin); + + /** + * Starts the process of taking a locally logged in user and doing a full online login including account permission checks. + * When the process has succeeded or failed, it will broadcast the OnUserInitializeComplete delegate. + * + * @param LocalPlayerIndex Index of existing LocalPlayer in Game Instance + * + * @returns true if the process was started, false if it failed before properly starting + */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool TryToLoginForOnlinePlay(int32 LocalPlayerIndex); + + /** + * Starts a general user login and initialization process, using the params structure to determine what to log in to. + * When the process has succeeded or failed, it will broadcast the OnUserInitializeComplete delegate. + * AsyncAction_CommonUserInitialize provides several wrapper functions for using this in an Event graph. + * + * @returns true if the process was started, false if it failed before properly starting + */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool TryToInitializeUser(FCommonUserInitializeParams Params); + + /** + * Starts the process of listening for user input for new and existing controllers and logging them. + * This will insert a key input handler on the active GameViewportClient and is turned off by calling again with empty key arrays. + * + * @param AnyUserKeys Listen for these keys for any user, even the default user. Set this for an initial press start screen or empty to disable + * @param NewUserKeys Listen for these keys for a new user without a player controller. Set this for splitscreen/local multiplayer or empty to disable + * @param Params Params passed to TryToInitializeUser after detecting key input + */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual void ListenForLoginKeyInput(TArray AnyUserKeys, TArray NewUserKeys, FCommonUserInitializeParams Params); + + /** Attempts to cancel an in-progress initialization attempt, this may not work on all platforms but will disable callbacks */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool CancelUserInitialization(int32 LocalPlayerIndex); + + /** Logs a player out of any online systems, and optionally destroys the player entirely if it's not the first one */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool TryToLogOutUser(int32 LocalPlayerIndex, bool bDestroyPlayer = false); + + /** Resets the login and initialization state when returning to the main menu after an error */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual void ResetUserState(); + + /** Returns true if this this could be a real platform user with a valid identity (even if not currently logged in) */ + virtual bool IsRealPlatformUserIndex(int32 PlatformUserIndex) const; + + /** Returns true if this this could be a real platform user with a valid identity (even if not currently logged in) */ + virtual bool IsRealPlatformUser(FPlatformUserId PlatformUser) const; + + /** Converts index to id */ + virtual FPlatformUserId GetPlatformUserIdForIndex(int32 PlatformUserIndex) const; + + /** Converts id to index */ + virtual int32 GetPlatformUserIndexForId(FPlatformUserId PlatformUser) const; + + /** Gets the user for an input device */ + virtual FPlatformUserId GetPlatformUserIdForInputDevice(FInputDeviceId InputDevice) const; + + /** Gets a user's primary input device id */ + virtual FInputDeviceId GetPrimaryInputDeviceForPlatformUser(FPlatformUserId PlatformUser) const; + + /** Call from game code to set the cached trait tags when platform state or options changes */ + virtual void SetTraitTags(const FGameplayTagContainer& InTags); + + /** Gets the current tags that affect feature avialability */ + const FGameplayTagContainer& GetTraitTags() const { return CachedTraitTags; } + + /** Checks if a specific platform/feature tag is enabled */ + UFUNCTION(BlueprintPure, Category=CommonUser) + bool HasTraitTag(const FGameplayTag TraitTag) const { return CachedTraitTags.HasTag(TraitTag); } + + /** Checks to see if we should display a press start/input confirmation screen at startup. Games can call this or check the trait tags directly */ + UFUNCTION(BlueprintPure, BlueprintPure, Category=CommonUser) + virtual bool ShouldWaitForStartInput() const; + + + // Functions for accessing low-level online system information + +#if COMMONUSER_OSSV1 + /** Returns OSS interface of specific type, will return null if there is no type */ + IOnlineSubsystem* GetOnlineSubsystem(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns identity interface of specific type, will return null if there is no type */ + IOnlineIdentity* GetOnlineIdentity(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns human readable name of OSS system */ + FName GetOnlineSubsystemName(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the current online connection status */ + EOnlineServerConnectionStatus::Type GetConnectionStatus(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; +#else + /** Get the services provider type, or None if there isn't one. */ + UE::Online::EOnlineServices GetOnlineServicesProvider(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns auth interface of specific type, will return null if there is no type */ + UE::Online::IAuthPtr GetOnlineAuth(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the current online connection status */ + UE::Online::EOnlineServicesConnectionStatus GetConnectionStatus(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; +#endif + + /** Returns true if we are currently connected to backend servers */ + bool HasOnlineConnection(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the current login status for a player on the specified online system, only works for real platform users */ + ELoginStatusType GetLocalUserLoginStatus(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the unique net id for a local platform user */ + FUniqueNetIdRepl GetLocalUserNetId(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the nickname for a local platform user, this is cached in common user Info */ + FString GetLocalUserNickname(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Convert a user id to a debug string */ + FString PlatformUserIdToString(FPlatformUserId UserId); + + /** Convert a context to a debug string */ + FString ECommonUserOnlineContextToString(ECommonUserOnlineContext Context); + + /** Returns human readable string for privilege checks */ + virtual FText GetPrivilegeDescription(ECommonUserPrivilege Privilege) const; + virtual FText GetPrivilegeResultDescription(ECommonUserPrivilegeResult Result) const; + + /** + * Starts the process of login for an existing local user, will return false if callback was not scheduled + * This activates the low level state machine and does not modify the initialization state on user info + */ + DECLARE_DELEGATE_FiveParams(FOnLocalUserLoginCompleteDelegate, const UCommonUserInfo* /*UserInfo*/, ELoginStatusType /*NewStatus*/, FUniqueNetIdRepl /*NetId*/, const TOptional& /*Error*/, ECommonUserOnlineContext /*Type*/); + virtual bool LoginLocalUser(const UCommonUserInfo* UserInfo, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext Context, FOnLocalUserLoginCompleteDelegate OnComplete); + + /** Assign a local player to a specific local user and call callbacks as needed */ + virtual void SetLocalPlayerUserInfo(ULocalPlayer* LocalPlayer, const UCommonUserInfo* UserInfo); + + /** Resolves a context that has default behavior into a specific context */ + ECommonUserOnlineContext ResolveOnlineContext(ECommonUserOnlineContext Context) const; + + /** True if there is a separate platform and service interface */ + bool HasSeparatePlatformContext() const; + +protected: + /** Internal structure that caches status and pointers for each online context */ + struct FOnlineContextCache + { +#if COMMONUSER_OSSV1 + /** Pointer to base subsystem, will stay valid as long as game instance does */ + IOnlineSubsystem* OnlineSubsystem = nullptr; + + /** Cached identity system, this will always be valid */ + IOnlineIdentityPtr IdentityInterface; + + /** Last connection status that was passed into the HandleNetworkConnectionStatusChanged hander */ + EOnlineServerConnectionStatus::Type CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; +#else + /** Online services, accessor to specific services */ + UE::Online::IOnlineServicesPtr OnlineServices; + /** Cached auth service */ + UE::Online::IAuthPtr AuthService; + /** Login status changed event handle */ + UE::Online::FOnlineEventDelegateHandle LoginStatusChangedHandle; + /** Connection status changed event handle */ + UE::Online::FOnlineEventDelegateHandle ConnectionStatusChangedHandle; + /** Last connection status that was passed into the HandleNetworkConnectionStatusChanged hander */ + UE::Online::EOnlineServicesConnectionStatus CurrentConnectionStatus = UE::Online::EOnlineServicesConnectionStatus::NotConnected; +#endif + + /** Resets state, important to clear all shared ptrs */ + void Reset() + { +#if COMMONUSER_OSSV1 + OnlineSubsystem = nullptr; + IdentityInterface.Reset(); + CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; +#else + OnlineServices.Reset(); + AuthService.Reset(); + CurrentConnectionStatus = UE::Online::EOnlineServicesConnectionStatus::NotConnected; +#endif + } + }; + + /** Internal structure to represent an in-progress login request */ + struct FUserLoginRequest : public TSharedFromThis + { + FUserLoginRequest(UCommonUserInfo* InUserInfo, ECommonUserPrivilege InPrivilege, ECommonUserOnlineContext InContext, FOnLocalUserLoginCompleteDelegate&& InDelegate) + : UserInfo(TWeakObjectPtr(InUserInfo)) + , DesiredPrivilege(InPrivilege) + , DesiredContext(InContext) + , Delegate(MoveTemp(InDelegate)) + {} + + /** Which local user is trying to log on */ + TWeakObjectPtr UserInfo; + + /** Overall state of login request, could come from many sources */ + ECommonUserAsyncTaskState OverallLoginState = ECommonUserAsyncTaskState::NotStarted; + + /** State of attempt to use platform auth. When started, this immediately transitions to Failed for OSSv1, as we do not support platform auth there. */ + ECommonUserAsyncTaskState TransferPlatformAuthState = ECommonUserAsyncTaskState::NotStarted; + + /** State of attempt to use AutoLogin */ + ECommonUserAsyncTaskState AutoLoginState = ECommonUserAsyncTaskState::NotStarted; + + /** State of attempt to use external login UI */ + ECommonUserAsyncTaskState LoginUIState = ECommonUserAsyncTaskState::NotStarted; + + /** Final privilege to that is requested */ + ECommonUserPrivilege DesiredPrivilege = ECommonUserPrivilege::Invalid_Count; + + /** State of attempt to request the relevant privilege */ + ECommonUserAsyncTaskState PrivilegeCheckState = ECommonUserAsyncTaskState::NotStarted; + + /** The final context to log into */ + ECommonUserOnlineContext DesiredContext = ECommonUserOnlineContext::Invalid; + + /** What online system we are currently logging into */ + ECommonUserOnlineContext CurrentContext = ECommonUserOnlineContext::Invalid; + + /** User callback for completion */ + FOnLocalUserLoginCompleteDelegate Delegate; + + /** Most recent/relevant error to display to user */ + TOptional Error; + }; + + + /** Create a new user info object */ + virtual UCommonUserInfo* CreateLocalUserInfo(int32 LocalPlayerIndex); + + /** Deconst wrapper for const getters */ + FORCEINLINE UCommonUserInfo* ModifyInfo(const UCommonUserInfo* Info) { return const_cast(Info); } + + /** Refresh user info from OSS */ + virtual void RefreshLocalUserInfo(UCommonUserInfo* UserInfo); + + /** Possibly send privilege availability notification, compares current value to cached old value */ + virtual void HandleChangedAvailability(UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserAvailability OldAvailability); + + /** Updates the cached privilege on a user and notifies delegate */ + virtual void UpdateUserPrivilegeResult(UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserPrivilegeResult Result, ECommonUserOnlineContext Context); + + /** Gets internal data for a type of online system, can return null for service */ + const FOnlineContextCache* GetContextCache(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + FOnlineContextCache* GetContextCache(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game); + + /** Create and set up system objects before delegates are bound */ + virtual void CreateOnlineContexts(); + virtual void DestroyOnlineContexts(); + + /** Bind online delegates */ + virtual void BindOnlineDelegates(); + + /** Forcibly logs out and deinitializes a single user */ + virtual void LogOutLocalUser(FPlatformUserId PlatformUser); + + /** Performs the next step of a login request, which could include completing it. Returns true if it's done */ + virtual void ProcessLoginRequest(TSharedRef Request); + + /** Call login on OSS, with platform auth from the platform OSS. Return true if AutoLogin started */ + virtual bool TransferPlatformAuth(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + + /** Call AutoLogin on OSS. Return true if AutoLogin started. */ + virtual bool AutoLogin(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + + /** Call ShowLoginUI on OSS. Return true if ShowLoginUI started. */ + virtual bool ShowLoginUI(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + + /** Call QueryUserPrivilege on OSS. Return true if QueryUserPrivilege started. */ + virtual bool QueryUserPrivilege(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + + /** OSS-specific functions */ +#if COMMONUSER_OSSV1 + virtual ECommonUserPrivilege ConvertOSSPrivilege(EUserPrivileges::Type Privilege) const; + virtual EUserPrivileges::Type ConvertOSSPrivilege(ECommonUserPrivilege Privilege) const; + virtual ECommonUserPrivilegeResult ConvertOSSPrivilegeResult(EUserPrivileges::Type Privilege, uint32 Results) const; + + void BindOnlineDelegatesOSSv1(); + bool AutoLoginOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool ShowLoginUIOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool QueryUserPrivilegeOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); +#else + virtual ECommonUserPrivilege ConvertOnlineServicesPrivilege(UE::Online::EUserPrivileges Privilege) const; + virtual UE::Online::EUserPrivileges ConvertOnlineServicesPrivilege(ECommonUserPrivilege Privilege) const; + virtual ECommonUserPrivilegeResult ConvertOnlineServicesPrivilegeResult(UE::Online::EUserPrivileges Privilege, UE::Online::EPrivilegeResults Results) const; + + void BindOnlineDelegatesOSSv2(); + void CacheConnectionStatus(ECommonUserOnlineContext Context); + bool TransferPlatformAuthOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool AutoLoginOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool ShowLoginUIOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool QueryUserPrivilegeOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + TSharedPtr GetOnlineServiceAccountInfo(UE::Online::IAuthPtr AuthService, FPlatformUserId InUserId) const; +#endif + + /** Callbacks for OSS functions */ +#if COMMONUSER_OSSV1 + virtual void HandleIdentityLoginStatusChanged(int32 PlatformUserIndex, ELoginStatus::Type OldStatus, ELoginStatus::Type NewStatus, const FUniqueNetId& NewId, ECommonUserOnlineContext Context); + virtual void HandleUserLoginCompleted(int32 PlatformUserIndex, bool bWasSuccessful, const FUniqueNetId& NetId, const FString& Error, ECommonUserOnlineContext Context); + virtual void HandleControllerPairingChanged(int32 PlatformUserIndex, FControllerPairingChangedUserInfo PreviousUser, FControllerPairingChangedUserInfo NewUser); + virtual void HandleNetworkConnectionStatusChanged(const FString& ServiceName, EOnlineServerConnectionStatus::Type LastConnectionStatus, EOnlineServerConnectionStatus::Type ConnectionStatus, ECommonUserOnlineContext Context); + virtual void HandleOnLoginUIClosed(TSharedPtr LoggedInNetId, const int PlatformUserIndex, const FOnlineError& Error, ECommonUserOnlineContext Context); + virtual void HandleCheckPrivilegesComplete(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults, ECommonUserPrivilege RequestedPrivilege, TWeakObjectPtr CommonUserInfo, ECommonUserOnlineContext Context); +#else + virtual void HandleAuthLoginStatusChanged(const UE::Online::FAuthLoginStatusChanged& EventParameters, ECommonUserOnlineContext Context); + virtual void HandleUserLoginCompletedV2(const UE::Online::TOnlineResult& Result, FPlatformUserId PlatformUser, ECommonUserOnlineContext Context); + virtual void HandleOnLoginUIClosedV2(const UE::Online::TOnlineResult& Result, FPlatformUserId PlatformUser, ECommonUserOnlineContext Context); + virtual void HandleNetworkConnectionStatusChanged(const UE::Online::FConnectionStatusChanged& EventParameters, ECommonUserOnlineContext Context); + virtual void HandleCheckPrivilegesComplete(const UE::Online::TOnlineResult& Result, TWeakObjectPtr CommonUserInfo, UE::Online::EUserPrivileges DesiredPrivilege, ECommonUserOnlineContext Context); +#endif + + /** + * Callback for when an input device (i.e. a gamepad) has been connected or disconnected. + */ + virtual void HandleInputDeviceConnectionChanged(EInputDeviceConnectionState NewConnectionState, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId); + + virtual void HandleLoginForUserInitialize(const UCommonUserInfo* UserInfo, ELoginStatusType NewStatus, FUniqueNetIdRepl NetId, const TOptional& Error, ECommonUserOnlineContext Context, FCommonUserInitializeParams Params); + virtual void HandleUserInitializeFailed(FCommonUserInitializeParams Params, FText Error); + virtual void HandleUserInitializeSucceeded(FCommonUserInitializeParams Params); + + /** Callback for handling press start/login logic */ + virtual bool OverrideInputKeyForLogin(FInputKeyEventArgs& EventArgs); + + + /** Previous override handler, will restore on cancel */ + FOverrideInputKeyHandler WrappedInputKeyHandler; + + /** List of keys to listen for from any user */ + TArray LoginKeysForAnyUser; + + /** List of keys to listen for a new unmapped user */ + TArray LoginKeysForNewUser; + + /** Params to use for a key-triggered login */ + FCommonUserInitializeParams ParamsForLoginKey; + + /** Maximum number of local players */ + int32 MaxNumberOfLocalPlayers = 0; + + /** True if this is a dedicated server, which doesn't require a LocalPlayer */ + bool bIsDedicatedServer = false; + + /** List of current in progress login requests */ + TArray> ActiveLoginRequests; + + /** Information about each local user, from local player index to user */ + UPROPERTY() + TMap> LocalUserInfos; + + /** Cached platform/mode trait tags */ + FGameplayTagContainer CachedTraitTags; + + /** Do not access this outside of initialization */ + FOnlineContextCache* DefaultContextInternal = nullptr; + FOnlineContextCache* ServiceContextInternal = nullptr; + FOnlineContextCache* PlatformContextInternal = nullptr; + + friend UCommonUserInfo; +}; diff --git a/Plugins/CommonUser/Source/CommonUser/Public/CommonUserTypes.h b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserTypes.h new file mode 100644 index 0000000..8120a64 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserTypes.h @@ -0,0 +1,218 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + + +#if COMMONUSER_OSSV1 + +// Online Subsystem (OSS v1) includes and forward declares +#include "OnlineSubsystemTypes.h" +class IOnlineSubsystem; +struct FOnlineError; +using FOnlineErrorType = FOnlineError; +using ELoginStatusType = ELoginStatus::Type; + +#else + +// Online Services (OSS v2) includes and forward declares +#include "Online/Connectivity.h" +#include "Online/OnlineError.h" +namespace UE::Online +{ + enum class ELoginStatus : uint8; + enum class EPrivilegeResults : uint32; + enum class EUserPrivileges : uint8; + using IAuthPtr = TSharedPtr; + using IOnlineServicesPtr = TSharedPtr; + template + class TOnlineResult; + struct FAuthLogin; + struct FConnectionStatusChanged; + struct FExternalUIShowLoginUI; + struct FAuthLoginStatusChanged; + struct FQueryUserPrivilege; + struct FAccountInfo; +} +using FOnlineErrorType = UE::Online::FOnlineError; +using ELoginStatusType = UE::Online::ELoginStatus; + +#endif + +#include "CommonUserTypes.generated.h" + + +/** Enum specifying where and how to run online queries */ +UENUM(BlueprintType) +enum class ECommonUserOnlineContext : uint8 +{ + /** Called from game code, this uses the default system but with special handling that could merge results from multiple contexts */ + Game, + + /** The default engine online system, this will always exist and will be the same as either Service or Platform */ + Default, + + /** Explicitly ask for the external service, which may not exist */ + Service, + + /** Looks for external service first, then falls back to default */ + ServiceOrDefault, + + /** Explicitly ask for the platform system, which may not exist */ + Platform, + + /** Looks for platform system first, then falls back to default */ + PlatformOrDefault, + + /** Invalid system */ + Invalid +}; + +/** Enum describing the state of initialization for a specific user */ +UENUM(BlueprintType) +enum class ECommonUserInitializationState : uint8 +{ + /** User has not started login process */ + Unknown, + + /** Player is in the process of acquiring a user id with local login */ + DoingInitialLogin, + + /** Player is performing the network login, they have already logged in locally */ + DoingNetworkLogin, + + /** Player failed to log in at all */ + FailedtoLogin, + + + /** Player is logged in and has access to online functionality */ + LoggedInOnline, + + /** Player is logged in locally (either guest or real user), but cannot perform online actions */ + LoggedInLocalOnly, + + + /** Invalid state or user */ + Invalid, +}; + +/** Enum specifying different privileges and capabilities available to a user */ +UENUM(BlueprintType) +enum class ECommonUserPrivilege : uint8 +{ + /** Whether the user can play at all, online or offline */ + CanPlay, + + /** Whether the user can play in online modes */ + CanPlayOnline, + + /** Whether the user can use text chat */ + CanCommunicateViaTextOnline, + + /** Whether the user can use voice chat */ + CanCommunicateViaVoiceOnline, + + /** Whether the user can access content generated by other users */ + CanUseUserGeneratedContent, + + /** Whether the user can ever participate in cross-play */ + CanUseCrossPlay, + + /** Invalid privilege (also the count of valid ones) */ + Invalid_Count UMETA(Hidden) +}; + +/** Enum specifying the general availability of a feature or privilege, this combines information from multiple sources */ +UENUM(BlueprintType) +enum class ECommonUserAvailability : uint8 +{ + /** State is completely unknown and needs to be queried */ + Unknown, + + /** This feature is fully available for use right now */ + NowAvailable, + + /** This might be available after the completion of normal login procedures */ + PossiblyAvailable, + + /** This feature is not available now because of something like network connectivity but may be available in the future */ + CurrentlyUnavailable, + + /** This feature will never be available for the rest of this session due to hard account or platform restrictions */ + AlwaysUnavailable, + + /** Invalid feature */ + Invalid, +}; + +/** Enum giving specific reasons why a user may or may not use a certain privilege */ +UENUM(BlueprintType) +enum class ECommonUserPrivilegeResult : uint8 +{ + /** State is unknown and needs to be queried */ + Unknown, + + /** This privilege is fully available for use */ + Available, + + /** User has not fully logged in */ + UserNotLoggedIn, + + /** User does not own the game or content */ + LicenseInvalid, + + /** The game needs to be updated or patched before this will be available */ + VersionOutdated, + + /** No network connection, this may be resolved by reconnecting */ + NetworkConnectionUnavailable, + + /** Parental control failure */ + AgeRestricted, + + /** Account does not have a required subscription or account type */ + AccountTypeRestricted, + + /** Another account/user restriction such as being banned by the service */ + AccountUseRestricted, + + /** Other platform-specific failure */ + PlatformFailure, +}; + +/** Used to track the progress of different asynchronous operations */ +enum class ECommonUserAsyncTaskState : uint8 +{ + /** The task has not been started */ + NotStarted, + /** The task is currently being processed */ + InProgress, + /** The task has completed successfully */ + Done, + /** The task failed to complete */ + Failed +}; + +/** Detailed information about the online error. Effectively a wrapper for FOnlineError. */ +USTRUCT(BlueprintType) +struct FOnlineResultInformation +{ + GENERATED_BODY() + + /** Whether the operation was successful or not. If it was successful, the error fields of this struct will not contain extra information. */ + UPROPERTY(BlueprintReadOnly) + bool bWasSuccessful = true; + + /** The unique error id. Can be used to compare against specific handled errors. */ + UPROPERTY(BlueprintReadOnly) + FString ErrorId; + + /** Error text to display to the user. */ + UPROPERTY(BlueprintReadOnly) + FText ErrorText; + + /** + * Initialize this from an FOnlineErrorType + * @param InOnlineError the online error to initialize from + */ + void COMMONUSER_API FromOnlineError(const FOnlineErrorType& InOnlineError); +}; diff --git a/Source/ols/Private/DataAssets/OLSExperienceActionSetDataAsset.cpp b/Source/ols/Private/DataAssets/OLSExperienceActionSetDataAsset.cpp index 4960b86..0a5b3d2 100644 --- a/Source/ols/Private/DataAssets/OLSExperienceActionSetDataAsset.cpp +++ b/Source/ols/Private/DataAssets/OLSExperienceActionSetDataAsset.cpp @@ -9,7 +9,7 @@ #include "GameFeatureAction.h" -#include UE_INLINE_GENERATED_CPP_BY_NAME(OLSExperienceActionSet) +#include UE_INLINE_GENERATED_CPP_BY_NAME(OLSExperienceActionSetDataAsset) #define LOCTEXT_NAMESPACE "OLSSystem" diff --git a/Source/ols/Private/DataAssets/OLSExperienceDefinitionDataAsset.cpp b/Source/ols/Private/DataAssets/OLSExperienceDefinitionDataAsset.cpp index 20e8121..ec357c9 100644 --- a/Source/ols/Private/DataAssets/OLSExperienceDefinitionDataAsset.cpp +++ b/Source/ols/Private/DataAssets/OLSExperienceDefinitionDataAsset.cpp @@ -8,7 +8,7 @@ #include "Misc/DataValidation.h" #endif -#include UE_INLINE_GENERATED_CPP_BY_NAME(OLSExperienceDefinitionPrimaryDataAsset) +#include UE_INLINE_GENERATED_CPP_BY_NAME(OLSExperienceDefinitionDataAsset) #define LOCTEXT_NAMESPACE "OLSSystem" diff --git a/Source/ols/Private/GameModes/OLSExperienceManagerComponent.cpp b/Source/ols/Private/GameModes/OLSExperienceManagerComponent.cpp index b1bf6a9..3993e1a 100644 --- a/Source/ols/Private/GameModes/OLSExperienceManagerComponent.cpp +++ b/Source/ols/Private/GameModes/OLSExperienceManagerComponent.cpp @@ -165,7 +165,7 @@ bool UOLSExperienceManagerComponent::IsExperienceLoaded() const } void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded_HighPriority( - FOnOLSExperienceLoaded::FDelegate&& delegate) + FOLSExperienceLoadedNativeDelegate::FDelegate&& delegate) { if (IsExperienceLoaded()) { @@ -177,7 +177,7 @@ void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded_HighPrior } } -void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded(FOnOLSExperienceLoaded::FDelegate&& delegate) +void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded(FOLSExperienceLoadedNativeDelegate::FDelegate&& delegate) { if (IsExperienceLoaded()) { @@ -190,7 +190,7 @@ void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded(FOnOLSExp } void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded_LowPriority( - FOnOLSExperienceLoaded::FDelegate&& delegate) + FOLSExperienceLoadedNativeDelegate::FDelegate&& delegate) { if (IsExperienceLoaded()) { diff --git a/Source/ols/Private/GameModes/OLSGameMode.cpp b/Source/ols/Private/GameModes/OLSGameMode.cpp new file mode 100644 index 0000000..e5a2ea9 --- /dev/null +++ b/Source/ols/Private/GameModes/OLSGameMode.cpp @@ -0,0 +1,529 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + + +#include "GameModes/OLSGameMode.h" + +#include "CommonUserSubsystem.h" +#include "GameMapsSettings.h" +#include "DataAssets/OLSExperienceDefinitionDataAsset.h" +#include "DataAssets/OLSPawnDataAsset.h" +#include "GameModes/OLSExperienceManagerComponent.h" +#include "Kismet/GameplayStatics.h" +#include "Player/OLSPlayerState.h" +#include "Systems/OLSAssetManager.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(OLSGameMode) + +// AOLSGameMode::AOLSGameMode(const FObjectInitializer& objectInitializer) : Super(objectInitializer) +// { +// // @TODO: Implement this. +// // GameStateClass = ALyraGameState::StaticClass(); +// // GameSessionClass = ALyraGameSession::StaticClass(); +// // PlayerControllerClass = ALyraPlayerController::StaticClass(); +// // ReplaySpectatorPlayerControllerClass = ALyraReplayPlayerController::StaticClass(); +// // PlayerStateClass = ALyraPlayerState::StaticClass(); +// // DefaultPawnClass = ALyraCharacter::StaticClass(); +// // HUDClass = ALyraHUD::StaticClass(); +// } + +const UOLSPawnDataAsset* AOLSGameMode::GetPawnDataForController(const AController* controller) const +{ + // See if pawn data is already set on the player state + if (controller) + { + if (const AOLSPlayerState* playerState = controller->GetPlayerState()) + { + if (const UOLSPawnDataAsset* pawnData = playerState->GetPawnData()) + { + return pawnData; + } + } + } + + // If not, fall back to the the default for the current experience + check(GameState); + UOLSExperienceManagerComponent* experienceComponent = GameState->FindComponentByClass(); + check(experienceComponent); + + if (experienceComponent->IsExperienceLoaded()) + { + const UOLSExperienceDefinitionDataAsset* experienceDataAsset = experienceComponent->GetCurrentExperienceChecked(); + if (experienceDataAsset->DefaultPawnData) + { + return experienceDataAsset->DefaultPawnData; + } + + // Experience is loaded and there's still no pawn data, fall back to the default for now + return UOLSAssetManager::Get().GetDefaultPawnData(); + } + + // Experience not loaded yet, so there is no pawn data to be had + return nullptr; +} + +void AOLSGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) +{ + Super::InitGame(MapName, Options, ErrorMessage); + + // Wait for the next frame to give time to initialize startup settings + GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::HandleMatchAssignmentIfNotExpectingOne); +} + +UClass* AOLSGameMode::GetDefaultPawnClassForController_Implementation(AController* controller) +{ + if (const UOLSPawnDataAsset* pawnData = GetPawnDataForController(controller)) + { + if (pawnData->PawnClass) + { + return pawnData->PawnClass; + } + } + + return Super::GetDefaultPawnClassForController_Implementation(controller); +} + +APawn* AOLSGameMode::SpawnDefaultPawnAtTransform_Implementation( + AController* newPlayer, + const FTransform& spawnTransform) +{ + FActorSpawnParameters spawnInfo; + spawnInfo.Instigator = GetInstigator(); + spawnInfo.ObjectFlags |= RF_Transient; // Never save the default player pawns into a map. + spawnInfo.bDeferConstruction = true; + + if (UClass* pawnClass = GetDefaultPawnClassForController(newPlayer)) + { + if (APawn* spawnedPawn = GetWorld()->SpawnActor(pawnClass, spawnTransform, spawnInfo)) + { + // @TODO: Implement this as well as implement UOLSawnExtensionComponent + // if (UOLSawnExtensionComponent* PawnExtComp = UOLSawnExtensionComponent::FindPawnExtensionComponent(spawnedPawn)) + // { + // if (const ULyraPawnData* PawnData = GetPawnDataForController(newPlayer)) + // { + // PawnExtComp->SetPawnData(PawnData); + // } + // else + // { // @TODO: Replace this with out custom log. + // UE_LOG(LogLyra, Error, TEXT("Game mode was unable to set PawnData on the spawned pawn [%s]."), *GetNameSafe(spawnedPawn } + // } + + spawnedPawn->FinishSpawning(spawnTransform); + + return spawnedPawn; + } + else + { + // @TODO: Replace this with out custom log. + // UE_LOG(LogLyra, Error, TEXT("Game mode was unable to spawn Pawn of class [%s] at [%s]."), *GetNameSafe(pawnClass), *spawnTransform.ToHumanReadableString()); + } + } + else + { + // @TODO: Replace this with out custom log. + // UE_LOG(LogLyra, Error, TEXT("Game mode was unable to spawn Pawn due to NULL pawn class.")); + } + + return nullptr; +} + +bool AOLSGameMode::ShouldSpawnAtStartSpot(AController* Player) +{ + // We never want to use the start spot, always use the spawn management component. + return false; +} + +void AOLSGameMode::HandleStartingNewPlayer_Implementation(APlayerController* newPlayer) +{ + // Delay starting new players until the experience has been loaded + // (players who log in prior to that will be started by OnExperienceLoaded) + if (IsExperienceLoaded()) + { + Super::HandleStartingNewPlayer_Implementation(newPlayer); + } +} + +AActor* AOLSGameMode::ChoosePlayerStart_Implementation(AController* player) +{ + // @TODO: Implement this as well as implement UOLSPlayerSpawningManagerComponent + // if (UOLSPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass()) + // { + // return PlayerSpawningComponent->ChoosePlayerStart(player); + // } + + return Super::ChoosePlayerStart_Implementation(player); +} + +void AOLSGameMode::FinishRestartPlayer(AController* newPlayer, const FRotator& startRotation) +{ + // @TODO: Implement this as well as implement UOLSPlayerSpawningManagerComponent + // if (UOLSPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass()) + // { + // PlayerSpawningComponent->FinishRestartPlayer(NewPlayer, StartRotation); + // } + + Super::FinishRestartPlayer(newPlayer, startRotation); +} + +bool AOLSGameMode::PlayerCanRestart_Implementation(APlayerController* player) +{ + return ControllerCanRestart(player); +} + +void AOLSGameMode::InitGameState() +{ + Super::InitGameState(); + + // Listen for the experience load to complete + UOLSExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass(); + check(ExperienceComponent); + ExperienceComponent->CallOrRegister_OnExperienceLoaded(FOLSExperienceLoadedNativeDelegate::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded)); + +} + +bool AOLSGameMode::UpdatePlayerStartSpot(AController* player, const FString& portal, FString& outErrorMessage) +{ + // Do nothing, we'll wait until PostLogin when we try to spawn the player for real. + // Doing anything right now is no good, systems like team assignment haven't even occurred yet. + return true; +} + +void AOLSGameMode::GenericPlayerInitialization(AController* newPlayer) +{ + Super::GenericPlayerInitialization(newPlayer); + + OnGameModePlayerInitialized.Broadcast(this, newPlayer); +} + +void AOLSGameMode::FailedToRestartPlayer(AController* newPlayer) +{ + Super::FailedToRestartPlayer(newPlayer); + + // If we tried to spawn a pawn and it failed, lets try again *note* check if there's actually a pawn class + // before we try this forever. + if (UClass* PawnClass = GetDefaultPawnClassForController(newPlayer)) + { + if (APlayerController* newPC = Cast(newPlayer)) + { + // If it's a player don't loop forever, maybe something changed and they can no longer restart if so stop trying. + if (PlayerCanRestart(newPC)) + { + RequestPlayerRestartNextFrame(newPlayer, false); + } + else + { + // @TODO: Replace this with our custom log. + // UE_LOG(LogLyra, Verbose, TEXT("FailedToRestartPlayer(%s) and PlayerCanRestart returned false, so we're not going to try again."), *GetPathNameSafe(newPlayer)); + } + } + else + { + RequestPlayerRestartNextFrame(newPlayer, false); + } + } + else + { + // @TODO: Replace this with our custom log. + // UE_LOG(LogLyra, Verbose, TEXT("FailedToRestartPlayer(%s) but there's no pawn class so giving up."), *GetPathNameSafe(newPlayer)); + } +} + +void AOLSGameMode::RequestPlayerRestartNextFrame(AController* controller, bool shouldForceReset) +{ + if (shouldForceReset && controller) + { + controller->Reset(); + } + + if (APlayerController* playerController = Cast(controller)) + { + GetWorldTimerManager().SetTimerForNextTick(playerController, &APlayerController::ServerRestartPlayer_Implementation); + } + // else if (AOLSPlayerBotController* BotController = Cast(controller)) + // { + // GetWorldTimerManager().SetTimerForNextTick(BotController, &ALyraPlayerBotController::ServerRestartController); + // } +} + +bool AOLSGameMode::ControllerCanRestart(AController* controller) +{ + if (APlayerController* playerController = Cast(controller)) + { + if (!Super::PlayerCanRestart_Implementation(playerController)) + { + return false; + } + } + else + { + // Bot version of Super::PlayerCanRestart_Implementation + if ((controller == nullptr) || controller->IsPendingKillPending()) + { + return false; + } + } + + // @TODO: Implement this after having PlayerSpawningManagerComponent + // if (ULyraPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass()) + // { + // return PlayerSpawningComponent->ControllerCanRestart(controller); + // } + + return true; +} + +void AOLSGameMode::OnExperienceLoaded(const UOLSExperienceDefinitionDataAsset* currentExperience) +{ + // Spawn any players that are already attached + //@TODO: Here we're handling only *player* controllers, but in GetDefaultPawnClassForController_Implementation we skipped all controllers + // GetDefaultPawnClassForController_Implementation might only be getting called for players anyways + for (FConstPlayerControllerIterator iterator = GetWorld()->GetPlayerControllerIterator(); iterator; ++iterator) + { + APlayerController* playerController = Cast(*iterator); + if (playerController && playerController->GetPawn()) + { + if (PlayerCanRestart(playerController)) + { + RestartPlayer(playerController); + } + } + } +} + +bool AOLSGameMode::IsExperienceLoaded() const +{ + check(GameState); + UOLSExperienceManagerComponent* experienceComponent = GameState->FindComponentByClass< + UOLSExperienceManagerComponent>(); + check(experienceComponent); + + return experienceComponent->IsExperienceLoaded(); +} + +void AOLSGameMode::OnMatchAssignmentGiven(FPrimaryAssetId experienceId, const FString& experienceIdSource) +{ + if (experienceId.IsValid()) + { + // @TODO: Replace this with our custom. + // UE_LOG(LogLyraExperience, Log, TEXT("Identified experience %s (Source: %s)"), *ExperienceId.ToString(), *ExperienceIdSource); + + UOLSExperienceManagerComponent* experienceComponent = GameState->FindComponentByClass(); + check(experienceComponent); + experienceComponent->SetCurrentExperience(experienceId); + } + else + { + // @TODO: Replace this with our custom. + // UE_LOG(LogLyraExperience, Error, TEXT("Failed to identify experience, loading screen will stay up forever")); + } +} + +void AOLSGameMode::HandleMatchAssignmentIfNotExpectingOne() +{ + FPrimaryAssetId experienceId; + FString experienceIdSource; + + // Precedence order (highest wins) + // - Matchmaking assignment (if present) + // - URL Options override + // - Developer Settings (PIE only) + // - Command Line override + // - World Settings + // - Dedicated server + // - Default experience + + UWorld* World = GetWorld(); + + if (!experienceId.IsValid() && UGameplayStatics::HasOption(OptionsString, TEXT("Experience"))) + { + const FString ExperienceFromOptions = UGameplayStatics::ParseOption(OptionsString, TEXT("Experience")); + experienceId = FPrimaryAssetId(FPrimaryAssetType(UOLSExperienceDefinitionDataAsset::StaticClass()->GetFName()), FName(*ExperienceFromOptions)); + experienceIdSource = TEXT("OptionsString"); + } + + if (!experienceId.IsValid() && World->IsPlayInEditor()) + { + // @TODO: Implement this when UOLSDeveloperSettings is implemented. + // experienceId = GetDefault()->ExperienceOverride; + experienceIdSource = TEXT("DeveloperSettings"); + } + + // see if the command line wants to set the experience + if (!experienceId.IsValid()) + { + FString ExperienceFromCommandLine; + if (FParse::Value(FCommandLine::Get(), TEXT("Experience="), ExperienceFromCommandLine)) + { + experienceId = FPrimaryAssetId::ParseTypeAndName(ExperienceFromCommandLine); + if (!experienceId.PrimaryAssetType.IsValid()) + { + experienceId = FPrimaryAssetId(FPrimaryAssetType(UOLSExperienceDefinitionDataAsset::StaticClass()->GetFName()), FName(*ExperienceFromCommandLine)); + } + experienceIdSource = TEXT("CommandLine"); + } + } + + // see if the world settings has a default experience + if (!experienceId.IsValid()) + { + // @TODO: Implement this when AOLSWorldSettings is implemented. + // if (AOLSWorldSettings* TypedWorldSettings = Cast(GetWorldSettings())) + // { + // experienceId = TypedWorldSettings->GetDefaultGameplayExperience(); + // experienceIdSource = TEXT("WorldSettings"); + // } + } + + UOLSAssetManager& assetManager = UOLSAssetManager::Get(); + FAssetData dummy; + if (experienceId.IsValid() && !assetManager.GetPrimaryAssetData(experienceId, /*out*/ dummy)) + { + // @TODO: Replace this with our custom. + // UE_LOG(LogLyraExperience, Error, TEXT("EXPERIENCE: Wanted to use %s but couldn't find it, falling back to the default)"), *experienceIding()); + experienceId = FPrimaryAssetId(); + } + + // Final fallback to the default experience + if (!experienceId.IsValid()) + { + if (TryDedicatedServerLogin()) + { + // This will start to host as a dedicated server + return; + } + + //@TODO: Pull this from a config setting or something + experienceId = FPrimaryAssetId(FPrimaryAssetType("LyraExperienceDefinition"), FName("B_LyraDefaultExperience")); + experienceIdSource = TEXT("Default"); + } + + OnMatchAssignmentGiven(experienceId, experienceIdSource); +} + +bool AOLSGameMode::TryDedicatedServerLogin() +{ + // Some basic code to register as an active dedicated server, this would be heavily modified by the game + // FString defaultMap = UGameMapsSettings::GetGameDefaultMap(); + // UWorld* world = GetWorld(); + // UGameInstance* gameInstance = GetGameInstance(); + // if (gameInstance && world && world->GetNetMode() == NM_DedicatedServer && world->URL.Map == defaultMap) + // { + // // Only register if this is the default map on a dedicated server + // UCommonUserSubsystem* userSubsystem = gameInstance->GetSubsystem(); + // + // // Dedicated servers may need to do an online login + // userSubsystem->OnUserInitializeComplete.AddDynamic(this, &ThisClass::OnUserInitializedForDedicatedServer); + // + // // There are no local users on dedicated server, but index 0 means the default platform user which is handled by the online login code + // if (!userSubsystem->TryToLoginForOnlinePlay(0)) + // { + // OnUserInitializedForDedicatedServer(nullptr, false, FText(), ECommonUserPrivilege::CanPlayOnline, ECommonUserOnlineContext::Default); + // } + // + // return true; + // } + + return false; +} + +void AOLSGameMode::HostDedicatedServerMatch(ECommonSessionOnlineMode onlineMode) +{ + // FPrimaryAssetType UserExperienceType = ULyraUserFacingExperienceDefinition::StaticClass()->GetFName(); + // + // // Figure out what UserFacingExperience to load + // FPrimaryAssetId UserExperienceId; + // FString UserExperienceFromCommandLine; + // if (FParse::Value(FCommandLine::Get(), TEXT("UserExperience="), UserExperienceFromCommandLine) || + // FParse::Value(FCommandLine::Get(), TEXT("Playlist="), UserExperienceFromCommandLine)) + // { + // UserExperienceId = FPrimaryAssetId::ParseTypeAndName(UserExperienceFromCommandLine); + // if (!UserExperienceId.PrimaryAssetType.IsValid()) + // { + // UserExperienceId = FPrimaryAssetId(FPrimaryAssetType(UserExperienceType), FName(*UserExperienceFromCommandLine)); + // } + // } + // + // // Search for the matching experience, it's fine to force load them because we're in dedicated server startup + // ULyraAssetManager& AssetManager = ULyraAssetManager::Get(); + // TSharedPtr Handle = AssetManager.LoadPrimaryAssetsWithType(UserExperienceType); + // if (ensure(Handle.IsValid())) + // { + // Handle->WaitUntilComplete(); + // } + // + // TArray UserExperiences; + // AssetManager.GetPrimaryAssetObjectList(UserExperienceType, UserExperiences); + // ULyraUserFacingExperienceDefinition* FoundExperience = nullptr; + // ULyraUserFacingExperienceDefinition* DefaultExperience = nullptr; + // + // for (UObject* Object : UserExperiences) + // { + // ULyraUserFacingExperienceDefinition* UserExperience = Cast(Object); + // if (ensure(UserExperience)) + // { + // if (UserExperience->GetPrimaryAssetId() == UserExperienceId) + // { + // FoundExperience = UserExperience; + // break; + // } + // + // if (UserExperience->bIsDefaultExperience && DefaultExperience == nullptr) + // { + // DefaultExperience = UserExperience; + // } + // } + // } + // + // if (FoundExperience == nullptr) + // { + // FoundExperience = DefaultExperience; + // } + // + // UGameInstance* GameInstance = GetGameInstance(); + // if (ensure(FoundExperience && GameInstance)) + // { + // // Actually host the game + // UCommonSession_HostSessionRequest* HostRequest = FoundExperience->CreateHostingRequest(this); + // if (ensure(HostRequest)) + // { + // HostRequest->OnlineMode = OnlineMode; + // + // // TODO override other parameters? + // + // UCommonSessionSubsystem* SessionSubsystem = GameInstance->GetSubsystem(); + // SessionSubsystem->HostSession(nullptr, HostRequest); + // + // // This will handle the map travel + // } + // } +} + +void AOLSGameMode::OnUserInitializedForDedicatedServer( + const UCommonUserInfo* userInfo, + bool isSuccess, + FText error, + ECommonUserPrivilege requestedPrivilege, + ECommonUserOnlineContext onlineContext) +{ + UGameInstance* gameInstance = GetGameInstance(); + if (gameInstance) + { + // Unbind + UCommonUserSubsystem* UserSubsystem = gameInstance->GetSubsystem(); + UserSubsystem->OnUserInitializeComplete.RemoveDynamic(this, &ThisClass::OnUserInitializedForDedicatedServer); + + // Dedicated servers do not require user login, but some online subsystems may expect it + if (isSuccess && ensure(userInfo)) + { + // @TODO: replace this with our custom. + // UE_LOG(LogLyraExperience, Log, TEXT("Dedicated server user login succeeded for id %s, starting online server"), *UserInfo->GetNetId().ToString()); + } + else + { + // @TODO: replace this with our custom. + // UE_LOG(LogLyraExperience, Log, TEXT("Dedicated server user login unsuccessful, starting online server as login is not required")); + } + + HostDedicatedServerMatch(ECommonSessionOnlineMode::Online); + } +} diff --git a/Source/ols/Private/Player/OLSPlayerState.cpp b/Source/ols/Private/Player/OLSPlayerState.cpp index 999964f..01f078e 100644 --- a/Source/ols/Private/Player/OLSPlayerState.cpp +++ b/Source/ols/Private/Player/OLSPlayerState.cpp @@ -6,6 +6,8 @@ #include "AbilitySystem/OLSAbilitySystemComponent.h" #include "Components/GameFrameworkComponentManager.h" #include "DataAssets/OLSAbilitySetPrimaryDataAsset.h" +#include "GameModes/OLSExperienceManagerComponent.h" +#include "GameModes/OLSGameMode.h" #include "Net/UnrealNetwork.h" #include "Player/OLSPlayerController.h" @@ -18,12 +20,6 @@ AOLSPlayerState::AOLSPlayerState(const FObjectInitializer& objectInitializer) : // Create attribute sets here. } -template -const T* AOLSPlayerState::GetPawnData() const -{ - return Cast(PawnData); -} - void AOLSPlayerState::SetPawnData(const UOLSPawnDataAsset* pawnData) { check(pawnData); @@ -55,6 +51,42 @@ void AOLSPlayerState::SetPawnData(const UOLSPawnDataAsset* pawnData) ForceNetUpdate(); } +void AOLSPlayerState::AddStatTagStack(FGameplayTag tag, int32 stackCount) +{ + StatTags.AddStack(tag, stackCount); +} + +void AOLSPlayerState::RemoveStatTagStack(FGameplayTag tag, int32 stackCount) +{ + StatTags.RemoveStack(tag, stackCount); +} + +int32 AOLSPlayerState::GetStatTagStackCount(FGameplayTag tag) const +{ + return StatTags.GetStackCount(tag); +} + +bool AOLSPlayerState::HasStatTag(FGameplayTag tag) const +{ + return StatTags.ContainsTag(tag); +} + +void AOLSPlayerState::OnExperienceLoaded(const UOLSExperienceDefinitionDataAsset* currentExperience) +{ + if (AOLSGameMode* gameMode = GetWorld()->GetAuthGameMode()) + { + if (const UOLSPawnDataAsset* newPawnData = gameMode->GetPawnDataForController(GetOwningController())) + { + SetPawnData(newPawnData); + } + else + { + // @T ODO: Replace this with our custom. + // UE_LOG(LogLyra, Error, TEXT("ALyraPlayerState::OnExperienceLoaded(): Unable to find PawnData to initialize player state [%s]!"), *GetNameSafe(this)); + } + } +} + void AOLSPlayerState::OnRep_PawnData() { } @@ -71,9 +103,9 @@ void AOLSPlayerState::PostInitializeComponents() { const TObjectPtr gameState = GetWorld()->GetGameState(); check(gameState); - // ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass(); - // check(ExperienceComponent); - // ExperienceComponent->CallOrRegister_OnExperienceLoaded(FOnLyraExperienceLoaded::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded)); + UOLSExperienceManagerComponent* ExperienceComponent = gameState->FindComponentByClass(); + check(ExperienceComponent); + ExperienceComponent->CallOrRegister_OnExperienceLoaded(FOLSExperienceLoadedNativeDelegate::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded)); } } diff --git a/Source/ols/Private/Systems/OLSAssetManager.cpp b/Source/ols/Private/Systems/OLSAssetManager.cpp index 7df5746..5f49b4a 100644 --- a/Source/ols/Private/Systems/OLSAssetManager.cpp +++ b/Source/ols/Private/Systems/OLSAssetManager.cpp @@ -7,7 +7,7 @@ #include "Misc/ScopedSlowTask.h" #include "Engine/Engine.h" #include "Systems/OLSAssetManagerStartupJob.h" -#include "DataAssets/OLSPawnPrimaryDataAsset.h" +#include "DataAssets/OLSPawnDataAsset.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(OLSAssetManager) @@ -105,7 +105,7 @@ void UOLSAssetManager::DumpLoadedAssets() { } -const UOLSPawnPrimaryDataAsset* UOLSAssetManager::GetDefaultPawnData() const +const UOLSPawnDataAsset* UOLSAssetManager::GetDefaultPawnData() const { return GetAsset(DefaultPawnData); } diff --git a/Source/ols/Private/Systems/OLSGameplayTagStack.cpp b/Source/ols/Private/Systems/OLSGameplayTagStack.cpp new file mode 100644 index 0000000..9df2146 --- /dev/null +++ b/Source/ols/Private/Systems/OLSGameplayTagStack.cpp @@ -0,0 +1,104 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + + +#include "Systems/OLSGameplayTagStack.h" +////////////////////////////////////////////////////////////////////// +// FGameplayTagStack + +FString FOLSGameplayTagStack::GetDebugString() const +{ + return FString::Printf(TEXT("%sx%d"), *Tag.ToString(), StackCount); +} + +////////////////////////////////////////////////////////////////////// +// FGameplayTagStackContainer + +void FOLSGameplayTagStackContainer::AddStack(FGameplayTag tag, int32 stackCount) +{ + if (!tag.IsValid()) + { + FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to AddStack"), ELogVerbosity::Warning); + return; + } + + if (stackCount > 0) + { + for (FOLSGameplayTagStack& stack : Stacks) + { + if (stack.Tag == tag) + { + const int32 newCount = stack.StackCount + stackCount; + stack.StackCount = newCount; + TagToCountMap[tag] = newCount; + MarkItemDirty(stack); + return; + } + } + + FOLSGameplayTagStack& newStack = Stacks.Emplace_GetRef(tag, stackCount); + MarkItemDirty(newStack); + TagToCountMap.Add(tag, stackCount); + } +} + +void FOLSGameplayTagStackContainer::RemoveStack(FGameplayTag tag, int32 stackCount) +{ + if (!tag.IsValid()) + { + FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to RemoveStack"), ELogVerbosity::Warning); + return; + } + + //@TODO: Should we error if you try to remove a stack that doesn't exist or has a smaller count? + if (stackCount > 0) + { + for (auto it = Stacks.CreateIterator(); it; ++it) + { + FOLSGameplayTagStack& stack = *it; + if (stack.Tag == tag) + { + if (stack.StackCount <= stackCount) + { + it.RemoveCurrent(); + TagToCountMap.Remove(tag); + MarkArrayDirty(); + } + else + { + const int32 newCount = stack.StackCount - stackCount; + stack.StackCount = newCount; + TagToCountMap[tag] = newCount; + MarkItemDirty(stack); + } + return; + } + } + } +} + +void FOLSGameplayTagStackContainer::PreReplicatedRemove(const TArrayView removedIndices, int32 finalSize) +{ + for (int32 index : removedIndices) + { + const FGameplayTag tag = Stacks[index].Tag; + TagToCountMap.Remove(tag); + } +} + +void FOLSGameplayTagStackContainer::PostReplicatedAdd(const TArrayView addedIndices, int32 finalSize) +{ + for (int32 index : addedIndices) + { + const FOLSGameplayTagStack& stack = Stacks[index]; + TagToCountMap.Add(stack.Tag, stack.StackCount); + } +} + +void FOLSGameplayTagStackContainer::PostReplicatedChange(const TArrayView changedIndices, int32 finalSize) +{ + for (int32 index : changedIndices) + { + const FOLSGameplayTagStack& stack = Stacks[index]; + TagToCountMap[stack.Tag] = stack.StackCount; + } +} diff --git a/Source/ols/Public/GameModes/OLSExperienceManagerComponent.h b/Source/ols/Public/GameModes/OLSExperienceManagerComponent.h index 1cc3ebf..4982e83 100644 --- a/Source/ols/Public/GameModes/OLSExperienceManagerComponent.h +++ b/Source/ols/Public/GameModes/OLSExperienceManagerComponent.h @@ -9,7 +9,7 @@ namespace UE::GameFeatures { struct FResult; } -DECLARE_MULTICAST_DELEGATE_OneParam(FOnOLSExperienceLoaded, const class UOLSExperienceDefinitionDataAsset* /*experience*/); +DECLARE_MULTICAST_DELEGATE_OneParam(FOLSExperienceLoadedNativeDelegate, const class UOLSExperienceDefinitionDataAsset* /*experience*/); enum class EOLSExperienceLoadState { @@ -55,15 +55,15 @@ public: // Ensures the delegate is called once the experience has been loaded, // before others are called. // However, if the experience has already loaded, calls the delegate immediately. - void CallOrRegister_OnExperienceLoaded_HighPriority(FOnOLSExperienceLoaded::FDelegate&& delegate); + void CallOrRegister_OnExperienceLoaded_HighPriority(FOLSExperienceLoadedNativeDelegate::FDelegate&& delegate); // Ensures the delegate is called once the experience has been loaded // If the experience has already loaded, calls the delegate immediately - void CallOrRegister_OnExperienceLoaded(FOnOLSExperienceLoaded::FDelegate&& delegate); + void CallOrRegister_OnExperienceLoaded(FOLSExperienceLoadedNativeDelegate::FDelegate&& delegate); // Ensures the delegate is called once the experience has been loaded // If the experience has already loaded, calls the delegate immediately - void CallOrRegister_OnExperienceLoaded_LowPriority(FOnOLSExperienceLoaded::FDelegate&& delegate); + void CallOrRegister_OnExperienceLoaded_LowPriority(FOLSExperienceLoadedNativeDelegate::FDelegate&& delegate); private: @@ -99,11 +99,11 @@ private: * Delegate called when the experience has finished loading just before others * (e.g., subsystems that set up for regular gameplay) */ - FOnOLSExperienceLoaded OnExperienceLoaded_HighPriority; + FOLSExperienceLoadedNativeDelegate OnExperienceLoaded_HighPriority; /** Delegate called when the experience has finished loading */ - FOnOLSExperienceLoaded OnExperienceLoaded; + FOLSExperienceLoadedNativeDelegate OnExperienceLoaded; /** Delegate called when the experience has finished loading */ - FOnOLSExperienceLoaded OnExperienceLoaded_LowPriority; + FOLSExperienceLoadedNativeDelegate OnExperienceLoaded_LowPriority; }; diff --git a/Source/ols/Public/GameModes/OLSGameMode.h b/Source/ols/Public/GameModes/OLSGameMode.h new file mode 100644 index 0000000..7ef23bf --- /dev/null +++ b/Source/ols/Public/GameModes/OLSGameMode.h @@ -0,0 +1,77 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonSessionSubsystem.h" +#include "ModularGameplayActors/OLSModularGameMode.h" +#include "OLSGameMode.generated.h" + +/** + * Post login event, triggered when a player or bot joins the game as well as after seamless and non seamless travel + * + * This is called after the player has finished initialization + */ +DECLARE_MULTICAST_DELEGATE_TwoParams(FOLSGameModePlayerInitializeNativeDelegate, class AGameModeBase* /*GameMode*/, + class AController* /*NewPlayer*/); + + +/** + * AOLSGameMode + * + * The base game mode class used by this project. + */ +UCLASS(Config = Game, Meta = (ShortTooltip = "The base game mode class used by this project.")) +class OLS_API AOLSGameMode : public AOLSModularGameModeBase +{ + GENERATED_BODY() + +public: + + // AOLSGameMode(const FObjectInitializer& objectInitializer); + + UFUNCTION(BlueprintCallable, Category = "OLS|Pawn") + const class UOLSPawnDataAsset* GetPawnDataForController(const class AController* controller) const; + + //~ Begin AGameModeBase interface + virtual void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override; + virtual UClass* GetDefaultPawnClassForController_Implementation(AController* controller) override; + virtual APawn* SpawnDefaultPawnAtTransform_Implementation(AController* newPlayer, const FTransform& spawnTransform) override; + virtual bool ShouldSpawnAtStartSpot(AController* Player) override; + virtual void HandleStartingNewPlayer_Implementation(APlayerController* newPlayer) override; + virtual AActor* ChoosePlayerStart_Implementation(AController* player) override; + virtual void FinishRestartPlayer(AController* newPlayer, const FRotator& startRotation) override; + virtual bool PlayerCanRestart_Implementation(APlayerController* player) override; + virtual void InitGameState() override; + virtual bool UpdatePlayerStartSpot(AController* player, const FString& portal, FString& outErrorMessage) override; + virtual void GenericPlayerInitialization(AController* newPlayer) override; + virtual void FailedToRestartPlayer(AController* newPlayer) override; + //~ End AGameModeBase interface + + // Restart (respawn) the specified player or bot next frame + // - If bForceReset is true, the controller will be reset this frame (abandoning the currently possessed pawn, if any) + UFUNCTION(BlueprintCallable) + void RequestPlayerRestartNextFrame(AController* controller, bool shouldForceReset = false); + + // Agnostic version of PlayerCanRestart that can be used for both player bots and players + virtual bool ControllerCanRestart(AController* controller); + + // Delegate called on player initialization, described above + FOLSGameModePlayerInitializeNativeDelegate OnGameModePlayerInitialized; + +protected: + + void OnExperienceLoaded(const class UOLSExperienceDefinitionDataAsset* currentExperience); + bool IsExperienceLoaded() const; + + void OnMatchAssignmentGiven(FPrimaryAssetId experienceId, const FString& experienceIdSource); + + void HandleMatchAssignmentIfNotExpectingOne(); + + bool TryDedicatedServerLogin(); + void HostDedicatedServerMatch(ECommonSessionOnlineMode onlineMode); + + UFUNCTION() + void OnUserInitializedForDedicatedServer(const UCommonUserInfo* userInfo, bool isSuccess, FText error, ECommonUserPrivilege requestedPrivilege, ECommonUserOnlineContext onlineContext); + +}; diff --git a/Source/ols/Public/Player/OLSPlayerState.h b/Source/ols/Public/Player/OLSPlayerState.h index 333df02..72b0ef0 100644 --- a/Source/ols/Public/Player/OLSPlayerState.h +++ b/Source/ols/Public/Player/OLSPlayerState.h @@ -4,6 +4,7 @@ #include "CoreMinimal.h" #include "ModularGameplayActors/OLSModularPlayerState.h" +#include "Systems/OLSGameplayTagStack.h" #include "OLSPlayerState.generated.h" @@ -37,10 +38,39 @@ public: class UOLSAbilitySystemComponent* GetOLSAbilitySystemComponent() const; template - const T* GetPawnData() const; + const T* GetPawnData() const + { + return Cast(PawnData); + } void SetPawnData(const class UOLSPawnDataAsset* pawnData); +public: + + ////////////////////////////////////////////////////////////////////// + // Stats + + // Adds a specified number of stacks to the tag (does nothing if StackCount is below 1) + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category= "OLS|Stats") + void AddStatTagStack(FGameplayTag tag, int32 stackCount); + + // Removes a specified number of stacks from the tag (does nothing if StackCount is below 1) + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category= "OLS|Stats") + void RemoveStatTagStack(FGameplayTag tag, int32 stackCount); + + // Returns the stack count of the specified tag (or 0 if the tag is not present) + UFUNCTION(BlueprintCallable, Category= "OLS|Stats") + int32 GetStatTagStackCount(FGameplayTag tag) const; + + // Returns true if there is at least one stack of the specified tag + UFUNCTION(BlueprintCallable, Category= "OLS|Stats") + bool HasStatTag(FGameplayTag tag) const; + +private: + + void OnExperienceLoaded(const class UOLSExperienceDefinitionDataAsset* currentExperience); + + protected: UFUNCTION() @@ -50,4 +80,9 @@ protected: UPROPERTY(ReplicatedUsing = OnRep_PawnData) TObjectPtr PawnData = nullptr; + +private: + + UPROPERTY(Replicated) + FOLSGameplayTagStackContainer StatTags; }; diff --git a/Source/ols/Public/Systems/OLSAssetManager.h b/Source/ols/Public/Systems/OLSAssetManager.h index a02a4cc..27d0d34 100644 --- a/Source/ols/Public/Systems/OLSAssetManager.h +++ b/Source/ols/Public/Systems/OLSAssetManager.h @@ -44,7 +44,7 @@ public: // @Todo implement this function. // const class UOLSPawnPrimaryDataAsset& GetGameData(); - const class UOLSPawnPrimaryDataAsset* GetDefaultPawnData() const; + const class UOLSPawnDataAsset* GetDefaultPawnData() const; protected: @@ -93,7 +93,7 @@ protected: // Pawn data used when spawning player pawns if there isn't one set on the player state. UPROPERTY(Config) - TSoftObjectPtr DefaultPawnData; + TSoftObjectPtr DefaultPawnData; private: diff --git a/Source/ols/Public/Systems/OLSGameplayTagStack.h b/Source/ols/Public/Systems/OLSGameplayTagStack.h new file mode 100644 index 0000000..fb1b65e --- /dev/null +++ b/Source/ols/Public/Systems/OLSGameplayTagStack.h @@ -0,0 +1,97 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + +#pragma once + +#include "GameplayTagContainer.h" +#include "Net/Serialization/FastArraySerializer.h" +#include "OLSGameplayTagStack.generated.h" + +/** + * Represents one stack of a gameplay tag (tag + count) + */ +USTRUCT(BlueprintType) +struct OLS_API FOLSGameplayTagStack : public FFastArraySerializerItem +{ + GENERATED_BODY() + + FOLSGameplayTagStack() + {} + + FOLSGameplayTagStack(FGameplayTag InTag, int32 InStackCount) + : Tag(InTag) + , StackCount(InStackCount) + { + } + + FString GetDebugString() const; + +private: + + friend struct FOLSGameplayTagStackContainer; + + UPROPERTY() + FGameplayTag Tag = FGameplayTag::EmptyTag; + + UPROPERTY() + int32 StackCount = 0; +}; + + +/** Container of gameplay tag stacks */ +USTRUCT(BlueprintType) +struct OLS_API FOLSGameplayTagStackContainer : public FFastArraySerializer +{ + GENERATED_BODY() + + FOLSGameplayTagStackContainer() + // : Owner(nullptr) + { + } + +public: + // Adds a specified number of stacks to the tag (does nothing if StackCount is below 1) + void AddStack(FGameplayTag tag, int32 stackCount); + + // Removes a specified number of stacks from the tag (does nothing if StackCount is below 1) + void RemoveStack(FGameplayTag tag, int32 stackCount); + + // Returns the stack count of the specified tag (or 0 if the tag is not present) + int32 GetStackCount(FGameplayTag Tag) const + { + return TagToCountMap.FindRef(Tag); + } + + // Returns true if there is at least one stack of the specified tag + bool ContainsTag(FGameplayTag Tag) const + { + return TagToCountMap.Contains(Tag); + } + + //~ Begin FFastArraySerializer contract + void PreReplicatedRemove(const TArrayView removedIndices, int32 finalSize); + void PostReplicatedAdd(const TArrayView addedIndices, int32 finalSize); + void PostReplicatedChange(const TArrayView changedIndices, int32 finalSize); + //~ End FFastArraySerializer contract + + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms) + { + return FFastArraySerializer::FastArrayDeltaSerialize(Stacks, DeltaParms, *this); + } + +private: + // Replicated list of gameplay tag stacks + UPROPERTY() + TArray Stacks; + + // Accelerated list of tag stacks for queries + TMap TagToCountMap; +}; + +template<> +struct TStructOpsTypeTraits : public TStructOpsTypeTraitsBase2 +{ + enum + { + WithNetDeltaSerializer = true, + }; +}; diff --git a/Source/ols/ols.Build.cs b/Source/ols/ols.Build.cs index f819267..cd1f7ee 100644 --- a/Source/ols/ols.Build.cs +++ b/Source/ols/ols.Build.cs @@ -19,7 +19,7 @@ public class ols : ModuleRules "GameFeatures", "ModularGameplay", "EnhancedInput", - "OLSAnimation", "AIModule", "CommonLoadingScreen", + "OLSAnimation", "AIModule", "CommonLoadingScreen", "CommonUser" }); PrivateDependencyModuleNames.AddRange(new[]