diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3f9421 --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# Visual Studio 2015 user specific files +.vs/ + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app +*.ipa + +# These project files can be generated by the engine +*.xcodeproj +*.xcworkspace +*.sln +*.suo +*.opensdf +*.sdf +*.VC.db +*.VC.opendb + +# Precompiled Assets +SourceArt/**/*.png +SourceArt/**/*.tga + +# Binary Files +Binaries/* +Plugins/*/Binaries/* + +# Builds +Build/* + +# Whitelist PakBlacklist-.txt files +!Build/*/ +Build/*/** +!Build/*/PakBlacklist*.txt + +# Don't ignore icon files in Build +!Build/**/*.ico + +# Built data for maps +*_BuiltData.uasset + +# Configuration files generated by the Editor +Saved/* + +# Compiled source files for the engine to use +Intermediate/* +Plugins/*/Intermediate/* + +# Cache files for the editor to use +DerivedDataCache/* + +# Packaged builds +Publish/* + +# Local development files +etc/ diff --git a/Config/DefaultCrypto.ini b/Config/DefaultCrypto.ini new file mode 100644 index 0000000..588f3ed --- /dev/null +++ b/Config/DefaultCrypto.ini @@ -0,0 +1,13 @@ + +[/Script/CryptoKeys.CryptoKeysSettings] +EncryptionKey=J9+627U3OIrN4np8Xz68NyGvCuCnYC0tf4oWVI8305Q= +bEncryptPakIniFiles=True +bEncryptPakIndex=True +bEncryptUAssetFiles=True +bEncryptAllAssetFiles=False +SigningPublicExponent=AQAB +SigningModulus=O38jjy2rvIwm6rLkGLvwLm8ocs9BNxCQYIeRGx5QL6XWAcDe8YhEDK2Yu9nZ149YCZv/biYDsKoVsIvgzdFp19mlQ61pdsP8MR0LwzquFJvsKIQW+MHJL5ryg0HYCtBGN43+pMgUOWDVT8HiDNw2h/VfrG5+0ULFWq+lYDNnPA52X8UaMBxcCiFv/xHklITcZIoJEo+OscwQUZnm4FtDGkPaxHQkS9IBUZEnZuLp6tSeUgnR9ezVYzH3ruTZRZH2Bwaq9uo3+JQS1naneZz873iMDsGpV/OTpeEXHKaKNSMagm33hFgk5rpRqhte9PR8l8NLIPS/k/YrfuhyKKeRhCxdd7upIy3BlQPgJW8E2jeo4HwYGtJ+Pjb1sl/1JAqsPcdDjeSE3yrLJNzkkai3b9o7ItijuD+pE1LYXUTkSOI72d9hY1ziXqigxddzTVjqdveLmrdhziGgYWFXw4grulXI1ccVZv+xjv7Pa0oRgeqVHidhJ/Ns7jSNdsqzZzxrkYoEIsmrRsvAh9ZhXTbc7I4/i6Sta+8zYahEFdXg6r7kfGic4OCTPLtDmUKsGWiYVfIaoPCRA8m0Qs0NbQUbvJbAH4HlcbHtC9jhGLl0JOlvvc+12+JDNFGTy9bSr6qcOp56WAr8oAmfWQWhKw4ty/IRw/2tCpB5GF5KJ55K6uE= +SigningPrivateExponent=UVwkZqtrAKT9sXz5d7zw+te3nOoOVihav3PetXD4BgX0fgztR0avSm45Q7OtN7ZhvAy2QnhDgTngSE59gC/ADJLFZ9OGRscnTX4hCPdyFxIP2Zb+rJPP/fV6b7VYzDR19bVb9rR/flcpBt7Fwrsz61v/o8Rp8AB7Z5k0jrbXknveJUx8ZkjcIfBdqKgy2SXGlEXSsk/7HixenBqSUPPRMfbbSxpKpHiLnmHkD+VXvElJzzS1NMECATzEjodb3J8oLEm6k6nL9fWp2pNkEpl0MJcrkUyisq1eMIJA43EYazswRT6N0Jvuu+1ViacPsDtXorQoXlUtcAzO6zERFrJHLC0piPhpRTOY8Wt5/Dm4DuzWuGmkClIlnnyI31JbjR34NP8DvnpOkGi+spFAvlr6QdpgTvkOjyS4N9UUCnt+KuStLNY4JhUlKpve+mq9uM8JbdIPNcc4XWEQfWM3le+XY13TGt2nrvbhK+MweB17a7mvjVko1Qj5TPXMud6E+ODQtsgtkbA8xZozDhnI8DiDArHf3QSiyK/oy/g6S8GPaiKw3+De6pIKZxfLbgvy/nrIdrUYa4iz+jIW62qb3VM7ImiTOD5GyU9Qs4wHjwIjsdhfviunMphkr/RMSkwBCGaFWGOVpjY+rQBkpLHMw2gb52O1zQSmUxlrOGJ83qytg88= +bEnablePakSigning=True + + diff --git a/Config/DefaultDeviceProfiles.ini b/Config/DefaultDeviceProfiles.ini new file mode 100644 index 0000000..4c2527d --- /dev/null +++ b/Config/DefaultDeviceProfiles.ini @@ -0,0 +1,6 @@ +[iPhone5S DeviceProfile] ++CVars=r.MobileContentScaleFactor=2 + +[iPadAir DeviceProfile] ++CVars=r.MobileContentScaleFactor=2 + diff --git a/Config/DefaultEditor.ini b/Config/DefaultEditor.ini new file mode 100644 index 0000000..9632ec3 --- /dev/null +++ b/Config/DefaultEditor.ini @@ -0,0 +1,17 @@ +[AllMaps] ++Map=/Game/Maps/ShooterEntry ++Map=/Game/Maps/Sanctuary ++Map=/Game/Maps/Highrise + +[AlwaysCookMaps] ++Map=/Game/Maps/ShooterEntry + +[UI] ++ContentDirectories=/Game/Sounds/InterfaceAudio + + +[EditoronlyBP] +bAllowClassAndBlueprintPinMatching=true +bReplaceBlueprintWithClass=true +bDontLoadBlueprintOutsideEditor=true +bBlueprintIsNotBlueprintType=true \ No newline at end of file diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini new file mode 100644 index 0000000..ebe3660 --- /dev/null +++ b/Config/DefaultEngine.ini @@ -0,0 +1,356 @@ +[Core.Log] +LogOnline=verbose +LogAnalytics=log + +[Core.System] +HangDuration=25.0 + +[/Script/Engine.Engine] +GameEngine=/Script/ShooterGame.ShooterEngine +NearClipPlane=3.0 +bEnableColorClear=true +LocalPlayerClassName=/Script/ShooterGame.ShooterLocalPlayer +GameUserSettingsClassName=/Script/ShooterGame.ShooterGameUserSettings +GameViewportClientClassName=/Script/ShooterGame.ShooterGameViewportClient +DefaultPhysMaterialName=/Game/Environment/PhysicalMaterials/M_Concrete.M_Concrete ++K2FieldRedirects=(OldFieldName="Pawn.Health",NewFieldName="ShooterCharacter.Health") + ++ActiveClassRedirects=(OldClassName="ShooterGameInfo",NewClassName="/Script/ShooterGame.ShooterGameMode") ++ActiveClassRedirects=(OldClassName="ShooterCamera",NewClassName="/Script/ShooterGame.ShooterPlayerCameraManager") ++ActiveClassRedirects=(OldClassName="SkeletalMeshComponent",OldSubobjName="ShooterPawnMesh0",NewSubobjName="CharacterMesh0") ++ActiveClassRedirects=(OldClassName="BTTask_HasLosTo",NewClassName="/Script/ShooterGame.BTDecorator_HasLoSTo") + +[/Script/Engine.DemoNetDriver] +NetConnectionClassName="/Script/Engine.DemoNetConnection" +DemoSpectatorClass="/Script/Shootergame.ShooterDemoSpectator" + +[/Script/UnrealEd.EditorEngine] +LocalPlayerClassName=/Script/ShooterGame.ShooterLocalPlayer + +[SystemSettings] +TEXTUREGROUP_Character=(MinLODSize=256,MaxLODSize=4096,LODBias=0) +TEXTUREGROUP_CharacterNormalMap=(MinLODSize=256,MaxLODSize=4096,LODBias=0) +TEXTUREGROUP_CharacterSpecular=(MinLODSize=256,MaxLODSize=4096,LODBias=0) +TEXTUREGROUP_Cinematic=(MinLODSize=256,MaxLODSize=4096,LODBias=0) +TEXTUREGROUP_Effects=(MinLODSize=128,MaxLODSize=512,LODBias=1) +TEXTUREGROUP_Lightmap=(MinLODSize=512,MaxLODSize=4096,LODBias=0) +TEXTUREGROUP_Shadowmap=(MinLODSize=512,MaxLODSize=4096,LODBias=0,NumStreamedMips=3) +TEXTUREGROUP_RenderTarget=(MinLODSize=1,MaxLODSize=4096,LODBias=0) +TEXTUREGROUP_Skybox=(MinLODSize=512,MaxLODSize=2048,LODBias=0) +TEXTUREGROUP_UI=(MinLODSize=512,MaxLODSize=1024,LODBias=1) +TEXTUREGROUP_Vehicle=(MinLODSize=512,MaxLODSize=1024,LODBias=0) +TEXTUREGROUP_VehicleNormalMap=(MinLODSize=512,MaxLODSize=1024,LODBias=0) +TEXTUREGROUP_VehicleSpecular=(MinLODSize=512,MaxLODSize=1024,LODBias=0) +TEXTUREGROUP_Weapon=(MinLODSize=256,MaxLODSize=1024,LODBias=0) +TEXTUREGROUP_WeaponNormalMap=(MinLODSize=256,MaxLODSize=1024,LODBias=0) +TEXTUREGROUP_WeaponSpecular=(MinLODSize=256,MaxLODSize=1024,LODBias=0) +TEXTUREGROUP_World=(MinLODSize=256,MaxLODSize=1024,LODBias=1) +TEXTUREGROUP_WorldNormalMap=(MinLODSize=256,MaxLODSize=1024,LODBias=1) +TEXTUREGROUP_WorldSpecular=(MinLODSize=256,MaxLODSize=1024,LODBias=1) +TEXTUREGROUP_MobileFlattened=(MinLODSize=8,MaxLODSize=256,LODBias=0) +r.setres=1280x720f + +[SystemSettingsEditor] +r.setres=1280x1024f + +[OnlineSubsystem] +DefaultPlatformService=Null +PollingIntervalInMs=20 +bHasVoiceEnabled=true + +[OnlineSubsystemSteam] +bEnabled=true +SteamDevAppId=212960 +GameServerQueryPort=27015 +bRelaunchInSteam=false +GameVersion=1.0.0.0 +bVACEnabled=1 +bAllowP2PPacketRelay=true +P2PConnectionTimeout=90 +Achievement_0_Id="ACH_FRAG_SOMEONE" +Achievement_1_Id="ACH_SOME_KILLS" +Achievement_2_Id="ACH_LOTS_KILLS" +Achievement_3_Id="ACH_FINISH_MATCH" +Achievement_4_Id="ACH_LOTS_MATCHES" +Achievement_5_Id="ACH_FIRST_WIN" +Achievement_6_Id="ACH_LOTS_WIN" +Achievement_7_Id="ACH_MANY_WIN" +Achievement_8_Id="ACH_SHOOT_BULLETS" +Achievement_9_Id="ACH_SHOOT_ROCKETS" +Achievement_10_Id="ACH_GOOD_SCORE" +Achievement_11_Id="ACH_GREAT_SCORE" +Achievement_12_Id="ACH_PLAY_SANCTUARY" +Achievement_13_Id="ACH_PLAY_HIGHRISE" +; This is to prevent subsystem from reading other achievements that may be defined in parent .ini +Achievement_14_Id="" + + +[OnlineSubsystemNull] +Achievement_0_Id=ACH_FRAG_SOMEONE +Achievement_0_bIsHidden=false +Achievement_0_Title="Fragged" +Achievement_0_LockedDesc="Frag someone" +Achievement_0_UnlockedDesc="Fragged someone" + +Achievement_1_Id=ACH_SOME_KILLS +Achievement_1_bIsHidden=false +Achievement_1_Title="Some kills" +Achievement_1_LockedDesc="Have some kills" +Achievement_1_UnlockedDesc="Had some kills" + +Achievement_2_Id=ACH_LOTS_KILLS +Achievement_2_bIsHidden=false +Achievement_2_Title="Lots of kills" +Achievement_2_LockedDesc="Have lots of kills" +Achievement_2_UnlockedDesc="Had lots of kills" + +Achievement_3_Id=ACH_FINISH_MATCH +Achievement_3_bIsHidden=false +Achievement_3_Title="Finished match" +Achievement_3_LockedDesc="Finish at least one match" +Achievement_3_UnlockedDesc="Finished at least one match" + +Achievement_4_Id=ACH_LOTS_MATCHES +Achievement_4_bIsHidden=false +Achievement_4_Title="Lots of matches" +Achievement_4_LockedDesc="Play lots of matches" +Achievement_4_UnlockedDesc="Played lots of matches" + +Achievement_5_Id=ACH_FIRST_WIN +Achievement_5_bIsHidden=false +Achievement_5_Title="First win" +Achievement_5_LockedDesc="Have the first win" +Achievement_5_UnlockedDesc="Had the first win" + +Achievement_6_Id=ACH_LOTS_WIN +Achievement_6_bIsHidden=false +Achievement_6_Title="Lots of win" +Achievement_6_LockedDesc="Have lots of win" +Achievement_6_UnlockedDesc="Had lots of win" + +Achievement_7_Id=ACH_MANY_WIN +Achievement_7_bIsHidden=false +Achievement_7_Title="Many win" +Achievement_7_LockedDesc="Have many win" +Achievement_7_UnlockedDesc="Had many win" + +Achievement_8_Id=ACH_SHOOT_BULLETS +Achievement_8_bIsHidden=false +Achievement_8_Title="Shoot bullets" +Achievement_8_LockedDesc="Shoot bullets" +Achievement_8_UnlockedDesc="Shot bullets" + +Achievement_9_Id=ACH_SHOOT_ROCKETS +Achievement_9_bIsHidden=false +Achievement_9_Title="Shoot rockets" +Achievement_9_LockedDesc="Shoot rockets" +Achievement_9_UnlockedDesc="Shot rockets" + +Achievement_10_Id=ACH_GOOD_SCORE +Achievement_10_bIsHidden=false +Achievement_10_Title="Good score" +Achievement_10_LockedDesc="Have a good score" +Achievement_10_UnlockedDesc="Had a good score" + +Achievement_11_Id=ACH_GREAT_SCORE +Achievement_11_bIsHidden=false +Achievement_11_Title="Great score" +Achievement_11_LockedDesc="Have a great score" +Achievement_11_UnlockedDesc="Had a great score" + +Achievement_12_Id=ACH_PLAY_SANCTUARY +Achievement_12_bIsHidden=false +Achievement_12_Title="Play Sanctuary map" +Achievement_12_LockedDesc="Play Sanctuary map" +Achievement_12_UnlockedDesc="Played Sanctuary map" + +Achievement_13_Id=ACH_PLAY_HIGHRISE +Achievement_13_bIsHidden=false +Achievement_13_Title="Play Highrise map" +Achievement_13_LockedDesc="Play Highrise map" +Achievement_13_UnlockedDesc="Played Highrise map" + +; This is to prevent subsystem from reading other achievements that may be defined in parent .ini +Achievement_14_Id="" + +[/Script/OnlineSubsystemSteam.SteamNetDriver] +NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection" +ReplicationDriverClassName="/Script/ShooterGame.ShooterReplicationGraph" +AllowDownloads=false + +[/Script/SteamSockets.SteamSocketsNetDriver] +NetConnectionClassName="/Script/SteamSockets.SteamSocketsNetConnection" +ReplicationDriverClassName="/Script/ShooterGame.ShooterReplicationGraph" +AllowDownloads=false +ConnectionTimeout=80.0 +InitialConnectTimeout=120.0 + +[Kismet] +AllowDerivedBlueprints=true + +[/Script/Engine.CollisionProfile] + +; customized game channel +; if you do this, make sure you define in native for convenience ++DefaultChannelResponses=(Channel=ECC_GameTraceChannel1, Name=Weapon, bTraceType=true) ++DefaultChannelResponses=(Channel=ECC_GameTraceChannel2, Name=Projectile) ++DefaultChannelResponses=(Channel=ECC_GameTraceChannel3, Name=Pickup) + +// customize engine profiles, have custom settings for custom responses +// to check the original set up, check BaseEngine.ini ++EditProfiles=(Name="OverlapAllDynamic",CustomResponses=((Channel=Weapon, Response=ECR_Ignore))) ++EditProfiles=(Name="InvisibleWall",CustomResponses=((Channel=Weapon, Response=ECR_Ignore))) ++EditProfiles=(Name="Trigger",CustomResponses=((Channel=Weapon, Response=ECR_Ignore), (Channel=Projectile, Response=ECR_Ignore))) ++EditProfiles=(Name="Pawn",CustomResponses=((Channel=Projectile, Response=ECR_Overlap),(Channel=Pickup, Response=ECR_Overlap))) + +[BehaviorTreesEd] +BehaviorTreeEditorEnabled=true + +[/Script/NavigationSystem.NavigationSystemV1] ++SupportedAgents=(AgentRadius=35.0,AgentHeight=144.0,Name="Common") + +[/Script/EngineSettings.GameMapsSettings] +EditorStartupMap=/Game/Maps/Highrise +TransitionMap= +GameDefaultMap=/Game/Maps/ShooterEntry +ServerDefaultMap=/Game/Maps/Sanctuary +GlobalDefaultGameMode=/Script/ShooterGame.ShooterGame_FreeForAll +GameInstanceClass=/Script/ShooterGame.ShooterGameInstance ++GameModeClassAliases=(Name="FFA",GameMode="/Script/ShooterGame.ShooterGame_FreeForAll") ++GameModeClassAliases=(Name="TDM",GameMode="/Script/ShooterGame.ShooterGame_TeamDeathMatch") + +[/Script/OnlineSubsystemUtils.IpNetDriver] +InitialConnectTimeout=120.0 + +[/Script/NavigationSystem.RecastNavMesh] +bDrawPolyEdges=False +bDistinctlyDrawTilesBeingBuilt=True +DrawOffset=10.000000 +bFixedTilePoolSize=False +TilePoolSize=1024 +TileSizeUU=1000.000000 +CellSize=19.000000 +CellHeight=10.000000 +AgentRadius=34.000000 +AgentHeight=144.000000 +AgentMaxHeight=160.000000 +AgentMaxSlope=44.000000 +AgentMaxStepHeight=35.000000 +MinRegionArea=0.000000 +MergeRegionSize=400.000000 +MaxSimplificationError=1.300000 +MaxSimultaneousTileGenerationJobsCount=1024 +TileNumberHardLimit=1048576 +DefaultDrawDistance=5000.000000 +DefaultMaxSearchNodes=2048.000000 +DefaultMaxHierarchicalSearchNodes=2048.000000 +RegionPartitioning=Watershed +LayerPartitioning=Watershed +RegionChunkSplits=2 +LayerChunkSplits=2 +bSortNavigationAreasByCost=False +bPerformVoxelFiltering=True +bMarkLowHeightAreas=False +bDoFullyAsyncNavDataGathering=False +bUseBetterOffsetsFromCorners=True +bUseVirtualFilters=True +bUseVoxelCache=False +TileSetUpdateInterval=1.000000 +HeuristicScale=0.999000 +VerticalDeviationFromGroundCompensation=0.000000 +bForceRebuildOnLoad=True + +[/Script/Engine.StreamingSettings] +s.AsyncLoadingThreadEnabled=True +s.EventDrivenLoaderEnabled=True +s.ProcessPrestreamingRequests=True + +[/Script/Engine.GarbageCollectionSettings] +gc.MaxObjectsNotConsideredByGC=50000 +gc.SizeOfPermanentObjectPool=6900000 +gc.ActorClusteringEnabled=True +gc.BlueprintClusteringEnabled=True + +[Voice] +bEnabled=true + +[/Script/Engine.UserInterfaceSettings] +UIScaleCurve=(EditorCurveData=(PreInfinityExtrap=RCCE_Constant,PostInfinityExtrap=RCCE_Constant,Keys=((Time=480.000000,Value=0.444000),(Time=720.000000,Value=1.000000),(Time=1080.000000,Value=1.000000),(Time=8640.000000,Value=8.000000)),DefaultValue=340282346638528859811704183484516925440.000000),ExternalCurve=None) +UIScaleRule=ShortestSide + + +[/Script/Engine.RendererSettings] +r.TonemapperFilm=0 + +[/Script/IOSRuntimeSettings.IOSRuntimeSettings] +MinimumiOSVersion=IOS_12 + +[/Script/Engine.PhysicsSettings] +DefaultGravityZ=-980.000000 +DefaultTerminalVelocity=4000.000000 +DefaultFluidFriction=0.300000 +SimulateScratchMemorySize=262144 +RagdollAggregateThreshold=4 +TriangleMeshTriangleMinAreaThreshold=5.000000 +bEnableShapeSharing=False +bEnablePCM=True +bEnableStabilization=False +bWarnMissingLocks=True +bEnable2DPhysics=False +PhysicErrorCorrection=(PingExtrapolation=0.100000,PingLimit=100.000000,ErrorPerLinearDifference=1.000000,ErrorPerAngularDifference=1.000000,MaxRestoredStateError=1.000000,MaxLinearHardSnapDistance=400.000000,PositionLerp=0.000000,AngleLerp=0.400000,LinearVelocityCoefficient=100.000000,AngularVelocityCoefficient=10.000000,ErrorAccumulationSeconds=0.500000,ErrorAccumulationDistanceSq=15.000000,ErrorAccumulationSimilarity=100.000000) +DefaultDegreesOfFreedom=Full3D +BounceThresholdVelocity=200.000000 +FrictionCombineMode=Average +RestitutionCombineMode=Average +MaxAngularVelocity=3600.000000 +MaxDepenetrationVelocity=0.000000 +ContactOffsetMultiplier=0.020000 +MinContactOffset=2.000000 +MaxContactOffset=8.000000 +bSimulateSkeletalMeshOnDedicatedServer=True +DefaultShapeComplexity=CTF_UseSimpleAndComplex +bSuppressFaceRemapTable=False +bSupportUVFromHitResults=False +bDisableActiveActors=False +bDisableKinematicStaticPairs=False +bDisableKinematicKinematicPairs=False +bDisableCCD=False +bEnableEnhancedDeterminism=False +AnimPhysicsMinDeltaTime=0.000000 +bSimulateAnimPhysicsAfterReset=False +MaxPhysicsDeltaTime=0.033333 +bSubstepping=False +bSubsteppingAsync=False +MaxSubstepDeltaTime=0.016667 +MaxSubsteps=6 +SyncSceneSmoothingFactor=0.000000 +InitialAverageFrameRate=0.016667 +PhysXTreeRebuildRate=10 ++PhysicalSurfaces=(Type=SurfaceType1,Name="Concrete") ++PhysicalSurfaces=(Type=SurfaceType2,Name="Dirt") ++PhysicalSurfaces=(Type=SurfaceType3,Name="Water") ++PhysicalSurfaces=(Type=SurfaceType4,Name="Metal") ++PhysicalSurfaces=(Type=SurfaceType5,Name="Wood") ++PhysicalSurfaces=(Type=SurfaceType6,Name="Grass") ++PhysicalSurfaces=(Type=SurfaceType7,Name="Glass") ++PhysicalSurfaces=(Type=SurfaceType8,Name="Flesh") ++PhysicalSurfaces=(Type=SurfaceType9,Name="Tile") +DefaultBroadphaseSettings=(bUseMBPOnClient=False,bUseMBPOnServer=False,bUseMBPOuterBounds=False,MBPBounds=(Min=(X=0.000000,Y=0.000000,Z=0.000000),Max=(X=0.000000,Y=0.000000,Z=0.000000),IsValid=0),MBPOuterBounds=(Min=(X=0.000000,Y=0.000000,Z=0.000000),Max=(X=0.000000,Y=0.000000,Z=0.000000),IsValid=0),MBPNumSubdivs=2) +ChaosSettings=(DefaultThreadingModel=TaskGraph,DedicatedThreadTickMode=VariableCappedWithTarget,DedicatedThreadBufferMode=Double) + +[/Script/Engine.AudioSettings] +DefaultMediaSoundClassName=/Game/Sounds/Audio_Settings/Class/SC_SFX.SC_SFX +DefaultSoundClassName=/Game/Sounds/Audio_Settings/Class/SC_SFX.SC_SFX +DefaultSoundConcurrencyName=/Game/Sounds/Audio_Settings/Concurrency/SCon_Default.SCon_Default +DefaultBaseSoundMix=/Game/Sounds/Audio_Settings/SoundMix/SMix_Default.SMix_Default +VoiPSoundClass=/Game/Sounds/Audio_Settings/Class/SC_voip.SC_voip +PanningMethod=EqualPower + +[/Script/WindowsTargetPlatform.WindowsTargetSettings] +AudioCallbackBufferFrameSize=256 +AudioNumBuffersToEnqueue=4 +AudioSampleRate=48000 + +[/Script/LuminRuntimeSettings.LuminRuntimeSettings] +IconPortalPath=(Path="") + diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini new file mode 100644 index 0000000..f55ca27 --- /dev/null +++ b/Config/DefaultGame.ini @@ -0,0 +1,115 @@ +[/Script/ShooterGame.ShooterGameMode] +WarmupTime=15 +RoundTime=300 +TimeBetweenMatches=15 +KillScore=2 +DeathScore=-1 +DamageSelfScale=0.3 +MaxBots=1 +PlatformPlayerControllerClass=Class'/Script/ShooterGame.ShooterPlayerController' + +[/Script/EngineSettings.GeneralProjectSettings] +Description=A example for a first person arena shooter game +ProjectID=C4A218164D8CE9D08583ADAF79DDA719 +ProjectName=Shooter Game + +[/Script/ShooterGame.ShooterGameInstance] +WelcomeScreenMap=/Game/Maps/ShooterEntry +MainMenuMap=/Game/Maps/ShooterEntry +MatchmakerEndpoint= + +[/Script/Engine.GameSession] +bRequiresPushToTalk=true + +[/Script/UnrealEd.ProjectPackagingSettings] +Build=IfProjectHasCode +BuildConfiguration=PPBC_Development +BuildTarget= +StagingDirectory=(Path="C:/Dev/UEProjects/ShooterGameDemo/ShooterGame/Publish") +FullRebuild=False +ForDistribution=False +IncludeDebugFiles=False +BlueprintNativizationMethod=Disabled +bIncludeNativizedAssetsInProjectGeneration=False +bExcludeMonolithicEngineHeadersInNativizedCode=False +UsePakFile=True +bUseIoStore=False +bGenerateChunks=False +bGenerateNoChunks=False +bChunkHardReferencesOnly=False +bForceOneChunkPerFile=False +MaxChunkSize=0 +bBuildHttpChunkInstallData=False +HttpChunkInstallDataDirectory=(Path="") +PakFileCompressionFormats= +PakFileAdditionalCompressionOptions= +HttpChunkInstallDataVersion= +IncludePrerequisites=True +IncludeAppLocalPrerequisites=False +bShareMaterialShaderCode=True +bDeterministicShaderCodeOrder=False +bSharedMaterialNativeLibraries=True +ApplocalPrerequisitesDirectory=(Path="") +IncludeCrashReporter=False +InternationalizationPreset=English +-CulturesToStage=en ++CulturesToStage=en +LocalizationTargetCatchAllChunkId=0 +bCookAll=False +bCookMapsOnly=False +bCompressed=False +bSkipEditorContent=False +bSkipMovies=False +-IniKeyBlacklist=KeyStorePassword +-IniKeyBlacklist=KeyPassword +-IniKeyBlacklist=rsa.privateexp +-IniKeyBlacklist=rsa.modulus +-IniKeyBlacklist=rsa.publicexp +-IniKeyBlacklist=aes.key +-IniKeyBlacklist=SigningPublicExponent +-IniKeyBlacklist=SigningModulus +-IniKeyBlacklist=SigningPrivateExponent +-IniKeyBlacklist=EncryptionKey +-IniKeyBlacklist=IniKeyBlacklist +-IniKeyBlacklist=IniSectionBlacklist ++IniKeyBlacklist=KeyStorePassword ++IniKeyBlacklist=KeyPassword ++IniKeyBlacklist=rsa.privateexp ++IniKeyBlacklist=rsa.modulus ++IniKeyBlacklist=rsa.publicexp ++IniKeyBlacklist=aes.key ++IniKeyBlacklist=SigningPublicExponent ++IniKeyBlacklist=SigningModulus ++IniKeyBlacklist=SigningPrivateExponent ++IniKeyBlacklist=EncryptionKey ++IniKeyBlacklist=IniKeyBlacklist ++IniKeyBlacklist=IniSectionBlacklist ++MapsToCook=(FilePath="/Game/Maps/ShooterEntry") ++MapsToCook=(FilePath="/Game/Maps/Highrise") + +[/Script/MoviePlayer.MoviePlayerSettings] ++StartupMovies=LoadingScreen + +[/Plugins/PerformanceMonitor/NoExit] +PerformanceMonitorInterval=2.5 ++PerformanceMonitorTimers=STAT_GPUParticleTickTime ++PerformanceMonitorTimers=STAT_ParticleRenderingTime +PerformanceMonitorMap= +PerformanceMonitorTimeout=60 + +[/Plugins/PerformanceMonitor/Exit] +PerformanceMonitorInterval=1.0 ++PerformanceMonitorTimers=STAT_GPUParticleTickTime ++PerformanceMonitorTimers=STAT_ParticleRenderingTime ++PerformanceMonitorStatGroups=Unit ++PerformanceMonitorStatGroups=Particles ++PerformanceMonitorStatGroups=Anim ++PerformanceMonitorStatGroups=GpuParticles +PerformanceMonitorMap= +PerformanceMonitorTimeout=60 +PerformanceMonitorExitOnFinish=true + +[/Script/ShooterGame.ShooterPlayerController] +bAnalogFireTrigger=false +FireTriggerThreshold=0.25 ; unused if bAnalogFireTrigger is false + diff --git a/Config/DefaultInput.ini b/Config/DefaultInput.ini new file mode 100644 index 0000000..2a314fc --- /dev/null +++ b/Config/DefaultInput.ini @@ -0,0 +1,113 @@ +[/Script/Engine.PlayerInput] ++DebugExecBindings=(Key=L,Command="ToggleInfiniteAmmo") ++DebugExecBindings=(Key=L,Command="ToggleInfiniteClip", Control=True) ++DebugExecBindings=(Key=T,Command="ToggleMatchTimer") ++DebugExecBindings=(Key=T, Command="ForceMatchStart", Control=True) + +[/Script/Engine.InputSettings] +-AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) +-AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) ++AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseWheelAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_TriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_Grip1Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_Grip2Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_TriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_Grip1Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_Grip2Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +bAltEnterTogglesFullscreen=True +bF11TogglesFullscreen=True +bUseMouseForTouch=False +bEnableMouseSmoothing=True +bEnableFOVScaling=True +FOVScale=0.011110 +DoubleClickTime=0.200000 +bCaptureMouseOnLaunch=True +DefaultViewportMouseCaptureMode=CapturePermanently_IncludingInitialMouseDown +bDefaultViewportMouseLock=False +DefaultViewportMouseLockMode=LockOnCapture ++ActionMappings=(ActionName="PushToTalk",Key=V,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Target",Key=RightMouseButton,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="InGameMenu",Key=Escape,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="InGameMenu",Key=Gamepad_Special_Right,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="InGameMenu",Key=Global_Menu,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="InGameMenu",Key=Global_Pause,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Scoreboard",Key=Tab,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Scoreboard",Key=Gamepad_Special_Left,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="ToggleScoreboard",Key=Global_View,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Fire",Key=LeftMouseButton,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Fire",Key=Gamepad_RightTrigger,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Targeting",Key=RightMouseButton,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Targeting",Key=Gamepad_LeftTrigger,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="NextWeapon",Key=Q,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="NextWeapon",Key=Gamepad_FaceButton_Top,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="NextWeapon",Key=MouseScrollUp,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="NextWeapon",Key=Gamepad_DPad_Right,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="PrevWeapon",Key=MouseScrollDown,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="PrevWeapon",Key=Gamepad_DPad_Left,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Reload",Key=R,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Reload",Key=Gamepad_FaceButton_Left,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Run",Key=LeftShift,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="RunToggle",Key=Gamepad_LeftThumbstick,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Jump",Key=SpaceBar,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="Jump",Key=Gamepad_FaceButton_Bottom,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="ToggleChat",Key=C,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++ActionMappings=(ActionName="ConditionalCloseScoreboard",Key=Global_Back,bShift=False,bCtrl=False,bAlt=False,bCmd=False) ++AxisMappings=(AxisName="MoveForward",Key=W,Scale=1.000000) ++AxisMappings=(AxisName="MoveForward",Key=S,Scale=-1.000000) ++AxisMappings=(AxisName="MoveForward",Key=Up,Scale=1.000000) ++AxisMappings=(AxisName="MoveForward",Key=Down,Scale=-1.000000) ++AxisMappings=(AxisName="MoveForward",Key=Gamepad_LeftY,Scale=1.000000) ++AxisMappings=(AxisName="MoveRight",Key=A,Scale=-1.000000) ++AxisMappings=(AxisName="MoveRight",Key=D,Scale=1.000000) ++AxisMappings=(AxisName="MoveRight",Key=Gamepad_LeftX,Scale=1.000000) ++AxisMappings=(AxisName="MoveUp",Key=Gamepad_LeftThumbstick,Scale=1.000000) ++AxisMappings=(AxisName="MoveUp",Key=Gamepad_RightThumbstick,Scale=-1.000000) ++AxisMappings=(AxisName="MoveUp",Key=Gamepad_FaceButton_Bottom,Scale=1.000000) ++AxisMappings=(AxisName="MoveUp",Key=LeftControl,Scale=-1.000000) ++AxisMappings=(AxisName="MoveUp",Key=SpaceBar,Scale=1.000000) ++AxisMappings=(AxisName="MoveUp",Key=C,Scale=-1.000000) ++AxisMappings=(AxisName="TurnRate",Key=Gamepad_RightX,Scale=1.000000) ++AxisMappings=(AxisName="TurnRate",Key=Left,Scale=-1.000000) ++AxisMappings=(AxisName="TurnRate",Key=Right,Scale=1.000000) ++AxisMappings=(AxisName="Turn",Key=MouseX,Scale=1.000000) ++AxisMappings=(AxisName="LookUpRate",Key=Gamepad_RightY,Scale=1.000000) ++AxisMappings=(AxisName="LookUp",Key=MouseY,Scale=-1.000000) ++AxisMappings=(AxisName="FireTrigger",Key=Gamepad_RightTriggerAxis,Scale=1.000000) +bAlwaysShowTouchInterface=False +bShowConsoleOnFourFingerTap=True +DefaultTouchInterface=/Engine/MobileResources/HUD/DefaultVirtualJoysticks.DefaultVirtualJoysticks +ConsoleKey=None +-ConsoleKeys=Tilde ++ConsoleKeys=Tilde + + diff --git a/Config/IOS/IOSEngine.ini b/Config/IOS/IOSEngine.ini new file mode 100644 index 0000000..f9e264b --- /dev/null +++ b/Config/IOS/IOSEngine.ini @@ -0,0 +1,2 @@ +[OnlineSubsystem] +DefaultPlatformService=Null \ No newline at end of file diff --git a/Config/Linux/LinuxEngine.ini b/Config/Linux/LinuxEngine.ini new file mode 100644 index 0000000..ead9bde --- /dev/null +++ b/Config/Linux/LinuxEngine.ini @@ -0,0 +1,14 @@ +[/Script/Engine.GameEngine] +!NetDriverDefinitions=ClearArray +;+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") ++NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") ++NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver") + +[OnlineSubsystem] +DefaultPlatformService=Steam + +[/Script/OnlineSubsystemUtils.IpNetDriver] +ReplicationDriverClassName="/Script/ShooterGame.ShooterReplicationGraph" + +[CrashReportClient] +bAgreeToCrashUpload=true diff --git a/Config/Linux/LinuxGame.ini b/Config/Linux/LinuxGame.ini new file mode 100644 index 0000000..e099249 --- /dev/null +++ b/Config/Linux/LinuxGame.ini @@ -0,0 +1,4 @@ +[/Script/Engine.PlayerController] +InputYawScale=4.0 +InputPitchScale=-3.0 + diff --git a/Config/LinuxAArch64/LinuxAArch64Engine.ini b/Config/LinuxAArch64/LinuxAArch64Engine.ini new file mode 100644 index 0000000..cb69bdf --- /dev/null +++ b/Config/LinuxAArch64/LinuxAArch64Engine.ini @@ -0,0 +1,19 @@ +[/Script/Engine.GameEngine] +!NetDriverDefinitions=ClearArray ++NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") +;+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") ++NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver") + +[OnlineSubsystem] +;DefaultPlatformService=Steam +DefaultPlatformService=IpNet +bUseSteamNetworking=false + +[PacketHandlerComponents] +;+Components=OnlineSubsystemSteam.SteamAuthComponentModuleInterface + +[/Script/OnlineSubsystemUtils.IpNetDriver] +ReplicationDriverClassName="/Script/ShooterGame.ShooterReplicationGraph" + +[CrashReportClient] +bAgreeToCrashUpload=true diff --git a/Config/LinuxAArch64/LinuxAArch64Game.ini b/Config/LinuxAArch64/LinuxAArch64Game.ini new file mode 100644 index 0000000..e099249 --- /dev/null +++ b/Config/LinuxAArch64/LinuxAArch64Game.ini @@ -0,0 +1,4 @@ +[/Script/Engine.PlayerController] +InputYawScale=4.0 +InputPitchScale=-3.0 + diff --git a/Config/Mac/MacEngine.ini b/Config/Mac/MacEngine.ini new file mode 100644 index 0000000..d68daa6 --- /dev/null +++ b/Config/Mac/MacEngine.ini @@ -0,0 +1,11 @@ +[/Script/Engine.GameEngine] +!NetDriverDefinitions=ClearArray +;+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") ++NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") ++NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver") + +[OnlineSubsystem] +DefaultPlatformService=Steam + +[/Script/OnlineSubsystemUtils.IpNetDriver] +ReplicationDriverClassName="/Script/ShooterGame.ShooterReplicationGraph" diff --git a/Config/OSS/Live/Achievements.json b/Config/OSS/Live/Achievements.json new file mode 100644 index 0000000..85d3b91 --- /dev/null +++ b/Config/OSS/Live/Achievements.json @@ -0,0 +1,20 @@ +{ + "AchievementEventName" : "TempActivateAchiement", + "AchievementMap" : + { + "ACH_FRAG_SOMEONE" : 1, + "ACH_SOME_KILLS" : 2, + "ACH_LOTS_KILLS" : 3, + "ACH_FINISH_MATCH" : 4, + "ACH_LOTS_MATCHES" : 5, + "ACH_FIRST_WIN" : 6, + "ACH_LOTS_WIN" : 7, + "ACH_MANY_WIN" : 8, + "ACH_SHOOT_BULLETS" : 9, + "ACH_SHOOT_ROCKETS" : 10, + "ACH_GOOD_SCORE" : 11, + "ACH_GREAT_SCORE" : 12, + "ACH_PLAY_SANCTUARY" : 13, + "ACH_PLAY_HIGHRISE" : 14 + } +} \ No newline at end of file diff --git a/Config/OSS/Live/Events-EPCC.1-45783947.h b/Config/OSS/Live/Events-EPCC.1-45783947.h new file mode 100644 index 0000000..4f664cb --- /dev/null +++ b/Config/OSS/Live/Events-EPCC.1-45783947.h @@ -0,0 +1,632 @@ + +//**********************************************************************` +//* This is an include file generated by EtwPlusTool. *` +//* *` +//* Copyright (c) Microsoft Corporation. All Rights Reserved. *` +//**********************************************************************` +#pragma once +#pragma pack(push, 16) + +#include "EtwPlus.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +// Field Descriptors, used in the ETX_EVENT_DESCRIPTOR array below +// +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_CollectPowerup_Fields[13] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Float,0},{EtxFieldType_Float,0},{EtxFieldType_Float,0},{EtxFieldType_UInt32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_GameProgress_Fields[4] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_Float,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_KillOponent_Fields[16] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Float,0},{EtxFieldType_Float,0},{EtxFieldType_Float,0},{EtxFieldType_Int32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_MediaUsage_Fields[27] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UInt32,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_UInt64,0},{EtxFieldType_UInt32,0},{EtxFieldType_Float,0},{EtxFieldType_UInt64,0},{EtxFieldType_UInt64,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Float,0},{EtxFieldType_UInt32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_MultiplayerRoundEnd_Fields[11] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Float,0},{EtxFieldType_Int32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_MultiplayerRoundStart_Fields[9] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_ObjectiveEnd_Fields[9] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_ObjectiveStart_Fields[8] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_PageAction_Fields[9] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_PageView_Fields[9] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_PlayerDeath_Fields[15] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Float,0},{EtxFieldType_Float,0},{EtxFieldType_Float,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_PlayerSessionEnd_Fields[11] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Boolean,0},{EtxFieldType_UnicodeString,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_PlayerSessionPause_Fields[4] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_PlayerSessionResume_Fields[6] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_PlayerSessionStart_Fields[7] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_UnicodeString,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_SectionEnd_Fields[8] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_SectionStart_Fields[7] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_GUID,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_Int32,0},{EtxFieldType_Int32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_TempActivateAchiement_Fields[4] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_UInt32,0}}; +EXTERN_C __declspec(selectany) ETX_FIELD_DESCRIPTOR EPCC_45783947_ViewOffer_Fields[5] = {{EtxFieldType_UnicodeString,0},{EtxFieldType_UnicodeString,0},{EtxFieldType_GUID,0},{EtxFieldType_GUID,0},{EtxFieldType_GUID,0}}; + +// Event name mapping +// +#define CollectPowerup_value 1 +#define GameProgress_value 2 +#define KillOponent_value 3 +#define MediaUsage_value 4 +#define MultiplayerRoundEnd_value 5 +#define MultiplayerRoundStart_value 6 +#define ObjectiveEnd_value 7 +#define ObjectiveStart_value 8 +#define PageAction_value 9 +#define PageView_value 10 +#define PlayerDeath_value 11 +#define PlayerSessionEnd_value 12 +#define PlayerSessionPause_value 13 +#define PlayerSessionResume_value 14 +#define PlayerSessionStart_value 15 +#define SectionEnd_value 16 +#define SectionStart_value 17 +#define TempActivateAchiement_value 18 +#define ViewOffer_value 19 + +// Event Descriptor array +// +EXTERN_C __declspec(selectany) ETX_EVENT_DESCRIPTOR EPCC_45783947Events[19] = { + {{ 1, 1, 0, 0, 0, 0, 0x0 }, "CollectPowerup", "0.7.IGIA-2.1", EPCC_45783947_CollectPowerup_Fields, 13, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 2, 0, 0, 0, 0, 0, 0x0 }, "GameProgress", "0.7.IGGP-2.0", EPCC_45783947_GameProgress_Fields, 4, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 3, 0, 0, 0, 0, 0, 0x0 }, "KillOponent", "0.7.IGED-2.0", EPCC_45783947_KillOponent_Fields, 16, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 4, 0, 0, 0, 0, 0, 0x0 }, "MediaUsage", "0.7.MAUMU-2.0", EPCC_45783947_MediaUsage_Fields, 27, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 5, 0, 0, 0, 0, 0, 0x0 }, "MultiplayerRoundEnd", "0.7.IGMRE-2.0", EPCC_45783947_MultiplayerRoundEnd_Fields, 11, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 6, 0, 0, 0, 0, 0, 0x0 }, "MultiplayerRoundStart", "0.7.IGMRS-2.0", EPCC_45783947_MultiplayerRoundStart_Fields, 9, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 7, 0, 0, 0, 0, 0, 0x0 }, "ObjectiveEnd", "0.7.IGOE-3.0", EPCC_45783947_ObjectiveEnd_Fields, 9, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 8, 0, 0, 0, 0, 0, 0x0 }, "ObjectiveStart", "0.7.IGOS-2.0", EPCC_45783947_ObjectiveStart_Fields, 8, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 9, 0, 0, 0, 0, 0, 0x0 }, "PageAction", "0.7.IGPA-1.0", EPCC_45783947_PageAction_Fields, 9, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 10, 0, 0, 0, 0, 0, 0x0 }, "PageView", "0.7.IGPV-1.0", EPCC_45783947_PageView_Fields, 9, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 11, 0, 0, 0, 0, 0, 0x0 }, "PlayerDeath", "0.7.IGPD-2.0", EPCC_45783947_PlayerDeath_Fields, 15, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 12, 2, 0, 0, 0, 0, 0x0 }, "PlayerSessionEnd", "0.7.IGPSE-2.2", EPCC_45783947_PlayerSessionEnd_Fields, 11, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 13, 0, 0, 0, 0, 0, 0x0 }, "PlayerSessionPause", "0.7.IGPSPA-2.0", EPCC_45783947_PlayerSessionPause_Fields, 4, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 14, 0, 0, 0, 0, 0, 0x0 }, "PlayerSessionResume", "0.7.IGPSR-2.0", EPCC_45783947_PlayerSessionResume_Fields, 6, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 15, 1, 0, 0, 0, 0, 0x0 }, "PlayerSessionStart", "0.7.IGPSS-2.1", EPCC_45783947_PlayerSessionStart_Fields, 7, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 16, 0, 0, 0, 0, 0, 0x0 }, "SectionEnd", "0.7.IGSE-2.0", EPCC_45783947_SectionEnd_Fields, 8, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 17, 0, 0, 0, 0, 0, 0x0 }, "SectionStart", "0.7.IGSS-2.0", EPCC_45783947_SectionStart_Fields, 7, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 18, 1, 0, 0, 0, 0, 0x0 }, "TempActivateAchiement", "0.7.IGB-2.1", EPCC_45783947_TempActivateAchiement_Fields, 4, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }, + {{ 19, 0, 0, 0, 0, 0, 0x0 }, "ViewOffer", "0.7.IGVO-1.0", EPCC_45783947_ViewOffer_Fields, 5, 0, EtxEventEnabledState_Undefined, EtxEventEnabledState_ProviderDefault, EtxPopulationSample_Undefined, EtxPopulationSample_UseProviderPopulationSample, EtxEventLatency_Undefined, EtxEventLatency_ProviderDefault, EtxEventPriority_Undefined, EtxEventPriority_ProviderDefault }}; + +// Provider Descriptor for EPCC_45783947 +// +EXTERN_C __declspec(selectany) ETX_PROVIDER_DESCRIPTOR EPCC_45783947Provider = {"EPCC_45783947", {0x9e60d34f,0xf186,0x4f72,{0xa7,0x37,0x3b,0x33,0x71,0x6d,0x02,0xb7}}, 19, (ETX_EVENT_DESCRIPTOR*)&EPCC_45783947Events, 0, EtxProviderEnabledState_Undefined, EtxProviderEnabledState_OnByDefault, 0, 100, EtxProviderLatency_Undefined, EtxProviderLatency_RealTime, EtxProviderPriority_Undefined, EtxProviderPriority_Critical}; + +// ETW handle for EPCC_45783947 +// +EXTERN_C __declspec(selectany) REGHANDLE EPCC_45783947Handle = (REGHANDLE)0; + +/*++ + +Routine Description: + + Register the provider with ETW+. + +Arguments: + + None + +Remarks: + + ERROR_SUCCESS if success or if the provider was already registered. + Otherwise, an error code. + +--*/ +#define EventRegisterEPCC_45783947() EtxRegister(&EPCC_45783947Provider, &EPCC_45783947Handle) + +/*++ + +Routine Description: + + Unregister the provider from ETW+. + +Arguments: + None +Remarks: + ERROR_SUCCESS if success or if the provider was not registered. + Otherwise, an error code. +--*/ +#define EventUnregisterEPCC_45783947() EtxUnregister(&EPCC_45783947Provider, &EPCC_45783947Handle) + +#define EventEnabledCollectPowerup() (TRUE) + +// Entry point to log the event CollectPowerup +// +__inline +ULONG +EventWriteCollectPowerup(__in_opt PCWSTR UserId, __in const signed int SectionId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId, __in const signed int ItemId, __in const signed int AcquisitionMethodId, __in const float LocationX, __in const float LocationY, __in const float LocationZ, __in const unsigned int ItemQty) +{ +#define ARGUMENT_COUNT_EPCC_45783947_CollectPowerup 13 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_CollectPowerup]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], &SectionId, sizeof(SectionId)); + EventDataDescCreate(&EventData[3], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[4], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[5], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[6], &DifficultyLevelId, sizeof(DifficultyLevelId)); + EventDataDescCreate(&EventData[7], &ItemId, sizeof(ItemId)); + EventDataDescCreate(&EventData[8], &AcquisitionMethodId, sizeof(AcquisitionMethodId)); + EventDataDescCreate(&EventData[9], &LocationX, sizeof(LocationX)); + EventDataDescCreate(&EventData[10], &LocationY, sizeof(LocationY)); + EventDataDescCreate(&EventData[11], &LocationZ, sizeof(LocationZ)); + EventDataDescCreate(&EventData[12], &ItemQty, sizeof(ItemQty)); + + return EtxEventWrite(&EPCC_45783947Events[0], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_CollectPowerup, EventData); +} +#define EventEnabledGameProgress() (TRUE) + +// Entry point to log the event GameProgress +// +__inline +ULONG +EventWriteGameProgress(__in_opt PCWSTR UserId, __in LPCGUID PlayerSessionId, __in const float CompletionPercent) +{ +#define ARGUMENT_COUNT_EPCC_45783947_GameProgress 4 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_GameProgress]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], &CompletionPercent, sizeof(CompletionPercent)); + + return EtxEventWrite(&EPCC_45783947Events[1], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_GameProgress, EventData); +} +#define EventEnabledKillOponent() (TRUE) + +// Entry point to log the event KillOponent +// +__inline +ULONG +EventWriteKillOponent(__in_opt PCWSTR UserId, __in const signed int SectionId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId, __in LPCGUID RoundId, __in const signed int PlayerRoleId, __in const signed int PlayerWeaponId, __in const signed int EnemyRoleId, __in const signed int KillTypeId, __in const float LocationX, __in const float LocationY, __in const float LocationZ, __in const signed int EnemyWeaponId) +{ +#define ARGUMENT_COUNT_EPCC_45783947_KillOponent 16 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_KillOponent]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], &SectionId, sizeof(SectionId)); + EventDataDescCreate(&EventData[3], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[4], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[5], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[6], &DifficultyLevelId, sizeof(DifficultyLevelId)); + EventDataDescCreate(&EventData[7], RoundId, sizeof(GUID)); + EventDataDescCreate(&EventData[8], &PlayerRoleId, sizeof(PlayerRoleId)); + EventDataDescCreate(&EventData[9], &PlayerWeaponId, sizeof(PlayerWeaponId)); + EventDataDescCreate(&EventData[10], &EnemyRoleId, sizeof(EnemyRoleId)); + EventDataDescCreate(&EventData[11], &KillTypeId, sizeof(KillTypeId)); + EventDataDescCreate(&EventData[12], &LocationX, sizeof(LocationX)); + EventDataDescCreate(&EventData[13], &LocationY, sizeof(LocationY)); + EventDataDescCreate(&EventData[14], &LocationZ, sizeof(LocationZ)); + EventDataDescCreate(&EventData[15], &EnemyWeaponId, sizeof(EnemyWeaponId)); + + return EtxEventWrite(&EPCC_45783947Events[2], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_KillOponent, EventData); +} +#define EventEnabledMediaUsage() (TRUE) + +// Entry point to log the event MediaUsage +// +__inline +ULONG +EventWriteMediaUsage(__in_opt PCWSTR AppSessionId, __in_opt PCWSTR AppSessionStartDateTime, __in const unsigned int UserIdType, __in_opt PCWSTR UserId, __in_opt PCWSTR SubscriptionTierType, __in_opt PCWSTR SubscriptionTier, __in_opt PCWSTR MediaType, __in_opt PCWSTR ProviderId, __in_opt PCWSTR ProviderMediaId, __in_opt PCWSTR ProviderMediaInstanceId, __in LPCGUID BingId, __in const unsigned __int64 MediaLengthMs, __in const unsigned int MediaControlAction, __in const float PlaybackSpeed, __in const unsigned __int64 MediaPositionMs, __in const unsigned __int64 PlaybackDurationMs, __in_opt PCWSTR AcquisitionType, __in_opt PCWSTR AcquisitionContext, __in_opt PCWSTR AcquisitionContextType, __in_opt PCWSTR AcquisitionContextId, __in const signed int PlaybackIsStream, __in const signed int PlaybackIsTethered, __in_opt PCWSTR MarketplaceLocation, __in_opt PCWSTR ContentLocale, __in const float TimeZoneOffset, __in const unsigned int ScreenState) +{ +#define ARGUMENT_COUNT_EPCC_45783947_MediaUsage 27 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_MediaUsage]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (AppSessionId != NULL) ? AppSessionId : L"", (AppSessionId != NULL) ? (ULONG)((wcslen(AppSessionId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], (AppSessionStartDateTime != NULL) ? AppSessionStartDateTime : L"", (AppSessionStartDateTime != NULL) ? (ULONG)((wcslen(AppSessionStartDateTime) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[3], &UserIdType, sizeof(UserIdType)); + EventDataDescCreate(&EventData[4], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[5], (SubscriptionTierType != NULL) ? SubscriptionTierType : L"", (SubscriptionTierType != NULL) ? (ULONG)((wcslen(SubscriptionTierType) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[6], (SubscriptionTier != NULL) ? SubscriptionTier : L"", (SubscriptionTier != NULL) ? (ULONG)((wcslen(SubscriptionTier) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[7], (MediaType != NULL) ? MediaType : L"", (MediaType != NULL) ? (ULONG)((wcslen(MediaType) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[8], (ProviderId != NULL) ? ProviderId : L"", (ProviderId != NULL) ? (ULONG)((wcslen(ProviderId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[9], (ProviderMediaId != NULL) ? ProviderMediaId : L"", (ProviderMediaId != NULL) ? (ULONG)((wcslen(ProviderMediaId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[10], (ProviderMediaInstanceId != NULL) ? ProviderMediaInstanceId : L"", (ProviderMediaInstanceId != NULL) ? (ULONG)((wcslen(ProviderMediaInstanceId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[11], BingId, sizeof(GUID)); + EventDataDescCreate(&EventData[12], &MediaLengthMs, sizeof(MediaLengthMs)); + EventDataDescCreate(&EventData[13], &MediaControlAction, sizeof(MediaControlAction)); + EventDataDescCreate(&EventData[14], &PlaybackSpeed, sizeof(PlaybackSpeed)); + EventDataDescCreate(&EventData[15], &MediaPositionMs, sizeof(MediaPositionMs)); + EventDataDescCreate(&EventData[16], &PlaybackDurationMs, sizeof(PlaybackDurationMs)); + EventDataDescCreate(&EventData[17], (AcquisitionType != NULL) ? AcquisitionType : L"", (AcquisitionType != NULL) ? (ULONG)((wcslen(AcquisitionType) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[18], (AcquisitionContext != NULL) ? AcquisitionContext : L"", (AcquisitionContext != NULL) ? (ULONG)((wcslen(AcquisitionContext) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[19], (AcquisitionContextType != NULL) ? AcquisitionContextType : L"", (AcquisitionContextType != NULL) ? (ULONG)((wcslen(AcquisitionContextType) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[20], (AcquisitionContextId != NULL) ? AcquisitionContextId : L"", (AcquisitionContextId != NULL) ? (ULONG)((wcslen(AcquisitionContextId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[21], &PlaybackIsStream, sizeof(PlaybackIsStream)); + EventDataDescCreate(&EventData[22], &PlaybackIsTethered, sizeof(PlaybackIsTethered)); + EventDataDescCreate(&EventData[23], (MarketplaceLocation != NULL) ? MarketplaceLocation : L"", (MarketplaceLocation != NULL) ? (ULONG)((wcslen(MarketplaceLocation) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[24], (ContentLocale != NULL) ? ContentLocale : L"", (ContentLocale != NULL) ? (ULONG)((wcslen(ContentLocale) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[25], &TimeZoneOffset, sizeof(TimeZoneOffset)); + EventDataDescCreate(&EventData[26], &ScreenState, sizeof(ScreenState)); + + return EtxEventWrite(&EPCC_45783947Events[3], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_MediaUsage, EventData); +} +#define EventEnabledMultiplayerRoundEnd() (TRUE) + +// Entry point to log the event MultiplayerRoundEnd +// +__inline +ULONG +EventWriteMultiplayerRoundEnd(__in_opt PCWSTR UserId, __in LPCGUID RoundId, __in const signed int SectionId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int MatchTypeId, __in const signed int DifficultyLevelId, __in const float TimeInSeconds, __in const signed int ExitStatusId) +{ +#define ARGUMENT_COUNT_EPCC_45783947_MultiplayerRoundEnd 11 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_MultiplayerRoundEnd]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], RoundId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], &SectionId, sizeof(SectionId)); + EventDataDescCreate(&EventData[4], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[5], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[6], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[7], &MatchTypeId, sizeof(MatchTypeId)); + EventDataDescCreate(&EventData[8], &DifficultyLevelId, sizeof(DifficultyLevelId)); + EventDataDescCreate(&EventData[9], &TimeInSeconds, sizeof(TimeInSeconds)); + EventDataDescCreate(&EventData[10], &ExitStatusId, sizeof(ExitStatusId)); + + return EtxEventWrite(&EPCC_45783947Events[4], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_MultiplayerRoundEnd, EventData); +} +#define EventEnabledMultiplayerRoundStart() (TRUE) + +// Entry point to log the event MultiplayerRoundStart +// +__inline +ULONG +EventWriteMultiplayerRoundStart(__in_opt PCWSTR UserId, __in LPCGUID RoundId, __in const signed int SectionId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int MatchTypeId, __in const signed int DifficultyLevelId) +{ +#define ARGUMENT_COUNT_EPCC_45783947_MultiplayerRoundStart 9 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_MultiplayerRoundStart]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], RoundId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], &SectionId, sizeof(SectionId)); + EventDataDescCreate(&EventData[4], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[5], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[6], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[7], &MatchTypeId, sizeof(MatchTypeId)); + EventDataDescCreate(&EventData[8], &DifficultyLevelId, sizeof(DifficultyLevelId)); + + return EtxEventWrite(&EPCC_45783947Events[5], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_MultiplayerRoundStart, EventData); +} +#define EventEnabledObjectiveEnd() (TRUE) + +// Entry point to log the event ObjectiveEnd +// +__inline +ULONG +EventWriteObjectiveEnd(__in_opt PCWSTR UserId, __in const signed int SectionId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId, __in const signed int ObjectiveId, __in const signed int ExitStatusId) +{ +#define ARGUMENT_COUNT_EPCC_45783947_ObjectiveEnd 9 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_ObjectiveEnd]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], &SectionId, sizeof(SectionId)); + EventDataDescCreate(&EventData[3], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[4], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[5], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[6], &DifficultyLevelId, sizeof(DifficultyLevelId)); + EventDataDescCreate(&EventData[7], &ObjectiveId, sizeof(ObjectiveId)); + EventDataDescCreate(&EventData[8], &ExitStatusId, sizeof(ExitStatusId)); + + return EtxEventWrite(&EPCC_45783947Events[6], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_ObjectiveEnd, EventData); +} +#define EventEnabledObjectiveStart() (TRUE) + +// Entry point to log the event ObjectiveStart +// +__inline +ULONG +EventWriteObjectiveStart(__in_opt PCWSTR UserId, __in const signed int SectionId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId, __in const signed int ObjectiveId) +{ +#define ARGUMENT_COUNT_EPCC_45783947_ObjectiveStart 8 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_ObjectiveStart]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], &SectionId, sizeof(SectionId)); + EventDataDescCreate(&EventData[3], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[4], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[5], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[6], &DifficultyLevelId, sizeof(DifficultyLevelId)); + EventDataDescCreate(&EventData[7], &ObjectiveId, sizeof(ObjectiveId)); + + return EtxEventWrite(&EPCC_45783947Events[7], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_ObjectiveStart, EventData); +} +#define EventEnabledPageAction() (TRUE) + +// Entry point to log the event PageAction +// +__inline +ULONG +EventWritePageAction(__in_opt PCWSTR UserId, __in LPCGUID PlayerSessionId, __in const signed int ActionTypeId, __in const signed int ActionInputMethodId, __in_opt PCWSTR Page, __in_opt PCWSTR TemplateId, __in_opt PCWSTR DestinationPage, __in_opt PCWSTR Content) +{ +#define ARGUMENT_COUNT_EPCC_45783947_PageAction 9 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_PageAction]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], &ActionTypeId, sizeof(ActionTypeId)); + EventDataDescCreate(&EventData[4], &ActionInputMethodId, sizeof(ActionInputMethodId)); + EventDataDescCreate(&EventData[5], (Page != NULL) ? Page : L"", (Page != NULL) ? (ULONG)((wcslen(Page) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[6], (TemplateId != NULL) ? TemplateId : L"", (TemplateId != NULL) ? (ULONG)((wcslen(TemplateId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[7], (DestinationPage != NULL) ? DestinationPage : L"", (DestinationPage != NULL) ? (ULONG)((wcslen(DestinationPage) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[8], (Content != NULL) ? Content : L"", (Content != NULL) ? (ULONG)((wcslen(Content) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + + return EtxEventWrite(&EPCC_45783947Events[8], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_PageAction, EventData); +} +#define EventEnabledPageView() (TRUE) + +// Entry point to log the event PageView +// +__inline +ULONG +EventWritePageView(__in_opt PCWSTR UserId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR Page, __in_opt PCWSTR RefererPage, __in const signed int PageTypeId, __in_opt PCWSTR PageTags, __in_opt PCWSTR TemplateId, __in_opt PCWSTR Content) +{ +#define ARGUMENT_COUNT_EPCC_45783947_PageView 9 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_PageView]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], (Page != NULL) ? Page : L"", (Page != NULL) ? (ULONG)((wcslen(Page) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[4], (RefererPage != NULL) ? RefererPage : L"", (RefererPage != NULL) ? (ULONG)((wcslen(RefererPage) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[5], &PageTypeId, sizeof(PageTypeId)); + EventDataDescCreate(&EventData[6], (PageTags != NULL) ? PageTags : L"", (PageTags != NULL) ? (ULONG)((wcslen(PageTags) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[7], (TemplateId != NULL) ? TemplateId : L"", (TemplateId != NULL) ? (ULONG)((wcslen(TemplateId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[8], (Content != NULL) ? Content : L"", (Content != NULL) ? (ULONG)((wcslen(Content) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + + return EtxEventWrite(&EPCC_45783947Events[9], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_PageView, EventData); +} +#define EventEnabledPlayerDeath() (TRUE) + +// Entry point to log the event PlayerDeath +// +__inline +ULONG +EventWritePlayerDeath(__in_opt PCWSTR UserId, __in const signed int SectionId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId, __in LPCGUID RoundId, __in const signed int PlayerRoleId, __in const signed int PlayerWeaponId, __in const signed int EnemyRoleId, __in const signed int EnemyWeaponId, __in const float LocationX, __in const float LocationY, __in const float LocationZ) +{ +#define ARGUMENT_COUNT_EPCC_45783947_PlayerDeath 15 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_PlayerDeath]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], &SectionId, sizeof(SectionId)); + EventDataDescCreate(&EventData[3], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[4], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[5], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[6], &DifficultyLevelId, sizeof(DifficultyLevelId)); + EventDataDescCreate(&EventData[7], RoundId, sizeof(GUID)); + EventDataDescCreate(&EventData[8], &PlayerRoleId, sizeof(PlayerRoleId)); + EventDataDescCreate(&EventData[9], &PlayerWeaponId, sizeof(PlayerWeaponId)); + EventDataDescCreate(&EventData[10], &EnemyRoleId, sizeof(EnemyRoleId)); + EventDataDescCreate(&EventData[11], &EnemyWeaponId, sizeof(EnemyWeaponId)); + EventDataDescCreate(&EventData[12], &LocationX, sizeof(LocationX)); + EventDataDescCreate(&EventData[13], &LocationY, sizeof(LocationY)); + EventDataDescCreate(&EventData[14], &LocationZ, sizeof(LocationZ)); + + return EtxEventWrite(&EPCC_45783947Events[10], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_PlayerDeath, EventData); +} +#define EventEnabledPlayerSessionEnd() (TRUE) + +// Entry point to log the event PlayerSessionEnd +// +__inline +ULONG +EventWritePlayerSessionEnd(__in_opt PCWSTR UserId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId, __in const signed int ExitStatusId, __in_opt PCWSTR MapName, __in const signed int PlayerScore, __in const BOOL PlayerWon, __in_opt PCWSTR MapNameString) +{ +#define ARGUMENT_COUNT_EPCC_45783947_PlayerSessionEnd 11 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_PlayerSessionEnd]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[4], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[5], &DifficultyLevelId, sizeof(DifficultyLevelId)); + EventDataDescCreate(&EventData[6], &ExitStatusId, sizeof(ExitStatusId)); + EventDataDescCreate(&EventData[7], (MapName != NULL) ? MapName : L"", (MapName != NULL) ? (ULONG)((wcslen(MapName) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[8], &PlayerScore, sizeof(PlayerScore)); + EventDataDescCreate(&EventData[9], &PlayerWon, sizeof(PlayerWon)); + EventDataDescCreate(&EventData[10], (MapNameString != NULL) ? MapNameString : L"", (MapNameString != NULL) ? (ULONG)((wcslen(MapNameString) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + + return EtxEventWrite(&EPCC_45783947Events[11], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_PlayerSessionEnd, EventData); +} +#define EventEnabledPlayerSessionPause() (TRUE) + +// Entry point to log the event PlayerSessionPause +// +__inline +ULONG +EventWritePlayerSessionPause(__in_opt PCWSTR UserId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId) +{ +#define ARGUMENT_COUNT_EPCC_45783947_PlayerSessionPause 4 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_PlayerSessionPause]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + + return EtxEventWrite(&EPCC_45783947Events[12], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_PlayerSessionPause, EventData); +} +#define EventEnabledPlayerSessionResume() (TRUE) + +// Entry point to log the event PlayerSessionResume +// +__inline +ULONG +EventWritePlayerSessionResume(__in_opt PCWSTR UserId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId) +{ +#define ARGUMENT_COUNT_EPCC_45783947_PlayerSessionResume 6 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_PlayerSessionResume]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[4], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[5], &DifficultyLevelId, sizeof(DifficultyLevelId)); + + return EtxEventWrite(&EPCC_45783947Events[13], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_PlayerSessionResume, EventData); +} +#define EventEnabledPlayerSessionStart() (TRUE) + +// Entry point to log the event PlayerSessionStart +// +__inline +ULONG +EventWritePlayerSessionStart(__in_opt PCWSTR UserId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId, __in_opt PCWSTR MapName) +{ +#define ARGUMENT_COUNT_EPCC_45783947_PlayerSessionStart 7 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_PlayerSessionStart]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[4], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[5], &DifficultyLevelId, sizeof(DifficultyLevelId)); + EventDataDescCreate(&EventData[6], (MapName != NULL) ? MapName : L"", (MapName != NULL) ? (ULONG)((wcslen(MapName) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + + return EtxEventWrite(&EPCC_45783947Events[14], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_PlayerSessionStart, EventData); +} +#define EventEnabledSectionEnd() (TRUE) + +// Entry point to log the event SectionEnd +// +__inline +ULONG +EventWriteSectionEnd(__in_opt PCWSTR UserId, __in const signed int SectionId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId, __in const signed int ExitStatusId) +{ +#define ARGUMENT_COUNT_EPCC_45783947_SectionEnd 8 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_SectionEnd]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], &SectionId, sizeof(SectionId)); + EventDataDescCreate(&EventData[3], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[4], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[5], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[6], &DifficultyLevelId, sizeof(DifficultyLevelId)); + EventDataDescCreate(&EventData[7], &ExitStatusId, sizeof(ExitStatusId)); + + return EtxEventWrite(&EPCC_45783947Events[15], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_SectionEnd, EventData); +} +#define EventEnabledSectionStart() (TRUE) + +// Entry point to log the event SectionStart +// +__inline +ULONG +EventWriteSectionStart(__in_opt PCWSTR UserId, __in const signed int SectionId, __in LPCGUID PlayerSessionId, __in_opt PCWSTR MultiplayerCorrelationId, __in const signed int GameplayModeId, __in const signed int DifficultyLevelId) +{ +#define ARGUMENT_COUNT_EPCC_45783947_SectionStart 7 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_SectionStart]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], &SectionId, sizeof(SectionId)); + EventDataDescCreate(&EventData[3], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[4], (MultiplayerCorrelationId != NULL) ? MultiplayerCorrelationId : L"", (MultiplayerCorrelationId != NULL) ? (ULONG)((wcslen(MultiplayerCorrelationId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[5], &GameplayModeId, sizeof(GameplayModeId)); + EventDataDescCreate(&EventData[6], &DifficultyLevelId, sizeof(DifficultyLevelId)); + + return EtxEventWrite(&EPCC_45783947Events[16], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_SectionStart, EventData); +} +#define EventEnabledTempActivateAchiement() (TRUE) + +// Entry point to log the event TempActivateAchiement +// +__inline +ULONG +EventWriteTempActivateAchiement(__in_opt PCWSTR UserId, __in LPCGUID PlayerSessionId, __in const unsigned int AchievementIndex) +{ +#define ARGUMENT_COUNT_EPCC_45783947_TempActivateAchiement 4 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_TempActivateAchiement]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], &AchievementIndex, sizeof(AchievementIndex)); + + return EtxEventWrite(&EPCC_45783947Events[17], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_TempActivateAchiement, EventData); +} +#define EventEnabledViewOffer() (TRUE) + +// Entry point to log the event ViewOffer +// +__inline +ULONG +EventWriteViewOffer(__in_opt PCWSTR UserId, __in LPCGUID PlayerSessionId, __in LPCGUID OfferGuid, __in LPCGUID ProductGuid) +{ +#define ARGUMENT_COUNT_EPCC_45783947_ViewOffer 5 + + EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_EPCC_45783947_ViewOffer]; + UINT8 scratch[64]; + + EtxFillCommonFields_v7(&EventData[0], scratch, 64); + + EventDataDescCreate(&EventData[1], (UserId != NULL) ? UserId : L"", (UserId != NULL) ? (ULONG)((wcslen(UserId) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"")); + EventDataDescCreate(&EventData[2], PlayerSessionId, sizeof(GUID)); + EventDataDescCreate(&EventData[3], OfferGuid, sizeof(GUID)); + EventDataDescCreate(&EventData[4], ProductGuid, sizeof(GUID)); + + return EtxEventWrite(&EPCC_45783947Events[18], &EPCC_45783947Provider, EPCC_45783947Handle, ARGUMENT_COUNT_EPCC_45783947_ViewOffer, EventData); +} +#if defined(__cplusplus) +}; +#endif + +#pragma pack(pop) diff --git a/Config/OSS/Live/Events.json b/Config/OSS/Live/Events.json new file mode 100644 index 0000000..e9386a1 --- /dev/null +++ b/Config/OSS/Live/Events.json @@ -0,0 +1,3 @@ +{ + "EventsHeaderName" : "Events-EPCC.1-45783947.h" +} \ No newline at end of file diff --git a/Config/Windows/WindowsEngine.ini b/Config/Windows/WindowsEngine.ini new file mode 100644 index 0000000..c1a8a55 --- /dev/null +++ b/Config/Windows/WindowsEngine.ini @@ -0,0 +1,21 @@ +[/Script/Engine.GameEngine] +!NetDriverDefinitions=ClearArray +;+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") ++NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") ++NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver") + +[OnlineSubsystem] +DefaultPlatformService=Steam + +; SteamAuth Packet Handler Configuration. +; Uncomment both PacketHandlerProfileConfig sections to use SteamAuth on Windows +; Add these lines to other platforms to enable SteamAuth on them as well + +;[GameNetDriver PacketHandlerProfileConfig] +;+Components=OnlineSubsystemSteam.SteamAuthComponentModuleInterface + +;[PendingNetDriver PacketHandlerProfileConfig] +;+Components=OnlineSubsystemSteam.SteamAuthComponentModuleInterface + +[/Script/OnlineSubsystemUtils.IpNetDriver] +ReplicationDriverClassName="/Script/ShooterGame.ShooterReplicationGraph" diff --git a/Content/Animations/FPP_Animations/FPP_LanucherReload.uasset b/Content/Animations/FPP_Animations/FPP_LanucherReload.uasset new file mode 100644 index 0000000..e618613 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LanucherReload.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherAim.uasset b/Content/Animations/FPP_Animations/FPP_LauncherAim.uasset new file mode 100644 index 0000000..2e6e2aa Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherAim.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherAimFire.uasset b/Content/Animations/FPP_Animations/FPP_LauncherAimFire.uasset new file mode 100644 index 0000000..7f6d18a Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherAimFire.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherEquip.uasset b/Content/Animations/FPP_Animations/FPP_LauncherEquip.uasset new file mode 100644 index 0000000..a429190 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherEquip.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherFire.uasset b/Content/Animations/FPP_Animations/FPP_LauncherFire.uasset new file mode 100644 index 0000000..b414b3f Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherFire.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherIdle.uasset b/Content/Animations/FPP_Animations/FPP_LauncherIdle.uasset new file mode 100644 index 0000000..dcaee28 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherIdle.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherJumpEnd.uasset b/Content/Animations/FPP_Animations/FPP_LauncherJumpEnd.uasset new file mode 100644 index 0000000..7c6b6b3 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherJumpEnd.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherJumpLoop.uasset b/Content/Animations/FPP_Animations/FPP_LauncherJumpLoop.uasset new file mode 100644 index 0000000..0804491 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherJumpLoop.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherJumpStart.uasset b/Content/Animations/FPP_Animations/FPP_LauncherJumpStart.uasset new file mode 100644 index 0000000..c036194 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherJumpStart.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherRoadieRun.uasset b/Content/Animations/FPP_Animations/FPP_LauncherRoadieRun.uasset new file mode 100644 index 0000000..e003dbc Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherRoadieRun.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_LauncherRun.uasset b/Content/Animations/FPP_Animations/FPP_LauncherRun.uasset new file mode 100644 index 0000000..7bf9225 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_LauncherRun.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleAim.uasset b/Content/Animations/FPP_Animations/FPP_RifleAim.uasset new file mode 100644 index 0000000..fa55aed Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleAim.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleAimFire.uasset b/Content/Animations/FPP_Animations/FPP_RifleAimFire.uasset new file mode 100644 index 0000000..82119e2 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleAimFire.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleEquip.uasset b/Content/Animations/FPP_Animations/FPP_RifleEquip.uasset new file mode 100644 index 0000000..61f4c65 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleEquip.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleFire.uasset b/Content/Animations/FPP_Animations/FPP_RifleFire.uasset new file mode 100644 index 0000000..33c6403 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleFire.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleIdle.uasset b/Content/Animations/FPP_Animations/FPP_RifleIdle.uasset new file mode 100644 index 0000000..5cf6d54 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleIdle.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleJumpEnd.uasset b/Content/Animations/FPP_Animations/FPP_RifleJumpEnd.uasset new file mode 100644 index 0000000..bc55c48 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleJumpEnd.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleJumpLoop.uasset b/Content/Animations/FPP_Animations/FPP_RifleJumpLoop.uasset new file mode 100644 index 0000000..9bf72b2 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleJumpLoop.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleJumpStart.uasset b/Content/Animations/FPP_Animations/FPP_RifleJumpStart.uasset new file mode 100644 index 0000000..5193a49 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleJumpStart.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleReload.uasset b/Content/Animations/FPP_Animations/FPP_RifleReload.uasset new file mode 100644 index 0000000..812096f Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleReload.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleRoadieRun.uasset b/Content/Animations/FPP_Animations/FPP_RifleRoadieRun.uasset new file mode 100644 index 0000000..23b7519 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleRoadieRun.uasset differ diff --git a/Content/Animations/FPP_Animations/FPP_RifleRun.uasset b/Content/Animations/FPP_Animations/FPP_RifleRun.uasset new file mode 100644 index 0000000..6febcb5 Binary files /dev/null and b/Content/Animations/FPP_Animations/FPP_RifleRun.uasset differ diff --git a/Content/Animations/FPP_Animations/HeroFPP_LauncherEquip_Montage.uasset b/Content/Animations/FPP_Animations/HeroFPP_LauncherEquip_Montage.uasset new file mode 100644 index 0000000..e0c9e11 Binary files /dev/null and b/Content/Animations/FPP_Animations/HeroFPP_LauncherEquip_Montage.uasset differ diff --git a/Content/Animations/FPP_Animations/HeroFPP_LauncherFire_Montage.uasset b/Content/Animations/FPP_Animations/HeroFPP_LauncherFire_Montage.uasset new file mode 100644 index 0000000..ed7b4ac Binary files /dev/null and b/Content/Animations/FPP_Animations/HeroFPP_LauncherFire_Montage.uasset differ diff --git a/Content/Animations/FPP_Animations/HeroFPP_LauncherReload_Montage.uasset b/Content/Animations/FPP_Animations/HeroFPP_LauncherReload_Montage.uasset new file mode 100644 index 0000000..8372d04 Binary files /dev/null and b/Content/Animations/FPP_Animations/HeroFPP_LauncherReload_Montage.uasset differ diff --git a/Content/Animations/FPP_Animations/HeroFPP_RifleEquip_Montage.uasset b/Content/Animations/FPP_Animations/HeroFPP_RifleEquip_Montage.uasset new file mode 100644 index 0000000..da76127 Binary files /dev/null and b/Content/Animations/FPP_Animations/HeroFPP_RifleEquip_Montage.uasset differ diff --git a/Content/Animations/FPP_Animations/HeroFPP_RifleReload_Montage.uasset b/Content/Animations/FPP_Animations/HeroFPP_RifleReload_Montage.uasset new file mode 100644 index 0000000..464dd58 Binary files /dev/null and b/Content/Animations/FPP_Animations/HeroFPP_RifleReload_Montage.uasset differ diff --git a/Content/Animations/FPP_Animations/HerroFPP_RifleFire_Montage.uasset b/Content/Animations/FPP_Animations/HerroFPP_RifleFire_Montage.uasset new file mode 100644 index 0000000..949bfcb Binary files /dev/null and b/Content/Animations/FPP_Animations/HerroFPP_RifleFire_Montage.uasset differ diff --git a/Content/Animations/TTP_Animations/AimOffsetDown.uasset b/Content/Animations/TTP_Animations/AimOffsetDown.uasset new file mode 100644 index 0000000..3c999f1 Binary files /dev/null and b/Content/Animations/TTP_Animations/AimOffsetDown.uasset differ diff --git a/Content/Animations/TTP_Animations/AimOffsetFwd.uasset b/Content/Animations/TTP_Animations/AimOffsetFwd.uasset new file mode 100644 index 0000000..84dcbed Binary files /dev/null and b/Content/Animations/TTP_Animations/AimOffsetFwd.uasset differ diff --git a/Content/Animations/TTP_Animations/AimOffsetLeft.uasset b/Content/Animations/TTP_Animations/AimOffsetLeft.uasset new file mode 100644 index 0000000..e9d5219 Binary files /dev/null and b/Content/Animations/TTP_Animations/AimOffsetLeft.uasset differ diff --git a/Content/Animations/TTP_Animations/AimOffsetLeftDown.uasset b/Content/Animations/TTP_Animations/AimOffsetLeftDown.uasset new file mode 100644 index 0000000..c89b24a Binary files /dev/null and b/Content/Animations/TTP_Animations/AimOffsetLeftDown.uasset differ diff --git a/Content/Animations/TTP_Animations/AimOffsetLeftUp.uasset b/Content/Animations/TTP_Animations/AimOffsetLeftUp.uasset new file mode 100644 index 0000000..cc8756e Binary files /dev/null and b/Content/Animations/TTP_Animations/AimOffsetLeftUp.uasset differ diff --git a/Content/Animations/TTP_Animations/AimOffsetRight.uasset b/Content/Animations/TTP_Animations/AimOffsetRight.uasset new file mode 100644 index 0000000..5a5eeda Binary files /dev/null and b/Content/Animations/TTP_Animations/AimOffsetRight.uasset differ diff --git a/Content/Animations/TTP_Animations/AimOffsetRightDown.uasset b/Content/Animations/TTP_Animations/AimOffsetRightDown.uasset new file mode 100644 index 0000000..2adb4ec Binary files /dev/null and b/Content/Animations/TTP_Animations/AimOffsetRightDown.uasset differ diff --git a/Content/Animations/TTP_Animations/AimOffsetRightUp.uasset b/Content/Animations/TTP_Animations/AimOffsetRightUp.uasset new file mode 100644 index 0000000..f6aee62 Binary files /dev/null and b/Content/Animations/TTP_Animations/AimOffsetRightUp.uasset differ diff --git a/Content/Animations/TTP_Animations/AimOffsetUp.uasset b/Content/Animations/TTP_Animations/AimOffsetUp.uasset new file mode 100644 index 0000000..f6068a1 Binary files /dev/null and b/Content/Animations/TTP_Animations/AimOffsetUp.uasset differ diff --git a/Content/Animations/TTP_Animations/Death.uasset b/Content/Animations/TTP_Animations/Death.uasset new file mode 100644 index 0000000..a1d92c9 Binary files /dev/null and b/Content/Animations/TTP_Animations/Death.uasset differ diff --git a/Content/Animations/TTP_Animations/Equip.uasset b/Content/Animations/TTP_Animations/Equip.uasset new file mode 100644 index 0000000..eb1c579 Binary files /dev/null and b/Content/Animations/TTP_Animations/Equip.uasset differ diff --git a/Content/Animations/TTP_Animations/Idle.uasset b/Content/Animations/TTP_Animations/Idle.uasset new file mode 100644 index 0000000..841a31f Binary files /dev/null and b/Content/Animations/TTP_Animations/Idle.uasset differ diff --git a/Content/Animations/TTP_Animations/JumpEnd.uasset b/Content/Animations/TTP_Animations/JumpEnd.uasset new file mode 100644 index 0000000..83fa401 Binary files /dev/null and b/Content/Animations/TTP_Animations/JumpEnd.uasset differ diff --git a/Content/Animations/TTP_Animations/JumpLoop.uasset b/Content/Animations/TTP_Animations/JumpLoop.uasset new file mode 100644 index 0000000..7d3ee91 Binary files /dev/null and b/Content/Animations/TTP_Animations/JumpLoop.uasset differ diff --git a/Content/Animations/TTP_Animations/JumpStart.uasset b/Content/Animations/TTP_Animations/JumpStart.uasset new file mode 100644 index 0000000..ed88789 Binary files /dev/null and b/Content/Animations/TTP_Animations/JumpStart.uasset differ diff --git a/Content/Animations/TTP_Animations/Launcher_Reload.uasset b/Content/Animations/TTP_Animations/Launcher_Reload.uasset new file mode 100644 index 0000000..e713487 Binary files /dev/null and b/Content/Animations/TTP_Animations/Launcher_Reload.uasset differ diff --git a/Content/Animations/TTP_Animations/Reload.uasset b/Content/Animations/TTP_Animations/Reload.uasset new file mode 100644 index 0000000..f4b8229 Binary files /dev/null and b/Content/Animations/TTP_Animations/Reload.uasset differ diff --git a/Content/Animations/TTP_Animations/RoadieRun_Fwd.uasset b/Content/Animations/TTP_Animations/RoadieRun_Fwd.uasset new file mode 100644 index 0000000..9769d47 Binary files /dev/null and b/Content/Animations/TTP_Animations/RoadieRun_Fwd.uasset differ diff --git a/Content/Animations/TTP_Animations/Run_Bwd.uasset b/Content/Animations/TTP_Animations/Run_Bwd.uasset new file mode 100644 index 0000000..3d20898 Binary files /dev/null and b/Content/Animations/TTP_Animations/Run_Bwd.uasset differ diff --git a/Content/Animations/TTP_Animations/Run_Fwd.uasset b/Content/Animations/TTP_Animations/Run_Fwd.uasset new file mode 100644 index 0000000..ce4eac3 Binary files /dev/null and b/Content/Animations/TTP_Animations/Run_Fwd.uasset differ diff --git a/Content/Animations/TTP_Animations/Run_Lt.uasset b/Content/Animations/TTP_Animations/Run_Lt.uasset new file mode 100644 index 0000000..c4fddce Binary files /dev/null and b/Content/Animations/TTP_Animations/Run_Lt.uasset differ diff --git a/Content/Animations/TTP_Animations/Run_Rt.uasset b/Content/Animations/TTP_Animations/Run_Rt.uasset new file mode 100644 index 0000000..042ece6 Binary files /dev/null and b/Content/Animations/TTP_Animations/Run_Rt.uasset differ diff --git a/Content/Blueprints/Environment/BP_HoloController.uasset b/Content/Blueprints/Environment/BP_HoloController.uasset new file mode 100644 index 0000000..647f5c5 Binary files /dev/null and b/Content/Blueprints/Environment/BP_HoloController.uasset differ diff --git a/Content/Blueprints/Pawns/BotBehavior.uasset b/Content/Blueprints/Pawns/BotBehavior.uasset new file mode 100644 index 0000000..a17a8e5 Binary files /dev/null and b/Content/Blueprints/Pawns/BotBehavior.uasset differ diff --git a/Content/Blueprints/Pawns/BotBlackboard.uasset b/Content/Blueprints/Pawns/BotBlackboard.uasset new file mode 100644 index 0000000..9c9974a Binary files /dev/null and b/Content/Blueprints/Pawns/BotBlackboard.uasset differ diff --git a/Content/Blueprints/Pawns/BotPawn.uasset b/Content/Blueprints/Pawns/BotPawn.uasset new file mode 100644 index 0000000..ee24472 Binary files /dev/null and b/Content/Blueprints/Pawns/BotPawn.uasset differ diff --git a/Content/Blueprints/Pawns/BotSearchEnemyLOS.uasset b/Content/Blueprints/Pawns/BotSearchEnemyLOS.uasset new file mode 100644 index 0000000..0b3c819 Binary files /dev/null and b/Content/Blueprints/Pawns/BotSearchEnemyLOS.uasset differ diff --git a/Content/Blueprints/Pawns/BotShootEnemy.uasset b/Content/Blueprints/Pawns/BotShootEnemy.uasset new file mode 100644 index 0000000..05cc3f9 Binary files /dev/null and b/Content/Blueprints/Pawns/BotShootEnemy.uasset differ diff --git a/Content/Blueprints/Pawns/PlayerPawn.uasset b/Content/Blueprints/Pawns/PlayerPawn.uasset new file mode 100644 index 0000000..1f4178b Binary files /dev/null and b/Content/Blueprints/Pawns/PlayerPawn.uasset differ diff --git a/Content/Blueprints/Pickups/Pickup_AmmoGun.uasset b/Content/Blueprints/Pickups/Pickup_AmmoGun.uasset new file mode 100644 index 0000000..480067f Binary files /dev/null and b/Content/Blueprints/Pickups/Pickup_AmmoGun.uasset differ diff --git a/Content/Blueprints/Pickups/Pickup_AmmoLauncher.uasset b/Content/Blueprints/Pickups/Pickup_AmmoLauncher.uasset new file mode 100644 index 0000000..150ddc7 Binary files /dev/null and b/Content/Blueprints/Pickups/Pickup_AmmoLauncher.uasset differ diff --git a/Content/Blueprints/Pickups/Pickup_Health.uasset b/Content/Blueprints/Pickups/Pickup_Health.uasset new file mode 100644 index 0000000..ac8fe44 Binary files /dev/null and b/Content/Blueprints/Pickups/Pickup_Health.uasset differ diff --git a/Content/Blueprints/TaskAlwaysTrue.uasset b/Content/Blueprints/TaskAlwaysTrue.uasset new file mode 100644 index 0000000..71b4f51 Binary files /dev/null and b/Content/Blueprints/TaskAlwaysTrue.uasset differ diff --git a/Content/Blueprints/Weapons/ProjRocket.uasset b/Content/Blueprints/Weapons/ProjRocket.uasset new file mode 100644 index 0000000..0b19bd6 Binary files /dev/null and b/Content/Blueprints/Weapons/ProjRocket.uasset differ diff --git a/Content/Blueprints/Weapons/ProjRocket_Explosion.uasset b/Content/Blueprints/Weapons/ProjRocket_Explosion.uasset new file mode 100644 index 0000000..05b8ad6 Binary files /dev/null and b/Content/Blueprints/Weapons/ProjRocket_Explosion.uasset differ diff --git a/Content/Blueprints/Weapons/WeapGun.uasset b/Content/Blueprints/Weapons/WeapGun.uasset new file mode 100644 index 0000000..02a7d71 Binary files /dev/null and b/Content/Blueprints/Weapons/WeapGun.uasset differ diff --git a/Content/Blueprints/Weapons/WeapGun_FireCameraShake.uasset b/Content/Blueprints/Weapons/WeapGun_FireCameraShake.uasset new file mode 100644 index 0000000..4d19329 Binary files /dev/null and b/Content/Blueprints/Weapons/WeapGun_FireCameraShake.uasset differ diff --git a/Content/Blueprints/Weapons/WeapGun_Impacts.uasset b/Content/Blueprints/Weapons/WeapGun_Impacts.uasset new file mode 100644 index 0000000..4271343 Binary files /dev/null and b/Content/Blueprints/Weapons/WeapGun_Impacts.uasset differ diff --git a/Content/Blueprints/Weapons/WeapLauncher.uasset b/Content/Blueprints/Weapons/WeapLauncher.uasset new file mode 100644 index 0000000..db5f096 Binary files /dev/null and b/Content/Blueprints/Weapons/WeapLauncher.uasset differ diff --git a/Content/Blueprints/Weapons/WeapLauncher_FireCameraShake.uasset b/Content/Blueprints/Weapons/WeapLauncher_FireCameraShake.uasset new file mode 100644 index 0000000..a85d93e Binary files /dev/null and b/Content/Blueprints/Weapons/WeapLauncher_FireCameraShake.uasset differ diff --git a/Content/Characters/HeroFPP/HeroFPP.uasset b/Content/Characters/HeroFPP/HeroFPP.uasset new file mode 100644 index 0000000..1a99f27 Binary files /dev/null and b/Content/Characters/HeroFPP/HeroFPP.uasset differ diff --git a/Content/Characters/HeroFPP/HeroFPP_AnimationBlueprint.uasset b/Content/Characters/HeroFPP/HeroFPP_AnimationBlueprint.uasset new file mode 100644 index 0000000..2b4ade2 Binary files /dev/null and b/Content/Characters/HeroFPP/HeroFPP_AnimationBlueprint.uasset differ diff --git a/Content/Characters/HeroFPP/HeroFPP_Skeleton.uasset b/Content/Characters/HeroFPP/HeroFPP_Skeleton.uasset new file mode 100644 index 0000000..6c7dda6 Binary files /dev/null and b/Content/Characters/HeroFPP/HeroFPP_Skeleton.uasset differ diff --git a/Content/Characters/HeroTPP/HeroTPP.uasset b/Content/Characters/HeroTPP/HeroTPP.uasset new file mode 100644 index 0000000..d6ce931 Binary files /dev/null and b/Content/Characters/HeroTPP/HeroTPP.uasset differ diff --git a/Content/Characters/HeroTPP/HeroTPP_AimOffsets.uasset b/Content/Characters/HeroTPP/HeroTPP_AimOffsets.uasset new file mode 100644 index 0000000..4169512 Binary files /dev/null and b/Content/Characters/HeroTPP/HeroTPP_AimOffsets.uasset differ diff --git a/Content/Characters/HeroTPP/HeroTPP_AnimBlueprint.uasset b/Content/Characters/HeroTPP/HeroTPP_AnimBlueprint.uasset new file mode 100644 index 0000000..86097bf Binary files /dev/null and b/Content/Characters/HeroTPP/HeroTPP_AnimBlueprint.uasset differ diff --git a/Content/Characters/HeroTPP/HeroTPP_Death.uasset b/Content/Characters/HeroTPP/HeroTPP_Death.uasset new file mode 100644 index 0000000..e1e9a74 Binary files /dev/null and b/Content/Characters/HeroTPP/HeroTPP_Death.uasset differ diff --git a/Content/Characters/HeroTPP/HeroTPP_Equip.uasset b/Content/Characters/HeroTPP/HeroTPP_Equip.uasset new file mode 100644 index 0000000..e285d84 Binary files /dev/null and b/Content/Characters/HeroTPP/HeroTPP_Equip.uasset differ diff --git a/Content/Characters/HeroTPP/HeroTPP_Physics.uasset b/Content/Characters/HeroTPP/HeroTPP_Physics.uasset new file mode 100644 index 0000000..838ffd6 Binary files /dev/null and b/Content/Characters/HeroTPP/HeroTPP_Physics.uasset differ diff --git a/Content/Characters/HeroTPP/HeroTPP_Skeleton.uasset b/Content/Characters/HeroTPP/HeroTPP_Skeleton.uasset new file mode 100644 index 0000000..61df088 Binary files /dev/null and b/Content/Characters/HeroTPP/HeroTPP_Skeleton.uasset differ diff --git a/Content/Characters/HeroTPP/HeroTTP_LauncherReload.uasset b/Content/Characters/HeroTPP/HeroTTP_LauncherReload.uasset new file mode 100644 index 0000000..9f60d42 Binary files /dev/null and b/Content/Characters/HeroTPP/HeroTTP_LauncherReload.uasset differ diff --git a/Content/Characters/HeroTPP/HeroTTP_RifleReload.uasset b/Content/Characters/HeroTPP/HeroTTP_RifleReload.uasset new file mode 100644 index 0000000..a06aa10 Binary files /dev/null and b/Content/Characters/HeroTPP/HeroTTP_RifleReload.uasset differ diff --git a/Content/Characters/HeroTPP/RunTPP.uasset b/Content/Characters/HeroTPP/RunTPP.uasset new file mode 100644 index 0000000..abbad77 Binary files /dev/null and b/Content/Characters/HeroTPP/RunTPP.uasset differ diff --git a/Content/Characters/Materials/HeroTPP.uasset b/Content/Characters/Materials/HeroTPP.uasset new file mode 100644 index 0000000..8a8cacb Binary files /dev/null and b/Content/Characters/Materials/HeroTPP.uasset differ diff --git a/Content/Characters/Materials/ML_Base.uasset b/Content/Characters/Materials/ML_Base.uasset new file mode 100644 index 0000000..2c06f58 Binary files /dev/null and b/Content/Characters/Materials/ML_Base.uasset differ diff --git a/Content/Characters/Materials/ML_Emissive_Custom.uasset b/Content/Characters/Materials/ML_Emissive_Custom.uasset new file mode 100644 index 0000000..d1a49d1 Binary files /dev/null and b/Content/Characters/Materials/ML_Emissive_Custom.uasset differ diff --git a/Content/Characters/Materials/ML_Glass.uasset b/Content/Characters/Materials/ML_Glass.uasset new file mode 100644 index 0000000..ec2b54a Binary files /dev/null and b/Content/Characters/Materials/ML_Glass.uasset differ diff --git a/Content/Characters/Materials/ML_Metal_Painted.uasset b/Content/Characters/Materials/ML_Metal_Painted.uasset new file mode 100644 index 0000000..4fad7eb Binary files /dev/null and b/Content/Characters/Materials/ML_Metal_Painted.uasset differ diff --git a/Content/Characters/Materials/ML_Metal_light.uasset b/Content/Characters/Materials/ML_Metal_light.uasset new file mode 100644 index 0000000..3b2459c Binary files /dev/null and b/Content/Characters/Materials/ML_Metal_light.uasset differ diff --git a/Content/Characters/Materials/ML_Rubber_dark.uasset b/Content/Characters/Materials/ML_Rubber_dark.uasset new file mode 100644 index 0000000..c067ad1 Binary files /dev/null and b/Content/Characters/Materials/ML_Rubber_dark.uasset differ diff --git a/Content/Characters/Materials/ML_Rubber_light.uasset b/Content/Characters/Materials/ML_Rubber_light.uasset new file mode 100644 index 0000000..f4d8d79 Binary files /dev/null and b/Content/Characters/Materials/ML_Rubber_light.uasset differ diff --git a/Content/Characters/Textures/Chr_FPS_02_M.uasset b/Content/Characters/Textures/Chr_FPS_02_M.uasset new file mode 100644 index 0000000..3f94294 Binary files /dev/null and b/Content/Characters/Textures/Chr_FPS_02_M.uasset differ diff --git a/Content/Characters/Textures/Chr_FPS_D.uasset b/Content/Characters/Textures/Chr_FPS_D.uasset new file mode 100644 index 0000000..d0f8384 Binary files /dev/null and b/Content/Characters/Textures/Chr_FPS_D.uasset differ diff --git a/Content/Characters/Textures/Chr_FPS_Fabric_N_01.uasset b/Content/Characters/Textures/Chr_FPS_Fabric_N_01.uasset new file mode 100644 index 0000000..a803db3 Binary files /dev/null and b/Content/Characters/Textures/Chr_FPS_Fabric_N_01.uasset differ diff --git a/Content/Characters/Textures/Chr_FPS_M.uasset b/Content/Characters/Textures/Chr_FPS_M.uasset new file mode 100644 index 0000000..f2889d9 Binary files /dev/null and b/Content/Characters/Textures/Chr_FPS_M.uasset differ diff --git a/Content/Characters/Textures/Chr_FPS_M_03tga.uasset b/Content/Characters/Textures/Chr_FPS_M_03tga.uasset new file mode 100644 index 0000000..97245f0 Binary files /dev/null and b/Content/Characters/Textures/Chr_FPS_M_03tga.uasset differ diff --git a/Content/Characters/Textures/Chr_FPS_Metal_01.uasset b/Content/Characters/Textures/Chr_FPS_Metal_01.uasset new file mode 100644 index 0000000..792150d Binary files /dev/null and b/Content/Characters/Textures/Chr_FPS_Metal_01.uasset differ diff --git a/Content/Characters/Textures/Chr_FPS_Metal_02.uasset b/Content/Characters/Textures/Chr_FPS_Metal_02.uasset new file mode 100644 index 0000000..be790c1 Binary files /dev/null and b/Content/Characters/Textures/Chr_FPS_Metal_02.uasset differ diff --git a/Content/Characters/Textures/Chr_FPS_N.uasset b/Content/Characters/Textures/Chr_FPS_N.uasset new file mode 100644 index 0000000..801af4e Binary files /dev/null and b/Content/Characters/Textures/Chr_FPS_N.uasset differ diff --git a/Content/Characters/Textures/Tiles/T_Sctraches_01.uasset b/Content/Characters/Textures/Tiles/T_Sctraches_01.uasset new file mode 100644 index 0000000..0b1a6da Binary files /dev/null and b/Content/Characters/Textures/Tiles/T_Sctraches_01.uasset differ diff --git a/Content/DmgType_Explosion.uasset b/Content/DmgType_Explosion.uasset new file mode 100644 index 0000000..50e999c Binary files /dev/null and b/Content/DmgType_Explosion.uasset differ diff --git a/Content/DmgType_Instant.uasset b/Content/DmgType_Instant.uasset new file mode 100644 index 0000000..33c0bab Binary files /dev/null and b/Content/DmgType_Instant.uasset differ diff --git a/Content/Effects/ForceFeedback/FFE_Damaged.uasset b/Content/Effects/ForceFeedback/FFE_Damaged.uasset new file mode 100644 index 0000000..f1883da Binary files /dev/null and b/Content/Effects/ForceFeedback/FFE_Damaged.uasset differ diff --git a/Content/Effects/ForceFeedback/FFE_Fire.uasset b/Content/Effects/ForceFeedback/FFE_Fire.uasset new file mode 100644 index 0000000..c96e034 Binary files /dev/null and b/Content/Effects/ForceFeedback/FFE_Fire.uasset differ diff --git a/Content/Effects/ForceFeedback/FFE_Killed.uasset b/Content/Effects/ForceFeedback/FFE_Killed.uasset new file mode 100644 index 0000000..852c7b1 Binary files /dev/null and b/Content/Effects/ForceFeedback/FFE_Killed.uasset differ diff --git a/Content/Effects/Materials/Air/MI_cloud_white_1.uasset b/Content/Effects/Materials/Air/MI_cloud_white_1.uasset new file mode 100644 index 0000000..8fcdb15 Binary files /dev/null and b/Content/Effects/Materials/Air/MI_cloud_white_1.uasset differ diff --git a/Content/Effects/Materials/Air/MI_cloud_white_2.uasset b/Content/Effects/Materials/Air/MI_cloud_white_2.uasset new file mode 100644 index 0000000..f379505 Binary files /dev/null and b/Content/Effects/Materials/Air/MI_cloud_white_2.uasset differ diff --git a/Content/Effects/Materials/Air/MI_cloud_white_3.uasset b/Content/Effects/Materials/Air/MI_cloud_white_3.uasset new file mode 100644 index 0000000..2c64983 Binary files /dev/null and b/Content/Effects/Materials/Air/MI_cloud_white_3.uasset differ diff --git a/Content/Effects/Materials/Air/M_Cloud_1_particle.uasset b/Content/Effects/Materials/Air/M_Cloud_1_particle.uasset new file mode 100644 index 0000000..cd22cd8 Binary files /dev/null and b/Content/Effects/Materials/Air/M_Cloud_1_particle.uasset differ diff --git a/Content/Effects/Materials/Air/M_Cloud_1_particle_Inst.uasset b/Content/Effects/Materials/Air/M_Cloud_1_particle_Inst.uasset new file mode 100644 index 0000000..47fc165 Binary files /dev/null and b/Content/Effects/Materials/Air/M_Cloud_1_particle_Inst.uasset differ diff --git a/Content/Effects/Materials/Air/M_Cloud_2_particle_Inst.uasset b/Content/Effects/Materials/Air/M_Cloud_2_particle_Inst.uasset new file mode 100644 index 0000000..0034b17 Binary files /dev/null and b/Content/Effects/Materials/Air/M_Cloud_2_particle_Inst.uasset differ diff --git a/Content/Effects/Materials/Air/M_Cloud_2_particle_Inst_2.uasset b/Content/Effects/Materials/Air/M_Cloud_2_particle_Inst_2.uasset new file mode 100644 index 0000000..b270f79 Binary files /dev/null and b/Content/Effects/Materials/Air/M_Cloud_2_particle_Inst_2.uasset differ diff --git a/Content/Effects/Materials/Air/M_cloud.uasset b/Content/Effects/Materials/Air/M_cloud.uasset new file mode 100644 index 0000000..0a7add4 Binary files /dev/null and b/Content/Effects/Materials/Air/M_cloud.uasset differ diff --git a/Content/Effects/Materials/Air/M_dust_specs_sparkling.uasset b/Content/Effects/Materials/Air/M_dust_specs_sparkling.uasset new file mode 100644 index 0000000..30a396f Binary files /dev/null and b/Content/Effects/Materials/Air/M_dust_specs_sparkling.uasset differ diff --git a/Content/Effects/Materials/Air/M_light_shaft.uasset b/Content/Effects/Materials/Air/M_light_shaft.uasset new file mode 100644 index 0000000..f622d84 Binary files /dev/null and b/Content/Effects/Materials/Air/M_light_shaft.uasset differ diff --git a/Content/Effects/Materials/Air/M_light_shaft_Inst.uasset b/Content/Effects/Materials/Air/M_light_shaft_Inst.uasset new file mode 100644 index 0000000..7746331 Binary files /dev/null and b/Content/Effects/Materials/Air/M_light_shaft_Inst.uasset differ diff --git a/Content/Effects/Materials/Air/M_light_shaft_particle.uasset b/Content/Effects/Materials/Air/M_light_shaft_particle.uasset new file mode 100644 index 0000000..b0d2edc Binary files /dev/null and b/Content/Effects/Materials/Air/M_light_shaft_particle.uasset differ diff --git a/Content/Effects/Materials/Air/M_lightning.uasset b/Content/Effects/Materials/Air/M_lightning.uasset new file mode 100644 index 0000000..8e01424 Binary files /dev/null and b/Content/Effects/Materials/Air/M_lightning.uasset differ diff --git a/Content/Effects/Materials/Energy/M_Electrical.uasset b/Content/Effects/Materials/Energy/M_Electrical.uasset new file mode 100644 index 0000000..6d363af Binary files /dev/null and b/Content/Effects/Materials/Energy/M_Electrical.uasset differ diff --git a/Content/Effects/Materials/Energy/M_WP_ShockRifle_Ring.uasset b/Content/Effects/Materials/Energy/M_WP_ShockRifle_Ring.uasset new file mode 100644 index 0000000..73dc708 Binary files /dev/null and b/Content/Effects/Materials/Energy/M_WP_ShockRifle_Ring.uasset differ diff --git a/Content/Effects/Materials/Explosion/M_explosion_subUV.uasset b/Content/Effects/Materials/Explosion/M_explosion_subUV.uasset new file mode 100644 index 0000000..67f5bc2 Binary files /dev/null and b/Content/Effects/Materials/Explosion/M_explosion_subUV.uasset differ diff --git a/Content/Effects/Materials/Gameplay/M_impact_splash_subUV.uasset b/Content/Effects/Materials/Gameplay/M_impact_splash_subUV.uasset new file mode 100644 index 0000000..25b89f9 Binary files /dev/null and b/Content/Effects/Materials/Gameplay/M_impact_splash_subUV.uasset differ diff --git a/Content/Effects/Materials/Gameplay/M_vehicle_bottom.uasset b/Content/Effects/Materials/Gameplay/M_vehicle_bottom.uasset new file mode 100644 index 0000000..4f0a8c0 Binary files /dev/null and b/Content/Effects/Materials/Gameplay/M_vehicle_bottom.uasset differ diff --git a/Content/Effects/Materials/Gameplay/M_vehicle_energy.uasset b/Content/Effects/Materials/Gameplay/M_vehicle_energy.uasset new file mode 100644 index 0000000..d5f5fc3 Binary files /dev/null and b/Content/Effects/Materials/Gameplay/M_vehicle_energy.uasset differ diff --git a/Content/Effects/Materials/Gameplay/M_vehicle_glass.uasset b/Content/Effects/Materials/Gameplay/M_vehicle_glass.uasset new file mode 100644 index 0000000..657c4ed Binary files /dev/null and b/Content/Effects/Materials/Gameplay/M_vehicle_glass.uasset differ diff --git a/Content/Effects/Materials/Gameplay/M_vehicle_light.uasset b/Content/Effects/Materials/Gameplay/M_vehicle_light.uasset new file mode 100644 index 0000000..7eaf622 Binary files /dev/null and b/Content/Effects/Materials/Gameplay/M_vehicle_light.uasset differ diff --git a/Content/Effects/Materials/Gameplay/M_vehicle_master.uasset b/Content/Effects/Materials/Gameplay/M_vehicle_master.uasset new file mode 100644 index 0000000..167ecc9 Binary files /dev/null and b/Content/Effects/Materials/Gameplay/M_vehicle_master.uasset differ diff --git a/Content/Effects/Materials/Gameplay/M_vehicle_master_vertexcolor.uasset b/Content/Effects/Materials/Gameplay/M_vehicle_master_vertexcolor.uasset new file mode 100644 index 0000000..17a6e90 Binary files /dev/null and b/Content/Effects/Materials/Gameplay/M_vehicle_master_vertexcolor.uasset differ diff --git a/Content/Effects/Materials/Gameplay/M_vehicle_red_light.uasset b/Content/Effects/Materials/Gameplay/M_vehicle_red_light.uasset new file mode 100644 index 0000000..3e88f5f Binary files /dev/null and b/Content/Effects/Materials/Gameplay/M_vehicle_red_light.uasset differ diff --git a/Content/Effects/Materials/Gameplay/Pickups/M_PickUp_Master.uasset b/Content/Effects/Materials/Gameplay/Pickups/M_PickUp_Master.uasset new file mode 100644 index 0000000..66b9e30 Binary files /dev/null and b/Content/Effects/Materials/Gameplay/Pickups/M_PickUp_Master.uasset differ diff --git a/Content/Effects/Materials/LensFlare/M_LensFlare_02.uasset b/Content/Effects/Materials/LensFlare/M_LensFlare_02.uasset new file mode 100644 index 0000000..1853463 Binary files /dev/null and b/Content/Effects/Materials/LensFlare/M_LensFlare_02.uasset differ diff --git a/Content/Effects/Materials/LensFlare/M_Lensflare_01.uasset b/Content/Effects/Materials/LensFlare/M_Lensflare_01.uasset new file mode 100644 index 0000000..113f2e2 Binary files /dev/null and b/Content/Effects/Materials/LensFlare/M_Lensflare_01.uasset differ diff --git a/Content/Effects/Materials/M_ramp.uasset b/Content/Effects/Materials/M_ramp.uasset new file mode 100644 index 0000000..c283341 Binary files /dev/null and b/Content/Effects/Materials/M_ramp.uasset differ diff --git a/Content/Effects/Materials/Ramp/M_SoftGradient_Muzzle.uasset b/Content/Effects/Materials/Ramp/M_SoftGradient_Muzzle.uasset new file mode 100644 index 0000000..c1da6e8 Binary files /dev/null and b/Content/Effects/Materials/Ramp/M_SoftGradient_Muzzle.uasset differ diff --git a/Content/Effects/Materials/Ramp/M_radial_ramp_soft.uasset b/Content/Effects/Materials/Ramp/M_radial_ramp_soft.uasset new file mode 100644 index 0000000..5820a03 Binary files /dev/null and b/Content/Effects/Materials/Ramp/M_radial_ramp_soft.uasset differ diff --git a/Content/Effects/Materials/Smoke/M_ImpactSmoke.uasset b/Content/Effects/Materials/Smoke/M_ImpactSmoke.uasset new file mode 100644 index 0000000..75f3e87 Binary files /dev/null and b/Content/Effects/Materials/Smoke/M_ImpactSmoke.uasset differ diff --git a/Content/Effects/Materials/Smoke/M_MuzzleSmokeDetail_GPU.uasset b/Content/Effects/Materials/Smoke/M_MuzzleSmokeDetail_GPU.uasset new file mode 100644 index 0000000..2980b5a Binary files /dev/null and b/Content/Effects/Materials/Smoke/M_MuzzleSmokeDetail_GPU.uasset differ diff --git a/Content/Effects/Materials/Smoke/M_SmokeBall.uasset b/Content/Effects/Materials/Smoke/M_SmokeBall.uasset new file mode 100644 index 0000000..5c7db5c Binary files /dev/null and b/Content/Effects/Materials/Smoke/M_SmokeBall.uasset differ diff --git a/Content/Effects/Materials/Weapon/M_Impact_Decal.uasset b/Content/Effects/Materials/Weapon/M_Impact_Decal.uasset new file mode 100644 index 0000000..3635391 Binary files /dev/null and b/Content/Effects/Materials/Weapon/M_Impact_Decal.uasset differ diff --git a/Content/Effects/Materials/Weapon/M_MuzzleGlow.uasset b/Content/Effects/Materials/Weapon/M_MuzzleGlow.uasset new file mode 100644 index 0000000..2d3057b Binary files /dev/null and b/Content/Effects/Materials/Weapon/M_MuzzleGlow.uasset differ diff --git a/Content/Effects/Materials/Weapon/M_WP_LinkGun_MF_3_new.uasset b/Content/Effects/Materials/Weapon/M_WP_LinkGun_MF_3_new.uasset new file mode 100644 index 0000000..c56f5e2 Binary files /dev/null and b/Content/Effects/Materials/Weapon/M_WP_LinkGun_MF_3_new.uasset differ diff --git a/Content/Effects/Materials/Weapon/M_WP_Linkgun_Connection.uasset b/Content/Effects/Materials/Weapon/M_WP_Linkgun_Connection.uasset new file mode 100644 index 0000000..8374524 Binary files /dev/null and b/Content/Effects/Materials/Weapon/M_WP_Linkgun_Connection.uasset differ diff --git a/Content/Effects/Materials/Weapon/M_WP_ShockRifle_Panning_Beam.uasset b/Content/Effects/Materials/Weapon/M_WP_ShockRifle_Panning_Beam.uasset new file mode 100644 index 0000000..1962ab1 Binary files /dev/null and b/Content/Effects/Materials/Weapon/M_WP_ShockRifle_Panning_Beam.uasset differ diff --git a/Content/Effects/Materials/Weapon/M_muzzle_flash_subUV.uasset b/Content/Effects/Materials/Weapon/M_muzzle_flash_subUV.uasset new file mode 100644 index 0000000..363b0b3 Binary files /dev/null and b/Content/Effects/Materials/Weapon/M_muzzle_flash_subUV.uasset differ diff --git a/Content/Effects/Materials/spark/M_spark_subUV.uasset b/Content/Effects/Materials/spark/M_spark_subUV.uasset new file mode 100644 index 0000000..c743830 Binary files /dev/null and b/Content/Effects/Materials/spark/M_spark_subUV.uasset differ diff --git a/Content/Effects/Materials/water/M_Steam_SubUV.uasset b/Content/Effects/Materials/water/M_Steam_SubUV.uasset new file mode 100644 index 0000000..17ceb50 Binary files /dev/null and b/Content/Effects/Materials/water/M_Steam_SubUV.uasset differ diff --git a/Content/Effects/Meshes/Gameplay/Pickups/Ammo.uasset b/Content/Effects/Meshes/Gameplay/Pickups/Ammo.uasset new file mode 100644 index 0000000..0cb92e8 Binary files /dev/null and b/Content/Effects/Meshes/Gameplay/Pickups/Ammo.uasset differ diff --git a/Content/Effects/Meshes/Gameplay/Pickups/Launcher.uasset b/Content/Effects/Meshes/Gameplay/Pickups/Launcher.uasset new file mode 100644 index 0000000..2c90aaa Binary files /dev/null and b/Content/Effects/Meshes/Gameplay/Pickups/Launcher.uasset differ diff --git a/Content/Effects/Meshes/Gameplay/Pickups/SM_Health.uasset b/Content/Effects/Meshes/Gameplay/Pickups/SM_Health.uasset new file mode 100644 index 0000000..3fb61a9 Binary files /dev/null and b/Content/Effects/Meshes/Gameplay/Pickups/SM_Health.uasset differ diff --git a/Content/Effects/Meshes/General/SM_flying_car_1.uasset b/Content/Effects/Meshes/General/SM_flying_car_1.uasset new file mode 100644 index 0000000..3025e65 Binary files /dev/null and b/Content/Effects/Meshes/General/SM_flying_car_1.uasset differ diff --git a/Content/Effects/Meshes/General/SM_flying_car_2.uasset b/Content/Effects/Meshes/General/SM_flying_car_2.uasset new file mode 100644 index 0000000..816176d Binary files /dev/null and b/Content/Effects/Meshes/General/SM_flying_car_2.uasset differ diff --git a/Content/Effects/Meshes/General/SM_flying_car_3.uasset b/Content/Effects/Meshes/General/SM_flying_car_3.uasset new file mode 100644 index 0000000..5783d31 Binary files /dev/null and b/Content/Effects/Meshes/General/SM_flying_car_3.uasset differ diff --git a/Content/Effects/Meshes/General/SM_flying_car_4.uasset b/Content/Effects/Meshes/General/SM_flying_car_4.uasset new file mode 100644 index 0000000..9aeca9a Binary files /dev/null and b/Content/Effects/Meshes/General/SM_flying_car_4.uasset differ diff --git a/Content/Effects/Meshes/General/SM_lightning.uasset b/Content/Effects/Meshes/General/SM_lightning.uasset new file mode 100644 index 0000000..d341dff Binary files /dev/null and b/Content/Effects/Meshes/General/SM_lightning.uasset differ diff --git a/Content/Effects/Meshes/General/S_Sphere.uasset b/Content/Effects/Meshes/General/S_Sphere.uasset new file mode 100644 index 0000000..97fb16f Binary files /dev/null and b/Content/Effects/Meshes/General/S_Sphere.uasset differ diff --git a/Content/Effects/Meshes/General/hologram_streak.uasset b/Content/Effects/Meshes/General/hologram_streak.uasset new file mode 100644 index 0000000..f19dd5c Binary files /dev/null and b/Content/Effects/Meshes/General/hologram_streak.uasset differ diff --git a/Content/Effects/Meshes/Weapon/SM_MuzzleCone.uasset b/Content/Effects/Meshes/Weapon/SM_MuzzleCone.uasset new file mode 100644 index 0000000..f9e9009 Binary files /dev/null and b/Content/Effects/Meshes/Weapon/SM_MuzzleCone.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_Steam_falling.uasset b/Content/Effects/ParticleSystems/Environment/P_Steam_falling.uasset new file mode 100644 index 0000000..93a9526 Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_Steam_falling.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_Steam_falling_line.uasset b/Content/Effects/ParticleSystems/Environment/P_Steam_falling_line.uasset new file mode 100644 index 0000000..a92a45c Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_Steam_falling_line.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_beam_light.uasset b/Content/Effects/ParticleSystems/Environment/P_beam_light.uasset new file mode 100644 index 0000000..156580b Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_beam_light.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_cars_straight_line.uasset b/Content/Effects/ParticleSystems/Environment/P_cars_straight_line.uasset new file mode 100644 index 0000000..1475886 Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_cars_straight_line.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_cars_turning.uasset b/Content/Effects/ParticleSystems/Environment/P_cars_turning.uasset new file mode 100644 index 0000000..589836a Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_cars_turning.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_cars_waving.uasset b/Content/Effects/ParticleSystems/Environment/P_cars_waving.uasset new file mode 100644 index 0000000..e1c69a9 Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_cars_waving.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_clouds_1.uasset b/Content/Effects/ParticleSystems/Environment/P_clouds_1.uasset new file mode 100644 index 0000000..fede788 Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_clouds_1.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_clouds_2.uasset b/Content/Effects/ParticleSystems/Environment/P_clouds_2.uasset new file mode 100644 index 0000000..66481f7 Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_clouds_2.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_clouds_2_brighter.uasset b/Content/Effects/ParticleSystems/Environment/P_clouds_2_brighter.uasset new file mode 100644 index 0000000..6b13c2b Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_clouds_2_brighter.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_clouds_highrise.uasset b/Content/Effects/ParticleSystems/Environment/P_clouds_highrise.uasset new file mode 100644 index 0000000..d2f135f Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_clouds_highrise.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_clouds_highrise_sky.uasset b/Content/Effects/ParticleSystems/Environment/P_clouds_highrise_sky.uasset new file mode 100644 index 0000000..ea5cb26 Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_clouds_highrise_sky.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_clouds_highrise_sky_single.uasset b/Content/Effects/ParticleSystems/Environment/P_clouds_highrise_sky_single.uasset new file mode 100644 index 0000000..c76d5bc Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_clouds_highrise_sky_single.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_dust_sparkles.uasset b/Content/Effects/ParticleSystems/Environment/P_dust_sparkles.uasset new file mode 100644 index 0000000..d8e1b12 Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_dust_sparkles.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_hologram.uasset b/Content/Effects/ParticleSystems/Environment/P_hologram.uasset new file mode 100644 index 0000000..8694f15 Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_hologram.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_light_shafts.uasset b/Content/Effects/ParticleSystems/Environment/P_light_shafts.uasset new file mode 100644 index 0000000..226ce6c Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_light_shafts.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_lightning_beam.uasset b/Content/Effects/ParticleSystems/Environment/P_lightning_beam.uasset new file mode 100644 index 0000000..2d4b1bd Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_lightning_beam.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_lightning_point.uasset b/Content/Effects/ParticleSystems/Environment/P_lightning_point.uasset new file mode 100644 index 0000000..c35fd10 Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_lightning_point.uasset differ diff --git a/Content/Effects/ParticleSystems/Environment/P_waterfall_mist.uasset b/Content/Effects/ParticleSystems/Environment/P_waterfall_mist.uasset new file mode 100644 index 0000000..941c3ee Binary files /dev/null and b/Content/Effects/ParticleSystems/Environment/P_waterfall_mist.uasset differ diff --git a/Content/Effects/ParticleSystems/Gameplay/Pick-ups/P_AssaultRifle_AMmo.uasset b/Content/Effects/ParticleSystems/Gameplay/Pick-ups/P_AssaultRifle_AMmo.uasset new file mode 100644 index 0000000..103cc6f Binary files /dev/null and b/Content/Effects/ParticleSystems/Gameplay/Pick-ups/P_AssaultRifle_AMmo.uasset differ diff --git a/Content/Effects/ParticleSystems/Gameplay/Pick-ups/P_Health.uasset b/Content/Effects/ParticleSystems/Gameplay/Pick-ups/P_Health.uasset new file mode 100644 index 0000000..bb5cff2 Binary files /dev/null and b/Content/Effects/ParticleSystems/Gameplay/Pick-ups/P_Health.uasset differ diff --git a/Content/Effects/ParticleSystems/Gameplay/Pick-ups/P_Launcher_Ammo.uasset b/Content/Effects/ParticleSystems/Gameplay/Pick-ups/P_Launcher_Ammo.uasset new file mode 100644 index 0000000..b156183 Binary files /dev/null and b/Content/Effects/ParticleSystems/Gameplay/Pick-ups/P_Launcher_Ammo.uasset differ diff --git a/Content/Effects/ParticleSystems/Gameplay/Player/P_body_bullet_impact.uasset b/Content/Effects/ParticleSystems/Gameplay/Player/P_body_bullet_impact.uasset new file mode 100644 index 0000000..bf7d0c4 Binary files /dev/null and b/Content/Effects/ParticleSystems/Gameplay/Player/P_body_bullet_impact.uasset differ diff --git a/Content/Effects/ParticleSystems/Weapons/AssaultRifle/Impacts/P_AssaultRifle_IH.uasset b/Content/Effects/ParticleSystems/Weapons/AssaultRifle/Impacts/P_AssaultRifle_IH.uasset new file mode 100644 index 0000000..087014f Binary files /dev/null and b/Content/Effects/ParticleSystems/Weapons/AssaultRifle/Impacts/P_AssaultRifle_IH.uasset differ diff --git a/Content/Effects/ParticleSystems/Weapons/AssaultRifle/Muzzle/P_AssaultRifle_MF.uasset b/Content/Effects/ParticleSystems/Weapons/AssaultRifle/Muzzle/P_AssaultRifle_MF.uasset new file mode 100644 index 0000000..65cfce5 Binary files /dev/null and b/Content/Effects/ParticleSystems/Weapons/AssaultRifle/Muzzle/P_AssaultRifle_MF.uasset differ diff --git a/Content/Effects/ParticleSystems/Weapons/AssaultRifle/Muzzle/P_AssaultRifle_Trail.uasset b/Content/Effects/ParticleSystems/Weapons/AssaultRifle/Muzzle/P_AssaultRifle_Trail.uasset new file mode 100644 index 0000000..751e28b Binary files /dev/null and b/Content/Effects/ParticleSystems/Weapons/AssaultRifle/Muzzle/P_AssaultRifle_Trail.uasset differ diff --git a/Content/Effects/ParticleSystems/Weapons/RocketLauncher/Impact/P_Launcher_IH.uasset b/Content/Effects/ParticleSystems/Weapons/RocketLauncher/Impact/P_Launcher_IH.uasset new file mode 100644 index 0000000..400cbf0 Binary files /dev/null and b/Content/Effects/ParticleSystems/Weapons/RocketLauncher/Impact/P_Launcher_IH.uasset differ diff --git a/Content/Effects/ParticleSystems/Weapons/RocketLauncher/Muzzle/P_Launcher_MF.uasset b/Content/Effects/ParticleSystems/Weapons/RocketLauncher/Muzzle/P_Launcher_MF.uasset new file mode 100644 index 0000000..af7423c Binary files /dev/null and b/Content/Effects/ParticleSystems/Weapons/RocketLauncher/Muzzle/P_Launcher_MF.uasset differ diff --git a/Content/Effects/ParticleSystems/Weapons/RocketLauncher/Muzzle/P_Launcher_proj.uasset b/Content/Effects/ParticleSystems/Weapons/RocketLauncher/Muzzle/P_Launcher_proj.uasset new file mode 100644 index 0000000..ae58926 Binary files /dev/null and b/Content/Effects/ParticleSystems/Weapons/RocketLauncher/Muzzle/P_Launcher_proj.uasset differ diff --git a/Content/Effects/Textures/Air/T_cloud_1.uasset b/Content/Effects/Textures/Air/T_cloud_1.uasset new file mode 100644 index 0000000..11e53f8 Binary files /dev/null and b/Content/Effects/Textures/Air/T_cloud_1.uasset differ diff --git a/Content/Effects/Textures/Air/T_cloud_2.uasset b/Content/Effects/Textures/Air/T_cloud_2.uasset new file mode 100644 index 0000000..5128c60 Binary files /dev/null and b/Content/Effects/Textures/Air/T_cloud_2.uasset differ diff --git a/Content/Effects/Textures/Air/T_cloud_4.uasset b/Content/Effects/Textures/Air/T_cloud_4.uasset new file mode 100644 index 0000000..b9dbcc0 Binary files /dev/null and b/Content/Effects/Textures/Air/T_cloud_4.uasset differ diff --git a/Content/Effects/Textures/Air/T_soft_smoke.uasset b/Content/Effects/Textures/Air/T_soft_smoke.uasset new file mode 100644 index 0000000..242f817 Binary files /dev/null and b/Content/Effects/Textures/Air/T_soft_smoke.uasset differ diff --git a/Content/Effects/Textures/Energy/T_EFX_Beam_Electricity_02.uasset b/Content/Effects/Textures/Energy/T_EFX_Beam_Electricity_02.uasset new file mode 100644 index 0000000..119541e Binary files /dev/null and b/Content/Effects/Textures/Energy/T_EFX_Beam_Electricity_02.uasset differ diff --git a/Content/Effects/Textures/Energy/T_EFX_Energy_Arc_SubUV.uasset b/Content/Effects/Textures/Energy/T_EFX_Energy_Arc_SubUV.uasset new file mode 100644 index 0000000..2db5f26 Binary files /dev/null and b/Content/Effects/Textures/Energy/T_EFX_Energy_Arc_SubUV.uasset differ diff --git a/Content/Effects/Textures/Energy/T_EFX_Energy_Loop_Free.uasset b/Content/Effects/Textures/Energy/T_EFX_Energy_Loop_Free.uasset new file mode 100644 index 0000000..861b36a Binary files /dev/null and b/Content/Effects/Textures/Energy/T_EFX_Energy_Loop_Free.uasset differ diff --git a/Content/Effects/Textures/LensFlare/Flare_02.uasset b/Content/Effects/Textures/LensFlare/Flare_02.uasset new file mode 100644 index 0000000..cc979b7 Binary files /dev/null and b/Content/Effects/Textures/LensFlare/Flare_02.uasset differ diff --git a/Content/Effects/Textures/LensFlare/T_Lensflare_01.uasset b/Content/Effects/Textures/LensFlare/T_Lensflare_01.uasset new file mode 100644 index 0000000..e850f0c Binary files /dev/null and b/Content/Effects/Textures/LensFlare/T_Lensflare_01.uasset differ diff --git a/Content/Effects/Textures/Mask/T_SquareMaskTightEdge.uasset b/Content/Effects/Textures/Mask/T_SquareMaskTightEdge.uasset new file mode 100644 index 0000000..6339bda Binary files /dev/null and b/Content/Effects/Textures/Mask/T_SquareMaskTightEdge.uasset differ diff --git a/Content/Effects/Textures/Noise/T_FX_RoilingFlameModulate.uasset b/Content/Effects/Textures/Noise/T_FX_RoilingFlameModulate.uasset new file mode 100644 index 0000000..4872097 Binary files /dev/null and b/Content/Effects/Textures/Noise/T_FX_RoilingFlameModulate.uasset differ diff --git a/Content/Effects/Textures/Noise/T_TileNoise.uasset b/Content/Effects/Textures/Noise/T_TileNoise.uasset new file mode 100644 index 0000000..821a0c4 Binary files /dev/null and b/Content/Effects/Textures/Noise/T_TileNoise.uasset differ diff --git a/Content/Effects/Textures/Noise/T_sparks_01.uasset b/Content/Effects/Textures/Noise/T_sparks_01.uasset new file mode 100644 index 0000000..063b037 Binary files /dev/null and b/Content/Effects/Textures/Noise/T_sparks_01.uasset differ diff --git a/Content/Effects/Textures/Ramp/T_FX_CameraDistort.uasset b/Content/Effects/Textures/Ramp/T_FX_CameraDistort.uasset new file mode 100644 index 0000000..c7cd3bd Binary files /dev/null and b/Content/Effects/Textures/Ramp/T_FX_CameraDistort.uasset differ diff --git a/Content/Effects/Textures/Ramp/T_FX_ColorDistort.uasset b/Content/Effects/Textures/Ramp/T_FX_ColorDistort.uasset new file mode 100644 index 0000000..a502d97 Binary files /dev/null and b/Content/Effects/Textures/Ramp/T_FX_ColorDistort.uasset differ diff --git a/Content/Effects/Textures/Smoke/T_Clouds_01.uasset b/Content/Effects/Textures/Smoke/T_Clouds_01.uasset new file mode 100644 index 0000000..f58e2d0 Binary files /dev/null and b/Content/Effects/Textures/Smoke/T_Clouds_01.uasset differ diff --git a/Content/Effects/Textures/Smoke/T_SmokeBall_01_8_8.uasset b/Content/Effects/Textures/Smoke/T_SmokeBall_01_8_8.uasset new file mode 100644 index 0000000..0526fc4 Binary files /dev/null and b/Content/Effects/Textures/Smoke/T_SmokeBall_01_8_8.uasset differ diff --git a/Content/Effects/Textures/Spark/T_SparkCore_2X2.uasset b/Content/Effects/Textures/Spark/T_SparkCore_2X2.uasset new file mode 100644 index 0000000..9ffe2fd Binary files /dev/null and b/Content/Effects/Textures/Spark/T_SparkCore_2X2.uasset differ diff --git a/Content/Effects/Textures/T_blood_various.uasset b/Content/Effects/Textures/T_blood_various.uasset new file mode 100644 index 0000000..7573bc5 Binary files /dev/null and b/Content/Effects/Textures/T_blood_various.uasset differ diff --git a/Content/Effects/Textures/T_blood_various_N.uasset b/Content/Effects/Textures/T_blood_various_N.uasset new file mode 100644 index 0000000..05c6a17 Binary files /dev/null and b/Content/Effects/Textures/T_blood_various_N.uasset differ diff --git a/Content/Effects/Textures/Water/T_Caustics_2.uasset b/Content/Effects/Textures/Water/T_Caustics_2.uasset new file mode 100644 index 0000000..6d61b69 Binary files /dev/null and b/Content/Effects/Textures/Water/T_Caustics_2.uasset differ diff --git a/Content/Effects/Textures/Water/T_Steam_02_Packed.uasset b/Content/Effects/Textures/Water/T_Steam_02_Packed.uasset new file mode 100644 index 0000000..17b564f Binary files /dev/null and b/Content/Effects/Textures/Water/T_Steam_02_Packed.uasset differ diff --git a/Content/Effects/Textures/Water/T_Steam_subUV.uasset b/Content/Effects/Textures/Water/T_Steam_subUV.uasset new file mode 100644 index 0000000..45dd3a4 Binary files /dev/null and b/Content/Effects/Textures/Water/T_Steam_subUV.uasset differ diff --git a/Content/Effects/Textures/Water/distortion_01.uasset b/Content/Effects/Textures/Water/distortion_01.uasset new file mode 100644 index 0000000..d08706c Binary files /dev/null and b/Content/Effects/Textures/Water/distortion_01.uasset differ diff --git a/Content/Effects/Textures/Weapon/T_Coil.uasset b/Content/Effects/Textures/Weapon/T_Coil.uasset new file mode 100644 index 0000000..41898e9 Binary files /dev/null and b/Content/Effects/Textures/Weapon/T_Coil.uasset differ diff --git a/Content/Effects/Textures/Weapon/T_bullet_hole_2.uasset b/Content/Effects/Textures/Weapon/T_bullet_hole_2.uasset new file mode 100644 index 0000000..39f2be9 Binary files /dev/null and b/Content/Effects/Textures/Weapon/T_bullet_hole_2.uasset differ diff --git a/Content/Effects/Textures/Weapon/T_muzzle_flashes.uasset b/Content/Effects/Textures/Weapon/T_muzzle_flashes.uasset new file mode 100644 index 0000000..d208d60 Binary files /dev/null and b/Content/Effects/Textures/Weapon/T_muzzle_flashes.uasset differ diff --git a/Content/Effects/Textures/fire/T_explo_SubUV.uasset b/Content/Effects/Textures/fire/T_explo_SubUV.uasset new file mode 100644 index 0000000..5be814c Binary files /dev/null and b/Content/Effects/Textures/fire/T_explo_SubUV.uasset differ diff --git a/Content/Environment/Materials/Doors_1.uasset b/Content/Environment/Materials/Doors_1.uasset new file mode 100644 index 0000000..20686ca Binary files /dev/null and b/Content/Environment/Materials/Doors_1.uasset differ diff --git a/Content/Environment/Materials/Holo_City.uasset b/Content/Environment/Materials/Holo_City.uasset new file mode 100644 index 0000000..fcc5db8 Binary files /dev/null and b/Content/Environment/Materials/Holo_City.uasset differ diff --git a/Content/Environment/Materials/Holo_l1.uasset b/Content/Environment/Materials/Holo_l1.uasset new file mode 100644 index 0000000..223f149 Binary files /dev/null and b/Content/Environment/Materials/Holo_l1.uasset differ diff --git a/Content/Environment/Materials/Holo_l2.uasset b/Content/Environment/Materials/Holo_l2.uasset new file mode 100644 index 0000000..137f7b9 Binary files /dev/null and b/Content/Environment/Materials/Holo_l2.uasset differ diff --git a/Content/Environment/Materials/M_Cloud_2.uasset b/Content/Environment/Materials/M_Cloud_2.uasset new file mode 100644 index 0000000..45c2e36 Binary files /dev/null and b/Content/Environment/Materials/M_Cloud_2.uasset differ diff --git a/Content/Environment/Materials/M_Enviro_assets_01.uasset b/Content/Environment/Materials/M_Enviro_assets_01.uasset new file mode 100644 index 0000000..0a01919 Binary files /dev/null and b/Content/Environment/Materials/M_Enviro_assets_01.uasset differ diff --git a/Content/Environment/Materials/M_FFA_ConcreteWallPlate_01.uasset b/Content/Environment/Materials/M_FFA_ConcreteWallPlate_01.uasset new file mode 100644 index 0000000..b6b967f Binary files /dev/null and b/Content/Environment/Materials/M_FFA_ConcreteWallPlate_01.uasset differ diff --git a/Content/Environment/Materials/M_FFA_Floor_02.uasset b/Content/Environment/Materials/M_FFA_Floor_02.uasset new file mode 100644 index 0000000..8ebb773 Binary files /dev/null and b/Content/Environment/Materials/M_FFA_Floor_02.uasset differ diff --git a/Content/Environment/Materials/M_FFA_Floor_02_Dark.uasset b/Content/Environment/Materials/M_FFA_Floor_02_Dark.uasset new file mode 100644 index 0000000..8e98013 Binary files /dev/null and b/Content/Environment/Materials/M_FFA_Floor_02_Dark.uasset differ diff --git a/Content/Environment/Materials/M_FFA_Wall_01.uasset b/Content/Environment/Materials/M_FFA_Wall_01.uasset new file mode 100644 index 0000000..d8a6682 Binary files /dev/null and b/Content/Environment/Materials/M_FFA_Wall_01.uasset differ diff --git a/Content/Environment/Materials/M_FFA_Wall_04.uasset b/Content/Environment/Materials/M_FFA_Wall_04.uasset new file mode 100644 index 0000000..21ef5c0 Binary files /dev/null and b/Content/Environment/Materials/M_FFA_Wall_04.uasset differ diff --git a/Content/Environment/Materials/M_FFA_Wall_04_Brighter.uasset b/Content/Environment/Materials/M_FFA_Wall_04_Brighter.uasset new file mode 100644 index 0000000..3dfd6fa Binary files /dev/null and b/Content/Environment/Materials/M_FFA_Wall_04_Brighter.uasset differ diff --git a/Content/Environment/Materials/M_FFA_Wall_04_Brighter_02.uasset b/Content/Environment/Materials/M_FFA_Wall_04_Brighter_02.uasset new file mode 100644 index 0000000..075a5bf Binary files /dev/null and b/Content/Environment/Materials/M_FFA_Wall_04_Brighter_02.uasset differ diff --git a/Content/Environment/Materials/M_FFA_Wall_05.uasset b/Content/Environment/Materials/M_FFA_Wall_05.uasset new file mode 100644 index 0000000..77719c4 Binary files /dev/null and b/Content/Environment/Materials/M_FFA_Wall_05.uasset differ diff --git a/Content/Environment/Materials/M_FPS_Planet.uasset b/Content/Environment/Materials/M_FPS_Planet.uasset new file mode 100644 index 0000000..56f63ba Binary files /dev/null and b/Content/Environment/Materials/M_FPS_Planet.uasset differ diff --git a/Content/Environment/Materials/M_FPS_Vista_Mountain.uasset b/Content/Environment/Materials/M_FPS_Vista_Mountain.uasset new file mode 100644 index 0000000..8062d79 Binary files /dev/null and b/Content/Environment/Materials/M_FPS_Vista_Mountain.uasset differ diff --git a/Content/Environment/Materials/M_Floor_Lights.uasset b/Content/Environment/Materials/M_Floor_Lights.uasset new file mode 100644 index 0000000..d904d90 Binary files /dev/null and b/Content/Environment/Materials/M_Floor_Lights.uasset differ diff --git a/Content/Environment/Materials/M_Forest_02.uasset b/Content/Environment/Materials/M_Forest_02.uasset new file mode 100644 index 0000000..28de51f Binary files /dev/null and b/Content/Environment/Materials/M_Forest_02.uasset differ diff --git a/Content/Environment/Materials/M_Forest_03.uasset b/Content/Environment/Materials/M_Forest_03.uasset new file mode 100644 index 0000000..dffa1e2 Binary files /dev/null and b/Content/Environment/Materials/M_Forest_03.uasset differ diff --git a/Content/Environment/Materials/M_Fps_Vista_City.uasset b/Content/Environment/Materials/M_Fps_Vista_City.uasset new file mode 100644 index 0000000..b912d63 Binary files /dev/null and b/Content/Environment/Materials/M_Fps_Vista_City.uasset differ diff --git a/Content/Environment/Materials/M_Fps_Vista_Walls.uasset b/Content/Environment/Materials/M_Fps_Vista_Walls.uasset new file mode 100644 index 0000000..582cf66 Binary files /dev/null and b/Content/Environment/Materials/M_Fps_Vista_Walls.uasset differ diff --git a/Content/Environment/Materials/M_Fps_Vista_Windows.uasset b/Content/Environment/Materials/M_Fps_Vista_Windows.uasset new file mode 100644 index 0000000..55fefa9 Binary files /dev/null and b/Content/Environment/Materials/M_Fps_Vista_Windows.uasset differ diff --git a/Content/Environment/Materials/M_Glass_Railing_01.uasset b/Content/Environment/Materials/M_Glass_Railing_01.uasset new file mode 100644 index 0000000..69410d2 Binary files /dev/null and b/Content/Environment/Materials/M_Glass_Railing_01.uasset differ diff --git a/Content/Environment/Materials/M_Grass2.uasset b/Content/Environment/Materials/M_Grass2.uasset new file mode 100644 index 0000000..355735b Binary files /dev/null and b/Content/Environment/Materials/M_Grass2.uasset differ diff --git a/Content/Environment/Materials/M_GrassLVL_Wall_01.uasset b/Content/Environment/Materials/M_GrassLVL_Wall_01.uasset new file mode 100644 index 0000000..f9a3884 Binary files /dev/null and b/Content/Environment/Materials/M_GrassLVL_Wall_01.uasset differ diff --git a/Content/Environment/Materials/M_GrassLVL_Wall_01_Inst.uasset b/Content/Environment/Materials/M_GrassLVL_Wall_01_Inst.uasset new file mode 100644 index 0000000..f9fe05e Binary files /dev/null and b/Content/Environment/Materials/M_GrassLVL_Wall_01_Inst.uasset differ diff --git a/Content/Environment/Materials/M_Laser.uasset b/Content/Environment/Materials/M_Laser.uasset new file mode 100644 index 0000000..1b7e032 Binary files /dev/null and b/Content/Environment/Materials/M_Laser.uasset differ diff --git a/Content/Environment/Materials/M_MASTER_Sky_Material_FFA.uasset b/Content/Environment/Materials/M_MASTER_Sky_Material_FFA.uasset new file mode 100644 index 0000000..8dfe4e6 Binary files /dev/null and b/Content/Environment/Materials/M_MASTER_Sky_Material_FFA.uasset differ diff --git a/Content/Environment/Materials/M_MASTER_Sky_Material_FFA_02.uasset b/Content/Environment/Materials/M_MASTER_Sky_Material_FFA_02.uasset new file mode 100644 index 0000000..85bcce2 Binary files /dev/null and b/Content/Environment/Materials/M_MASTER_Sky_Material_FFA_02.uasset differ diff --git a/Content/Environment/Materials/M_Park.uasset b/Content/Environment/Materials/M_Park.uasset new file mode 100644 index 0000000..0a3dca3 Binary files /dev/null and b/Content/Environment/Materials/M_Park.uasset differ diff --git a/Content/Environment/Materials/M_Park4.uasset b/Content/Environment/Materials/M_Park4.uasset new file mode 100644 index 0000000..fe6518f Binary files /dev/null and b/Content/Environment/Materials/M_Park4.uasset differ diff --git a/Content/Environment/Materials/M_Park_2.uasset b/Content/Environment/Materials/M_Park_2.uasset new file mode 100644 index 0000000..13d344f Binary files /dev/null and b/Content/Environment/Materials/M_Park_2.uasset differ diff --git a/Content/Environment/Materials/M_PublicPhone_01.uasset b/Content/Environment/Materials/M_PublicPhone_01.uasset new file mode 100644 index 0000000..a349725 Binary files /dev/null and b/Content/Environment/Materials/M_PublicPhone_01.uasset differ diff --git a/Content/Environment/Materials/M_PublicPhone_holo_01.uasset b/Content/Environment/Materials/M_PublicPhone_holo_01.uasset new file mode 100644 index 0000000..8f26010 Binary files /dev/null and b/Content/Environment/Materials/M_PublicPhone_holo_01.uasset differ diff --git a/Content/Environment/Materials/M_Sky_01.uasset b/Content/Environment/Materials/M_Sky_01.uasset new file mode 100644 index 0000000..ca0c6ef Binary files /dev/null and b/Content/Environment/Materials/M_Sky_01.uasset differ diff --git a/Content/Environment/Materials/M_Vista_Grass.uasset b/Content/Environment/Materials/M_Vista_Grass.uasset new file mode 100644 index 0000000..0c3063f Binary files /dev/null and b/Content/Environment/Materials/M_Vista_Grass.uasset differ diff --git a/Content/Environment/Materials/M_lvl_Bench_01.uasset b/Content/Environment/Materials/M_lvl_Bench_01.uasset new file mode 100644 index 0000000..9a26bb8 Binary files /dev/null and b/Content/Environment/Materials/M_lvl_Bench_01.uasset differ diff --git a/Content/Environment/Materials/M_skydome_2.uasset b/Content/Environment/Materials/M_skydome_2.uasset new file mode 100644 index 0000000..f71222d Binary files /dev/null and b/Content/Environment/Materials/M_skydome_2.uasset differ diff --git a/Content/Environment/Materials/M_water_Lake_01.uasset b/Content/Environment/Materials/M_water_Lake_01.uasset new file mode 100644 index 0000000..f51cedb Binary files /dev/null and b/Content/Environment/Materials/M_water_Lake_01.uasset differ diff --git a/Content/Environment/Materials/Metal.uasset b/Content/Environment/Materials/Metal.uasset new file mode 100644 index 0000000..da9fada Binary files /dev/null and b/Content/Environment/Materials/Metal.uasset differ diff --git a/Content/Environment/Materials/Metal_gray.uasset b/Content/Environment/Materials/Metal_gray.uasset new file mode 100644 index 0000000..15e77f3 Binary files /dev/null and b/Content/Environment/Materials/Metal_gray.uasset differ diff --git a/Content/Environment/Materials/Metal_tiled.uasset b/Content/Environment/Materials/Metal_tiled.uasset new file mode 100644 index 0000000..84ebb47 Binary files /dev/null and b/Content/Environment/Materials/Metal_tiled.uasset differ diff --git a/Content/Environment/Materials/Metal_tiled_Brighter.uasset b/Content/Environment/Materials/Metal_tiled_Brighter.uasset new file mode 100644 index 0000000..e5b3136 Binary files /dev/null and b/Content/Environment/Materials/Metal_tiled_Brighter.uasset differ diff --git a/Content/Environment/Materials/Metal_tiled_Darker.uasset b/Content/Environment/Materials/Metal_tiled_Darker.uasset new file mode 100644 index 0000000..f210fa6 Binary files /dev/null and b/Content/Environment/Materials/Metal_tiled_Darker.uasset differ diff --git a/Content/Environment/Materials/Metal_tiled_gray.uasset b/Content/Environment/Materials/Metal_tiled_gray.uasset new file mode 100644 index 0000000..352996b Binary files /dev/null and b/Content/Environment/Materials/Metal_tiled_gray.uasset differ diff --git a/Content/Environment/Materials/Metal_tiled_hex_gray.uasset b/Content/Environment/Materials/Metal_tiled_hex_gray.uasset new file mode 100644 index 0000000..cf9cacc Binary files /dev/null and b/Content/Environment/Materials/Metal_tiled_hex_gray.uasset differ diff --git a/Content/Environment/Materials/Metal_tiled_hex_gray_em.uasset b/Content/Environment/Materials/Metal_tiled_hex_gray_em.uasset new file mode 100644 index 0000000..b13cd8e Binary files /dev/null and b/Content/Environment/Materials/Metal_tiled_hex_gray_em.uasset differ diff --git a/Content/Environment/Materials/Metal_tiled_lines_gray2.uasset b/Content/Environment/Materials/Metal_tiled_lines_gray2.uasset new file mode 100644 index 0000000..3e5fb3f Binary files /dev/null and b/Content/Environment/Materials/Metal_tiled_lines_gray2.uasset differ diff --git a/Content/Environment/Materials/Metal_tiled_little_dark.uasset b/Content/Environment/Materials/Metal_tiled_little_dark.uasset new file mode 100644 index 0000000..7b1744e Binary files /dev/null and b/Content/Environment/Materials/Metal_tiled_little_dark.uasset differ diff --git a/Content/Environment/Materials/Metal_tiled_masked.uasset b/Content/Environment/Materials/Metal_tiled_masked.uasset new file mode 100644 index 0000000..c7539a8 Binary files /dev/null and b/Content/Environment/Materials/Metal_tiled_masked.uasset differ diff --git a/Content/Environment/Materials/Simple_emmisive2.uasset b/Content/Environment/Materials/Simple_emmisive2.uasset new file mode 100644 index 0000000..a70e223 Binary files /dev/null and b/Content/Environment/Materials/Simple_emmisive2.uasset differ diff --git a/Content/Environment/Materials/Simple_emmisive22.uasset b/Content/Environment/Materials/Simple_emmisive22.uasset new file mode 100644 index 0000000..d6b4f96 Binary files /dev/null and b/Content/Environment/Materials/Simple_emmisive22.uasset differ diff --git a/Content/Environment/Materials/Simple_emmisive3.uasset b/Content/Environment/Materials/Simple_emmisive3.uasset new file mode 100644 index 0000000..11c6dce Binary files /dev/null and b/Content/Environment/Materials/Simple_emmisive3.uasset differ diff --git a/Content/Environment/Materials/Unreal_Holo_1.uasset b/Content/Environment/Materials/Unreal_Holo_1.uasset new file mode 100644 index 0000000..560c1a5 Binary files /dev/null and b/Content/Environment/Materials/Unreal_Holo_1.uasset differ diff --git a/Content/Environment/Materials/Unreal_Holo_2.uasset b/Content/Environment/Materials/Unreal_Holo_2.uasset new file mode 100644 index 0000000..049c5b7 Binary files /dev/null and b/Content/Environment/Materials/Unreal_Holo_2.uasset differ diff --git a/Content/Environment/Materials/Unreal_Holo_4.uasset b/Content/Environment/Materials/Unreal_Holo_4.uasset new file mode 100644 index 0000000..89b1dc9 Binary files /dev/null and b/Content/Environment/Materials/Unreal_Holo_4.uasset differ diff --git a/Content/Environment/Materials/Windows_glass_2_trans.uasset b/Content/Environment/Materials/Windows_glass_2_trans.uasset new file mode 100644 index 0000000..2a8e7c6 Binary files /dev/null and b/Content/Environment/Materials/Windows_glass_2_trans.uasset differ diff --git a/Content/Environment/Materials/Windows_glass_2_trans_22.uasset b/Content/Environment/Materials/Windows_glass_2_trans_22.uasset new file mode 100644 index 0000000..0aa70ab Binary files /dev/null and b/Content/Environment/Materials/Windows_glass_2_trans_22.uasset differ diff --git a/Content/Environment/Materials/Windows_glass_trans.uasset b/Content/Environment/Materials/Windows_glass_trans.uasset new file mode 100644 index 0000000..33b0517 Binary files /dev/null and b/Content/Environment/Materials/Windows_glass_trans.uasset differ diff --git a/Content/Environment/Materials/Windows_glass_trans_2.uasset b/Content/Environment/Materials/Windows_glass_trans_2.uasset new file mode 100644 index 0000000..7873cd8 Binary files /dev/null and b/Content/Environment/Materials/Windows_glass_trans_2.uasset differ diff --git a/Content/Environment/Materials/Windows_glass_trans_2_tile.uasset b/Content/Environment/Materials/Windows_glass_trans_2_tile.uasset new file mode 100644 index 0000000..3af4261 Binary files /dev/null and b/Content/Environment/Materials/Windows_glass_trans_2_tile.uasset differ diff --git a/Content/Environment/Materials/Windows_glass_trans_holo.uasset b/Content/Environment/Materials/Windows_glass_trans_holo.uasset new file mode 100644 index 0000000..b8b8d4a Binary files /dev/null and b/Content/Environment/Materials/Windows_glass_trans_holo.uasset differ diff --git a/Content/Environment/Materials/Windows_glass_trans_tiled.uasset b/Content/Environment/Materials/Windows_glass_trans_tiled.uasset new file mode 100644 index 0000000..220ecf7 Binary files /dev/null and b/Content/Environment/Materials/Windows_glass_trans_tiled.uasset differ diff --git a/Content/Environment/Materials/unreal_holo_4_1.uasset b/Content/Environment/Materials/unreal_holo_4_1.uasset new file mode 100644 index 0000000..ab1e75a Binary files /dev/null and b/Content/Environment/Materials/unreal_holo_4_1.uasset differ diff --git a/Content/Environment/Materials/unreal_holo_4_1_1.uasset b/Content/Environment/Materials/unreal_holo_4_1_1.uasset new file mode 100644 index 0000000..3f7b712 Binary files /dev/null and b/Content/Environment/Materials/unreal_holo_4_1_1.uasset differ diff --git a/Content/Environment/Materials/unreal_holo_4_2.uasset b/Content/Environment/Materials/unreal_holo_4_2.uasset new file mode 100644 index 0000000..4df9032 Binary files /dev/null and b/Content/Environment/Materials/unreal_holo_4_2.uasset differ diff --git a/Content/Environment/Meshes/City_DamThreshold_01.uasset b/Content/Environment/Meshes/City_DamThreshold_01.uasset new file mode 100644 index 0000000..b36acb9 Binary files /dev/null and b/Content/Environment/Meshes/City_DamThreshold_01.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/City_Holographic_01.uasset b/Content/Environment/Meshes/City_TDM/New_ver/City_Holographic_01.uasset new file mode 100644 index 0000000..8134714 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/City_Holographic_01.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/MainHall_Arc_01.uasset b/Content/Environment/Meshes/City_TDM/New_ver/MainHall_Arc_01.uasset new file mode 100644 index 0000000..d2a0e22 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/MainHall_Arc_01.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/MainHall_Arc_02.uasset b/Content/Environment/Meshes/City_TDM/New_ver/MainHall_Arc_02.uasset new file mode 100644 index 0000000..660073d Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/MainHall_Arc_02.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/MainHall_WallGrass_02.uasset b/Content/Environment/Meshes/City_TDM/New_ver/MainHall_WallGrass_02.uasset new file mode 100644 index 0000000..df8fbcf Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/MainHall_WallGrass_02.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/MainHall_WallGrass_05.uasset b/Content/Environment/Meshes/City_TDM/New_ver/MainHall_WallGrass_05.uasset new file mode 100644 index 0000000..e2a8cd3 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/MainHall_WallGrass_05.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/Railings_01_midle.uasset b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01_midle.uasset new file mode 100644 index 0000000..9a5e1d1 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01_midle.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/Railings_01_midle_a.uasset b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01_midle_a.uasset new file mode 100644 index 0000000..9d81cdb Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01_midle_a.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/Railings_01a.uasset b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01a.uasset new file mode 100644 index 0000000..205d91b Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01a.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/Railings_01b.uasset b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01b.uasset new file mode 100644 index 0000000..83a3e71 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01b.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/Railings_01c.uasset b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01c.uasset new file mode 100644 index 0000000..1e1d46a Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01c.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/Railings_01d.uasset b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01d.uasset new file mode 100644 index 0000000..3e5fe94 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01d.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/Railings_01e.uasset b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01e.uasset new file mode 100644 index 0000000..ea2f13e Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01e.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/Railings_01f.uasset b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01f.uasset new file mode 100644 index 0000000..69723b7 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/Railings_01f.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/SM_Grass_plane.uasset b/Content/Environment/Meshes/City_TDM/New_ver/SM_Grass_plane.uasset new file mode 100644 index 0000000..4716f9a Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/SM_Grass_plane.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/back_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/back_1.uasset new file mode 100644 index 0000000..c8c4465 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/back_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/back_1_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/back_1_glass.uasset new file mode 100644 index 0000000..4a6c407 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/back_1_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/back_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/back_2.uasset new file mode 100644 index 0000000..747ba3b Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/back_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/back_2_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/back_2_glass.uasset new file mode 100644 index 0000000..a73dd6a Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/back_2_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/back_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/back_3.uasset new file mode 100644 index 0000000..895c3fc Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/back_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bal_m.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bal_m.uasset new file mode 100644 index 0000000..982c1a9 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bal_m.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bal_mb.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bal_mb.uasset new file mode 100644 index 0000000..09630a6 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bal_mb.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bal_r.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bal_r.uasset new file mode 100644 index 0000000..cb16b2c Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bal_r.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bal_r2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bal_r2.uasset new file mode 100644 index 0000000..4c0e8ed Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bal_r2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_1.uasset new file mode 100644 index 0000000..d8dfe32 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_10.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_10.uasset new file mode 100644 index 0000000..a351e86 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_10.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_11.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_11.uasset new file mode 100644 index 0000000..89af015 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_11.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_2.uasset new file mode 100644 index 0000000..e424b8f Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_3.uasset new file mode 100644 index 0000000..4142209 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_4.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_4.uasset new file mode 100644 index 0000000..c702c62 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_4.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_5.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_5.uasset new file mode 100644 index 0000000..4f6a3ed Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_5.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_6.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_6.uasset new file mode 100644 index 0000000..af585fb Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_6.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_7.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_7.uasset new file mode 100644 index 0000000..c5b9622 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_7.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_8.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_8.uasset new file mode 100644 index 0000000..4a648d6 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_8.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/barier_9.uasset b/Content/Environment/Meshes/City_TDM/New_ver/barier_9.uasset new file mode 100644 index 0000000..cc3b394 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/barier_9.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bottom_p1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bottom_p1.uasset new file mode 100644 index 0000000..d495f08 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bottom_p1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bottom_p2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bottom_p2.uasset new file mode 100644 index 0000000..5c4452c Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bottom_p2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bottom_p4.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bottom_p4.uasset new file mode 100644 index 0000000..fc60c4d Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bottom_p4.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bottom_p4_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bottom_p4_glass.uasset new file mode 100644 index 0000000..cb344cb Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bottom_p4_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bridge_ele_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bridge_ele_1.uasset new file mode 100644 index 0000000..05a6f31 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bridge_ele_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bridge_ele_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bridge_ele_2.uasset new file mode 100644 index 0000000..f5205a2 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bridge_ele_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim.uasset new file mode 100644 index 0000000..2cf18d4 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim2.uasset new file mode 100644 index 0000000..921ca4a Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim2_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim2_glass.uasset new file mode 100644 index 0000000..d51fb48 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim2_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim3.uasset new file mode 100644 index 0000000..d4589c3 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim3_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim3_glass.uasset new file mode 100644 index 0000000..dec3bea Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/bridge_trim3_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/doors_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/doors_1.uasset new file mode 100644 index 0000000..1c6ec8d Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/doors_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_floor_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_floor_1.uasset new file mode 100644 index 0000000..da187d7 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_floor_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_1.uasset new file mode 100644 index 0000000..a2e20da Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_2.uasset new file mode 100644 index 0000000..a2b9c22 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_2_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_2_1.uasset new file mode 100644 index 0000000..db9cf1c Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_2_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_3.uasset new file mode 100644 index 0000000..dab188e Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_3_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_3_2.uasset new file mode 100644 index 0000000..df5d5c3 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_stairs_3_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1.uasset new file mode 100644 index 0000000..744cb01 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1_glass.uasset new file mode 100644 index 0000000..5511cf8 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1_glass_hex.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1_glass_hex.uasset new file mode 100644 index 0000000..1d7601d Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1_glass_hex.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1_railing.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1_railing.uasset new file mode 100644 index 0000000..1456367 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_1_railing.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_2.uasset new file mode 100644 index 0000000..166a2c4 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_2_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_2_glass.uasset new file mode 100644 index 0000000..1e59d97 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_2_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_3.uasset new file mode 100644 index 0000000..fefca0a Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_3_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_3_glass.uasset new file mode 100644 index 0000000..46b91e3 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_3_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_center.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_center.uasset new file mode 100644 index 0000000..5443133 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_center.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_center_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_center_glass.uasset new file mode 100644 index 0000000..3fbba76 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/entrance_wall_center_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/filler_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/filler_1.uasset new file mode 100644 index 0000000..9e63255 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/filler_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/floor_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/floor_1.uasset new file mode 100644 index 0000000..a63ba9e Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/floor_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/floor_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/floor_2.uasset new file mode 100644 index 0000000..f2d0659 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/floor_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/floor_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/floor_3.uasset new file mode 100644 index 0000000..d60e2e3 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/floor_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/floor_4.uasset b/Content/Environment/Meshes/City_TDM/New_ver/floor_4.uasset new file mode 100644 index 0000000..1337d8c Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/floor_4.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/floor_5.uasset b/Content/Environment/Meshes/City_TDM/New_ver/floor_5.uasset new file mode 100644 index 0000000..995718a Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/floor_5.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/floor_5_Glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/floor_5_Glass.uasset new file mode 100644 index 0000000..01a7501 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/floor_5_Glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/floor_5_det.uasset b/Content/Environment/Meshes/City_TDM/New_ver/floor_5_det.uasset new file mode 100644 index 0000000..e298af1 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/floor_5_det.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/glass_front.uasset b/Content/Environment/Meshes/City_TDM/New_ver/glass_front.uasset new file mode 100644 index 0000000..02a0ebd Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/glass_front.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/glass_front_det_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/glass_front_det_1.uasset new file mode 100644 index 0000000..a81badd Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/glass_front_det_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/glass_front_masked.uasset b/Content/Environment/Meshes/City_TDM/New_ver/glass_front_masked.uasset new file mode 100644 index 0000000..6b2f092 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/glass_front_masked.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/glass_inside_a.uasset b/Content/Environment/Meshes/City_TDM/New_ver/glass_inside_a.uasset new file mode 100644 index 0000000..ea38bf1 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/glass_inside_a.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/glass_inside_b.uasset b/Content/Environment/Meshes/City_TDM/New_ver/glass_inside_b.uasset new file mode 100644 index 0000000..ee08ddd Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/glass_inside_b.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/glass_inside_c.uasset b/Content/Environment/Meshes/City_TDM/New_ver/glass_inside_c.uasset new file mode 100644 index 0000000..4071f64 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/glass_inside_c.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/glass_left.uasset b/Content/Environment/Meshes/City_TDM/New_ver/glass_left.uasset new file mode 100644 index 0000000..b1df4b9 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/glass_left.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_glass.uasset new file mode 100644 index 0000000..a348438 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_main.uasset b/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_main.uasset new file mode 100644 index 0000000..d398b43 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_main.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_ring.uasset b/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_ring.uasset new file mode 100644 index 0000000..d43fac4 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_ring.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_tunnel.uasset b/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_tunnel.uasset new file mode 100644 index 0000000..181e5f4 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/hi_bottom_tunnel.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holes_cover_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holes_cover_1.uasset new file mode 100644 index 0000000..cabe717 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holes_cover_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holes_cover_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holes_cover_2.uasset new file mode 100644 index 0000000..0e87742 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holes_cover_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holes_cover_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holes_cover_3.uasset new file mode 100644 index 0000000..274872d Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holes_cover_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_l1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_l1.uasset new file mode 100644 index 0000000..f65277d Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_l1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_l2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_l2.uasset new file mode 100644 index 0000000..a102f3f Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_l2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_l5.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_l5.uasset new file mode 100644 index 0000000..73416fc Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_l5.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_ll3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_ll3.uasset new file mode 100644 index 0000000..5ac2e76 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_ll3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_ll4.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_ll4.uasset new file mode 100644 index 0000000..8342d72 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_ll4.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_1.uasset new file mode 100644 index 0000000..f41094b Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_1_ele_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_1_ele_1.uasset new file mode 100644 index 0000000..4f7da7c Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_1_ele_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_1_ele_1_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_1_ele_1_2.uasset new file mode 100644 index 0000000..840e84b Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_1_ele_1_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_2.uasset new file mode 100644 index 0000000..cbddb53 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_3.uasset new file mode 100644 index 0000000..9048d6c Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/holo_proj_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/interior_bottom_wall_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/interior_bottom_wall_1.uasset new file mode 100644 index 0000000..c0e42e6 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/interior_bottom_wall_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/interior_ceiling.uasset b/Content/Environment/Meshes/City_TDM/New_ver/interior_ceiling.uasset new file mode 100644 index 0000000..62c64a5 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/interior_ceiling.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/interior_ceiling_trim.uasset b/Content/Environment/Meshes/City_TDM/New_ver/interior_ceiling_trim.uasset new file mode 100644 index 0000000..a25bd72 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/interior_ceiling_trim.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/interior_wall_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/interior_wall_2.uasset new file mode 100644 index 0000000..6a48b9b Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/interior_wall_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/interior_wall_column_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/interior_wall_column_3.uasset new file mode 100644 index 0000000..d7923ef Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/interior_wall_column_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/interior_wall_stairs_railings.uasset b/Content/Environment/Meshes/City_TDM/New_ver/interior_wall_stairs_railings.uasset new file mode 100644 index 0000000..8e5581e Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/interior_wall_stairs_railings.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/interior_walls_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/interior_walls_1.uasset new file mode 100644 index 0000000..df094c4 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/interior_walls_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_Bench_01.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_Bench_01.uasset new file mode 100644 index 0000000..440a32b Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_Bench_01.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_Lights_01.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_Lights_01.uasset new file mode 100644 index 0000000..2ffe09d Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_Lights_01.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_PublicPhone_01.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_PublicPhone_01.uasset new file mode 100644 index 0000000..406ba2d Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_PublicPhone_01.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_PublicPhone_holo_01.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_PublicPhone_holo_01.uasset new file mode 100644 index 0000000..70cf056 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_PublicPhone_holo_01.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_Trshcan_01.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_Trshcan_01.uasset new file mode 100644 index 0000000..a3bb48c Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_Trshcan_01.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_c_inside.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_c_inside.uasset new file mode 100644 index 0000000..246ea62 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_c_inside.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_inside.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_inside.uasset new file mode 100644 index 0000000..0de7873 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_inside.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_inside_Pillar_01.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_inside_Pillar_01.uasset new file mode 100644 index 0000000..e8e66b9 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_inside_Pillar_01.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_inside_b.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_inside_b.uasset new file mode 100644 index 0000000..b00018f Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_inside_b.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/lvl_left.uasset b/Content/Environment/Meshes/City_TDM/New_ver/lvl_left.uasset new file mode 100644 index 0000000..d3a7a0b Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/lvl_left.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/main_front.uasset b/Content/Environment/Meshes/City_TDM/New_ver/main_front.uasset new file mode 100644 index 0000000..33fd9bc Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/main_front.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/main_left.uasset b/Content/Environment/Meshes/City_TDM/New_ver/main_left.uasset new file mode 100644 index 0000000..fb21591 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/main_left.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/main_left_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/main_left_2.uasset new file mode 100644 index 0000000..2b9f4e2 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/main_left_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/near_entrance_wall.uasset b/Content/Environment/Meshes/City_TDM/New_ver/near_entrance_wall.uasset new file mode 100644 index 0000000..a3c5c02 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/near_entrance_wall.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/near_entrance_wall_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/near_entrance_wall_glass.uasset new file mode 100644 index 0000000..6bb4e44 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/near_entrance_wall_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_1.uasset new file mode 100644 index 0000000..4372006 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_1_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_1_glass.uasset new file mode 100644 index 0000000..8849d51 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_1_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_floor_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_floor_1.uasset new file mode 100644 index 0000000..e9a2de3 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_floor_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_floor_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_floor_2.uasset new file mode 100644 index 0000000..868682b Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_floor_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_interior.uasset b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_interior.uasset new file mode 100644 index 0000000..aeadf83 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_interior.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_stairs_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_stairs_1.uasset new file mode 100644 index 0000000..6d5ff97 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/spawn_point_stairs_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/stairs_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/stairs_1.uasset new file mode 100644 index 0000000..95cd9d4 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/stairs_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/stairs_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/stairs_2.uasset new file mode 100644 index 0000000..ffe83b8 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/stairs_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/stairs_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/stairs_3.uasset new file mode 100644 index 0000000..8548bb9 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/stairs_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/stairs_4.uasset b/Content/Environment/Meshes/City_TDM/New_ver/stairs_4.uasset new file mode 100644 index 0000000..2bb7f19 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/stairs_4.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/stairs_5.uasset b/Content/Environment/Meshes/City_TDM/New_ver/stairs_5.uasset new file mode 100644 index 0000000..3187d54 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/stairs_5.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/stairs_6.uasset b/Content/Environment/Meshes/City_TDM/New_ver/stairs_6.uasset new file mode 100644 index 0000000..8d94548 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/stairs_6.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/stairs_7.uasset b/Content/Environment/Meshes/City_TDM/New_ver/stairs_7.uasset new file mode 100644 index 0000000..b54a6c7 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/stairs_7.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_1.uasset new file mode 100644 index 0000000..cfa7c34 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_2.uasset new file mode 100644 index 0000000..13c3e81 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_3.uasset new file mode 100644 index 0000000..4520a49 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_4.uasset b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_4.uasset new file mode 100644 index 0000000..bf2c5c6 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_4.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_5.uasset b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_5.uasset new file mode 100644 index 0000000..10f8a10 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/unreal_holo_5.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/upper_p1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/upper_p1.uasset new file mode 100644 index 0000000..c88f4a5 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/upper_p1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/upper_p1_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/upper_p1_glass.uasset new file mode 100644 index 0000000..5e0b205 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/upper_p1_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wall_left.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wall_left.uasset new file mode 100644 index 0000000..7691223 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wall_left.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wall_left_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wall_left_2.uasset new file mode 100644 index 0000000..2e59702 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wall_left_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wall_left_2_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wall_left_2_glass.uasset new file mode 100644 index 0000000..5eb562c Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wall_left_2_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_1.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_1.uasset new file mode 100644 index 0000000..889be36 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_1.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_1_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_1_glass.uasset new file mode 100644 index 0000000..a8c5fcd Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_1_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_2.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_2.uasset new file mode 100644 index 0000000..c66d38d Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_2.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_2_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_2_glass.uasset new file mode 100644 index 0000000..bfa5bb0 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_2_glass.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_3.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_3.uasset new file mode 100644 index 0000000..945e1e1 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_3.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_4.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_4.uasset new file mode 100644 index 0000000..05b8a68 Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_4.uasset differ diff --git a/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_4_glass.uasset b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_4_glass.uasset new file mode 100644 index 0000000..93070ef Binary files /dev/null and b/Content/Environment/Meshes/City_TDM/New_ver/wing_wall_4_glass.uasset differ diff --git a/Content/Environment/Meshes/CloudPlane_01.uasset b/Content/Environment/Meshes/CloudPlane_01.uasset new file mode 100644 index 0000000..03d7487 Binary files /dev/null and b/Content/Environment/Meshes/CloudPlane_01.uasset differ diff --git a/Content/Environment/Meshes/FFA_BlockVista_01.uasset b/Content/Environment/Meshes/FFA_BlockVista_01.uasset new file mode 100644 index 0000000..9e4162b Binary files /dev/null and b/Content/Environment/Meshes/FFA_BlockVista_01.uasset differ diff --git a/Content/Environment/Meshes/SM_Fps_Vista_GP_03.uasset b/Content/Environment/Meshes/SM_Fps_Vista_GP_03.uasset new file mode 100644 index 0000000..0340c2d Binary files /dev/null and b/Content/Environment/Meshes/SM_Fps_Vista_GP_03.uasset differ diff --git a/Content/Environment/Meshes/SM_Sky_Clouds.uasset b/Content/Environment/Meshes/SM_Sky_Clouds.uasset new file mode 100644 index 0000000..4acdb67 Binary files /dev/null and b/Content/Environment/Meshes/SM_Sky_Clouds.uasset differ diff --git a/Content/Environment/Meshes/SM_Vista_Plane.uasset b/Content/Environment/Meshes/SM_Vista_Plane.uasset new file mode 100644 index 0000000..127393d Binary files /dev/null and b/Content/Environment/Meshes/SM_Vista_Plane.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/City_BasePlane_01.uasset b/Content/Environment/Meshes/Vista_Meshes/City_BasePlane_01.uasset new file mode 100644 index 0000000..dc90aac Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/City_BasePlane_01.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_FPS_Vista_Mountain.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_FPS_Vista_Mountain.uasset new file mode 100644 index 0000000..b5c8f6d Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_FPS_Vista_Mountain.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_01.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_01.uasset new file mode 100644 index 0000000..4635e7b Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_01.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_02.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_02.uasset new file mode 100644 index 0000000..4a8b98a Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_02.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_03.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_03.uasset new file mode 100644 index 0000000..bfc2058 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_03.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_04.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_04.uasset new file mode 100644 index 0000000..7b434cb Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_Building_04.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_GP_01.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_GP_01.uasset new file mode 100644 index 0000000..c6d8801 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_GP_01.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_GP_02.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_GP_02.uasset new file mode 100644 index 0000000..ad2b30a Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Fps_Vista_GP_02.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_3.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_3.uasset new file mode 100644 index 0000000..a3418da Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_3.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_4.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_4.uasset new file mode 100644 index 0000000..4608e00 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_4.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_5.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_5.uasset new file mode 100644 index 0000000..c19385a Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_5.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_7.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_7.uasset new file mode 100644 index 0000000..1a82666 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_7.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_8.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_8.uasset new file mode 100644 index 0000000..c369abb Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_8.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_9.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_9.uasset new file mode 100644 index 0000000..0cbda16 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_City_Building_9.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_Vista_coast_1.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_Vista_coast_1.uasset new file mode 100644 index 0000000..9044da6 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_Vista_coast_1.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_Vista_coast_triangle.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_Vista_coast_triangle.uasset new file mode 100644 index 0000000..6349c2b Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Shooter_Vista_coast_triangle.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_SkyDome.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_SkyDome.uasset new file mode 100644 index 0000000..550d86d Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_SkyDome.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_1.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_1.uasset new file mode 100644 index 0000000..ba026d0 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_1.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_2.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_2.uasset new file mode 100644 index 0000000..9d15727 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_2.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_3.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_3.uasset new file mode 100644 index 0000000..dddedcc Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_3.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_4.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_4.uasset new file mode 100644 index 0000000..e6e03c8 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_4.uasset differ diff --git a/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_5.uasset b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_5.uasset new file mode 100644 index 0000000..fb83c16 Binary files /dev/null and b/Content/Environment/Meshes/Vista_Meshes/SM_Vista_Trees_5.uasset differ diff --git a/Content/Environment/PhysicalMaterials/M_BodyDamage.uasset b/Content/Environment/PhysicalMaterials/M_BodyDamage.uasset new file mode 100644 index 0000000..4d0e24f Binary files /dev/null and b/Content/Environment/PhysicalMaterials/M_BodyDamage.uasset differ diff --git a/Content/Environment/PhysicalMaterials/M_Concrete.uasset b/Content/Environment/PhysicalMaterials/M_Concrete.uasset new file mode 100644 index 0000000..0efb08d Binary files /dev/null and b/Content/Environment/PhysicalMaterials/M_Concrete.uasset differ diff --git a/Content/Environment/PhysicalMaterials/M_Glass.uasset b/Content/Environment/PhysicalMaterials/M_Glass.uasset new file mode 100644 index 0000000..bac0ab0 Binary files /dev/null and b/Content/Environment/PhysicalMaterials/M_Glass.uasset differ diff --git a/Content/Environment/PhysicalMaterials/M_Grass.uasset b/Content/Environment/PhysicalMaterials/M_Grass.uasset new file mode 100644 index 0000000..f1756dc Binary files /dev/null and b/Content/Environment/PhysicalMaterials/M_Grass.uasset differ diff --git a/Content/Environment/PhysicalMaterials/M_Metal.uasset b/Content/Environment/PhysicalMaterials/M_Metal.uasset new file mode 100644 index 0000000..02c691c Binary files /dev/null and b/Content/Environment/PhysicalMaterials/M_Metal.uasset differ diff --git a/Content/Environment/PhysicalMaterials/M_Tile.uasset b/Content/Environment/PhysicalMaterials/M_Tile.uasset new file mode 100644 index 0000000..c34bd4e Binary files /dev/null and b/Content/Environment/PhysicalMaterials/M_Tile.uasset differ diff --git a/Content/Environment/Textures/Enviro_assets_01_M.uasset b/Content/Environment/Textures/Enviro_assets_01_M.uasset new file mode 100644 index 0000000..fba0aa9 Binary files /dev/null and b/Content/Environment/Textures/Enviro_assets_01_M.uasset differ diff --git a/Content/Environment/Textures/FPS_Vista_Planet_D.uasset b/Content/Environment/Textures/FPS_Vista_Planet_D.uasset new file mode 100644 index 0000000..cb68a61 Binary files /dev/null and b/Content/Environment/Textures/FPS_Vista_Planet_D.uasset differ diff --git a/Content/Environment/Textures/LUT_Highrise.uasset b/Content/Environment/Textures/LUT_Highrise.uasset new file mode 100644 index 0000000..643258f Binary files /dev/null and b/Content/Environment/Textures/LUT_Highrise.uasset differ diff --git a/Content/Environment/Textures/T_Carpet_M.uasset b/Content/Environment/Textures/T_Carpet_M.uasset new file mode 100644 index 0000000..5bf92d8 Binary files /dev/null and b/Content/Environment/Textures/T_Carpet_M.uasset differ diff --git a/Content/Environment/Textures/T_Carpet_N.uasset b/Content/Environment/Textures/T_Carpet_N.uasset new file mode 100644 index 0000000..214fa72 Binary files /dev/null and b/Content/Environment/Textures/T_Carpet_N.uasset differ diff --git a/Content/Environment/Textures/T_Enviro_assets_01_D.uasset b/Content/Environment/Textures/T_Enviro_assets_01_D.uasset new file mode 100644 index 0000000..7db0987 Binary files /dev/null and b/Content/Environment/Textures/T_Enviro_assets_01_D.uasset differ diff --git a/Content/Environment/Textures/T_Enviro_assets_01_N.uasset b/Content/Environment/Textures/T_Enviro_assets_01_N.uasset new file mode 100644 index 0000000..8c1a767 Binary files /dev/null and b/Content/Environment/Textures/T_Enviro_assets_01_N.uasset differ diff --git a/Content/Environment/Textures/T_FFA_ConcreteGroundPlane_01_N.uasset b/Content/Environment/Textures/T_FFA_ConcreteGroundPlane_01_N.uasset new file mode 100644 index 0000000..8638f95 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_ConcreteGroundPlane_01_N.uasset differ diff --git a/Content/Environment/Textures/T_FFA_ConcreteWallPlate_01_D.uasset b/Content/Environment/Textures/T_FFA_ConcreteWallPlate_01_D.uasset new file mode 100644 index 0000000..1cf90b7 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_ConcreteWallPlate_01_D.uasset differ diff --git a/Content/Environment/Textures/T_FFA_ConcreteWallPlate_01_N.uasset b/Content/Environment/Textures/T_FFA_ConcreteWallPlate_01_N.uasset new file mode 100644 index 0000000..26df556 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_ConcreteWallPlate_01_N.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Detail_01_N.uasset b/Content/Environment/Textures/T_FFA_Detail_01_N.uasset new file mode 100644 index 0000000..c776a42 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Detail_01_N.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Floor_02_D.uasset b/Content/Environment/Textures/T_FFA_Floor_02_D.uasset new file mode 100644 index 0000000..f3b1cb3 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Floor_02_D.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Floor_02_M.uasset b/Content/Environment/Textures/T_FFA_Floor_02_M.uasset new file mode 100644 index 0000000..02660db Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Floor_02_M.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Floor_02_N.uasset b/Content/Environment/Textures/T_FFA_Floor_02_N.uasset new file mode 100644 index 0000000..5cf4433 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Floor_02_N.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_01_D.uasset b/Content/Environment/Textures/T_FFA_Wall_01_D.uasset new file mode 100644 index 0000000..40513a5 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_01_D.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_01_M.uasset b/Content/Environment/Textures/T_FFA_Wall_01_M.uasset new file mode 100644 index 0000000..94b7a09 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_01_M.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_01_N.uasset b/Content/Environment/Textures/T_FFA_Wall_01_N.uasset new file mode 100644 index 0000000..b101021 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_01_N.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_02_D.uasset b/Content/Environment/Textures/T_FFA_Wall_02_D.uasset new file mode 100644 index 0000000..2c21c56 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_02_D.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_02_M.uasset b/Content/Environment/Textures/T_FFA_Wall_02_M.uasset new file mode 100644 index 0000000..93bc16d Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_02_M.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_02_N.uasset b/Content/Environment/Textures/T_FFA_Wall_02_N.uasset new file mode 100644 index 0000000..997d8c0 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_02_N.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_03_D.uasset b/Content/Environment/Textures/T_FFA_Wall_03_D.uasset new file mode 100644 index 0000000..42b0466 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_03_D.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_03_M.uasset b/Content/Environment/Textures/T_FFA_Wall_03_M.uasset new file mode 100644 index 0000000..414e60c Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_03_M.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_03_N.uasset b/Content/Environment/Textures/T_FFA_Wall_03_N.uasset new file mode 100644 index 0000000..851ff36 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_03_N.uasset differ diff --git a/Content/Environment/Textures/T_FFA_Wall_Dirt_01_D.uasset b/Content/Environment/Textures/T_FFA_Wall_Dirt_01_D.uasset new file mode 100644 index 0000000..1839cd6 Binary files /dev/null and b/Content/Environment/Textures/T_FFA_Wall_Dirt_01_D.uasset differ diff --git a/Content/Environment/Textures/T_FPS_HDR_01A.uasset b/Content/Environment/Textures/T_FPS_HDR_01A.uasset new file mode 100644 index 0000000..ef05066 Binary files /dev/null and b/Content/Environment/Textures/T_FPS_HDR_01A.uasset differ diff --git a/Content/Environment/Textures/T_FPS_HDR_01B.uasset b/Content/Environment/Textures/T_FPS_HDR_01B.uasset new file mode 100644 index 0000000..90e069d Binary files /dev/null and b/Content/Environment/Textures/T_FPS_HDR_01B.uasset differ diff --git a/Content/Environment/Textures/T_FPS_Vista_Gardens_M.uasset b/Content/Environment/Textures/T_FPS_Vista_Gardens_M.uasset new file mode 100644 index 0000000..7bda7cd Binary files /dev/null and b/Content/Environment/Textures/T_FPS_Vista_Gardens_M.uasset differ diff --git a/Content/Environment/Textures/T_FPS_Vista_Mountain_M.uasset b/Content/Environment/Textures/T_FPS_Vista_Mountain_M.uasset new file mode 100644 index 0000000..fa21c1a Binary files /dev/null and b/Content/Environment/Textures/T_FPS_Vista_Mountain_M.uasset differ diff --git a/Content/Environment/Textures/T_FPS_Vista_Mountain_NM.uasset b/Content/Environment/Textures/T_FPS_Vista_Mountain_NM.uasset new file mode 100644 index 0000000..1bd3dac Binary files /dev/null and b/Content/Environment/Textures/T_FPS_Vista_Mountain_NM.uasset differ diff --git a/Content/Environment/Textures/T_Forest_01.uasset b/Content/Environment/Textures/T_Forest_01.uasset new file mode 100644 index 0000000..0e6b572 Binary files /dev/null and b/Content/Environment/Textures/T_Forest_01.uasset differ diff --git a/Content/Environment/Textures/T_Forest_2_NM.uasset b/Content/Environment/Textures/T_Forest_2_NM.uasset new file mode 100644 index 0000000..b3fe4ab Binary files /dev/null and b/Content/Environment/Textures/T_Forest_2_NM.uasset differ diff --git a/Content/Environment/Textures/T_Forest_NM.uasset b/Content/Environment/Textures/T_Forest_NM.uasset new file mode 100644 index 0000000..44e1419 Binary files /dev/null and b/Content/Environment/Textures/T_Forest_NM.uasset differ diff --git a/Content/Environment/Textures/T_Fps_Vista_Ref_Temp.uasset b/Content/Environment/Textures/T_Fps_Vista_Ref_Temp.uasset new file mode 100644 index 0000000..0257638 Binary files /dev/null and b/Content/Environment/Textures/T_Fps_Vista_Ref_Temp.uasset differ diff --git a/Content/Environment/Textures/T_Fps_Vista_Wall_D.uasset b/Content/Environment/Textures/T_Fps_Vista_Wall_D.uasset new file mode 100644 index 0000000..54ca32b Binary files /dev/null and b/Content/Environment/Textures/T_Fps_Vista_Wall_D.uasset differ diff --git a/Content/Environment/Textures/T_Fps_Vista_Windows_M.uasset b/Content/Environment/Textures/T_Fps_Vista_Windows_M.uasset new file mode 100644 index 0000000..17393c5 Binary files /dev/null and b/Content/Environment/Textures/T_Fps_Vista_Windows_M.uasset differ diff --git a/Content/Environment/Textures/T_Park_N.uasset b/Content/Environment/Textures/T_Park_N.uasset new file mode 100644 index 0000000..b0b87f5 Binary files /dev/null and b/Content/Environment/Textures/T_Park_N.uasset differ diff --git a/Content/Environment/Textures/T_PublicPhone_01_D.uasset b/Content/Environment/Textures/T_PublicPhone_01_D.uasset new file mode 100644 index 0000000..9175b86 Binary files /dev/null and b/Content/Environment/Textures/T_PublicPhone_01_D.uasset differ diff --git a/Content/Environment/Textures/T_PublicPhone_01_M.uasset b/Content/Environment/Textures/T_PublicPhone_01_M.uasset new file mode 100644 index 0000000..de5ba59 Binary files /dev/null and b/Content/Environment/Textures/T_PublicPhone_01_M.uasset differ diff --git a/Content/Environment/Textures/T_PublicPhone_01_N.uasset b/Content/Environment/Textures/T_PublicPhone_01_N.uasset new file mode 100644 index 0000000..43f2ad8 Binary files /dev/null and b/Content/Environment/Textures/T_PublicPhone_01_N.uasset differ diff --git a/Content/Environment/Textures/T_Railings_01_M.uasset b/Content/Environment/Textures/T_Railings_01_M.uasset new file mode 100644 index 0000000..3d6134e Binary files /dev/null and b/Content/Environment/Textures/T_Railings_01_M.uasset differ diff --git a/Content/Environment/Textures/T_Railings_01_N.uasset b/Content/Environment/Textures/T_Railings_01_N.uasset new file mode 100644 index 0000000..c168c1a Binary files /dev/null and b/Content/Environment/Textures/T_Railings_01_N.uasset differ diff --git a/Content/Environment/Textures/T_Sky_Clouds_D.uasset b/Content/Environment/Textures/T_Sky_Clouds_D.uasset new file mode 100644 index 0000000..132a8cf Binary files /dev/null and b/Content/Environment/Textures/T_Sky_Clouds_D.uasset differ diff --git a/Content/Environment/Textures/T_Skydome_01.uasset b/Content/Environment/Textures/T_Skydome_01.uasset new file mode 100644 index 0000000..95dded2 Binary files /dev/null and b/Content/Environment/Textures/T_Skydome_01.uasset differ diff --git a/Content/Environment/Textures/T_Vista_Grass_D.uasset b/Content/Environment/Textures/T_Vista_Grass_D.uasset new file mode 100644 index 0000000..c164eb4 Binary files /dev/null and b/Content/Environment/Textures/T_Vista_Grass_D.uasset differ diff --git a/Content/Environment/Textures/T_Vista_Grass_N.uasset b/Content/Environment/Textures/T_Vista_Grass_N.uasset new file mode 100644 index 0000000..786bd82 Binary files /dev/null and b/Content/Environment/Textures/T_Vista_Grass_N.uasset differ diff --git a/Content/Environment/Textures/T_Water_Sea_01.uasset b/Content/Environment/Textures/T_Water_Sea_01.uasset new file mode 100644 index 0000000..d87fe4a Binary files /dev/null and b/Content/Environment/Textures/T_Water_Sea_01.uasset differ diff --git a/Content/Environment/Textures/T_Water_Sea_01_N.uasset b/Content/Environment/Textures/T_Water_Sea_01_N.uasset new file mode 100644 index 0000000..d819504 Binary files /dev/null and b/Content/Environment/Textures/T_Water_Sea_01_N.uasset differ diff --git a/Content/Environment/Textures/T_lvl_Bench_01_M.uasset b/Content/Environment/Textures/T_lvl_Bench_01_M.uasset new file mode 100644 index 0000000..e2d009e Binary files /dev/null and b/Content/Environment/Textures/T_lvl_Bench_01_M.uasset differ diff --git a/Content/Environment/Textures/T_lvl_Bench_01_N.uasset b/Content/Environment/Textures/T_lvl_Bench_01_N.uasset new file mode 100644 index 0000000..2acb024 Binary files /dev/null and b/Content/Environment/Textures/T_lvl_Bench_01_N.uasset differ diff --git a/Content/Environment/Textures/doors_1_dff.uasset b/Content/Environment/Textures/doors_1_dff.uasset new file mode 100644 index 0000000..d62e941 Binary files /dev/null and b/Content/Environment/Textures/doors_1_dff.uasset differ diff --git a/Content/Environment/Textures/doors_1_nm.uasset b/Content/Environment/Textures/doors_1_nm.uasset new file mode 100644 index 0000000..bf9147f Binary files /dev/null and b/Content/Environment/Textures/doors_1_nm.uasset differ diff --git a/Content/Environment/Textures/doors_1_occlusion.uasset b/Content/Environment/Textures/doors_1_occlusion.uasset new file mode 100644 index 0000000..8fd3a1f Binary files /dev/null and b/Content/Environment/Textures/doors_1_occlusion.uasset differ diff --git a/Content/Environment/Textures/glass_1_ao.uasset b/Content/Environment/Textures/glass_1_ao.uasset new file mode 100644 index 0000000..958f9a9 Binary files /dev/null and b/Content/Environment/Textures/glass_1_ao.uasset differ diff --git a/Content/Environment/Textures/glass_1_nm.uasset b/Content/Environment/Textures/glass_1_nm.uasset new file mode 100644 index 0000000..5f00a21 Binary files /dev/null and b/Content/Environment/Textures/glass_1_nm.uasset differ diff --git a/Content/Environment/Textures/glass_2_2_nm.uasset b/Content/Environment/Textures/glass_2_2_nm.uasset new file mode 100644 index 0000000..38a003b Binary files /dev/null and b/Content/Environment/Textures/glass_2_2_nm.uasset differ diff --git a/Content/Environment/Textures/glass_2_3_nm.uasset b/Content/Environment/Textures/glass_2_3_nm.uasset new file mode 100644 index 0000000..adcc962 Binary files /dev/null and b/Content/Environment/Textures/glass_2_3_nm.uasset differ diff --git a/Content/Environment/Textures/glass_2_mask.uasset b/Content/Environment/Textures/glass_2_mask.uasset new file mode 100644 index 0000000..a9dfb74 Binary files /dev/null and b/Content/Environment/Textures/glass_2_mask.uasset differ diff --git a/Content/Environment/Textures/glass_2_nm.uasset b/Content/Environment/Textures/glass_2_nm.uasset new file mode 100644 index 0000000..b60889d Binary files /dev/null and b/Content/Environment/Textures/glass_2_nm.uasset differ diff --git a/Content/Environment/Textures/holo_layer_1.uasset b/Content/Environment/Textures/holo_layer_1.uasset new file mode 100644 index 0000000..240be39 Binary files /dev/null and b/Content/Environment/Textures/holo_layer_1.uasset differ diff --git a/Content/Environment/Textures/holo_layer_1_4.uasset b/Content/Environment/Textures/holo_layer_1_4.uasset new file mode 100644 index 0000000..e8c8325 Binary files /dev/null and b/Content/Environment/Textures/holo_layer_1_4.uasset differ diff --git a/Content/Environment/Textures/holo_layer_1_5.uasset b/Content/Environment/Textures/holo_layer_1_5.uasset new file mode 100644 index 0000000..99aade6 Binary files /dev/null and b/Content/Environment/Textures/holo_layer_1_5.uasset differ diff --git a/Content/Environment/Textures/unreal_engine.uasset b/Content/Environment/Textures/unreal_engine.uasset new file mode 100644 index 0000000..0487d82 Binary files /dev/null and b/Content/Environment/Textures/unreal_engine.uasset differ diff --git a/Content/Maps/Highrise.umap b/Content/Maps/Highrise.umap new file mode 100644 index 0000000..cbd750c Binary files /dev/null and b/Content/Maps/Highrise.umap differ diff --git a/Content/Maps/HighriseLabel.uasset b/Content/Maps/HighriseLabel.uasset new file mode 100644 index 0000000..5f79b13 Binary files /dev/null and b/Content/Maps/HighriseLabel.uasset differ diff --git a/Content/Maps/Highrise_Audio.umap b/Content/Maps/Highrise_Audio.umap new file mode 100644 index 0000000..dca017c Binary files /dev/null and b/Content/Maps/Highrise_Audio.umap differ diff --git a/Content/Maps/Highrise_Collisions_Temp.umap b/Content/Maps/Highrise_Collisions_Temp.umap new file mode 100644 index 0000000..3adca67 Binary files /dev/null and b/Content/Maps/Highrise_Collisions_Temp.umap differ diff --git a/Content/Maps/Highrise_Gameplay.umap b/Content/Maps/Highrise_Gameplay.umap new file mode 100644 index 0000000..a329b1b Binary files /dev/null and b/Content/Maps/Highrise_Gameplay.umap differ diff --git a/Content/Maps/Highrise_Lights.umap b/Content/Maps/Highrise_Lights.umap new file mode 100644 index 0000000..ad972a8 Binary files /dev/null and b/Content/Maps/Highrise_Lights.umap differ diff --git a/Content/Maps/Highrise_Meshing.umap b/Content/Maps/Highrise_Meshing.umap new file mode 100644 index 0000000..b7f0da6 Binary files /dev/null and b/Content/Maps/Highrise_Meshing.umap differ diff --git a/Content/Maps/Highrise_Vista.umap b/Content/Maps/Highrise_Vista.umap new file mode 100644 index 0000000..dfe1e8d Binary files /dev/null and b/Content/Maps/Highrise_Vista.umap differ diff --git a/Content/Maps/Sanctuary.umap b/Content/Maps/Sanctuary.umap new file mode 100644 index 0000000..9f0973a Binary files /dev/null and b/Content/Maps/Sanctuary.umap differ diff --git a/Content/Maps/SanctuaryLabel.uasset b/Content/Maps/SanctuaryLabel.uasset new file mode 100644 index 0000000..e778ad4 Binary files /dev/null and b/Content/Maps/SanctuaryLabel.uasset differ diff --git a/Content/Maps/Sanctuary_Audio.umap b/Content/Maps/Sanctuary_Audio.umap new file mode 100644 index 0000000..de98c63 Binary files /dev/null and b/Content/Maps/Sanctuary_Audio.umap differ diff --git a/Content/Maps/ShooterEntry.umap b/Content/Maps/ShooterEntry.umap new file mode 100644 index 0000000..16bc1fc Binary files /dev/null and b/Content/Maps/ShooterEntry.umap differ diff --git a/Content/Maps/StartupLabel.uasset b/Content/Maps/StartupLabel.uasset new file mode 100644 index 0000000..37952bb Binary files /dev/null and b/Content/Maps/StartupLabel.uasset differ diff --git a/Content/Movies/LoadingScreen.mp4 b/Content/Movies/LoadingScreen.mp4 new file mode 100644 index 0000000..0a8c5d9 Binary files /dev/null and b/Content/Movies/LoadingScreen.mp4 differ diff --git a/Content/Movies/LoadingScreen.webm b/Content/Movies/LoadingScreen.webm new file mode 100644 index 0000000..4744e1b Binary files /dev/null and b/Content/Movies/LoadingScreen.webm differ diff --git a/Content/Slate/Fonts/Google Android License.txt b/Content/Slate/Fonts/Google Android License.txt new file mode 100644 index 0000000..1a96dfd --- /dev/null +++ b/Content/Slate/Fonts/Google Android License.txt @@ -0,0 +1,18 @@ +Copyright (C) 2008 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +########## + +This directory contains the fonts for the platform. They are licensed +under the Apache 2 license. diff --git a/Content/Slate/Fonts/Roboto-Black.ttf b/Content/Slate/Fonts/Roboto-Black.ttf new file mode 100644 index 0000000..86ec2b2 Binary files /dev/null and b/Content/Slate/Fonts/Roboto-Black.ttf differ diff --git a/Content/Slate/Fonts/Roboto-Medium.ttf b/Content/Slate/Fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..8798341 Binary files /dev/null and b/Content/Slate/Fonts/Roboto-Medium.ttf differ diff --git a/Content/Slate/Fonts/Roboto-Regular.ttf b/Content/Slate/Fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..7d9a6c4 Binary files /dev/null and b/Content/Slate/Fonts/Roboto-Regular.ttf differ diff --git a/Content/Slate/Images/ReplayPause.png b/Content/Slate/Images/ReplayPause.png new file mode 100644 index 0000000..c58c0fa Binary files /dev/null and b/Content/Slate/Images/ReplayPause.png differ diff --git a/Content/Slate/Images/ReplayTimeline.png b/Content/Slate/Images/ReplayTimeline.png new file mode 100644 index 0000000..a00b3bf Binary files /dev/null and b/Content/Slate/Images/ReplayTimeline.png differ diff --git a/Content/Slate/Images/ReplayTimelineIndicator.png b/Content/Slate/Images/ReplayTimelineIndicator.png new file mode 100644 index 0000000..61ed74c Binary files /dev/null and b/Content/Slate/Images/ReplayTimelineIndicator.png differ diff --git a/Content/Slate/Images/SoundCue_SpeakerIcon.png b/Content/Slate/Images/SoundCue_SpeakerIcon.png new file mode 100644 index 0000000..b90cc46 Binary files /dev/null and b/Content/Slate/Images/SoundCue_SpeakerIcon.png differ diff --git a/Content/Slate/Images/SwitchButtonDown.png b/Content/Slate/Images/SwitchButtonDown.png new file mode 100644 index 0000000..5df0ed1 Binary files /dev/null and b/Content/Slate/Images/SwitchButtonDown.png differ diff --git a/Content/Slate/Images/SwitchButtonLeft.png b/Content/Slate/Images/SwitchButtonLeft.png new file mode 100644 index 0000000..e2ea00a Binary files /dev/null and b/Content/Slate/Images/SwitchButtonLeft.png differ diff --git a/Content/Slate/Images/SwitchButtonRight.png b/Content/Slate/Images/SwitchButtonRight.png new file mode 100644 index 0000000..ec28172 Binary files /dev/null and b/Content/Slate/Images/SwitchButtonRight.png differ diff --git a/Content/Slate/Images/SwitchButtonUp.png b/Content/Slate/Images/SwitchButtonUp.png new file mode 100644 index 0000000..aee2ba7 Binary files /dev/null and b/Content/Slate/Images/SwitchButtonUp.png differ diff --git a/Content/Sounds/AmbientLoops/Air_Wind/SCue_Amb_Wind_LP.uasset b/Content/Sounds/AmbientLoops/Air_Wind/SCue_Amb_Wind_LP.uasset new file mode 100644 index 0000000..5167a6f Binary files /dev/null and b/Content/Sounds/AmbientLoops/Air_Wind/SCue_Amb_Wind_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Air_Wind/wav/sw_Amb_Wind_LP.uasset b/Content/Sounds/AmbientLoops/Air_Wind/wav/sw_Amb_Wind_LP.uasset new file mode 100644 index 0000000..03bf0ea Binary files /dev/null and b/Content/Sounds/AmbientLoops/Air_Wind/wav/sw_Amb_Wind_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Amb_Bed/SCue_Amb_Bed_Highrise_Atrium.uasset b/Content/Sounds/AmbientLoops/Amb_Bed/SCue_Amb_Bed_Highrise_Atrium.uasset new file mode 100644 index 0000000..870ffec Binary files /dev/null and b/Content/Sounds/AmbientLoops/Amb_Bed/SCue_Amb_Bed_Highrise_Atrium.uasset differ diff --git a/Content/Sounds/AmbientLoops/Amb_Bed/SCue_Amb_Bed_Highrise_Room.uasset b/Content/Sounds/AmbientLoops/Amb_Bed/SCue_Amb_Bed_Highrise_Room.uasset new file mode 100644 index 0000000..1dc8a3b Binary files /dev/null and b/Content/Sounds/AmbientLoops/Amb_Bed/SCue_Amb_Bed_Highrise_Room.uasset differ diff --git a/Content/Sounds/AmbientLoops/Amb_Bed/SCue_Amb_Bed_Sanctuary_Interior.uasset b/Content/Sounds/AmbientLoops/Amb_Bed/SCue_Amb_Bed_Sanctuary_Interior.uasset new file mode 100644 index 0000000..30a0a50 Binary files /dev/null and b/Content/Sounds/AmbientLoops/Amb_Bed/SCue_Amb_Bed_Sanctuary_Interior.uasset differ diff --git a/Content/Sounds/AmbientLoops/Amb_Bed/wav/sw_Amb_Bed_HighRise_Atrium_LP.uasset b/Content/Sounds/AmbientLoops/Amb_Bed/wav/sw_Amb_Bed_HighRise_Atrium_LP.uasset new file mode 100644 index 0000000..2c8ae93 Binary files /dev/null and b/Content/Sounds/AmbientLoops/Amb_Bed/wav/sw_Amb_Bed_HighRise_Atrium_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Amb_Bed/wav/sw_Amb_Bed_HighRise_Room_LP.uasset b/Content/Sounds/AmbientLoops/Amb_Bed/wav/sw_Amb_Bed_HighRise_Room_LP.uasset new file mode 100644 index 0000000..172d852 Binary files /dev/null and b/Content/Sounds/AmbientLoops/Amb_Bed/wav/sw_Amb_Bed_HighRise_Room_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Amb_Bed/wav/sw_Amb_Bed_Sanctuary_LP.uasset b/Content/Sounds/AmbientLoops/Amb_Bed/wav/sw_Amb_Bed_Sanctuary_LP.uasset new file mode 100644 index 0000000..48b2302 Binary files /dev/null and b/Content/Sounds/AmbientLoops/Amb_Bed/wav/sw_Amb_Bed_Sanctuary_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Env_Objects/SCue_Amb_Fence_LP.uasset b/Content/Sounds/AmbientLoops/Env_Objects/SCue_Amb_Fence_LP.uasset new file mode 100644 index 0000000..a98de30 Binary files /dev/null and b/Content/Sounds/AmbientLoops/Env_Objects/SCue_Amb_Fence_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Env_Objects/SCue_Amb_PhoneBox_LP.uasset b/Content/Sounds/AmbientLoops/Env_Objects/SCue_Amb_PhoneBox_LP.uasset new file mode 100644 index 0000000..20f8bdc Binary files /dev/null and b/Content/Sounds/AmbientLoops/Env_Objects/SCue_Amb_PhoneBox_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Env_Objects/SCue_Amb_UE_Marquee_LP.uasset b/Content/Sounds/AmbientLoops/Env_Objects/SCue_Amb_UE_Marquee_LP.uasset new file mode 100644 index 0000000..ffba169 Binary files /dev/null and b/Content/Sounds/AmbientLoops/Env_Objects/SCue_Amb_UE_Marquee_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Env_Objects/wav/sw_Amb_Fence_LP.uasset b/Content/Sounds/AmbientLoops/Env_Objects/wav/sw_Amb_Fence_LP.uasset new file mode 100644 index 0000000..7bb517d Binary files /dev/null and b/Content/Sounds/AmbientLoops/Env_Objects/wav/sw_Amb_Fence_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Env_Objects/wav/sw_Amb_PhoneBox_LP.uasset b/Content/Sounds/AmbientLoops/Env_Objects/wav/sw_Amb_PhoneBox_LP.uasset new file mode 100644 index 0000000..4543022 Binary files /dev/null and b/Content/Sounds/AmbientLoops/Env_Objects/wav/sw_Amb_PhoneBox_LP.uasset differ diff --git a/Content/Sounds/AmbientLoops/Env_Objects/wav/sw_Amb_UnrealMarquee_LP.uasset b/Content/Sounds/AmbientLoops/Env_Objects/wav/sw_Amb_UnrealMarquee_LP.uasset new file mode 100644 index 0000000..5bc5acf Binary files /dev/null and b/Content/Sounds/AmbientLoops/Env_Objects/wav/sw_Amb_UnrealMarquee_LP.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Air_Wind/SCue_Amb_WindGust_OS.uasset b/Content/Sounds/Ambient_OneShots/Air_Wind/SCue_Amb_WindGust_OS.uasset new file mode 100644 index 0000000..c953cd2 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Air_Wind/SCue_Amb_WindGust_OS.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_01.uasset b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_01.uasset new file mode 100644 index 0000000..f020c62 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_01.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_02.uasset b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_02.uasset new file mode 100644 index 0000000..d8ea767 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_02.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_03.uasset b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_03.uasset new file mode 100644 index 0000000..0f4b16e Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_03.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_04.uasset b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_04.uasset new file mode 100644 index 0000000..fc53492 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_04.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_05.uasset b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_05.uasset new file mode 100644 index 0000000..0f3e5dc Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_05.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_06.uasset b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_06.uasset new file mode 100644 index 0000000..88b1108 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_06.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_07.uasset b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_07.uasset new file mode 100644 index 0000000..ce819e9 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_07.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_08.uasset b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_08.uasset new file mode 100644 index 0000000..266cb6e Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Air_Wind/wav/sx_Amb_WindGust_08.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Highrise_Specific/SCue_Amb_OS_Highrise_Atrium.uasset b/Content/Sounds/Ambient_OneShots/Highrise_Specific/SCue_Amb_OS_Highrise_Atrium.uasset new file mode 100644 index 0000000..fe463dd Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Highrise_Specific/SCue_Amb_OS_Highrise_Atrium.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Highrise_Specific/SCue_Amb_OS_Highrise_Room.uasset b/Content/Sounds/Ambient_OneShots/Highrise_Specific/SCue_Amb_OS_Highrise_Room.uasset new file mode 100644 index 0000000..83fcdcb Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Highrise_Specific/SCue_Amb_OS_Highrise_Room.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Sanctuary_Specific/SCue_Amb_OS_Sanctuary_Balcony.uasset b/Content/Sounds/Ambient_OneShots/Sanctuary_Specific/SCue_Amb_OS_Sanctuary_Balcony.uasset new file mode 100644 index 0000000..675eb85 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Sanctuary_Specific/SCue_Amb_OS_Sanctuary_Balcony.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/Sanctuary_Specific/SCue_Amb_OS_Sanctuary_Interior.uasset b/Content/Sounds/Ambient_OneShots/Sanctuary_Specific/SCue_Amb_OS_Sanctuary_Interior.uasset new file mode 100644 index 0000000..1f21946 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/Sanctuary_Specific/SCue_Amb_OS_Sanctuary_Interior.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_01.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_01.uasset new file mode 100644 index 0000000..8ac79c5 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_01.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_02.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_02.uasset new file mode 100644 index 0000000..5b914d8 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_02.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_03.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_03.uasset new file mode 100644 index 0000000..ded340d Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_03.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_04.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_04.uasset new file mode 100644 index 0000000..25d466f Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_04.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_05.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_05.uasset new file mode 100644 index 0000000..32d9602 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_05.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_06.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_06.uasset new file mode 100644 index 0000000..751b583 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_06.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_07.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_07.uasset new file mode 100644 index 0000000..5485103 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_07.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_08.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_08.uasset new file mode 100644 index 0000000..49c9be7 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_08.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_09.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_09.uasset new file mode 100644 index 0000000..86989ae Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_09.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_10.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_10.uasset new file mode 100644 index 0000000..04383d2 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_10.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_11.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_11.uasset new file mode 100644 index 0000000..72ced85 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_11.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_12.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_12.uasset new file mode 100644 index 0000000..439e078 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_12.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_13.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_13.uasset new file mode 100644 index 0000000..dba300f Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_13.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_14.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_14.uasset new file mode 100644 index 0000000..685ad22 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_14.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_15.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_15.uasset new file mode 100644 index 0000000..27ab6a6 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_15.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_16.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_16.uasset new file mode 100644 index 0000000..eb9df6e Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_16.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_17.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_17.uasset new file mode 100644 index 0000000..e1deba3 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_17.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_18.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_18.uasset new file mode 100644 index 0000000..2e37296 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_18.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_19.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_19.uasset new file mode 100644 index 0000000..f757016 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_19.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_20.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_20.uasset new file mode 100644 index 0000000..4f3f666 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_20.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_21.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_21.uasset new file mode 100644 index 0000000..c1eac39 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_21.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_22.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_22.uasset new file mode 100644 index 0000000..bcff9a7 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_22.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_23.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_23.uasset new file mode 100644 index 0000000..42da5aa Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_23.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_24.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_24.uasset new file mode 100644 index 0000000..cda6cf2 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_24.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_25.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_25.uasset new file mode 100644 index 0000000..2848108 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_25.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_26.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_26.uasset new file mode 100644 index 0000000..1a21735 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_26.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_27.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_27.uasset new file mode 100644 index 0000000..2916a45 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_27.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_28.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_28.uasset new file mode 100644 index 0000000..fda1214 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_28.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_29.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_29.uasset new file mode 100644 index 0000000..141f6ed Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_29.uasset differ diff --git a/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_30.uasset b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_30.uasset new file mode 100644 index 0000000..7332d96 Binary files /dev/null and b/Content/Sounds/Ambient_OneShots/wav/sw_Amb_OS_30.uasset differ diff --git a/Content/Sounds/Audio_Blueprints/BP_Audio_Spline.uasset b/Content/Sounds/Audio_Blueprints/BP_Audio_Spline.uasset new file mode 100644 index 0000000..12a2ad4 Binary files /dev/null and b/Content/Sounds/Audio_Blueprints/BP_Audio_Spline.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/Ambient_OneShot.uasset b/Content/Sounds/Audio_Settings/Attenuation/Ambient_OneShot.uasset new file mode 100644 index 0000000..9cdca96 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/Ambient_OneShot.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/BulletImpacts.uasset b/Content/Sounds/Audio_Settings/Attenuation/BulletImpacts.uasset new file mode 100644 index 0000000..f71f1f4 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/BulletImpacts.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/Explosion.uasset b/Content/Sounds/Audio_Settings/Attenuation/Explosion.uasset new file mode 100644 index 0000000..575b151 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/Explosion.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/Foley.uasset b/Content/Sounds/Audio_Settings/Attenuation/Foley.uasset new file mode 100644 index 0000000..b835983 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/Foley.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/Footsteps.uasset b/Content/Sounds/Audio_Settings/Attenuation/Footsteps.uasset new file mode 100644 index 0000000..977fa0b Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/Footsteps.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/Pickups.uasset b/Content/Sounds/Audio_Settings/Attenuation/Pickups.uasset new file mode 100644 index 0000000..c08270b Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/Pickups.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/Projectile.uasset b/Content/Sounds/Audio_Settings/Attenuation/Projectile.uasset new file mode 100644 index 0000000..c07f849 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/Projectile.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/SA_Kiosk.uasset b/Content/Sounds/Audio_Settings/Attenuation/SA_Kiosk.uasset new file mode 100644 index 0000000..7fb95b6 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/SA_Kiosk.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/Wep_Shoot.uasset b/Content/Sounds/Audio_Settings/Attenuation/Wep_Shoot.uasset new file mode 100644 index 0000000..be76ad9 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/Wep_Shoot.uasset differ diff --git a/Content/Sounds/Audio_Settings/Attenuation/Wind_Balcony.uasset b/Content/Sounds/Audio_Settings/Attenuation/Wind_Balcony.uasset new file mode 100644 index 0000000..ee9ade3 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Attenuation/Wind_Balcony.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Ambience.uasset b/Content/Sounds/Audio_Settings/Class/SC_Ambience.uasset new file mode 100644 index 0000000..718c10c Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Ambience.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Ambient_Bed.uasset b/Content/Sounds/Audio_Settings/Class/SC_Ambient_Bed.uasset new file mode 100644 index 0000000..620f119 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Ambient_Bed.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Ambient_Loop.uasset b/Content/Sounds/Audio_Settings/Class/SC_Ambient_Loop.uasset new file mode 100644 index 0000000..4729e85 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Ambient_Loop.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Ambient_OneShot.uasset b/Content/Sounds/Audio_Settings/Class/SC_Ambient_OneShot.uasset new file mode 100644 index 0000000..014de68 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Ambient_OneShot.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Characters.uasset b/Content/Sounds/Audio_Settings/Class/SC_Characters.uasset new file mode 100644 index 0000000..5123838 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Characters.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Explosion.uasset b/Content/Sounds/Audio_Settings/Class/SC_Explosion.uasset new file mode 100644 index 0000000..c948f7d Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Explosion.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Foley.uasset b/Content/Sounds/Audio_Settings/Class/SC_Foley.uasset new file mode 100644 index 0000000..1ed5b27 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Foley.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Footsteps.uasset b/Content/Sounds/Audio_Settings/Class/SC_Footsteps.uasset new file mode 100644 index 0000000..1dbd304 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Footsteps.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Impacts.uasset b/Content/Sounds/Audio_Settings/Class/SC_Impacts.uasset new file mode 100644 index 0000000..7b2eb8b Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Impacts.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_MASTER.uasset b/Content/Sounds/Audio_Settings/Class/SC_MASTER.uasset new file mode 100644 index 0000000..7794daa Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_MASTER.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_MUSIC.uasset b/Content/Sounds/Audio_Settings/Class/SC_MUSIC.uasset new file mode 100644 index 0000000..34b8e85 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_MUSIC.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Pickups.uasset b/Content/Sounds/Audio_Settings/Class/SC_Pickups.uasset new file mode 100644 index 0000000..e594152 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Pickups.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Projectile.uasset b/Content/Sounds/Audio_Settings/Class/SC_Projectile.uasset new file mode 100644 index 0000000..9c0a9c0 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Projectile.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_SFX.uasset b/Content/Sounds/Audio_Settings/Class/SC_SFX.uasset new file mode 100644 index 0000000..a060e56 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_SFX.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_Stinger.uasset b/Content/Sounds/Audio_Settings/Class/SC_Stinger.uasset new file mode 100644 index 0000000..9406144 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_Stinger.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_UI.uasset b/Content/Sounds/Audio_Settings/Class/SC_UI.uasset new file mode 100644 index 0000000..3feca3a Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_UI.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_VOICE.uasset b/Content/Sounds/Audio_Settings/Class/SC_VOICE.uasset new file mode 100644 index 0000000..7304ae1 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_VOICE.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_WEAPON.uasset b/Content/Sounds/Audio_Settings/Class/SC_WEAPON.uasset new file mode 100644 index 0000000..d5444d7 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_WEAPON.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_WeaponShoot_1P.uasset b/Content/Sounds/Audio_Settings/Class/SC_WeaponShoot_1P.uasset new file mode 100644 index 0000000..ba019a6 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_WeaponShoot_1P.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_WeaponShoot_3P.uasset b/Content/Sounds/Audio_Settings/Class/SC_WeaponShoot_3P.uasset new file mode 100644 index 0000000..7993c32 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_WeaponShoot_3P.uasset differ diff --git a/Content/Sounds/Audio_Settings/Class/SC_voip.uasset b/Content/Sounds/Audio_Settings/Class/SC_voip.uasset new file mode 100644 index 0000000..a2e0f9d Binary files /dev/null and b/Content/Sounds/Audio_Settings/Class/SC_voip.uasset differ diff --git a/Content/Sounds/Audio_Settings/Concurrency/SCon_Default.uasset b/Content/Sounds/Audio_Settings/Concurrency/SCon_Default.uasset new file mode 100644 index 0000000..1cecd69 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Concurrency/SCon_Default.uasset differ diff --git a/Content/Sounds/Audio_Settings/ReverbPresets/Rvb_Highrise_Atrium.uasset b/Content/Sounds/Audio_Settings/ReverbPresets/Rvb_Highrise_Atrium.uasset new file mode 100644 index 0000000..8282788 Binary files /dev/null and b/Content/Sounds/Audio_Settings/ReverbPresets/Rvb_Highrise_Atrium.uasset differ diff --git a/Content/Sounds/Audio_Settings/ReverbPresets/Rvb_Highrise_Room.uasset b/Content/Sounds/Audio_Settings/ReverbPresets/Rvb_Highrise_Room.uasset new file mode 100644 index 0000000..3b3eb58 Binary files /dev/null and b/Content/Sounds/Audio_Settings/ReverbPresets/Rvb_Highrise_Room.uasset differ diff --git a/Content/Sounds/Audio_Settings/ReverbPresets/Rvb_Sanctuary_Interior.uasset b/Content/Sounds/Audio_Settings/ReverbPresets/Rvb_Sanctuary_Interior.uasset new file mode 100644 index 0000000..8f60bf1 Binary files /dev/null and b/Content/Sounds/Audio_Settings/ReverbPresets/Rvb_Sanctuary_Interior.uasset differ diff --git a/Content/Sounds/Audio_Settings/SoundMix/Explosion.uasset b/Content/Sounds/Audio_Settings/SoundMix/Explosion.uasset new file mode 100644 index 0000000..9da2276 Binary files /dev/null and b/Content/Sounds/Audio_Settings/SoundMix/Explosion.uasset differ diff --git a/Content/Sounds/Audio_Settings/SoundMix/SMix_Default.uasset b/Content/Sounds/Audio_Settings/SoundMix/SMix_Default.uasset new file mode 100644 index 0000000..f2f868f Binary files /dev/null and b/Content/Sounds/Audio_Settings/SoundMix/SMix_Default.uasset differ diff --git a/Content/Sounds/Audio_Settings/SoundMix/SMix_Stinger.uasset b/Content/Sounds/Audio_Settings/SoundMix/SMix_Stinger.uasset new file mode 100644 index 0000000..3d9f083 Binary files /dev/null and b/Content/Sounds/Audio_Settings/SoundMix/SMix_Stinger.uasset differ diff --git a/Content/Sounds/Audio_Settings/Submixes/VibrationSubmix.uasset b/Content/Sounds/Audio_Settings/Submixes/VibrationSubmix.uasset new file mode 100644 index 0000000..49ab201 Binary files /dev/null and b/Content/Sounds/Audio_Settings/Submixes/VibrationSubmix.uasset differ diff --git a/Content/Sounds/Foley/SCue_Foley_Aim.uasset b/Content/Sounds/Foley/SCue_Foley_Aim.uasset new file mode 100644 index 0000000..0f26003 Binary files /dev/null and b/Content/Sounds/Foley/SCue_Foley_Aim.uasset differ diff --git a/Content/Sounds/Foley/SCue_Foley_Bodyfall.uasset b/Content/Sounds/Foley/SCue_Foley_Bodyfall.uasset new file mode 100644 index 0000000..2aa9084 Binary files /dev/null and b/Content/Sounds/Foley/SCue_Foley_Bodyfall.uasset differ diff --git a/Content/Sounds/Foley/SCue_Foley_Jump.uasset b/Content/Sounds/Foley/SCue_Foley_Jump.uasset new file mode 100644 index 0000000..8039ade Binary files /dev/null and b/Content/Sounds/Foley/SCue_Foley_Jump.uasset differ diff --git a/Content/Sounds/Foley/SCue_Foley_Jumpland_Grass.uasset b/Content/Sounds/Foley/SCue_Foley_Jumpland_Grass.uasset new file mode 100644 index 0000000..92ce00f Binary files /dev/null and b/Content/Sounds/Foley/SCue_Foley_Jumpland_Grass.uasset differ diff --git a/Content/Sounds/Foley/SCue_Foley_Jumpland_Metal.uasset b/Content/Sounds/Foley/SCue_Foley_Jumpland_Metal.uasset new file mode 100644 index 0000000..b4c610a Binary files /dev/null and b/Content/Sounds/Foley/SCue_Foley_Jumpland_Metal.uasset differ diff --git a/Content/Sounds/Foley/SCue_Foley_Jumpland_Tile.uasset b/Content/Sounds/Foley/SCue_Foley_Jumpland_Tile.uasset new file mode 100644 index 0000000..f1b44f0 Binary files /dev/null and b/Content/Sounds/Foley/SCue_Foley_Jumpland_Tile.uasset differ diff --git a/Content/Sounds/Foley/SCue_Foley_Run.uasset b/Content/Sounds/Foley/SCue_Foley_Run.uasset new file mode 100644 index 0000000..5cc3d5d Binary files /dev/null and b/Content/Sounds/Foley/SCue_Foley_Run.uasset differ diff --git a/Content/Sounds/Foley/SCue_Foley_SwipeCloth.uasset b/Content/Sounds/Foley/SCue_Foley_SwipeCloth.uasset new file mode 100644 index 0000000..6dc2ae9 Binary files /dev/null and b/Content/Sounds/Foley/SCue_Foley_SwipeCloth.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Aim_01.uasset b/Content/Sounds/Foley/wav/sw_Foley_Aim_01.uasset new file mode 100644 index 0000000..a1a67f3 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Aim_01.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Bodyfall_01.uasset b/Content/Sounds/Foley/wav/sw_Foley_Bodyfall_01.uasset new file mode 100644 index 0000000..4dafd94 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Bodyfall_01.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Bodyfall_02.uasset b/Content/Sounds/Foley/wav/sw_Foley_Bodyfall_02.uasset new file mode 100644 index 0000000..225c943 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Bodyfall_02.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Grass_01.uasset b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Grass_01.uasset new file mode 100644 index 0000000..4161a3b Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Grass_01.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Grass_02.uasset b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Grass_02.uasset new file mode 100644 index 0000000..90be484 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Grass_02.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Grass_03.uasset b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Grass_03.uasset new file mode 100644 index 0000000..b4aadd2 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Grass_03.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Metal_01.uasset b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Metal_01.uasset new file mode 100644 index 0000000..52f58ef Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Metal_01.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Metal_02.uasset b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Metal_02.uasset new file mode 100644 index 0000000..ea829bd Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Metal_02.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Metal_03.uasset b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Metal_03.uasset new file mode 100644 index 0000000..130baa2 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Metal_03.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Tile_01.uasset b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Tile_01.uasset new file mode 100644 index 0000000..92d73f8 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Tile_01.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Tile_02.uasset b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Tile_02.uasset new file mode 100644 index 0000000..5b8b4c3 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Tile_02.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Tile_03.uasset b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Tile_03.uasset new file mode 100644 index 0000000..638678e Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_JumpLand_Tile_03.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Jump_01.uasset b/Content/Sounds/Foley/wav/sw_Foley_Jump_01.uasset new file mode 100644 index 0000000..d76a09f Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Jump_01.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Jump_02.uasset b/Content/Sounds/Foley/wav/sw_Foley_Jump_02.uasset new file mode 100644 index 0000000..647cb25 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Jump_02.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Jump_03.uasset b/Content/Sounds/Foley/wav/sw_Foley_Jump_03.uasset new file mode 100644 index 0000000..92c195b Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Jump_03.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_01.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_01.uasset new file mode 100644 index 0000000..fd7133f Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_01.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_02.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_02.uasset new file mode 100644 index 0000000..9f2c5a2 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_02.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_03.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_03.uasset new file mode 100644 index 0000000..f40ecb0 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_03.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_04.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_04.uasset new file mode 100644 index 0000000..47f0321 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_04.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_05.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_05.uasset new file mode 100644 index 0000000..791ea60 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_05.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_06.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_06.uasset new file mode 100644 index 0000000..0c23acf Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_06.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_07.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_07.uasset new file mode 100644 index 0000000..177afbc Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_07.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_08.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_08.uasset new file mode 100644 index 0000000..a36884c Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_08.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_09.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_09.uasset new file mode 100644 index 0000000..edbf2f2 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_09.uasset differ diff --git a/Content/Sounds/Foley/wav/sw_Foley_Run_10.uasset b/Content/Sounds/Foley/wav/sw_Foley_Run_10.uasset new file mode 100644 index 0000000..7e19f42 Binary files /dev/null and b/Content/Sounds/Foley/wav/sw_Foley_Run_10.uasset differ diff --git a/Content/Sounds/Footsteps/SCue_FS_Grass.uasset b/Content/Sounds/Footsteps/SCue_FS_Grass.uasset new file mode 100644 index 0000000..eb840b1 Binary files /dev/null and b/Content/Sounds/Footsteps/SCue_FS_Grass.uasset differ diff --git a/Content/Sounds/Footsteps/SCue_FS_Metal.uasset b/Content/Sounds/Footsteps/SCue_FS_Metal.uasset new file mode 100644 index 0000000..5c3bbd5 Binary files /dev/null and b/Content/Sounds/Footsteps/SCue_FS_Metal.uasset differ diff --git a/Content/Sounds/Footsteps/SCue_FS_Tile.uasset b/Content/Sounds/Footsteps/SCue_FS_Tile.uasset new file mode 100644 index 0000000..f6d1f9d Binary files /dev/null and b/Content/Sounds/Footsteps/SCue_FS_Tile.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_01.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_01.uasset new file mode 100644 index 0000000..b9570cb Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_01.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_02.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_02.uasset new file mode 100644 index 0000000..5e84eb5 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_02.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_03.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_03.uasset new file mode 100644 index 0000000..b8101ea Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_03.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_04.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_04.uasset new file mode 100644 index 0000000..70eab5d Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_04.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_05.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_05.uasset new file mode 100644 index 0000000..117e0b4 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_05.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_06.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_06.uasset new file mode 100644 index 0000000..679d027 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_06.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_07.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_07.uasset new file mode 100644 index 0000000..e0ac46a Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_07.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_08.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_08.uasset new file mode 100644 index 0000000..fe9ebd7 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_08.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_09.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_09.uasset new file mode 100644 index 0000000..5616577 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_09.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Grass_10.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Grass_10.uasset new file mode 100644 index 0000000..43517fb Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Grass_10.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_01.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_01.uasset new file mode 100644 index 0000000..e759c87 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_01.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_02.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_02.uasset new file mode 100644 index 0000000..367e2d7 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_02.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_03.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_03.uasset new file mode 100644 index 0000000..36f72f7 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_03.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_04.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_04.uasset new file mode 100644 index 0000000..1f767b5 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_04.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_05.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_05.uasset new file mode 100644 index 0000000..f8bbec2 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_05.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_06.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_06.uasset new file mode 100644 index 0000000..46c1591 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_06.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_07.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_07.uasset new file mode 100644 index 0000000..645e8d9 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_07.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_08.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_08.uasset new file mode 100644 index 0000000..e91d2bf Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_08.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_09.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_09.uasset new file mode 100644 index 0000000..966c82e Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_09.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Metal_10.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Metal_10.uasset new file mode 100644 index 0000000..1c65afe Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Metal_10.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_01.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_01.uasset new file mode 100644 index 0000000..6e27356 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_01.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_02.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_02.uasset new file mode 100644 index 0000000..87dadea Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_02.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_03.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_03.uasset new file mode 100644 index 0000000..ebe93c3 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_03.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_04.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_04.uasset new file mode 100644 index 0000000..696e585 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_04.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_05.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_05.uasset new file mode 100644 index 0000000..5341c9b Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_05.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_06.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_06.uasset new file mode 100644 index 0000000..89769f8 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_06.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_07.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_07.uasset new file mode 100644 index 0000000..f6a20b8 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_07.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_08.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_08.uasset new file mode 100644 index 0000000..72ffe69 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_08.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_09.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_09.uasset new file mode 100644 index 0000000..b0e6f48 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_09.uasset differ diff --git a/Content/Sounds/Footsteps/wav/sw_FS_Tile_10.uasset b/Content/Sounds/Footsteps/wav/sw_FS_Tile_10.uasset new file mode 100644 index 0000000..81ffaa5 Binary files /dev/null and b/Content/Sounds/Footsteps/wav/sw_FS_Tile_10.uasset differ diff --git a/Content/Sounds/Impacts/SCue_Impt_Bullet_Default.uasset b/Content/Sounds/Impacts/SCue_Impt_Bullet_Default.uasset new file mode 100644 index 0000000..8b161fa Binary files /dev/null and b/Content/Sounds/Impacts/SCue_Impt_Bullet_Default.uasset differ diff --git a/Content/Sounds/Impacts/SCue_Impt_Bullet_Flesh.uasset b/Content/Sounds/Impacts/SCue_Impt_Bullet_Flesh.uasset new file mode 100644 index 0000000..b14c161 Binary files /dev/null and b/Content/Sounds/Impacts/SCue_Impt_Bullet_Flesh.uasset differ diff --git a/Content/Sounds/Impacts/SCue_Impt_Bullet_Metal.uasset b/Content/Sounds/Impacts/SCue_Impt_Bullet_Metal.uasset new file mode 100644 index 0000000..72a6267 Binary files /dev/null and b/Content/Sounds/Impacts/SCue_Impt_Bullet_Metal.uasset differ diff --git a/Content/Sounds/Impacts/SCue_Impt_Bullet_Water.uasset b/Content/Sounds/Impacts/SCue_Impt_Bullet_Water.uasset new file mode 100644 index 0000000..e961e52 Binary files /dev/null and b/Content/Sounds/Impacts/SCue_Impt_Bullet_Water.uasset differ diff --git a/Content/Sounds/Impacts/SCue_Impt_Launcher.uasset b/Content/Sounds/Impacts/SCue_Impt_Launcher.uasset new file mode 100644 index 0000000..afb9881 Binary files /dev/null and b/Content/Sounds/Impacts/SCue_Impt_Launcher.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_01.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_01.uasset new file mode 100644 index 0000000..a7d1e0b Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_01.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_02.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_02.uasset new file mode 100644 index 0000000..e4be56b Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_02.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_03.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_03.uasset new file mode 100644 index 0000000..7dbcfab Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_03.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_04.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_04.uasset new file mode 100644 index 0000000..bb68492 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_04.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_05.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_05.uasset new file mode 100644 index 0000000..d0e1518 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_05.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_06.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_06.uasset new file mode 100644 index 0000000..23b6f07 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_06.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_07.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_07.uasset new file mode 100644 index 0000000..c80cd92 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_07.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_08.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_08.uasset new file mode 100644 index 0000000..c8e2348 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_08.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_09.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_09.uasset new file mode 100644 index 0000000..1bf9ef3 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_09.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_10.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_10.uasset new file mode 100644 index 0000000..4c201db Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_10.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_11.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_11.uasset new file mode 100644 index 0000000..34f9a99 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_11.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_12.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_12.uasset new file mode 100644 index 0000000..9d17d7c Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_12.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_13.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_13.uasset new file mode 100644 index 0000000..76ae3db Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_13.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_14.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_14.uasset new file mode 100644 index 0000000..7afba68 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_14.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_15.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_15.uasset new file mode 100644 index 0000000..0762b62 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_15.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_16.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_16.uasset new file mode 100644 index 0000000..3001296 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Default_16.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_01.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_01.uasset new file mode 100644 index 0000000..d9fe863 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_01.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_02.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_02.uasset new file mode 100644 index 0000000..655e6bf Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_02.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_03.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_03.uasset new file mode 100644 index 0000000..5672562 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_03.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_04.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_04.uasset new file mode 100644 index 0000000..63db656 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_04.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_05.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_05.uasset new file mode 100644 index 0000000..859efe2 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_05.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_06.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_06.uasset new file mode 100644 index 0000000..9ba623f Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_06.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_07.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_07.uasset new file mode 100644 index 0000000..4498eef Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_07.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_08.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_08.uasset new file mode 100644 index 0000000..8f06cb4 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_08.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_09.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_09.uasset new file mode 100644 index 0000000..632b072 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_09.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_10.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_10.uasset new file mode 100644 index 0000000..5453e2e Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Flesh_10.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_01.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_01.uasset new file mode 100644 index 0000000..9d493ae Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_01.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_02.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_02.uasset new file mode 100644 index 0000000..da1920f Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_02.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_03.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_03.uasset new file mode 100644 index 0000000..5a897e3 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_03.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_04.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_04.uasset new file mode 100644 index 0000000..a6c945b Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_04.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_05.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_05.uasset new file mode 100644 index 0000000..74d08f0 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_05.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_06.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_06.uasset new file mode 100644 index 0000000..9438f3b Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_06.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_07.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_07.uasset new file mode 100644 index 0000000..7662e86 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_07.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_08.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_08.uasset new file mode 100644 index 0000000..b36cecd Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_08.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_09.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_09.uasset new file mode 100644 index 0000000..2ecc877 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_09.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_10.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_10.uasset new file mode 100644 index 0000000..ff22adb Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_10.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_11.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_11.uasset new file mode 100644 index 0000000..778629a Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_11.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_12.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_12.uasset new file mode 100644 index 0000000..0f095c9 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_12.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_13.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_13.uasset new file mode 100644 index 0000000..a3ca0ff Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_13.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_14.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_14.uasset new file mode 100644 index 0000000..736e725 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_14.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_15.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_15.uasset new file mode 100644 index 0000000..d8a1519 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_15.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_16.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_16.uasset new file mode 100644 index 0000000..78c85b2 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Mtl_16.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_01.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_01.uasset new file mode 100644 index 0000000..dd82e2d Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_01.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_02.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_02.uasset new file mode 100644 index 0000000..8f790a2 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_02.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_03.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_03.uasset new file mode 100644 index 0000000..dedbb66 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_03.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_04.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_04.uasset new file mode 100644 index 0000000..84b6d3d Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_04.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_05.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_05.uasset new file mode 100644 index 0000000..2896c5f Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_05.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_06.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_06.uasset new file mode 100644 index 0000000..ad23659 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_06.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_07.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_07.uasset new file mode 100644 index 0000000..07e7857 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_07.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_08.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_08.uasset new file mode 100644 index 0000000..7b83b78 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_08.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_09.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_09.uasset new file mode 100644 index 0000000..bca56f0 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_09.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_10.uasset b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_10.uasset new file mode 100644 index 0000000..317b92f Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_AK_Water_10.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_01.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_01.uasset new file mode 100644 index 0000000..4dc89f3 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_01.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_02.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_02.uasset new file mode 100644 index 0000000..7dd7b06 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_02.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_03.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_03.uasset new file mode 100644 index 0000000..0997b6a Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_03.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_04.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_04.uasset new file mode 100644 index 0000000..c2dd3d8 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_04.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_05.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_05.uasset new file mode 100644 index 0000000..acc34c8 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Close_05.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_01.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_01.uasset new file mode 100644 index 0000000..4a2fad4 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_01.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_02.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_02.uasset new file mode 100644 index 0000000..98780c1 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_02.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_03.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_03.uasset new file mode 100644 index 0000000..9ce02cc Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_03.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_04.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_04.uasset new file mode 100644 index 0000000..e5cc5a3 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_04.uasset differ diff --git a/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_05.uasset b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_05.uasset new file mode 100644 index 0000000..d595191 Binary files /dev/null and b/Content/Sounds/Impacts/wav/sw_Impt_Launcher_Dist_05.uasset differ diff --git a/Content/Sounds/Pickups/SCue_Pickup_Ammo.uasset b/Content/Sounds/Pickups/SCue_Pickup_Ammo.uasset new file mode 100644 index 0000000..3f4dd04 Binary files /dev/null and b/Content/Sounds/Pickups/SCue_Pickup_Ammo.uasset differ diff --git a/Content/Sounds/Pickups/SCue_Pickup_Health.uasset b/Content/Sounds/Pickups/SCue_Pickup_Health.uasset new file mode 100644 index 0000000..f9a725b Binary files /dev/null and b/Content/Sounds/Pickups/SCue_Pickup_Health.uasset differ diff --git a/Content/Sounds/Pickups/wav/sw_Pickup_Ammo.uasset b/Content/Sounds/Pickups/wav/sw_Pickup_Ammo.uasset new file mode 100644 index 0000000..c8ece4a Binary files /dev/null and b/Content/Sounds/Pickups/wav/sw_Pickup_Ammo.uasset differ diff --git a/Content/Sounds/Pickups/wav/sw_Pickup_Health.uasset b/Content/Sounds/Pickups/wav/sw_Pickup_Health.uasset new file mode 100644 index 0000000..a03fd5c Binary files /dev/null and b/Content/Sounds/Pickups/wav/sw_Pickup_Health.uasset differ diff --git a/Content/Sounds/Projectiles/SCue_Proj_Grenade_LP.uasset b/Content/Sounds/Projectiles/SCue_Proj_Grenade_LP.uasset new file mode 100644 index 0000000..04fc829 Binary files /dev/null and b/Content/Sounds/Projectiles/SCue_Proj_Grenade_LP.uasset differ diff --git a/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_01.uasset b/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_01.uasset new file mode 100644 index 0000000..b88c83b Binary files /dev/null and b/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_01.uasset differ diff --git a/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_02.uasset b/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_02.uasset new file mode 100644 index 0000000..5324ad4 Binary files /dev/null and b/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_02.uasset differ diff --git a/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_03.uasset b/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_03.uasset new file mode 100644 index 0000000..20824a9 Binary files /dev/null and b/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_03.uasset differ diff --git a/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_04.uasset b/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_04.uasset new file mode 100644 index 0000000..db747d9 Binary files /dev/null and b/Content/Sounds/Projectiles/wav/sw_Proj_Flight_LP_04.uasset differ diff --git a/Content/Sounds/Stingers/SCue_PlayerDeath_Sting.uasset b/Content/Sounds/Stingers/SCue_PlayerDeath_Sting.uasset new file mode 100644 index 0000000..5e21b44 Binary files /dev/null and b/Content/Sounds/Stingers/SCue_PlayerDeath_Sting.uasset differ diff --git a/Content/Sounds/Stingers/SCue_Respawn_Sting.uasset b/Content/Sounds/Stingers/SCue_Respawn_Sting.uasset new file mode 100644 index 0000000..043030a Binary files /dev/null and b/Content/Sounds/Stingers/SCue_Respawn_Sting.uasset differ diff --git a/Content/Sounds/Stingers/wav/sw_Sting_PlayerDeath.uasset b/Content/Sounds/Stingers/wav/sw_Sting_PlayerDeath.uasset new file mode 100644 index 0000000..1912835 Binary files /dev/null and b/Content/Sounds/Stingers/wav/sw_Sting_PlayerDeath.uasset differ diff --git a/Content/Sounds/Stingers/wav/sw_Sting_Respawn.uasset b/Content/Sounds/Stingers/wav/sw_Sting_Respawn.uasset new file mode 100644 index 0000000..17e45b3 Binary files /dev/null and b/Content/Sounds/Stingers/wav/sw_Sting_Respawn.uasset differ diff --git a/Content/Sounds/UI/SCue_GameExit.uasset b/Content/Sounds/UI/SCue_GameExit.uasset new file mode 100644 index 0000000..7afed46 Binary files /dev/null and b/Content/Sounds/UI/SCue_GameExit.uasset differ diff --git a/Content/Sounds/UI/SCue_GameStart.uasset b/Content/Sounds/UI/SCue_GameStart.uasset new file mode 100644 index 0000000..6172e50 Binary files /dev/null and b/Content/Sounds/UI/SCue_GameStart.uasset differ diff --git a/Content/Sounds/UI/SCue_UI_Back.uasset b/Content/Sounds/UI/SCue_UI_Back.uasset new file mode 100644 index 0000000..b84790a Binary files /dev/null and b/Content/Sounds/UI/SCue_UI_Back.uasset differ diff --git a/Content/Sounds/UI/SCue_UI_Hover.uasset b/Content/Sounds/UI/SCue_UI_Hover.uasset new file mode 100644 index 0000000..60e2bb1 Binary files /dev/null and b/Content/Sounds/UI/SCue_UI_Hover.uasset differ diff --git a/Content/Sounds/UI/SCue_UI_Message_Rx.uasset b/Content/Sounds/UI/SCue_UI_Message_Rx.uasset new file mode 100644 index 0000000..502cb1c Binary files /dev/null and b/Content/Sounds/UI/SCue_UI_Message_Rx.uasset differ diff --git a/Content/Sounds/UI/SCue_UI_Message_Tx.uasset b/Content/Sounds/UI/SCue_UI_Message_Tx.uasset new file mode 100644 index 0000000..dc9e9ac Binary files /dev/null and b/Content/Sounds/UI/SCue_UI_Message_Tx.uasset differ diff --git a/Content/Sounds/UI/SCue_UI_Open.uasset b/Content/Sounds/UI/SCue_UI_Open.uasset new file mode 100644 index 0000000..758322a Binary files /dev/null and b/Content/Sounds/UI/SCue_UI_Open.uasset differ diff --git a/Content/Sounds/UI/SCue_UI_Select.uasset b/Content/Sounds/UI/SCue_UI_Select.uasset new file mode 100644 index 0000000..8c0ee7d Binary files /dev/null and b/Content/Sounds/UI/SCue_UI_Select.uasset differ diff --git a/Content/Sounds/UI/SCue_UI_Toggle.uasset b/Content/Sounds/UI/SCue_UI_Toggle.uasset new file mode 100644 index 0000000..19b573b Binary files /dev/null and b/Content/Sounds/UI/SCue_UI_Toggle.uasset differ diff --git a/Content/Sounds/UI/wav/sw_UI_Back.uasset b/Content/Sounds/UI/wav/sw_UI_Back.uasset new file mode 100644 index 0000000..8eee747 Binary files /dev/null and b/Content/Sounds/UI/wav/sw_UI_Back.uasset differ diff --git a/Content/Sounds/UI/wav/sw_UI_GameExit.uasset b/Content/Sounds/UI/wav/sw_UI_GameExit.uasset new file mode 100644 index 0000000..42f23dc Binary files /dev/null and b/Content/Sounds/UI/wav/sw_UI_GameExit.uasset differ diff --git a/Content/Sounds/UI/wav/sw_UI_GameStart.uasset b/Content/Sounds/UI/wav/sw_UI_GameStart.uasset new file mode 100644 index 0000000..b45dc6f Binary files /dev/null and b/Content/Sounds/UI/wav/sw_UI_GameStart.uasset differ diff --git a/Content/Sounds/UI/wav/sw_UI_Hover.uasset b/Content/Sounds/UI/wav/sw_UI_Hover.uasset new file mode 100644 index 0000000..8593c92 Binary files /dev/null and b/Content/Sounds/UI/wav/sw_UI_Hover.uasset differ diff --git a/Content/Sounds/UI/wav/sw_UI_Message_Rx.uasset b/Content/Sounds/UI/wav/sw_UI_Message_Rx.uasset new file mode 100644 index 0000000..7acbe0d Binary files /dev/null and b/Content/Sounds/UI/wav/sw_UI_Message_Rx.uasset differ diff --git a/Content/Sounds/UI/wav/sw_UI_Message_Tx.uasset b/Content/Sounds/UI/wav/sw_UI_Message_Tx.uasset new file mode 100644 index 0000000..e7f83d8 Binary files /dev/null and b/Content/Sounds/UI/wav/sw_UI_Message_Tx.uasset differ diff --git a/Content/Sounds/UI/wav/sw_UI_Open.uasset b/Content/Sounds/UI/wav/sw_UI_Open.uasset new file mode 100644 index 0000000..dd1387d Binary files /dev/null and b/Content/Sounds/UI/wav/sw_UI_Open.uasset differ diff --git a/Content/Sounds/UI/wav/sw_UI_Select.uasset b/Content/Sounds/UI/wav/sw_UI_Select.uasset new file mode 100644 index 0000000..d2d1a87 Binary files /dev/null and b/Content/Sounds/UI/wav/sw_UI_Select.uasset differ diff --git a/Content/Sounds/UI/wav/sw_UI_Toggle.uasset b/Content/Sounds/UI/wav/sw_UI_Toggle.uasset new file mode 100644 index 0000000..b13ddbc Binary files /dev/null and b/Content/Sounds/UI/wav/sw_UI_Toggle.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/SCue_AR_Dryfire.uasset b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Dryfire.uasset new file mode 100644 index 0000000..b346cdd Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Dryfire.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/SCue_AR_Equip.uasset b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Equip.uasset new file mode 100644 index 0000000..66b0a7f Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Equip.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/SCue_AR_Reload.uasset b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Reload.uasset new file mode 100644 index 0000000..2dabdde Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Reload.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/SCue_AR_Shoot_LP.uasset b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Shoot_LP.uasset new file mode 100644 index 0000000..b725ddd Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Shoot_LP.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/SCue_AR_Shoot_LP_Tail.uasset b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Shoot_LP_Tail.uasset new file mode 100644 index 0000000..b41a94d Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/SCue_AR_Shoot_LP_Tail.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Equip_01.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Equip_01.uasset new file mode 100644 index 0000000..f227641 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Equip_01.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Equip_02.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Equip_02.uasset new file mode 100644 index 0000000..237e683 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Equip_02.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Equip_03.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Equip_03.uasset new file mode 100644 index 0000000..6ac599b Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Equip_03.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_01.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_01.uasset new file mode 100644 index 0000000..446c0b7 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_01.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_02.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_02.uasset new file mode 100644 index 0000000..8dbe15b Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_02.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_01.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_01.uasset new file mode 100644 index 0000000..330241f Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_01.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_02.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_02.uasset new file mode 100644 index 0000000..14c7529 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_02.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_End_01.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_End_01.uasset new file mode 100644 index 0000000..a91d3a1 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_End_01.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_End_02.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_End_02.uasset new file mode 100644 index 0000000..06b79de Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_Dist_End_02.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_End_01.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_End_01.uasset new file mode 100644 index 0000000..b3a7087 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_End_01.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_End_02.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_End_02.uasset new file mode 100644 index 0000000..6433bd8 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_3D_LP_End_02.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_01.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_01.uasset new file mode 100644 index 0000000..084b717 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_01.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_End_01.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_End_01.uasset new file mode 100644 index 0000000..001a9ab Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_End_01.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_End_02.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_End_02.uasset new file mode 100644 index 0000000..8b25992 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_End_02.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_End_03.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_End_03.uasset new file mode 100644 index 0000000..8f03d94 Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_AR_Shoot_LP_End_03.uasset differ diff --git a/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_Reload_AR.uasset b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_Reload_AR.uasset new file mode 100644 index 0000000..cf00a8d Binary files /dev/null and b/Content/Sounds/Weapons/AssultRifle/wav/sw_Wep_Reload_AR.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Dryfire.uasset b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Dryfire.uasset new file mode 100644 index 0000000..0c1c06c Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Dryfire.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Equip.uasset b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Equip.uasset new file mode 100644 index 0000000..9f8ee47 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Equip.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Reload_End.uasset b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Reload_End.uasset new file mode 100644 index 0000000..49d1dc8 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Reload_End.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Reload_Start.uasset b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Reload_Start.uasset new file mode 100644 index 0000000..3293ddb Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Reload_Start.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Shoot.uasset b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Shoot.uasset new file mode 100644 index 0000000..4e8ceac Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/SCue_Launcher_Shoot.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Dryfire_01.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Dryfire_01.uasset new file mode 100644 index 0000000..7dbeaee Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Dryfire_01.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Dryfire_02.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Dryfire_02.uasset new file mode 100644 index 0000000..afd9b6e Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Dryfire_02.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Dryfire_03.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Dryfire_03.uasset new file mode 100644 index 0000000..7e7b16a Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Dryfire_03.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_01.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_01.uasset new file mode 100644 index 0000000..6404893 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_01.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_02.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_02.uasset new file mode 100644 index 0000000..ab7117c Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_02.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_03.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_03.uasset new file mode 100644 index 0000000..c9bf390 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_03.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_04.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_04.uasset new file mode 100644 index 0000000..444280b Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_04.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_01.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_01.uasset new file mode 100644 index 0000000..a61d387 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_01.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_02.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_02.uasset new file mode 100644 index 0000000..77237b3 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_02.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_03.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_03.uasset new file mode 100644 index 0000000..584bd70 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_03.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_04.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_04.uasset new file mode 100644 index 0000000..d1a5d04 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_04.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_01.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_01.uasset new file mode 100644 index 0000000..62e0b16 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_01.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_02.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_02.uasset new file mode 100644 index 0000000..f572ce6 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_02.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_03.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_03.uasset new file mode 100644 index 0000000..b41566d Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_03.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_04.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_04.uasset new file mode 100644 index 0000000..195b2b4 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Launcher_Shoot_3D_Dist_04.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Reload_Launcher_End.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Reload_Launcher_End.uasset new file mode 100644 index 0000000..5902e77 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Reload_Launcher_End.uasset differ diff --git a/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Reload_Launcher_Start.uasset b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Reload_Launcher_Start.uasset new file mode 100644 index 0000000..a8ec830 Binary files /dev/null and b/Content/Sounds/Weapons/GrenadeLauncher/wav/sw_Wep_Reload_Launcher_Start.uasset differ diff --git a/Content/UI/Chat/ChatBoxBacking.uasset b/Content/UI/Chat/ChatBoxBacking.uasset new file mode 100644 index 0000000..c91cb26 Binary files /dev/null and b/Content/UI/Chat/ChatBoxBacking.uasset differ diff --git a/Content/UI/Chat/ChatBoxEntryBacking.uasset b/Content/UI/Chat/ChatBoxEntryBacking.uasset new file mode 100644 index 0000000..a765a04 Binary files /dev/null and b/Content/UI/Chat/ChatBoxEntryBacking.uasset differ diff --git a/Content/UI/Chat/ChatLog_Icon_Star.uasset b/Content/UI/Chat/ChatLog_Icon_Star.uasset new file mode 100644 index 0000000..407fb40 Binary files /dev/null and b/Content/UI/Chat/ChatLog_Icon_Star.uasset differ diff --git a/Content/UI/HUD/HUDAssets02.uasset b/Content/UI/HUD/HUDAssets02.uasset new file mode 100644 index 0000000..b270c06 Binary files /dev/null and b/Content/UI/HUD/HUDAssets02.uasset differ diff --git a/Content/UI/HUD/HUDMain.uasset b/Content/UI/HUD/HUDMain.uasset new file mode 100644 index 0000000..bca42be Binary files /dev/null and b/Content/UI/HUD/HUDMain.uasset differ diff --git a/Content/UI/HUD/HitIndicator.uasset b/Content/UI/HUD/HitIndicator.uasset new file mode 100644 index 0000000..401b00e Binary files /dev/null and b/Content/UI/HUD/HitIndicator.uasset differ diff --git a/Content/UI/HUD/LowHealthOverlay.uasset b/Content/UI/HUD/LowHealthOverlay.uasset new file mode 100644 index 0000000..af9010a Binary files /dev/null and b/Content/UI/HUD/LowHealthOverlay.uasset differ diff --git a/Content/UI/HUD/Roboto18.uasset b/Content/UI/HUD/Roboto18.uasset new file mode 100644 index 0000000..c9eb7a5 Binary files /dev/null and b/Content/UI/HUD/Roboto18.uasset differ diff --git a/Content/UI/HUD/Roboto51.uasset b/Content/UI/HUD/Roboto51.uasset new file mode 100644 index 0000000..72d8750 Binary files /dev/null and b/Content/UI/HUD/Roboto51.uasset differ diff --git a/Content/UI/Menu/LoadingScreen.uasset b/Content/UI/Menu/LoadingScreen.uasset new file mode 100644 index 0000000..afb9578 Binary files /dev/null and b/Content/UI/Menu/LoadingScreen.uasset differ diff --git a/Content/UI/Menu/LoadingScreen_Mat.uasset b/Content/UI/Menu/LoadingScreen_Mat.uasset new file mode 100644 index 0000000..3dc41cb Binary files /dev/null and b/Content/UI/Menu/LoadingScreen_Mat.uasset differ diff --git a/Content/UI/Menu/MenuArrowLeft.uasset b/Content/UI/Menu/MenuArrowLeft.uasset new file mode 100644 index 0000000..1cda857 Binary files /dev/null and b/Content/UI/Menu/MenuArrowLeft.uasset differ diff --git a/Content/UI/Menu/MenuArrowRight.uasset b/Content/UI/Menu/MenuArrowRight.uasset new file mode 100644 index 0000000..ccac61e Binary files /dev/null and b/Content/UI/Menu/MenuArrowRight.uasset differ diff --git a/Content/UI/Menu/MenuBgLeft.uasset b/Content/UI/Menu/MenuBgLeft.uasset new file mode 100644 index 0000000..ce06bfe Binary files /dev/null and b/Content/UI/Menu/MenuBgLeft.uasset differ diff --git a/Content/UI/Menu/MenuBgRight.uasset b/Content/UI/Menu/MenuBgRight.uasset new file mode 100644 index 0000000..bc4a0f1 Binary files /dev/null and b/Content/UI/Menu/MenuBgRight.uasset differ diff --git a/Content/UI/Menu/MenuHeaderBg.uasset b/Content/UI/Menu/MenuHeaderBg.uasset new file mode 100644 index 0000000..bfa39d0 Binary files /dev/null and b/Content/UI/Menu/MenuHeaderBg.uasset differ diff --git a/Content/UI/Menu/MenuItemBg.uasset b/Content/UI/Menu/MenuItemBg.uasset new file mode 100644 index 0000000..5781885 Binary files /dev/null and b/Content/UI/Menu/MenuItemBg.uasset differ diff --git a/Content/UI/MenuPlane.uasset b/Content/UI/MenuPlane.uasset new file mode 100644 index 0000000..b15e6da Binary files /dev/null and b/Content/UI/MenuPlane.uasset differ diff --git a/Content/UI/Styles/DefaultShooterButtonStyle.uasset b/Content/UI/Styles/DefaultShooterButtonStyle.uasset new file mode 100644 index 0000000..5fb1dd6 Binary files /dev/null and b/Content/UI/Styles/DefaultShooterButtonStyle.uasset differ diff --git a/Content/UI/Styles/DefaultShooterChatStyle.uasset b/Content/UI/Styles/DefaultShooterChatStyle.uasset new file mode 100644 index 0000000..2519064 Binary files /dev/null and b/Content/UI/Styles/DefaultShooterChatStyle.uasset differ diff --git a/Content/UI/Styles/DefaultShooterMenuItemStyle.uasset b/Content/UI/Styles/DefaultShooterMenuItemStyle.uasset new file mode 100644 index 0000000..945e6cf Binary files /dev/null and b/Content/UI/Styles/DefaultShooterMenuItemStyle.uasset differ diff --git a/Content/UI/Styles/DefaultShooterMenuSoundsStyle.uasset b/Content/UI/Styles/DefaultShooterMenuSoundsStyle.uasset new file mode 100644 index 0000000..394eee2 Binary files /dev/null and b/Content/UI/Styles/DefaultShooterMenuSoundsStyle.uasset differ diff --git a/Content/UI/Styles/DefaultShooterMenuStyle.uasset b/Content/UI/Styles/DefaultShooterMenuStyle.uasset new file mode 100644 index 0000000..6e87994 Binary files /dev/null and b/Content/UI/Styles/DefaultShooterMenuStyle.uasset differ diff --git a/Content/UI/Styles/DefaultShooterOptionsStyle.uasset b/Content/UI/Styles/DefaultShooterOptionsStyle.uasset new file mode 100644 index 0000000..469f673 Binary files /dev/null and b/Content/UI/Styles/DefaultShooterOptionsStyle.uasset differ diff --git a/Content/UI/Styles/DefaultShooterScoreboardStyle.uasset b/Content/UI/Styles/DefaultShooterScoreboardStyle.uasset new file mode 100644 index 0000000..bd0bd69 Binary files /dev/null and b/Content/UI/Styles/DefaultShooterScoreboardStyle.uasset differ diff --git a/Content/Weapons/Launcher.uasset b/Content/Weapons/Launcher.uasset new file mode 100644 index 0000000..3972cf3 Binary files /dev/null and b/Content/Weapons/Launcher.uasset differ diff --git a/Content/Weapons/Launcher_Skeleton.uasset b/Content/Weapons/Launcher_Skeleton.uasset new file mode 100644 index 0000000..03e09be Binary files /dev/null and b/Content/Weapons/Launcher_Skeleton.uasset differ diff --git a/Content/Weapons/MaterialLayers/ML_BoltMetal.uasset b/Content/Weapons/MaterialLayers/ML_BoltMetal.uasset new file mode 100644 index 0000000..fecea4f Binary files /dev/null and b/Content/Weapons/MaterialLayers/ML_BoltMetal.uasset differ diff --git a/Content/Weapons/MaterialLayers/ML_Metal_Copper.uasset b/Content/Weapons/MaterialLayers/ML_Metal_Copper.uasset new file mode 100644 index 0000000..8dd2955 Binary files /dev/null and b/Content/Weapons/MaterialLayers/ML_Metal_Copper.uasset differ diff --git a/Content/Weapons/MaterialLayers/ML_Metal_Gray.uasset b/Content/Weapons/MaterialLayers/ML_Metal_Gray.uasset new file mode 100644 index 0000000..1f6b607 Binary files /dev/null and b/Content/Weapons/MaterialLayers/ML_Metal_Gray.uasset differ diff --git a/Content/Weapons/MaterialLayers/ML_Paint.uasset b/Content/Weapons/MaterialLayers/ML_Paint.uasset new file mode 100644 index 0000000..c67b06a Binary files /dev/null and b/Content/Weapons/MaterialLayers/ML_Paint.uasset differ diff --git a/Content/Weapons/MaterialLayers/ML_Rubber.uasset b/Content/Weapons/MaterialLayers/ML_Rubber.uasset new file mode 100644 index 0000000..558bc7a Binary files /dev/null and b/Content/Weapons/MaterialLayers/ML_Rubber.uasset differ diff --git a/Content/Weapons/MaterialLayers/T_ExampleLayers_Metal02_N.uasset b/Content/Weapons/MaterialLayers/T_ExampleLayers_Metal02_N.uasset new file mode 100644 index 0000000..10cf2e9 Binary files /dev/null and b/Content/Weapons/MaterialLayers/T_ExampleLayers_Metal02_N.uasset differ diff --git a/Content/Weapons/MaterialLayers/T_Metal_Metal02_D.uasset b/Content/Weapons/MaterialLayers/T_Metal_Metal02_D.uasset new file mode 100644 index 0000000..7c7a439 Binary files /dev/null and b/Content/Weapons/MaterialLayers/T_Metal_Metal02_D.uasset differ diff --git a/Content/Weapons/MaterialLayers/T_PaintedMetal01_N.uasset b/Content/Weapons/MaterialLayers/T_PaintedMetal01_N.uasset new file mode 100644 index 0000000..2ecc008 Binary files /dev/null and b/Content/Weapons/MaterialLayers/T_PaintedMetal01_N.uasset differ diff --git a/Content/Weapons/MaterialLayers/T_PaintedMetal02.uasset b/Content/Weapons/MaterialLayers/T_PaintedMetal02.uasset new file mode 100644 index 0000000..b5fb533 Binary files /dev/null and b/Content/Weapons/MaterialLayers/T_PaintedMetal02.uasset differ diff --git a/Content/Weapons/MaterialLayers/T_Rubber.uasset b/Content/Weapons/MaterialLayers/T_Rubber.uasset new file mode 100644 index 0000000..3fda4af Binary files /dev/null and b/Content/Weapons/MaterialLayers/T_Rubber.uasset differ diff --git a/Content/Weapons/Materials/Launcher_MatA.uasset b/Content/Weapons/Materials/Launcher_MatA.uasset new file mode 100644 index 0000000..2afb93d Binary files /dev/null and b/Content/Weapons/Materials/Launcher_MatA.uasset differ diff --git a/Content/Weapons/Materials/Launcher_MatA_Inst.uasset b/Content/Weapons/Materials/Launcher_MatA_Inst.uasset new file mode 100644 index 0000000..0c07c10 Binary files /dev/null and b/Content/Weapons/Materials/Launcher_MatA_Inst.uasset differ diff --git a/Content/Weapons/Materials/Launcher_MatB.uasset b/Content/Weapons/Materials/Launcher_MatB.uasset new file mode 100644 index 0000000..2d18f7f Binary files /dev/null and b/Content/Weapons/Materials/Launcher_MatB.uasset differ diff --git a/Content/Weapons/Materials/Launcher_MatB_Inst.uasset b/Content/Weapons/Materials/Launcher_MatB_Inst.uasset new file mode 100644 index 0000000..94acb00 Binary files /dev/null and b/Content/Weapons/Materials/Launcher_MatB_Inst.uasset differ diff --git a/Content/Weapons/Materials/MagGun_MatA.uasset b/Content/Weapons/Materials/MagGun_MatA.uasset new file mode 100644 index 0000000..5126c1b Binary files /dev/null and b/Content/Weapons/Materials/MagGun_MatA.uasset differ diff --git a/Content/Weapons/Materials/MagGun_MatA_Inst3.uasset b/Content/Weapons/Materials/MagGun_MatA_Inst3.uasset new file mode 100644 index 0000000..ba0c018 Binary files /dev/null and b/Content/Weapons/Materials/MagGun_MatA_Inst3.uasset differ diff --git a/Content/Weapons/Materials/MagGun_MatB.uasset b/Content/Weapons/Materials/MagGun_MatB.uasset new file mode 100644 index 0000000..d78151d Binary files /dev/null and b/Content/Weapons/Materials/MagGun_MatB.uasset differ diff --git a/Content/Weapons/Materials/MagGun_MatB_Inst2.uasset b/Content/Weapons/Materials/MagGun_MatB_Inst2.uasset new file mode 100644 index 0000000..fd5e734 Binary files /dev/null and b/Content/Weapons/Materials/MagGun_MatB_Inst2.uasset differ diff --git a/Content/Weapons/Materials/MagGun_SightLens_Mat.uasset b/Content/Weapons/Materials/MagGun_SightLens_Mat.uasset new file mode 100644 index 0000000..ff96b7b Binary files /dev/null and b/Content/Weapons/Materials/MagGun_SightLens_Mat.uasset differ diff --git a/Content/Weapons/Materials/MagGun_SightLens_Mat_Inst.uasset b/Content/Weapons/Materials/MagGun_SightLens_Mat_Inst.uasset new file mode 100644 index 0000000..d3077da Binary files /dev/null and b/Content/Weapons/Materials/MagGun_SightLens_Mat_Inst.uasset differ diff --git a/Content/Weapons/Rifle.uasset b/Content/Weapons/Rifle.uasset new file mode 100644 index 0000000..f734d5c Binary files /dev/null and b/Content/Weapons/Rifle.uasset differ diff --git a/Content/Weapons/Rifle_Skeleton.uasset b/Content/Weapons/Rifle_Skeleton.uasset new file mode 100644 index 0000000..b2ea91c Binary files /dev/null and b/Content/Weapons/Rifle_Skeleton.uasset differ diff --git a/Content/Weapons/Textures/LauncherT1_section.uasset b/Content/Weapons/Textures/LauncherT1_section.uasset new file mode 100644 index 0000000..ea024b7 Binary files /dev/null and b/Content/Weapons/Textures/LauncherT1_section.uasset differ diff --git a/Content/Weapons/Textures/LauncherT1_shade.uasset b/Content/Weapons/Textures/LauncherT1_shade.uasset new file mode 100644 index 0000000..cfc4a6b Binary files /dev/null and b/Content/Weapons/Textures/LauncherT1_shade.uasset differ diff --git a/Content/Weapons/Textures/LauncherT2_section.uasset b/Content/Weapons/Textures/LauncherT2_section.uasset new file mode 100644 index 0000000..e0ce3ba Binary files /dev/null and b/Content/Weapons/Textures/LauncherT2_section.uasset differ diff --git a/Content/Weapons/Textures/LauncherT2_shade.uasset b/Content/Weapons/Textures/LauncherT2_shade.uasset new file mode 100644 index 0000000..0a8b0bc Binary files /dev/null and b/Content/Weapons/Textures/LauncherT2_shade.uasset differ diff --git a/Content/Weapons/Textures/Launcher_01_N.uasset b/Content/Weapons/Textures/Launcher_01_N.uasset new file mode 100644 index 0000000..77e75d0 Binary files /dev/null and b/Content/Weapons/Textures/Launcher_01_N.uasset differ diff --git a/Content/Weapons/Textures/Launcher_02_N.uasset b/Content/Weapons/Textures/Launcher_02_N.uasset new file mode 100644 index 0000000..cea24e1 Binary files /dev/null and b/Content/Weapons/Textures/Launcher_02_N.uasset differ diff --git a/Content/Weapons/Textures/MagGunT1_section.uasset b/Content/Weapons/Textures/MagGunT1_section.uasset new file mode 100644 index 0000000..fdd1681 Binary files /dev/null and b/Content/Weapons/Textures/MagGunT1_section.uasset differ diff --git a/Content/Weapons/Textures/MagGunT1_shade.uasset b/Content/Weapons/Textures/MagGunT1_shade.uasset new file mode 100644 index 0000000..8aa65c2 Binary files /dev/null and b/Content/Weapons/Textures/MagGunT1_shade.uasset differ diff --git a/Content/Weapons/Textures/MagGunT2_section.uasset b/Content/Weapons/Textures/MagGunT2_section.uasset new file mode 100644 index 0000000..ab3f24b Binary files /dev/null and b/Content/Weapons/Textures/MagGunT2_section.uasset differ diff --git a/Content/Weapons/Textures/MagGunT2_shade.uasset b/Content/Weapons/Textures/MagGunT2_shade.uasset new file mode 100644 index 0000000..6c123c1 Binary files /dev/null and b/Content/Weapons/Textures/MagGunT2_shade.uasset differ diff --git a/Content/Weapons/Textures/MagGun_01_N.uasset b/Content/Weapons/Textures/MagGun_01_N.uasset new file mode 100644 index 0000000..58dd5c7 Binary files /dev/null and b/Content/Weapons/Textures/MagGun_01_N.uasset differ diff --git a/Content/Weapons/Textures/MagGun_02_N.uasset b/Content/Weapons/Textures/MagGun_02_N.uasset new file mode 100644 index 0000000..84d1006 Binary files /dev/null and b/Content/Weapons/Textures/MagGun_02_N.uasset differ diff --git a/Content/Weapons/Textures/MagGun_Decals.uasset b/Content/Weapons/Textures/MagGun_Decals.uasset new file mode 100644 index 0000000..f21761c Binary files /dev/null and b/Content/Weapons/Textures/MagGun_Decals.uasset differ diff --git a/Content/Weapons/Textures/MagGun_Display_EM.uasset b/Content/Weapons/Textures/MagGun_Display_EM.uasset new file mode 100644 index 0000000..008d862 Binary files /dev/null and b/Content/Weapons/Textures/MagGun_Display_EM.uasset differ diff --git a/Content/Weapons/Textures/SightLens_maskA.uasset b/Content/Weapons/Textures/SightLens_maskA.uasset new file mode 100644 index 0000000..9c1c8f4 Binary files /dev/null and b/Content/Weapons/Textures/SightLens_maskA.uasset differ diff --git a/Content/Weapons/Textures/SightLens_maskB.uasset b/Content/Weapons/Textures/SightLens_maskB.uasset new file mode 100644 index 0000000..a19449c Binary files /dev/null and b/Content/Weapons/Textures/SightLens_maskB.uasset differ diff --git a/Docs/basic-zeuz-support.md b/Docs/basic-zeuz-support.md new file mode 100644 index 0000000..780c684 --- /dev/null +++ b/Docs/basic-zeuz-support.md @@ -0,0 +1,50 @@ +## Basic zeuz support +In order to make the game server exit when the match is over, we can adapt the [default game mode timer](../Source/ShooterGame/Private/Online/ShooterGameMode.cpp) which handles the transitions between the game's states. +To gracefully achieve this, the client should be sent back to the main menu before the server exits. + +### Send client back to the main menu +```c++ +// Source/ShooterGame/Private/Online/ShooterGameMode.cpp::DefaultTimer + +if (GetMatchState() == MatchState::WaitingPostMatch) +{ + // Send the players back to the main menu + for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It) + { + (*It)->ClientReturnToMainMenuWithTextReason(NSLOCTEXT("GameMessages", "MatchEnded", "The match has ended.")); + AShooterPlayerController* ShooterPlayerController = Cast(*It); + ShooterPlayerController->HandleReturnToMainMenu(); + } + // ... + } +``` + +Instead of the game server calling `RestartGame()` when it is transitioning out of its post-match state, we instead instruct clients to return to the main menu, using the RPC `ClientReturnToMainMenuWithTextReason`. +Note that this RPC implementation is the same as the existing `ShooterPlayerController` definition of `ClientReturnToMainMenu` with added message propagation (in fact the deprecated `ClientReturnToMainMenu` method was updated with the new `ClientReturnToMainMenuWithTextReason` method definition). + +### Exit the process +```c++ +// Source/ShooterGame/Private/Online/ShooterGameMode.cpp::DefaultTimer + +// ... +else if (GetMatchState() == MatchState::WaitingPostMatch) +{ + // The post match is over (no time left) so wait for clients to be disconnected + if (GetNumPlayers() == 0) + { + // All players have disconnected, exit the server + FGenericPlatformMisc::RequestExit(false); + } +} +``` + +Once the game server has instructed the clients to return to the main menu, we then wait for all of the players to disconnect. +This *waiting* also occurs in the `DefaultTimer` method, as once we instruct players to return to the main menu (and therefore disconnect), we do not transition the game state, staying in post-match. +Subsequent calls to `DefaultTimer` then enter the case in the snippet above, where the number of connected players is checked and the game server is exited if there are no connected players. + +### Other changes +Besides the behavioural changes above, the on-screen messaging was changed to reflect the new player flow. +Instead of *'Starting new match...'* displaying during the countdown on the post-match scoreboard, the text now reads *'Match ending...'*. +Method names were also updated to reflect the change. + +This change was made in the [scoreboard widget](../Source/ShooterGame/Private/UI/Widgets/SShooterScoreboardWidget.cpp) (see method `SShooterScoreboardWidget::GetMatchEndText`, which was previously `SShooterScoreboardWidget::GetMatchRestartText`). \ No newline at end of file diff --git a/Docs/ccu-tracking.md b/Docs/ccu-tracking.md new file mode 100644 index 0000000..f81134a --- /dev/null +++ b/Docs/ccu-tracking.md @@ -0,0 +1,83 @@ +## CCU tracking (A2S protocol) +To support the A2S (Any to Server) protocol, an A2S server component is dropped in. +This keeps changes simple and makes the component much more manageable. + +The component also has settings, configurable from the Unreal Engine editor or the command line (command line options prioritised). + +### A2S server lifecycle +The A2S server is created and started during the game mode's `BeginPlay` event, since we only wish to serve A2S queries when the game has started. +The server component is then added to the root, so that it isn't garbage collected. +```c++ +// Source/ShooterGame/Private/Online/ShooterGameMode.cpp + +void AShooterGameMode::BeginPlay() +{ + // ... + A2SServer = NewObject(this); + A2SServer->AddToRoot(); + A2SServer->Settings = Settings; + A2SServer->Start(); +} +``` + +The settings assigned to the A2S server are the [A2S settings](../Source/ShooterGame/Private/Online/A2S/A2SServerSettings.h) which can be configured in the Unreal editor. +Following this assignment, in `A2SServer::Start()`, the command line flags are checked for any settings overwrites and applied if there are any. +This is to make the server configurable at runtime. + +On the other hand, the A2S server is stopped and made eligible for garbage collection in the `EndPlay` event of the game mode. +```c++ +// Source/ShooterGame/Private/Online/ShooterGameMode.cpp + +void AShooterGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + A2SServer->Stop(); + A2SServer->RemoveFromRoot(); + // ... +} +``` + +### Starting the server +The [A2S server](../Source/ShooterGame/Private/Online/A2S/A2SServer.cpp) uses Unreal's UDP socket API to build a send (see `UA2SServer::OpenSendSocket`) and receive (see `UA2SServer::OpenReceiveSocket`) socket which use the same port. +It is advisable that the same port is used for both sending and receiving as some implementations of A2S clients expect responses from the same address they make queries to. + +### Stopping the server +Unreal's UDP socket API also offers ways to close and destroy sockets, which we make use of. +It's also important to note that a mutex is used, to ensure that the socket isn't closed before a query is handled (i.e. a response sent). + +### Building a response +Building an `A2S_INFO` response is low level and requires byte-level operations. +A fully documented example response is available in `UA2SServer::BuildA2SInfoResponse` but it is strongly advised that you read [the protocol documentation](https://developer.valvesoftware.com/wiki/Server_queries) when implementing your own response. + +### Gotcha! +Unreal's internal representation of strings offsets UTF-8 encoding by +1, as *"this keeps anything from being put into the string as a null terminator"* (see `UnrealString.h` in your Unreal Engine source build). +This causes problems when converting strings to bytes in A2S responses, so be sure to implement your own `BytesToString` equivalent. +For this purpose, the `AddStringToByteArray` utility function, which adds a string to an existing byte array, is defined. +```c++ +// Source/ShooterGame/Private/Online/A2S/A2SServer.cpp + +void AddStringToByteArray(TArray& Bytes, FString InString) +{ + // Encode string as UTF-8 (default encoding is UTF-16) + const FTCHARToUTF8 InStringUtf8(*InString); + + // Copy string into byte buffer + // Note: Unreal's 'StringToBytes' method doesn't undo the +1 offset of its internal UTF-8 encoding so write our own + // conversion + int32 NumBytes = 0; + const ANSICHAR* CharPos = InStringUtf8.Get(); + + while (*CharPos && NumBytes < InStringUtf8.Length()) + { + Bytes.Add(static_cast(*CharPos)); + CharPos++; + NumBytes++;; + } + + // Add string terminating byte + Bytes.Add(0x00); +} +``` + +### Dependencies +This component requires the `Networking` and `Sockets` packages. +There were added to [ShooterGame.Build.cs](../Source/ShooterGame/ShooterGame.Build.cs). diff --git a/Docs/server-waiting.md b/Docs/server-waiting.md new file mode 100644 index 0000000..164872d --- /dev/null +++ b/Docs/server-waiting.md @@ -0,0 +1,50 @@ +## Server waiting +We wish to make the server wait until a player has joined before the pre-match countdown begins. + +The first change is to track whether or not a player has joined yet (in the [`ShooterGameMode` class](../Source/ShooterGame/Public/Online/ShooterGameMode.h)) and update it when a player connects. +```c++ +// Source/ShooterGame/Public/Online/ShooterGameMode.h + +class AShooterGameMode : public AGameMode +{ + // ... + /** Tracks if a game has joined the server at least once */ + bool bHasPlayerConnected = false; + // ... +} +``` + +```c++ +// Source/ShooterGame/Public/Online/ShooterGameMode.cpp + +void AShooterGameMode::PostLogin(APlayerController* NewPlayer) +{ + Super::PostLogin(NewPlayer); + + bHasPlayerConnected = true; + // ... +} +``` + +Lastly, we can use this variable in the timer, to specify whether to decrement the timer, or not. +```c++ +// Source/ShooterGame/Public/Online/ShooterGameMode.cpp + +void AShooterGameMode::DefaultTimer() +{ + // ... + if (MyGameState && MyGameState->RemainingTime > 0 && !MyGameState->bTimerPaused) + { + // If we are waiting for match to start (MatchState::WaitingToStart) and there has not yet been a connected + // player, do not decrement the counter + if (GetMatchState() == MatchState::WaitingToStart && !bHasPlayerConnected) + { + return; + } + + MyGameState->RemainingTime--; + // ... + } + // ... +} +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..28e3a89 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# zeuz-unreal-demo +An adaptation of the Unreal Engine [ShooterGame](https://docs.unrealengine.com/en-US/Resources/SampleGames/ShooterGame/index.html) example game project to demonstrate how to support [_zeuz_](https://zeuz.io/) orchestration. + +For information on uploading and hosting game servers on zeuz, please see [doc.zeuz.io](https://doc.zeuz.io/). + + +## Before getting started +Each section addresses a different behaviour missing from the unmodified [ShooterGame](https://docs.unrealengine.com/en-US/Resources/SampleGames/ShooterGame/index.html) project. +You should use this project as an example to read the changes you need to make to your game to better support zeuz orchestration. + +The payload commands in each section show only the fields required to enable the section's behaviour. +To enable all of the behaviours use the following payload command: +``` +/opt/zeuz/bin/payloadrunner +run +binaryactivename=ShooterServer +binaryexecpath=/opt/zeuz/gameserver/ShooterServer.sh +execargs=/Game/Maps/Highrise -log -NOSTEAM -PORT=${servicePort:PortName} -statsPort ${statsPort} -payloadId ${payloadID} +apikey= +apisecret= +apiendpoint=https://zcp.zeuz.io/api/v1 +``` + +Before you follow this example, there are a couple of general points to note: +- The '**Files changed**' list for each section only notes the most significant changes. + - For example, the files changed to edit the on-screen messaging text at the end of the game in ['Basic zeuz support'](#basic-zeuz-support) are omitted. +- The menu of the original ShooterGame project has been modified to reflect the connections available when this game is used with zeuz. For more information, see the [UI Changes section](#ui-changes). + - Note: These UI changes are **not** necessary to make your game support zeuz orchestration. + +Lastly, if you wish to follow along, making similar changes to the [ShooterGame](https://docs.unrealengine.com/en-US/Resources/SampleGames/ShooterGame/index.html) project, you should ensure you are able to package the unmodified game as a dedicated server and connect with a client. +There is an [Unreal Engine tutorial](https://docs.unrealengine.com/en-US/InteractiveExperiences/Networking/HowTo/DedicatedServers/index.html) for this. + + +## Base game hosting +Without any changes, the unmodified [ShooterGame project](https://docs.unrealengine.com/en-US/Resources/SampleGames/ShooterGame/index.html) can be built and published to zeuz. The payload command for this is: +``` +/opt/zeuz/bin/payloadrunner +run +binaryactivename=ShooterServer +binaryexecpath=/opt/zeuz/gameserver/ShooterServer.sh +execargs=/Game/Maps/Highrise -log -NOSTEAM -PORT=${servicePort:PortName} +``` +The execution flags specify: +- The map to launch (`/Game/Maps/Highrise`). +- Enable logging (`-log`). +- Disable Steam (`-NOSTEAM`). +- The port to host on (`-PORT=${servicePort:PortName}`). + - This makes use of the port variable zeuz makes available to you. + +Whilst this is a functional game hosted on zeuz, there are a few problems which are addressed in this project: +- The server keeps restarting matches. + - See [Basic zeuz support](#basic-zeuz-support). +- zeuz cannot read the number of players connected to the server. + - See [CCU tracking (A2S protocol)](#ccu-tracking-a2s-protocol). +- Since the payload image is started whenever a payload is ready (and unreserved), matches can start for unreserved payloads. + - See [Server waiting](#server-waiting). + + +## Basic zeuz support +> **For a full description of the changes with code snippets, see the [full docs](Docs/basic-zeuz-support.md) of this change.** + +To support [automatic payload release](https://doc.zeuz.io/docs/payload-definition#automatic-payload-release), your game server executable should terminate at the end of the match. + +**Files changed:** [ShooterGameMode.cpp](Source/ShooterGame/Private/Online/ShooterGameMode.cpp) + +Server lifetime controlled in the [game mode (`ShooterGameMode`)](Source/ShooterGame/Private/Online/ShooterGameMode.cpp) by sending an RPC to clients to return to the main menu and then exiting the game server when all clients are disconnected (see [`ShooterGameMode::DefaultTimer`](Source/ShooterGame/Private/Online/ShooterGameMode.cpp)). + +Once the code changes are implemented, the payload command needs to be updated with an API key, so that the payload can automatically be unreserved: +``` +/opt/zeuz/bin/payloadrunner +run +binaryactivename=ShooterServer +binaryexecpath=/opt/zeuz/gameserver/ShooterServer.sh +execargs=/Game/Maps/Highrise -log -NOSTEAM -PORT=${servicePort:PortName} +apikey= +apisecret= +apiendpoint=https://zcp.zeuz.io/api/v1 +``` + + +## CCU tracking (A2S protocol) +> **For a full description of the changes with code snippets, see the [full docs](Docs/ccu-tracking.md) of this change.** + +With zeuz, you can use [CCU tracking](https://doc.zeuz.io/docs/ccu-tracking) to view how many concurrent players are connected to your game servers. +When creating or editing an allocation, you can select various CCU tracking options at the end of the 'Payload Definition' section. +If you don't see this, speak to your zeuz account manager. + +**Files changed:** [A2SServer.h](Source/ShooterGame/Private/Online/A2S/A2SServer.h), +[A2SServer.cpp](Source/ShooterGame/Private/Online/A2S/A2SServer.cpp), +[A2SServerSettings.h](Source/ShooterGame/Private/Online/A2S/A2SServerSettings.h), +[ShooterGameMode.h](Source/ShooterGame/Public/Online/ShooterGameMode.h), +[ShooterGameMode.cpp](Source/ShooterGame/Private/Online/ShooterGameMode.cpp), +[ShooterGame.Build.cs](Source/ShooterGame/ShooterGame.Build.cs) + +To support CCU tracking, the game server must implement the [A2S (Any to Server) protocol](https://developer.valvesoftware.com/wiki/Server_queries). +For this, an [`A2SServer` component](Source/ShooterGame/Private/Online/A2S/A2SServer.h) is created which utilises Unreal's `UDPSocket` API to listen to and respond to A2S queries. +This example only supports `A2S_INFO` queries, as is required by zeuz CCU tracking, but you may wish to support further A2S features for your own purposes. + +This `A2SServer` component is created by the [game mode (`ShooterGameMode`)](Source/ShooterGame/Private/Online/ShooterGameMode.cpp) and has its lifetime controlled by the `BeginPlay/EndPlay` events, since the game mode exists only on the server. + +Lastly, the payload command of the allocation needs updating to ensure that the game server responds to A2S queries on the port it expects. +The `${statsPort}` variable is used in the payload command to specify this: +``` +/opt/zeuz/bin/payloadrunner +run +binaryactivename=ShooterServer +binaryexecpath=/opt/zeuz/gameserver/ShooterServer.sh +execargs=/Game/Maps/Highrise -log -NOSTEAM -PORT=${servicePort:PortName} -statsPort ${statsPort} -payloadId ${payloadID} +``` +In addition to `${statsPort}` being used in the `execargs`, the payload ID (accessible through `${payloadId}`) is used as the server name. +This is not crucial for zeuz to track CCUs, but useful if you wish to track CCUs per payload for your own queries. + + +## Server waiting +> **For a full description of the changes with code snippets, see the [full docs](Docs/server-waiting.md) of this change.** + +When a zeuz payload starts, the image it is launched with is started straight away, meaning that the game server starts executing before the payload is reserved. + +**Files changed:** [ShooterGameMode.h](Source/ShooterGame/Public/Online/ShooterGameMode.h), +[ShooterGameMode.cpp](Source/ShooterGame/Private/Online/ShooterGameMode.cpp) + +A ShooterGame server moves through three phases ('pre-match', 'in-match', 'post-match') after specified intervals of time. +This can cause problems if a payload spends a long amount of time as unreserved before players begin to connect to it as the server may not be in its pre-match phase when a player connects. + +To overcome this, the timer of the game (see [`ShooterGameMode::DefaultTimer`](Source/ShooterGame/Private/Online/ShooterGameMode.cpp)) doesn't count down when the match is waiting to start **and** there has not yet been a connected player. +After the first player connects, the timer will count down to the start of the game. + + +## UI changes +Whilst not necessary for supporting zeuz orchestration, the base ShooterGame example UI has been modified to allow players to connect to zeuz-hosted game servers. + +**Files changed:** [DefaultGame.ini](Config/DefaultGame.ini), +[ShooterGameInstance.h](Source/ShooterGame/Public/ShooterGameInstance.h), +[ShooterGameInstance.cpp](Source/ShooterGame/Private/ShooterGameInstance.cpp), +[ShooterMainMenu.cpp](Source/ShooterGame/Private/UI/Menu/ShooterMainMenu.cpp), +[SShooterDirectConnect.cpp](Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDirectConnect.cpp), +[SShooterDirectConnect.h](Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDirectConnect.h) + +The option for 'DIRECT CONNECT' allows players to enter the address of the game server they wish to connect to. +Unsupported options for 'HOST', 'LEADERBOARDS', 'ONLINE STORE' and 'DEMOS' have been removed from the menu, but their source code still exists. + +This is a new [`DirectConnect` widget](Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDirectConnect.cpp) that is added to the [main menu](Source/ShooterGame/Private/UI/Menu/ShooterMainMenu.cpp). diff --git a/ShooterGame.png b/ShooterGame.png new file mode 100644 index 0000000..29fa0e7 Binary files /dev/null and b/ShooterGame.png differ diff --git a/ShooterGame.uproject b/ShooterGame.uproject new file mode 100644 index 0000000..aba22a2 --- /dev/null +++ b/ShooterGame.uproject @@ -0,0 +1,95 @@ +{ + "FileVersion": 3, + "EngineAssociation": "{BF778BA0-4B0E-5EF7-01A1-8CAEC626B841}", + "Category": "Samples", + "Description": "", + "Modules": [ + { + "Name": "ShooterGame", + "Type": "Runtime", + "LoadingPhase": "Default" + }, + { + "Name": "ShooterGameLoadingScreen", + "Type": "Runtime", + "LoadingPhase": "PreLoadingScreen" + } + ], + "Plugins": [ + { + "Name": "PlatformCrypto", + "Enabled": true + }, + { + "Name": "AESHandlerComponent", + "Enabled": true + }, + { + "Name": "OnlineSubsystem", + "Enabled": true + }, + { + "Name": "OnlineSubsystemUtils", + "Enabled": true + }, + { + "Name": "OnlineSubsystemSteam", + "Enabled": true + }, + { + "Name": "OnlineSubsystemPS4", + "Enabled": true, + "WhitelistPlatforms": [ + "PS4" + ], + "SupportedTargetPlatforms": [ + "PS4" + ] + }, + { + "Name": "OnlineSubsystemLive", + "Enabled": true, + "WhitelistPlatforms": [ + "XboxOne" + ], + "SupportedTargetPlatforms": [ + "XboxOne" + ] + }, + { + "Name": "OnlineSubsystemNull", + "Enabled": true + }, + { + "Name": "SteamController", + "Enabled": true + }, + { + "Name": "ReplicationGraph", + "Enabled": true + }, + { + "Name": "Gauntlet", + "Enabled": true + }, + { + "Name": "Synthesis", + "Enabled": true + } + ], + "TargetPlatforms": [ + "MacNoEditor", + "PS4", + "WindowsNoEditor", + "XboxOne", + "Switch", + "Quail", + "LinuxNoEditor", + "LinuxServer", + "LinuxClient", + "LinuxAArch64NoEditor", + "LinuxAArch64Server", + "LinuxAArchClient" + ], + "EpicSampleNameHash": "3868556100" +} \ No newline at end of file diff --git a/Source/ShooterClient.Target.cs b/Source/ShooterClient.Target.cs new file mode 100644 index 0000000..5ce58be --- /dev/null +++ b/Source/ShooterClient.Target.cs @@ -0,0 +1,15 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class ShooterClientTarget : TargetRules +{ + public ShooterClientTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Client; + bUsesSteam = true; + + ExtraModuleNames.Add("ShooterGame"); + } +} diff --git a/Source/ShooterGame.Target.cs b/Source/ShooterGame.Target.cs new file mode 100644 index 0000000..b43c2d6 --- /dev/null +++ b/Source/ShooterGame.Target.cs @@ -0,0 +1,15 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class ShooterGameTarget : TargetRules +{ + public ShooterGameTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Game; + bUsesSteam = false; + + ExtraModuleNames.Add("ShooterGame"); + } +} diff --git a/Source/ShooterGame/Private/Bots/BTDecorator_HasLoSTo.cpp b/Source/ShooterGame/Private/Bots/BTDecorator_HasLoSTo.cpp new file mode 100644 index 0000000..74ae2c4 --- /dev/null +++ b/Source/ShooterGame/Private/Bots/BTDecorator_HasLoSTo.cpp @@ -0,0 +1,194 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "BTDecorator_HasLoSTo.h" +#include "BehaviorTree/BlackboardComponent.h" +#include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h" +#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h" +#include "Bots/ShooterBot.h" +#include "Bots/ShooterAIController.h" +#include "Online/ShooterPlayerState.h" + +UBTDecorator_HasLoSTo::UBTDecorator_HasLoSTo(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + NodeName = "Has LoS To"; + // accept only actors and vectors + EnemyKey.AddObjectFilter(this, *NodeName, AActor::StaticClass()); + EnemyKey.AddVectorFilter(this, *NodeName); +} + +/* +bool UBTDecorator_HasLoSTo::CalculateRawConditionValue(class UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory) const +{ + const UBlackboardComponent* BlackboardComp = OwnerComp->GetBlackboardComponent(); + if (BlackboardComp == NULL) + { + return false; + } + + FVector PointA = FVector::ZeroVector; + FVector PointB = FVector::ZeroVector; + const bool bHasPointA = BlackboardComp->GetLocationFromEntry(BlackboardKeyA.GetSelectedKeyID(), PointA); + const bool bHasPointB = BlackboardComp->GetLocationFromEntry(BlackboardKeyB.GetSelectedKeyID(), PointB); + + bool bHasPath = false; + + const UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(OwnerComp->GetWorld()); + if (NavSys && bHasPointA && bHasPointB) + { + const AAIController* AIOwner = Cast(OwnerComp->GetOwner()); + const ANavigationData* NavData = AIOwner && AIOwner->NavComponent ? AIOwner->NavComponent->GetNavData() : NULL; + TSharedPtr QueryFilter = UNavigationQueryFilter::GetQueryFilter(NavData, FilterClass); + + if (PathQueryType == EPathExistanceQueryType::NavmeshRaycast2D) + { +#if WITH_RECAST + const ARecastNavMesh* RecastNavMesh = Cast(NavData); + bHasPath = RecastNavMesh && RecastNavMesh->IsSegmentOnNavmesh(PointA, PointB, QueryFilter); +#endif + } + else + { + EPathFindingMode::Type TestMode = (PathQueryType == EPathExistanceQueryType::HierarchicalQuery) ? EPathFindingMode::Hierarchical : EPathFindingMode::Regular; + bHasPath = NavSys->TestPathSync(FPathFindingQuery(AIOwner, NavData, PointA, PointB, QueryFilter), TestMode); + } + } + + return bHasPath; +}*/ + +bool UBTDecorator_HasLoSTo::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const +{ + const UBlackboardComponent* MyBlackboard = OwnerComp.GetBlackboardComponent(); + AAIController* MyController = OwnerComp.GetAIOwner(); + bool HasLOS = false; + + if (MyController && MyBlackboard) + { + auto MyID = MyBlackboard->GetKeyID(EnemyKey.SelectedKeyName); + auto TargetKeyType = MyBlackboard->GetKeyType(MyID); + + FVector TargetLocation; + bool bGotTarget = false; + AActor* EnemyActor = NULL; + if (TargetKeyType == UBlackboardKeyType_Object::StaticClass()) + { + UObject* KeyValue = MyBlackboard->GetValue(MyID); + EnemyActor = Cast(KeyValue); + if (EnemyActor) + { + TargetLocation = EnemyActor->GetActorLocation(); + bGotTarget = true; + } + } + else if (TargetKeyType == UBlackboardKeyType_Vector::StaticClass()) + { + TargetLocation = MyBlackboard->GetValue(MyID); + bGotTarget = true; + } + + if (bGotTarget== true ) + { + if (LOSTrace(OwnerComp.GetOwner(), EnemyActor, TargetLocation) == true) + { + HasLOS = true; + } + } + } + + return HasLOS; +} + +bool UBTDecorator_HasLoSTo::LOSTrace(AActor* InActor, AActor* InEnemyActor, const FVector& EndLocation) const +{ + AShooterAIController* MyController = Cast(InActor); + AShooterBot* MyBot = MyController ? Cast(MyController->GetPawn()) : NULL; + + bool bHasLOS = false; + { + if (MyBot != NULL) + { + // Perform trace to retrieve hit info + FCollisionQueryParams TraceParams(SCENE_QUERY_STAT(AILosTrace), true, InActor); + + TraceParams.bReturnPhysicalMaterial = true; + TraceParams.AddIgnoredActor(MyBot); + const FVector StartLocation = MyBot->GetActorLocation(); + FHitResult Hit(ForceInit); + GetWorld()->LineTraceSingleByChannel(Hit, StartLocation, EndLocation, COLLISION_WEAPON, TraceParams); + if (Hit.bBlockingHit == true) + { + // We hit something. If we have an actor supplied, just check if the hit actor is an enemy. If it is consider that 'has LOS' + AActor* HitActor = Hit.GetActor(); + if (Hit.GetActor() != NULL) + { + // If the hit is our target actor consider it LOS + if (HitActor == InActor) + { + bHasLOS = true; + } + else + { + // Check the team of us against the team of the actor we hit if we are able. If they dont match good to go. + ACharacter* HitChar = Cast(HitActor); + if ( (HitChar != NULL) + && (MyController->PlayerState != NULL) && (HitChar->GetPlayerState() != NULL)) + { + AShooterPlayerState* HitPlayerState = Cast(HitChar->GetPlayerState()); + AShooterPlayerState* MyPlayerState = Cast(MyController->PlayerState); + if ((HitPlayerState != NULL) && (MyPlayerState != NULL)) + { + if (HitPlayerState->GetTeamNum() != MyPlayerState->GetTeamNum()) + { + bHasLOS = true; + } + } + } + } + } + else //we didnt hit an actor + { + if (InEnemyActor == NULL) + { + // We were not given an actor - so check of the distance between what we hit and the target. If what we hit is further away than the target we should be able to hit our target. + FVector HitDelta = Hit.ImpactPoint - StartLocation; + FVector TargetDelta = EndLocation - StartLocation; + if (TargetDelta.SizeSquared() < HitDelta.SizeSquared()) + { + bHasLOS = true; + } + } + } + } + } + } + + return bHasLOS; +} +// +// FString UBTDecorator_HasLoSTo::GetStaticDescription() const +// { +// FString KeyDesc("invalid"); +// if (BlackboardKey.SelectedKeyType == UBlackboardKeyType_Object::StaticClass() || +// BlackboardKey.SelectedKeyType == UBlackboardKeyType_Vector::StaticClass()) +// { +// KeyDesc = BlackboardKey.SelectedKeyName.ToString(); +// } +// +// return FString::Printf(TEXT("%s: %s"), *Super::GetStaticDescription(), *KeyDesc); +// } +// +// void UBTDecorator_HasLoSTo::DescribeRuntimeValues(const class UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray& Values) const +// { +// Super::DescribeRuntimeValues(OwnerComp, NodeMemory, Verbosity, Values); +// +// const UBlackboardComponent* BlackboardComp = OwnerComp->GetBlackboardComponent(); +// +// if (BlackboardComp) +// { +// FString KeyValue = BlackboardComp->DescribeKeyValue(BlackboardKey.GetSelectedKeyID(), EBlackboardDescription::OnlyValue); +// Values.Add(FString::Printf(TEXT("LOS target: %s"), *KeyValue)); +// } +// } + diff --git a/Source/ShooterGame/Private/Bots/BTTask_FindPickup.cpp b/Source/ShooterGame/Private/Bots/BTTask_FindPickup.cpp new file mode 100644 index 0000000..4adf95f --- /dev/null +++ b/Source/ShooterGame/Private/Bots/BTTask_FindPickup.cpp @@ -0,0 +1,58 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Bots/BTTask_FindPickup.h" +#include "BehaviorTree/BehaviorTreeComponent.h" +#include "BehaviorTree/BlackboardComponent.h" +#include "BehaviorTree/Blackboard/BlackboardKeyAllTypes.h" +#include "Bots/ShooterAIController.h" +#include "Bots/ShooterBot.h" +#include "Pickups/ShooterPickup_Ammo.h" +#include "Weapons/ShooterWeapon_Instant.h" + +UBTTask_FindPickup::UBTTask_FindPickup(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +EBTNodeResult::Type UBTTask_FindPickup::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) +{ + AShooterAIController* MyController = Cast(OwnerComp.GetAIOwner()); + AShooterBot* MyBot = MyController ? Cast(MyController->GetPawn()) : NULL; + if (MyBot == NULL) + { + return EBTNodeResult::Failed; + } + + AShooterGameMode* GameMode = MyBot->GetWorld()->GetAuthGameMode(); + if (GameMode == NULL) + { + return EBTNodeResult::Failed; + } + + const FVector MyLoc = MyBot->GetActorLocation(); + AShooterPickup_Ammo* BestPickup = NULL; + float BestDistSq = MAX_FLT; + + for (int32 i = 0; i < GameMode->LevelPickups.Num(); ++i) + { + AShooterPickup_Ammo* AmmoPickup = Cast(GameMode->LevelPickups[i]); + if (AmmoPickup && AmmoPickup->IsForWeapon(AShooterWeapon_Instant::StaticClass()) && AmmoPickup->CanBePickedUp(MyBot)) + { + const float DistSq = (AmmoPickup->GetActorLocation() - MyLoc).SizeSquared(); + if (BestDistSq == -1 || DistSq < BestDistSq) + { + BestDistSq = DistSq; + BestPickup = AmmoPickup; + } + } + } + + if (BestPickup) + { + OwnerComp.GetBlackboardComponent()->SetValue(BlackboardKey.GetSelectedKeyID(), BestPickup->GetActorLocation()); + return EBTNodeResult::Succeeded; + } + + return EBTNodeResult::Failed; +} diff --git a/Source/ShooterGame/Private/Bots/BTTask_FindPointNearEnemy.cpp b/Source/ShooterGame/Private/Bots/BTTask_FindPointNearEnemy.cpp new file mode 100644 index 0000000..08b680b --- /dev/null +++ b/Source/ShooterGame/Private/Bots/BTTask_FindPointNearEnemy.cpp @@ -0,0 +1,41 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Bots/BTTask_FindPointNearEnemy.h" +#include "Bots/ShooterAIController.h" +#include "BehaviorTree/BehaviorTreeComponent.h" +#include "BehaviorTree/BlackboardComponent.h" +#include "BehaviorTree/Blackboard/BlackboardKeyAllTypes.h" +#include "NavigationSystem.h" + + +UBTTask_FindPointNearEnemy::UBTTask_FindPointNearEnemy(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +EBTNodeResult::Type UBTTask_FindPointNearEnemy::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) +{ + AShooterAIController* MyController = Cast(OwnerComp.GetAIOwner()); + if (MyController == NULL) + { + return EBTNodeResult::Failed; + } + + APawn* MyBot = MyController->GetPawn(); + AShooterCharacter* Enemy = MyController->GetEnemy(); + if (Enemy && MyBot) + { + const float SearchRadius = 200.0f; + const FVector SearchOrigin = Enemy->GetActorLocation() + 600.0f * (MyBot->GetActorLocation() - Enemy->GetActorLocation()).GetSafeNormal(); + FVector Loc(0); + UNavigationSystemV1::K2_GetRandomReachablePointInRadius(MyController, SearchOrigin, Loc, SearchRadius); + if (Loc != FVector::ZeroVector) + { + OwnerComp.GetBlackboardComponent()->SetValue(BlackboardKey.GetSelectedKeyID(), Loc); + return EBTNodeResult::Succeeded; + } + } + + return EBTNodeResult::Failed; +} diff --git a/Source/ShooterGame/Private/Bots/ShooterAIController.cpp b/Source/ShooterGame/Private/Bots/ShooterAIController.cpp new file mode 100644 index 0000000..52b059d --- /dev/null +++ b/Source/ShooterGame/Private/Bots/ShooterAIController.cpp @@ -0,0 +1,289 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Bots/ShooterAIController.h" +#include "Bots/ShooterBot.h" +#include "Online/ShooterPlayerState.h" +#include "BehaviorTree/BehaviorTree.h" +#include "BehaviorTree/BehaviorTreeComponent.h" +#include "BehaviorTree/BlackboardComponent.h" +#include "BehaviorTree/Blackboard/BlackboardKeyType_Bool.h" +#include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h" +#include "Weapons/ShooterWeapon.h" + +AShooterAIController::AShooterAIController(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + BlackboardComp = ObjectInitializer.CreateDefaultSubobject(this, TEXT("BlackBoardComp")); + + BrainComponent = BehaviorComp = ObjectInitializer.CreateDefaultSubobject(this, TEXT("BehaviorComp")); + + bWantsPlayerState = true; +} + +void AShooterAIController::OnPossess(APawn* InPawn) +{ + Super::OnPossess(InPawn); + + AShooterBot* Bot = Cast(InPawn); + + // start behavior + if (Bot && Bot->BotBehavior) + { + if (Bot->BotBehavior->BlackboardAsset) + { + BlackboardComp->InitializeBlackboard(*Bot->BotBehavior->BlackboardAsset); + } + + EnemyKeyID = BlackboardComp->GetKeyID("Enemy"); + NeedAmmoKeyID = BlackboardComp->GetKeyID("NeedAmmo"); + + BehaviorComp->StartTree(*(Bot->BotBehavior)); + } +} + +void AShooterAIController::OnUnPossess() +{ + Super::OnUnPossess(); + + BehaviorComp->StopTree(); +} + +void AShooterAIController::BeginInactiveState() +{ + Super::BeginInactiveState(); + + AGameStateBase const* const GameState = GetWorld()->GetGameState(); + + const float MinRespawnDelay = GameState ? GameState->GetPlayerRespawnDelay(this) : 1.0f; + + GetWorldTimerManager().SetTimer(TimerHandle_Respawn, this, &AShooterAIController::Respawn, MinRespawnDelay); +} + +void AShooterAIController::Respawn() +{ + GetWorld()->GetAuthGameMode()->RestartPlayer(this); +} + +void AShooterAIController::FindClosestEnemy() +{ + APawn* MyBot = GetPawn(); + if (MyBot == NULL) + { + return; + } + + const FVector MyLoc = MyBot->GetActorLocation(); + float BestDistSq = MAX_FLT; + AShooterCharacter* BestPawn = NULL; + + for (AShooterCharacter* TestPawn : TActorRange(GetWorld())) + { + if (TestPawn->IsAlive() && TestPawn->IsEnemyFor(this)) + { + const float DistSq = (TestPawn->GetActorLocation() - MyLoc).SizeSquared(); + if (DistSq < BestDistSq) + { + BestDistSq = DistSq; + BestPawn = TestPawn; + } + } + } + + if (BestPawn) + { + SetEnemy(BestPawn); + } +} + +bool AShooterAIController::FindClosestEnemyWithLOS(AShooterCharacter* ExcludeEnemy) +{ + bool bGotEnemy = false; + APawn* MyBot = GetPawn(); + if (MyBot != NULL) + { + const FVector MyLoc = MyBot->GetActorLocation(); + float BestDistSq = MAX_FLT; + AShooterCharacter* BestPawn = NULL; + + for (AShooterCharacter* TestPawn : TActorRange(GetWorld())) + { + if (TestPawn != ExcludeEnemy && TestPawn->IsAlive() && TestPawn->IsEnemyFor(this)) + { + if (HasWeaponLOSToEnemy(TestPawn, true) == true) + { + const float DistSq = (TestPawn->GetActorLocation() - MyLoc).SizeSquared(); + if (DistSq < BestDistSq) + { + BestDistSq = DistSq; + BestPawn = TestPawn; + } + } + } + } + if (BestPawn) + { + SetEnemy(BestPawn); + bGotEnemy = true; + } + } + return bGotEnemy; +} + +bool AShooterAIController::HasWeaponLOSToEnemy(AActor* InEnemyActor, const bool bAnyEnemy) const +{ + + AShooterBot* MyBot = Cast(GetPawn()); + + bool bHasLOS = false; + // Perform trace to retrieve hit info + FCollisionQueryParams TraceParams(SCENE_QUERY_STAT(AIWeaponLosTrace), true, GetPawn()); + + TraceParams.bReturnPhysicalMaterial = true; + FVector StartLocation = MyBot->GetActorLocation(); + StartLocation.Z += GetPawn()->BaseEyeHeight; //look from eyes + + FHitResult Hit(ForceInit); + const FVector EndLocation = InEnemyActor->GetActorLocation(); + GetWorld()->LineTraceSingleByChannel(Hit, StartLocation, EndLocation, COLLISION_WEAPON, TraceParams); + if (Hit.bBlockingHit == true) + { + // Theres a blocking hit - check if its our enemy actor + AActor* HitActor = Hit.GetActor(); + if (Hit.GetActor() != NULL) + { + if (HitActor == InEnemyActor) + { + bHasLOS = true; + } + else if (bAnyEnemy == true) + { + // Its not our actor, maybe its still an enemy ? + ACharacter* HitChar = Cast(HitActor); + if (HitChar != NULL) + { + AShooterPlayerState* HitPlayerState = Cast(HitChar->GetPlayerState()); + AShooterPlayerState* MyPlayerState = Cast(PlayerState); + if ((HitPlayerState != NULL) && (MyPlayerState != NULL)) + { + if (HitPlayerState->GetTeamNum() != MyPlayerState->GetTeamNum()) + { + bHasLOS = true; + } + } + } + } + } + } + + + + return bHasLOS; +} + +void AShooterAIController::ShootEnemy() +{ + AShooterBot* MyBot = Cast(GetPawn()); + AShooterWeapon* MyWeapon = MyBot ? MyBot->GetWeapon() : NULL; + if (MyWeapon == NULL) + { + return; + } + + bool bCanShoot = false; + AShooterCharacter* Enemy = GetEnemy(); + if ( Enemy && ( Enemy->IsAlive() )&& (MyWeapon->GetCurrentAmmo() > 0) && ( MyWeapon->CanFire() == true ) ) + { + if (LineOfSightTo(Enemy, MyBot->GetActorLocation())) + { + bCanShoot = true; + } + } + + if (bCanShoot) + { + MyBot->StartWeaponFire(); + } + else + { + MyBot->StopWeaponFire(); + } +} + +void AShooterAIController::CheckAmmo(const class AShooterWeapon* CurrentWeapon) +{ + if (CurrentWeapon && BlackboardComp) + { + const int32 Ammo = CurrentWeapon->GetCurrentAmmo(); + const int32 MaxAmmo = CurrentWeapon->GetMaxAmmo(); + const float Ratio = (float) Ammo / (float) MaxAmmo; + + BlackboardComp->SetValue(NeedAmmoKeyID, (Ratio <= 0.1f)); + } +} + +void AShooterAIController::SetEnemy(class APawn* InPawn) +{ + if (BlackboardComp) + { + BlackboardComp->SetValue(EnemyKeyID, InPawn); + SetFocus(InPawn); + } +} + +class AShooterCharacter* AShooterAIController::GetEnemy() const +{ + if (BlackboardComp) + { + return Cast(BlackboardComp->GetValue(EnemyKeyID)); + } + + return NULL; +} + + +void AShooterAIController::UpdateControlRotation(float DeltaTime, bool bUpdatePawn) +{ + // Look toward focus + FVector FocalPoint = GetFocalPoint(); + if( !FocalPoint.IsZero() && GetPawn()) + { + FVector Direction = FocalPoint - GetPawn()->GetActorLocation(); + FRotator NewControlRotation = Direction.Rotation(); + + NewControlRotation.Yaw = FRotator::ClampAxis(NewControlRotation.Yaw); + + SetControlRotation(NewControlRotation); + + APawn* const P = GetPawn(); + if (P && bUpdatePawn) + { + P->FaceRotation(NewControlRotation, DeltaTime); + } + + } +} + +void AShooterAIController::GameHasEnded(AActor* EndGameFocus, bool bIsWinner) +{ + // Stop the behaviour tree/logic + BehaviorComp->StopTree(); + + // Stop any movement we already have + StopMovement(); + + // Cancel the repsawn timer + GetWorldTimerManager().ClearTimer(TimerHandle_Respawn); + + // Clear any enemy + SetEnemy(NULL); + + // Finally stop firing + AShooterBot* MyBot = Cast(GetPawn()); + AShooterWeapon* MyWeapon = MyBot ? MyBot->GetWeapon() : NULL; + if (MyWeapon == NULL) + { + return; + } + MyBot->StopWeaponFire(); +} + diff --git a/Source/ShooterGame/Private/Bots/ShooterBot.cpp b/Source/ShooterGame/Private/Bots/ShooterBot.cpp new file mode 100644 index 0000000..473625d --- /dev/null +++ b/Source/ShooterGame/Private/Bots/ShooterBot.cpp @@ -0,0 +1,27 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Bots/ShooterBot.h" +#include "Bots/ShooterAIController.h" + +AShooterBot::AShooterBot(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + AIControllerClass = AShooterAIController::StaticClass(); + + UpdatePawnMeshes(); + + bUseControllerRotationYaw = true; +} + +bool AShooterBot::IsFirstPerson() const +{ + return false; +} + +void AShooterBot::FaceRotation(FRotator NewRotation, float DeltaTime) +{ + FRotator CurrentRotation = FMath::RInterpTo(GetActorRotation(), NewRotation, DeltaTime, 8.0f); + + Super::FaceRotation(CurrentRotation, DeltaTime); +} diff --git a/Source/ShooterGame/Private/Effects/ShooterExplosionEffect.cpp b/Source/ShooterGame/Private/Effects/ShooterExplosionEffect.cpp new file mode 100644 index 0000000..698dc9c --- /dev/null +++ b/Source/ShooterGame/Private/Effects/ShooterExplosionEffect.cpp @@ -0,0 +1,68 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterExplosionEffect.h" + +AShooterExplosionEffect::AShooterExplosionEffect(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + ExplosionLightComponentName = TEXT("ExplosionLight"); + + PrimaryActorTick.bCanEverTick = true; + + ExplosionLight = ObjectInitializer.CreateDefaultSubobject(this, ExplosionLightComponentName); + RootComponent = ExplosionLight; + ExplosionLight->AttenuationRadius = 400.0; + ExplosionLight->Intensity = 500.0f; + ExplosionLight->bUseInverseSquaredFalloff = false; + ExplosionLight->LightColor = FColor(255, 185, 35); + ExplosionLight->CastShadows = false; + ExplosionLight->SetVisibleFlag(true); + + ExplosionLightFadeOut = 0.2f; +} + +void AShooterExplosionEffect::BeginPlay() +{ + Super::BeginPlay(); + + if (ExplosionFX) + { + UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionFX, GetActorLocation(), GetActorRotation()); + } + + if (ExplosionSound) + { + UGameplayStatics::PlaySoundAtLocation(this, ExplosionSound, GetActorLocation()); + } + + if (Decal.DecalMaterial) + { + FRotator RandomDecalRotation = SurfaceHit.ImpactNormal.Rotation(); + RandomDecalRotation.Roll = FMath::FRandRange(-180.0f, 180.0f); + + UGameplayStatics::SpawnDecalAttached(Decal.DecalMaterial, FVector(Decal.DecalSize, Decal.DecalSize, 1.0f), + SurfaceHit.Component.Get(), SurfaceHit.BoneName, + SurfaceHit.ImpactPoint, RandomDecalRotation, EAttachLocation::KeepWorldPosition, + Decal.LifeSpan); + } +} + +void AShooterExplosionEffect::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + const float TimeAlive = GetWorld()->GetTimeSeconds() - CreationTime; + const float TimeRemaining = FMath::Max(0.0f, ExplosionLightFadeOut - TimeAlive); + + if (TimeRemaining > 0) + { + const float FadeAlpha = 1.0f - FMath::Square(TimeRemaining / ExplosionLightFadeOut); + + UPointLightComponent* DefLight = Cast(GetClass()->GetDefaultSubobjectByName(ExplosionLightComponentName)); + ExplosionLight->SetIntensity(DefLight->Intensity * FadeAlpha); + } + else + { + Destroy(); + } +} diff --git a/Source/ShooterGame/Private/Effects/ShooterImpactEffect.cpp b/Source/ShooterGame/Private/Effects/ShooterImpactEffect.cpp new file mode 100644 index 0000000..9a64fd1 --- /dev/null +++ b/Source/ShooterGame/Private/Effects/ShooterImpactEffect.cpp @@ -0,0 +1,82 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterImpactEffect.h" + +AShooterImpactEffect::AShooterImpactEffect(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + SetAutoDestroyWhenFinished(true); +} + +void AShooterImpactEffect::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + + UPhysicalMaterial* HitPhysMat = SurfaceHit.PhysMaterial.Get(); + EPhysicalSurface HitSurfaceType = UPhysicalMaterial::DetermineSurfaceType(HitPhysMat); + + // show particles + UParticleSystem* ImpactFX = GetImpactFX(HitSurfaceType); + if (ImpactFX) + { + UGameplayStatics::SpawnEmitterAtLocation(this, ImpactFX, GetActorLocation(), GetActorRotation()); + } + + // play sound + USoundCue* ImpactSound = GetImpactSound(HitSurfaceType); + if (ImpactSound) + { + UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation()); + } + + if (DefaultDecal.DecalMaterial) + { + FRotator RandomDecalRotation = SurfaceHit.ImpactNormal.Rotation(); + RandomDecalRotation.Roll = FMath::FRandRange(-180.0f, 180.0f); + + UGameplayStatics::SpawnDecalAttached(DefaultDecal.DecalMaterial, FVector(1.0f, DefaultDecal.DecalSize, DefaultDecal.DecalSize), + SurfaceHit.Component.Get(), SurfaceHit.BoneName, + SurfaceHit.ImpactPoint, RandomDecalRotation, EAttachLocation::KeepWorldPosition, + DefaultDecal.LifeSpan); + } +} + +UParticleSystem* AShooterImpactEffect::GetImpactFX(TEnumAsByte SurfaceType) const +{ + UParticleSystem* ImpactFX = NULL; + + switch (SurfaceType) + { + case SHOOTER_SURFACE_Concrete: ImpactFX = ConcreteFX; break; + case SHOOTER_SURFACE_Dirt: ImpactFX = DirtFX; break; + case SHOOTER_SURFACE_Water: ImpactFX = WaterFX; break; + case SHOOTER_SURFACE_Metal: ImpactFX = MetalFX; break; + case SHOOTER_SURFACE_Wood: ImpactFX = WoodFX; break; + case SHOOTER_SURFACE_Grass: ImpactFX = GrassFX; break; + case SHOOTER_SURFACE_Glass: ImpactFX = GlassFX; break; + case SHOOTER_SURFACE_Flesh: ImpactFX = FleshFX; break; + default: ImpactFX = DefaultFX; break; + } + + return ImpactFX; +} + +USoundCue* AShooterImpactEffect::GetImpactSound(TEnumAsByte SurfaceType) const +{ + USoundCue* ImpactSound = NULL; + + switch (SurfaceType) + { + case SHOOTER_SURFACE_Concrete: ImpactSound = ConcreteSound; break; + case SHOOTER_SURFACE_Dirt: ImpactSound = DirtSound; break; + case SHOOTER_SURFACE_Water: ImpactSound = WaterSound; break; + case SHOOTER_SURFACE_Metal: ImpactSound = MetalSound; break; + case SHOOTER_SURFACE_Wood: ImpactSound = WoodSound; break; + case SHOOTER_SURFACE_Grass: ImpactSound = GrassSound; break; + case SHOOTER_SURFACE_Glass: ImpactSound = GlassSound; break; + case SHOOTER_SURFACE_Flesh: ImpactSound = FleshSound; break; + default: ImpactSound = DefaultSound; break; + } + + return ImpactSound; +} diff --git a/Source/ShooterGame/Private/Online/A2S/A2SServer.cpp b/Source/ShooterGame/Private/Online/A2S/A2SServer.cpp new file mode 100644 index 0000000..a26db24 --- /dev/null +++ b/Source/ShooterGame/Private/Online/A2S/A2SServer.cpp @@ -0,0 +1,226 @@ +#include "ShooterGame.h" +#include "A2SServer.h" + +DEFINE_LOG_CATEGORY(LogA2S); + +const uint8 REQUEST_HEADER = 0x54; +const uint8 RESPONSE_HEADER = 0x49; +const uint8 PROTOCOL_VERSION = 0x11; // (= 17d) +const uint8 SERVER_TYPE_DEDICATED = 0x64; +const uint8 SERVER_OS_LINUX = 0x6C; +const uint8 EXTRA_DATA_FLAG = 0x80; // Only extra field is 'port' + +void UA2SServer::Start() +{ + if (IsStarted) + { + UE_LOG(LogA2S, Display, TEXT("Ignoring duplicated `A2SServer->Start` call")); + return; + } + + ParseCLIOptions(); + + OpenReceiveSocket(); + OpenSendSocket(); + + UDPReceiver->Start(); + IsStarted = true; + UE_LOG(LogA2S, Display, TEXT("Started A2S server on port %d (payloadId: %s)"), Settings.Port, *PayloadId); +} + +void UA2SServer::Stop() +{ + FScopeLock ScopeLock(&Mutex); + UDPReceiver->Stop(); + + CloseReceiveSocket(); + CloseSendSocket(); + + UE_LOG(LogA2S, Display, TEXT("Stopping A2S server")); +} + +void UA2SServer::ParseCLIOptions() +{ + if (FParse::Value(FCommandLine::Get(), TEXT("statsPort"), Settings.Port)) + { + UE_LOG(LogA2S, Display, TEXT("Overwriting A2S port with port given in CLI (-statsPort): %d"), Settings.Port); + } + + if (!FParse::Value(FCommandLine::Get(), TEXT("payloadId"), PayloadId)) + { + UE_LOG(LogA2S, Display, TEXT("No payload ID given in CLI (-payloadId), defaulting to %s"), *PayloadId); + } +} + +void UA2SServer::OpenReceiveSocket() +{ + const FIPv4Endpoint Endpoint(FIPv4Address::Any, Settings.Port); + ReceiverSocket = FUdpSocketBuilder(FString(TEXT("ue4-a2s-receive"))) + .AsNonBlocking() + .AsReusable() + .BoundToEndpoint(Endpoint) + .WithReceiveBufferSize(Settings.BufferSize) + .Build(); + + const FTimespan ThreadWaitTime = FTimespan::FromMilliseconds(100); + const FString ThreadName = FString::Printf(TEXT("UDP-RECEIVER-A2S")); + UDPReceiver = MakeUnique(ReceiverSocket, ThreadWaitTime, *ThreadName); + + UDPReceiver->OnDataReceived().BindLambda([this](const FArrayReaderPtr& DataPtr, const FIPv4Endpoint& Endpoint) + { + TArray Data; + Data.AddUninitialized(DataPtr->TotalSize()); + DataPtr->Serialize(Data.GetData(), DataPtr->TotalSize()); + + FScopeLock ScopeLock(&Mutex); + this->HandleRequest(Data, Endpoint); + }); +} + +void UA2SServer::OpenSendSocket() +{ + SenderSocket = FUdpSocketBuilder(FString(TEXT("ue4-a2s-send"))) + .AsNonBlocking() + .BoundToPort(Settings.Port) + .AsReusable() + .WithReceiveBufferSize(Settings.BufferSize) + .WithSendBufferSize(Settings.BufferSize) + .Build(); +} + +void UA2SServer::CloseReceiveSocket() +{ + UDPReceiver.Reset(); + UDPReceiver = nullptr; + + ReceiverSocket->Close(); + ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ReceiverSocket); + ReceiverSocket = nullptr; +} + +void UA2SServer::CloseSendSocket() +{ + SenderSocket->Close(); + ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(SenderSocket); + SenderSocket = nullptr; +} + +void UA2SServer::HandleRequest(const TArray& Data, const FIPv4Endpoint& Endpoint) +{ + UE_LOG(LogA2S, Display, TEXT("Handling A2S request from %s"), *Endpoint.ToString()); + + if (!IsQueryValid(Data)) + { + // Do not respond + return; + } + + TArray ResponseBytes = BuildA2SInfoResponse(); + + UE_LOG(LogA2S, Display, TEXT("Responding A2S to %s"), *Endpoint.ToString()); + int32 BytesSent; + SenderSocket->SendTo(ResponseBytes.GetData(), ResponseBytes.Num(), BytesSent, *Endpoint.ToInternetAddr()); + + if (BytesSent != ResponseBytes.Num()) + { + UE_LOG(LogA2S, Warning, TEXT("Not all bytes sent in A2S response (expected: %d, actual: %d)"), + ResponseBytes.Num(), BytesSent); + } +} + +/** This server only supports A2S_INFO requests */ +bool UA2SServer::IsQueryValid(const TArray& Data) +{ + if (Data.Num() < 5) + { + // At least 4 bytes needed packet Header + 1 byte for request Header + UE_LOG(LogA2S, Warning, TEXT("A2S request too short: %d bytes"), Data.Num()); + return false; + } + + // Check the Header (the first 4 bytes) are 0xFF + // (this indicates a single-packet request, multi-packet requests are not supported) + if (Data[0] != 0xFF || Data[1] != 0xFF || Data[2] != 0xFF || Data[3] != 0xFF) + { + UE_LOG(LogA2S, Warning, TEXT("A2S packet header invalid: %02x %02x %02x %02x"), Data[0], Data[1], Data[2], + Data[3]); + return false; + } + + // Check request Header (the 5th byte) denotes an A2S_INFO request + if (Data[4] != REQUEST_HEADER) + { + UE_LOG(LogA2S, Warning, TEXT("A2S request header invalid: %x"), Data[4]); + return false; + } + + return true; +} + +void AddStringToByteArray(TArray& Bytes, FString InString) +{ + // Encode string as UTF-8 (default encoding is UTF-16) + const FTCHARToUTF8 InStringUtf8(*InString); + + // Copy string into byte buffer + // Note: Unreal's 'StringToBytes' method doesn't undo the +1 offset of its internal UTF-8 encoding so write our own + // conversion + int32 NumBytes = 0; + const ANSICHAR* CharPos = InStringUtf8.Get(); + + while (*CharPos && NumBytes < InStringUtf8.Length()) + { + Bytes.Add(static_cast(*CharPos)); + CharPos++; + NumBytes++;; + } + + // Add string terminating byte + Bytes.Add(0x00); +} + +TArray UA2SServer::BuildA2SInfoResponse() +{ + TArray Bytes; + + // Add packet header (to show single packet response) + uint8 Header[] = {0xFF, 0xFF, 0xFF, 0xFF}; + Bytes.Append(Header, UE_ARRAY_COUNT(Header)); + + const UWorld* World = GetOuter()->GetWorld(); + if (!World) + { + UE_LOG(LogA2S, Warning, TEXT("World is null")); + return Bytes; + } + + Bytes.Add(RESPONSE_HEADER); // Add response header (for A2S_INFO only) + Bytes.Add(PROTOCOL_VERSION); // Add protocol version (17) + AddStringToByteArray(Bytes, PayloadId); // Add server name + AddStringToByteArray(Bytes, World->GetMapName()); // Add map name + AddStringToByteArray(Bytes, FString(TEXT("ShooterGame"))); // Add name of folder containing game files + AddStringToByteArray(Bytes, FString(TEXT("ShooterGame"))); // Add name of game + Bytes.Add(0x0); // Add Steam ID of game (unset) (first part of a 2-byte short) + Bytes.Add(0x0); // Add Steam ID of game (unset) (second part of a 2-byte short) + Bytes.Add(World->GetAuthGameMode()->GetNumPlayers()); // Add number of players + Bytes.Add(World->GetAuthGameMode()->GameSession->MaxPlayers); // Add max. number of players + Bytes.Add(0x0); // Add number of bots + Bytes.Add(SERVER_TYPE_DEDICATED); // Add server type (dedicated = 'd' = 0x64 UTF-8) + Bytes.Add(SERVER_OS_LINUX); // Add server OS (linux = 'l' = 0x6C UTF-8) + Bytes.Add(0x0); // Add visibility (indicates if the server needs a password) + Bytes.Add(0x0); // Add VAC (Valve Anti Cheat) + AddStringToByteArray(Bytes, FString(TEXT("1.0.0"))); // Add version of the game + + // Add EDF (Extra Data Flag, determines which of the next optional fields feature) + // Only specify that the port field is included + Bytes.Add(EXTRA_DATA_FLAG); + + // Port is uint16 and shorts are little endian so split port (is uint32 but can fit into uint16) and add in parts + Bytes.Add(World->URL.Port & 0xFF); // Add port (first part, lower bits) + Bytes.Add(World->URL.Port >> 8); // Add port (second part, higher bits) + + // Add terminating 0x0 + Bytes.Add(0x0); + + return Bytes; +} diff --git a/Source/ShooterGame/Private/Online/A2S/A2SServer.h b/Source/ShooterGame/Private/Online/A2S/A2SServer.h new file mode 100644 index 0000000..d075646 --- /dev/null +++ b/Source/ShooterGame/Private/Online/A2S/A2SServer.h @@ -0,0 +1,73 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" + +#include "A2SServerSettings.h" +#include "Object.h" +#include "Common/UdpSocketBuilder.h" +#include "Common/UdpSocketReceiver.h" +#include "Common/UdpSocketSender.h" + +#include "A2SServer.generated.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogA2S, Log, All); + +UCLASS() +class UA2SServer : public UObject +{ + GENERATED_BODY() + +public: + UA2SServer() { } + + ~UA2SServer() { } + + /** Start the A2S server */ + UFUNCTION(BlueprintCallable, Category = "A2S Server Functions") + void Start(); + + /** Stop the A2S server */ + UFUNCTION(BlueprintCallable, Category = "A2S Server Functions") + void Stop(); + + /** Settings for the A2S server */ + FA2SServerSettings Settings; + +private: + /** Overwrite blueprint settings with any settings in CLI */ + void ParseCLIOptions(); + + /** Open a UDP socket for receiving */ + void OpenReceiveSocket(); + + /** Open a UDP socket for sending */ + void OpenSendSocket(); + + /** Close the UDP socket created by 'OpenSendSocket' */ + void CloseReceiveSocket(); + + /** Close the UDP socket created by 'OpenReceiveSocket' */ + void CloseSendSocket(); + + /** Handle an A2S query */ + void HandleRequest(const TArray& Data, const FIPv4Endpoint& Endpoint); + + /** Validate an A2S query */ + bool IsQueryValid(const TArray& Data); + + /** Build the raw bytes response to an A2S_INFO request */ + TArray BuildA2SInfoResponse(); + + FSocket* SenderSocket; + FSocket* ReceiverSocket; + TUniquePtr UDPReceiver; + + FString PayloadId = TEXT("UNSET-PAYLOAD-ID"); + + bool IsStarted = false; + + /** Mutex to ensure the server isn't cleaned up whilst handling a request */ + FCriticalSection Mutex; +}; diff --git a/Source/ShooterGame/Private/Online/A2S/A2SServerSettings.h b/Source/ShooterGame/Private/Online/A2S/A2SServerSettings.h new file mode 100644 index 0000000..5bd24dc --- /dev/null +++ b/Source/ShooterGame/Private/Online/A2S/A2SServerSettings.h @@ -0,0 +1,19 @@ +#pragma once + +#include "A2SServerSettings.generated.h" + +//UDP Connection Settings +USTRUCT(BlueprintType) +struct FA2SServerSettings +{ + GENERATED_USTRUCT_BODY() + + /** Port for A2S server to listen and send on */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "A2S Server Properties") + int Port = 29001; + + /** UDP socket buffer size */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "A2S Server Properties") + int32 BufferSize = 1800; // A2S packet size + some headroom +}; + diff --git a/Source/ShooterGame/Private/Online/Discoverability/Discoverability.cpp b/Source/ShooterGame/Private/Online/Discoverability/Discoverability.cpp new file mode 100644 index 0000000..173d643 --- /dev/null +++ b/Source/ShooterGame/Private/Online/Discoverability/Discoverability.cpp @@ -0,0 +1,119 @@ +#include "ShooterGame.h" +#include "Discoverability.h" + +#include "IHttpResponse.h" + +DEFINE_LOG_CATEGORY(LogDiscoverability); + +UDiscoverability::UDiscoverability() +{ + Http = &FHttpModule::Get(); +} + +void UDiscoverability::Start() +{ + ParseCLIOptions(); + + if (Settings.MatchmakerEndpoint.IsEmpty()) + { + UE_LOG(LogDiscoverability, Warning, TEXT("No matchmaker address specified, not starting discoverability")); + return; + } + + const UWorld* World = GetOuter()->GetWorld(); + if (!World) + { + UE_LOG(LogDiscoverability, Warning, TEXT("Could not start discoverability as world is null")); + return; + } + + World->GetTimerManager().SetTimer(TimerHandle, this, &UDiscoverability::SendUpdate, Settings.Interval, true); + + UE_LOG(LogDiscoverability, Display, + TEXT("Started notifying matchmaker of availability (every %ds, endpoint: %s, payloadId: %s, payloadIp: %s)"), + Settings.Interval, *Settings.MatchmakerEndpoint, *PayloadId, *PayloadIp); +} + +void UDiscoverability::Stop() +{ + if (Settings.MatchmakerEndpoint.IsEmpty()) + { + UE_LOG(LogDiscoverability, Warning, TEXT("No matchmaker address specified, not starting discoverability")); + return; + } + + const UWorld* World = GetOuter()->GetWorld(); + if (!World) + { + UE_LOG(LogDiscoverability, Warning, TEXT("Could not stop discoverability as world is null")); + return; + } + + World->GetTimerManager().ClearTimer(TimerHandle); + UE_LOG(LogDiscoverability, Display, TEXT("Stopped notifying matchmaker of availability")); +} + +void UDiscoverability::ParseCLIOptions() +{ + if (FParse::Value(FCommandLine::Get(), TEXT("matchmakerAddr"), Settings.MatchmakerEndpoint)) + { + UE_LOG(LogDiscoverability, Display, + TEXT("Overwriting matchmaker address with address given in CLI (-matchmakerAddr): %s"), + *Settings.MatchmakerEndpoint); + } + + if (!FParse::Value(FCommandLine::Get(), TEXT("payloadId"), PayloadId)) + { + UE_LOG(LogDiscoverability, Display, TEXT("No payload ID given in CLI (-payloadId), defaulting to %s"), + *PayloadId); + } + + if (!FParse::Value(FCommandLine::Get(), TEXT("payloadIp"), PayloadIp)) + { + UE_LOG(LogDiscoverability, Display, TEXT("No payload IP given in CLI (-payloadIp), defaulting to %s"), + *PayloadIp); + } +} + +void UDiscoverability::SendUpdate() +{ + const UWorld* World = GetOuter()->GetWorld(); + if (!World) + { + UE_LOG(LogDiscoverability, Warning, TEXT("World is null")); + return; + } + + TSharedPtr JsonObject = MakeShareable(new FJsonObject()); + JsonObject->SetNumberField(TEXT("Ccu"), World->GetAuthGameMode()->GetNumPlayers()); + JsonObject->SetStringField(TEXT("IP"), PayloadIp); + JsonObject->SetNumberField(TEXT("Port"), World->URL.Port); + + FString Body; + const TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&Body); + FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter); + + TSharedRef Request = Http->CreateRequest(); + Request->OnProcessRequestComplete().BindUObject(this, &UDiscoverability::OnResponse); + Request->SetURL(FString::Printf(TEXT("%s/%s"), *Settings.MatchmakerEndpoint, *PayloadId)); + Request->SetVerb("POST"); + Request->SetHeader("Content-Type", TEXT("application/json")); + Request->SetContentAsString(Body); + Request->ProcessRequest(); +} + +void UDiscoverability::OnResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +{ + if (!bWasSuccessful || !Response.IsValid()) + { + UE_LOG(LogDiscoverability, Warning, TEXT("Posting to matchmaker failed")); + return; + } + + const int32 ResponseCode = Response->GetResponseCode(); + if (ResponseCode < 200 || ResponseCode >= 300) + { + UE_LOG(LogDiscoverability, Warning, TEXT("Posting to matchmaker failed with error code %d (message: %s)"), + ResponseCode, *Response->GetContentAsString()); + } +} diff --git a/Source/ShooterGame/Private/Online/Discoverability/Discoverability.h b/Source/ShooterGame/Private/Online/Discoverability/Discoverability.h new file mode 100644 index 0000000..689211b --- /dev/null +++ b/Source/ShooterGame/Private/Online/Discoverability/Discoverability.h @@ -0,0 +1,47 @@ +#pragma once + +#include "CoreMinimal.h" + +#include "DiscoverabilitySettings.h" +#include "HttpModule.h" + +#include "Discoverability.generated.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogDiscoverability, Log, All); + +UCLASS() +class UDiscoverability : public UObject +{ + GENERATED_BODY() + +public: + UDiscoverability(); + + ~UDiscoverability() { } + + /** Start notifying the matchmaker of availability */ + UFUNCTION(BlueprintCallable, Category = "Discoverability Functions") + void Start(); + + /** Stop notifying the matchmaker of availability */ + UFUNCTION(BlueprintCallable, Category = "Discoverability Functions") + void Stop(); + + /** Settings for the discoverability component */ + FDiscoverabilitySettings Settings; + +private: + /** Overwrite blueprint settings with any settings in CLI */ + void ParseCLIOptions(); + + UFUNCTION() + void SendUpdate(); + + void OnResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); + + FString PayloadId = TEXT("UNSET-PAYLOAD-ID"); + FString PayloadIp = TEXT("UNSET-PAYLOAD-IP"); + + FHttpModule* Http; + FTimerHandle TimerHandle; +}; diff --git a/Source/ShooterGame/Private/Online/Discoverability/DiscoverabilitySettings.h b/Source/ShooterGame/Private/Online/Discoverability/DiscoverabilitySettings.h new file mode 100644 index 0000000..749042e --- /dev/null +++ b/Source/ShooterGame/Private/Online/Discoverability/DiscoverabilitySettings.h @@ -0,0 +1,17 @@ +#pragma once + +#include "DiscoverabilitySettings.generated.h" + +USTRUCT(BlueprintType) +struct FDiscoverabilitySettings +{ + GENERATED_USTRUCT_BODY() + + /** Endpoint of the matchmaker to send updates to */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discoverability Properties") + FString MatchmakerEndpoint = TEXT(""); + + /** Time in seconds between updates sent to the matchmaker */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discoverability Properties") + int Interval = 5; +}; diff --git a/Source/ShooterGame/Private/Online/ShooterGameMode.cpp b/Source/ShooterGame/Private/Online/ShooterGameMode.cpp new file mode 100644 index 0000000..d116f27 --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterGameMode.cpp @@ -0,0 +1,626 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterGameInstance.h" +#include "UI/ShooterHUD.h" +#include "Player/ShooterSpectatorPawn.h" +#include "Player/ShooterDemoSpectator.h" +#include "Online/ShooterGameMode.h" +#include "Online/ShooterPlayerState.h" +#include "Online/ShooterGameSession.h" +#include "Bots/ShooterAIController.h" +#include "ShooterTeamStart.h" + + +AShooterGameMode::AShooterGameMode(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + static ConstructorHelpers::FClassFinder PlayerPawnOb(TEXT("/Game/Blueprints/Pawns/PlayerPawn")); + DefaultPawnClass = PlayerPawnOb.Class; + + static ConstructorHelpers::FClassFinder BotPawnOb(TEXT("/Game/Blueprints/Pawns/BotPawn")); + BotPawnClass = BotPawnOb.Class; + + HUDClass = AShooterHUD::StaticClass(); + PlayerControllerClass = AShooterPlayerController::StaticClass(); + PlayerStateClass = AShooterPlayerState::StaticClass(); + SpectatorClass = AShooterSpectatorPawn::StaticClass(); + GameStateClass = AShooterGameState::StaticClass(); + ReplaySpectatorPlayerControllerClass = AShooterDemoSpectator::StaticClass(); + + MinRespawnDelay = 5.0f; + + bAllowBots = true; + bNeedsBotCreation = true; + bUseSeamlessTravel = FParse::Param(FCommandLine::Get(), TEXT("NoSeamlessTravel")) ? false : true; +} + +void AShooterGameMode::PostInitProperties() +{ + Super::PostInitProperties(); + if (PlatformPlayerControllerClass != nullptr) + { + PlayerControllerClass = PlatformPlayerControllerClass; + } +} + +FString AShooterGameMode::GetBotsCountOptionName() +{ + return FString(TEXT("Bots")); +} + +void AShooterGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) +{ + const int32 BotsCountOptionValue = UGameplayStatics::GetIntOption(Options, GetBotsCountOptionName(), 0); + SetAllowBots(BotsCountOptionValue > 0 ? true : false, BotsCountOptionValue); + Super::InitGame(MapName, Options, ErrorMessage); + + const UGameInstance* GameInstance = GetGameInstance(); + if (GameInstance && Cast(GameInstance)->GetOnlineMode() != EOnlineMode::Offline) + { + bPauseable = false; + } +} + +void AShooterGameMode::SetAllowBots(bool bInAllowBots, int32 InMaxBots) +{ + bAllowBots = bInAllowBots; + MaxBots = InMaxBots; +} + +/** Returns game session class to use */ +TSubclassOf AShooterGameMode::GetGameSessionClass() const +{ + return AShooterGameSession::StaticClass(); +} + +void AShooterGameMode::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + GetWorldTimerManager().SetTimer(TimerHandle_DefaultTimer, this, &AShooterGameMode::DefaultTimer, GetWorldSettings()->GetEffectiveTimeDilation(), true); +} + +void AShooterGameMode::BeginPlay() +{ + Super::BeginPlay(); + + A2SServer = NewObject(this); + A2SServer->AddToRoot(); + A2SServer->Settings = A2SSettings; + A2SServer->Start(); + + Discoverability = NewObject(this); + Discoverability->AddToRoot(); + Discoverability->Settings = DiscoverabilitySettings; + Discoverability->Start(); +} + +void AShooterGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + A2SServer->Stop(); + A2SServer->RemoveFromRoot(); + + Discoverability->RemoveFromRoot(); + + Super::EndPlay(EndPlayReason); +} + +void AShooterGameMode::DefaultTimer() +{ + // don't update timers for Play In Editor mode, it's not real match + if (GetWorld()->IsPlayInEditor()) + { + // start match if necessary. + if (GetMatchState() == MatchState::WaitingToStart) + { + StartMatch(); + } + return; + } + + AShooterGameState* const MyGameState = Cast(GameState); + if (MyGameState && MyGameState->RemainingTime > 0 && !MyGameState->bTimerPaused) + { + // If we are waiting for match to start (MatchState::WaitingToStart) and there has not yet been a connected + // player, do not decrement the counter + if (GetMatchState() == MatchState::WaitingToStart && !bHasPlayerConnected) + { + return; + } + + MyGameState->RemainingTime--; + + if (MyGameState->RemainingTime <= 0) + { + if (GetMatchState() == MatchState::WaitingPostMatch) + { + // Send the players back to the main menu + for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It) + { + (*It)->ClientReturnToMainMenuWithTextReason(NSLOCTEXT("GameMessages", "MatchEnded", "The match has ended.")); + AShooterPlayerController* ShooterPlayerController = Cast(*It); + ShooterPlayerController->HandleReturnToMainMenu(); + } + + // Stop discoverability as the game is ending + // Note: During the time between the last update from the game server and the matchmaker marking the + // game server as 'not ready' (after no update is received from game server after N seconds), the player + // may still connect to the game server (which is in post-match) but this is handled gracefully. + Discoverability->Stop(); + } + else if (GetMatchState() == MatchState::InProgress) + { + FinishMatch(); + + // Send end round events + for (FConstControllerIterator It = GetWorld()->GetControllerIterator(); It; ++It) + { + AShooterPlayerController* PlayerController = Cast(*It); + + if (PlayerController && MyGameState) + { + AShooterPlayerState* PlayerState = Cast((*It)->PlayerState); + const bool bIsWinner = IsWinner(PlayerState); + + PlayerController->ClientSendRoundEndEvent(bIsWinner, MyGameState->ElapsedTime); + } + } + } + else if (GetMatchState() == MatchState::WaitingToStart) + { + StartMatch(); + } + } + } + else if (GetMatchState() == MatchState::WaitingPostMatch) + { + // The post match is over (no time left) so wait for clients to be disconnected + if (GetNumPlayers() == 0) + { + // All players have disconnected, exit the server + FGenericPlatformMisc::RequestExit(false); + } + } +} + +void AShooterGameMode::HandleMatchIsWaitingToStart() +{ + Super::HandleMatchIsWaitingToStart(); + + if (bNeedsBotCreation) + { + CreateBotControllers(); + bNeedsBotCreation = false; + } + + if (bDelayedStart) + { + // start warmup if needed + AShooterGameState* const MyGameState = Cast(GameState); + if (MyGameState && MyGameState->RemainingTime == 0) + { + const bool bWantsMatchWarmup = !GetWorld()->IsPlayInEditor(); + if (bWantsMatchWarmup && WarmupTime > 0) + { + MyGameState->RemainingTime = WarmupTime; + } + else + { + MyGameState->RemainingTime = 0.0f; + } + } + } +} + +void AShooterGameMode::HandleMatchHasStarted() +{ + bNeedsBotCreation = true; + Super::HandleMatchHasStarted(); + + AShooterGameState* const MyGameState = Cast(GameState); + MyGameState->RemainingTime = RoundTime; + StartBots(); + + // notify players + for (FConstControllerIterator It = GetWorld()->GetControllerIterator(); It; ++It) + { + AShooterPlayerController* PC = Cast(*It); + if (PC) + { + PC->ClientGameStarted(); + } + } +} + +void AShooterGameMode::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) +{ + Super::HandleStartingNewPlayer_Implementation(NewPlayer); +} + +void AShooterGameMode::FinishMatch() +{ + AShooterGameState* const MyGameState = Cast(GameState); + if (IsMatchInProgress()) + { + EndMatch(); + DetermineMatchWinner(); + + // notify players + for (FConstControllerIterator It = GetWorld()->GetControllerIterator(); It; ++It) + { + AShooterPlayerState* PlayerState = Cast((*It)->PlayerState); + const bool bIsWinner = IsWinner(PlayerState); + + (*It)->GameHasEnded(NULL, bIsWinner); + } + + // lock all pawns + // pawns are not marked as keep for seamless travel, so we will create new pawns on the next match rather than + // turning these back on. + for (APawn* Pawn : TActorRange(GetWorld())) + { + Pawn->TurnOff(); + } + + // set up to restart the match + MyGameState->RemainingTime = TimeBetweenMatches; + } +} + +void AShooterGameMode::RequestFinishAndExitToMainMenu() +{ + FinishMatch(); + + UShooterGameInstance* const GameInstance = Cast(GetGameInstance()); + if (GameInstance) + { + GameInstance->RemoveSplitScreenPlayers(); + } + + AShooterPlayerController* LocalPrimaryController = nullptr; + for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator) + { + AShooterPlayerController* Controller = Cast(*Iterator); + + if (Controller == NULL) + { + continue; + } + + if (!Controller->IsLocalController()) + { + const FText RemoteReturnReason = NSLOCTEXT("NetworkErrors", "HostHasLeft", "Host has left the game."); + Controller->ClientReturnToMainMenuWithTextReason(RemoteReturnReason); + } + else + { + LocalPrimaryController = Controller; + } + } + + // GameInstance should be calling this from an EndState. So call the PC function that performs cleanup, not the one that sets GI state. + if (LocalPrimaryController != NULL) + { + LocalPrimaryController->HandleReturnToMainMenu(); + } +} + +void AShooterGameMode::DetermineMatchWinner() +{ + // nothing to do here +} + +bool AShooterGameMode::IsWinner(class AShooterPlayerState* PlayerState) const +{ + return false; +} + +void AShooterGameMode::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) +{ + AShooterGameState* const MyGameState = Cast(GameState); + const bool bMatchIsOver = MyGameState && MyGameState->HasMatchEnded(); + if( bMatchIsOver ) + { + ErrorMessage = TEXT("Match is over!"); + } + else + { + // GameSession can be NULL if the match is over + Super::PreLogin(Options, Address, UniqueId, ErrorMessage); + } +} + + +void AShooterGameMode::PostLogin(APlayerController* NewPlayer) +{ + Super::PostLogin(NewPlayer); + + bHasPlayerConnected = true; + + // update spectator location for client + AShooterPlayerController* NewPC = Cast(NewPlayer); + if (NewPC && NewPC->GetPawn() == NULL) + { + NewPC->ClientSetSpectatorCamera(NewPC->GetSpawnLocation(), NewPC->GetControlRotation()); + } + + // notify new player if match is already in progress + if (NewPC && IsMatchInProgress()) + { + NewPC->ClientGameStarted(); + NewPC->ClientStartOnlineGame(); + } +} + +void AShooterGameMode::Killed(AController* Killer, AController* KilledPlayer, APawn* KilledPawn, const UDamageType* DamageType) +{ + AShooterPlayerState* KillerPlayerState = Killer ? Cast(Killer->PlayerState) : NULL; + AShooterPlayerState* VictimPlayerState = KilledPlayer ? Cast(KilledPlayer->PlayerState) : NULL; + + if (KillerPlayerState && KillerPlayerState != VictimPlayerState) + { + KillerPlayerState->ScoreKill(VictimPlayerState, KillScore); + KillerPlayerState->InformAboutKill(KillerPlayerState, DamageType, VictimPlayerState); + } + + if (VictimPlayerState) + { + VictimPlayerState->ScoreDeath(KillerPlayerState, DeathScore); + VictimPlayerState->BroadcastDeath(KillerPlayerState, DamageType, VictimPlayerState); + } +} + +float AShooterGameMode::ModifyDamage(float Damage, AActor* DamagedActor, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) const +{ + float ActualDamage = Damage; + + AShooterCharacter* DamagedPawn = Cast(DamagedActor); + if (DamagedPawn && EventInstigator) + { + AShooterPlayerState* DamagedPlayerState = Cast(DamagedPawn->GetPlayerState()); + AShooterPlayerState* InstigatorPlayerState = Cast(EventInstigator->PlayerState); + + // disable friendly fire + if (!CanDealDamage(InstigatorPlayerState, DamagedPlayerState)) + { + ActualDamage = 0.0f; + } + + // scale self instigated damage + if (InstigatorPlayerState == DamagedPlayerState) + { + ActualDamage *= DamageSelfScale; + } + } + + return ActualDamage; +} + +bool AShooterGameMode::CanDealDamage(class AShooterPlayerState* DamageInstigator, class AShooterPlayerState* DamagedPlayer) const +{ + return true; +} + +bool AShooterGameMode::AllowCheats(APlayerController* P) +{ + return true; +} + +bool AShooterGameMode::ShouldSpawnAtStartSpot(AController* Player) +{ + return false; +} + +UClass* AShooterGameMode::GetDefaultPawnClassForController_Implementation(AController* InController) +{ + if (InController->IsA()) + { + return BotPawnClass; + } + + return Super::GetDefaultPawnClassForController_Implementation(InController); +} + +void AShooterGameMode::RestartPlayer(AController* NewPlayer) +{ + Super::RestartPlayer(NewPlayer); + + AShooterPlayerController* PC = Cast(NewPlayer); + if (PC) + { + // Since initial weapon is equipped before the pawn is added to the replication graph, need to resend the notify so that it can be added as a dependent actor + AShooterCharacter* Character = Cast(PC->GetCharacter()); + if (Character) + { + AShooterCharacter::NotifyEquipWeapon.Broadcast(Character, Character->GetWeapon()); + } + + PC->ClientGameStarted(); + } +} + +AActor* AShooterGameMode::ChoosePlayerStart_Implementation(AController* Player) +{ + TArray PreferredSpawns; + TArray FallbackSpawns; + + APlayerStart* BestStart = NULL; + for (TActorIterator It(GetWorld()); It; ++It) + { + APlayerStart* TestSpawn = *It; + if (TestSpawn->IsA()) + { + // Always prefer the first "Play from Here" PlayerStart, if we find one while in PIE mode + BestStart = TestSpawn; + break; + } + else + { + if (IsSpawnpointAllowed(TestSpawn, Player)) + { + if (IsSpawnpointPreferred(TestSpawn, Player)) + { + PreferredSpawns.Add(TestSpawn); + } + else + { + FallbackSpawns.Add(TestSpawn); + } + } + } + } + + + if (BestStart == NULL) + { + if (PreferredSpawns.Num() > 0) + { + BestStart = PreferredSpawns[FMath::RandHelper(PreferredSpawns.Num())]; + } + else if (FallbackSpawns.Num() > 0) + { + BestStart = FallbackSpawns[FMath::RandHelper(FallbackSpawns.Num())]; + } + } + + return BestStart ? BestStart : Super::ChoosePlayerStart_Implementation(Player); +} + +bool AShooterGameMode::IsSpawnpointAllowed(APlayerStart* SpawnPoint, AController* Player) const +{ + AShooterTeamStart* ShooterSpawnPoint = Cast(SpawnPoint); + if (ShooterSpawnPoint) + { + AShooterAIController* AIController = Cast(Player); + if (ShooterSpawnPoint->bNotForBots && AIController) + { + return false; + } + + if (ShooterSpawnPoint->bNotForPlayers && AIController == NULL) + { + return false; + } + return true; + } + + return false; +} + +bool AShooterGameMode::IsSpawnpointPreferred(APlayerStart* SpawnPoint, AController* Player) const +{ + ACharacter* MyPawn = Cast((*DefaultPawnClass)->GetDefaultObject()); + AShooterAIController* AIController = Cast(Player); + if( AIController != nullptr ) + { + MyPawn = Cast(BotPawnClass->GetDefaultObject()); + } + + if (MyPawn) + { + const FVector SpawnLocation = SpawnPoint->GetActorLocation(); + for (ACharacter* OtherPawn : TActorRange(GetWorld())) + { + if (OtherPawn != MyPawn) + { + const float CombinedHeight = (MyPawn->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() + OtherPawn->GetCapsuleComponent()->GetScaledCapsuleHalfHeight()) * 2.0f; + const float CombinedRadius = MyPawn->GetCapsuleComponent()->GetScaledCapsuleRadius() + OtherPawn->GetCapsuleComponent()->GetScaledCapsuleRadius(); + const FVector OtherLocation = OtherPawn->GetActorLocation(); + + // check if player start overlaps this pawn + if (FMath::Abs(SpawnLocation.Z - OtherLocation.Z) < CombinedHeight && (SpawnLocation - OtherLocation).Size2D() < CombinedRadius) + { + return false; + } + } + } + } + else + { + return false; + } + + return true; +} + +void AShooterGameMode::CreateBotControllers() +{ + UWorld* World = GetWorld(); + int32 ExistingBots = 0; + for (FConstControllerIterator It = World->GetControllerIterator(); It; ++It) + { + AShooterAIController* AIC = Cast(*It); + if (AIC) + { + ++ExistingBots; + } + } + + // Create any necessary AIControllers. Hold off on Pawn creation until pawns are actually necessary or need recreating. + int32 BotNum = ExistingBots; + for (int32 i = 0; i < MaxBots - ExistingBots; ++i) + { + CreateBot(BotNum + i); + } +} + +AShooterAIController* AShooterGameMode::CreateBot(int32 BotNum) +{ + FActorSpawnParameters SpawnInfo; + SpawnInfo.Instigator = nullptr; + SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + SpawnInfo.OverrideLevel = nullptr; + + UWorld* World = GetWorld(); + AShooterAIController* AIC = World->SpawnActor(SpawnInfo); + InitBot(AIC, BotNum); + + return AIC; +} + +void AShooterGameMode::StartBots() +{ + // checking number of existing human player. + UWorld* World = GetWorld(); + for (FConstControllerIterator It = World->GetControllerIterator(); It; ++It) + { + AShooterAIController* AIC = Cast(*It); + if (AIC) + { + RestartPlayer(AIC); + } + } +} + +void AShooterGameMode::InitBot(AShooterAIController* AIController, int32 BotNum) +{ + if (AIController) + { + if (AIController->PlayerState) + { + FString BotName = FString::Printf(TEXT("Bot %d"), BotNum); + AIController->PlayerState->SetPlayerName(BotName); + } + } +} + +void AShooterGameMode::RestartGame() +{ + // Hide the scoreboard too ! + for (FConstControllerIterator It = GetWorld()->GetControllerIterator(); It; ++It) + { + AShooterPlayerController* PlayerController = Cast(*It); + if (PlayerController != nullptr) + { + AShooterHUD* ShooterHUD = Cast(PlayerController->GetHUD()); + if (ShooterHUD != nullptr) + { + // Passing true to bFocus here ensures that focus is returned to the game viewport. + ShooterHUD->ShowScoreboard(false, true); + } + } + } + + Super::RestartGame(); +} + diff --git a/Source/ShooterGame/Private/Online/ShooterGameSession.cpp b/Source/ShooterGame/Private/Online/ShooterGameSession.cpp new file mode 100644 index 0000000..60e6ae1 --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterGameSession.cpp @@ -0,0 +1,503 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterGameSession.h" +#include "ShooterOnlineGameSettings.h" +#include "OnlineSubsystemSessionSettings.h" +#include "OnlineSubsystemUtils.h" + +namespace +{ + const FString CustomMatchKeyword("Custom"); +} + +AShooterGameSession::AShooterGameSession(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + if (!HasAnyFlags(RF_ClassDefaultObject)) + { + OnCreateSessionCompleteDelegate = FOnCreateSessionCompleteDelegate::CreateUObject(this, &AShooterGameSession::OnCreateSessionComplete); + OnDestroySessionCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &AShooterGameSession::OnDestroySessionComplete); + + OnFindSessionsCompleteDelegate = FOnFindSessionsCompleteDelegate::CreateUObject(this, &AShooterGameSession::OnFindSessionsComplete); + OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &AShooterGameSession::OnJoinSessionComplete); + + OnStartSessionCompleteDelegate = FOnStartSessionCompleteDelegate::CreateUObject(this, &AShooterGameSession::OnStartOnlineGameComplete); + } +} + +/** + * Delegate fired when a session start request has completed + * + * @param SessionName the name of the session this callback is for + * @param bWasSuccessful true if the async action completed without error, false if there was an error + */ +void AShooterGameSession::OnStartOnlineGameComplete(FName InSessionName, bool bWasSuccessful) +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid()) + { + Sessions->ClearOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegateHandle); + } + } + + if (bWasSuccessful) + { + // tell non-local players to start online game + for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It) + { + AShooterPlayerController* PC = Cast(*It); + if (PC && !PC->IsLocalPlayerController()) + { + PC->ClientStartOnlineGame(); + } + } + } +} + +/** Handle starting the match */ +void AShooterGameSession::HandleMatchHasStarted() +{ + // start online game locally and wait for completion + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && (Sessions->GetNamedSession(NAME_GameSession) != nullptr)) + { + UE_LOG(LogOnlineGame, Log, TEXT("Starting session %s on server"), *FName(NAME_GameSession).ToString()); + OnStartSessionCompleteDelegateHandle = Sessions->AddOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegate); + Sessions->StartSession(NAME_GameSession); + } + } +} + +/** + * Ends a game session + * + */ +void AShooterGameSession::HandleMatchHasEnded() +{ + // end online game locally + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && (Sessions->GetNamedSession(NAME_GameSession) != nullptr)) + { + // tell the clients to end + for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It) + { + AShooterPlayerController* PC = Cast(*It); + if (PC && !PC->IsLocalPlayerController()) + { + PC->ClientEndOnlineGame(); + } + } + + // server is handled here + UE_LOG(LogOnlineGame, Log, TEXT("Ending session %s on server"), *FName(NAME_GameSession).ToString() ); + Sessions->EndSession(NAME_GameSession); + } + } +} + +bool AShooterGameSession::IsBusy() const +{ + if (HostSettings.IsValid() || SearchSettings.IsValid()) + { + return true; + } + + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid()) + { + EOnlineSessionState::Type GameSessionState = Sessions->GetSessionState(NAME_GameSession); + EOnlineSessionState::Type PartySessionState = Sessions->GetSessionState(NAME_PartySession); + if (GameSessionState != EOnlineSessionState::NoSession || PartySessionState != EOnlineSessionState::NoSession) + { + return true; + } + } + } + + return false; +} + +EOnlineAsyncTaskState::Type AShooterGameSession::GetSearchResultStatus(int32& SearchResultIdx, int32& NumSearchResults) +{ + SearchResultIdx = 0; + NumSearchResults = 0; + + if (SearchSettings.IsValid()) + { + if (SearchSettings->SearchState == EOnlineAsyncTaskState::Done) + { + SearchResultIdx = CurrentSessionParams.BestSessionIdx; + NumSearchResults = SearchSettings->SearchResults.Num(); + } + return SearchSettings->SearchState; + } + + return EOnlineAsyncTaskState::NotStarted; +} + +/** + * Get the search results. + * + * @return Search results + */ +const TArray & AShooterGameSession::GetSearchResults() const +{ + return SearchSettings->SearchResults; +}; + + +/** + * Delegate fired when a session create request has completed + * + * @param SessionName the name of the session this callback is for + * @param bWasSuccessful true if the async action completed without error, false if there was an error + */ +void AShooterGameSession::OnCreateSessionComplete(FName InSessionName, bool bWasSuccessful) +{ + UE_LOG(LogOnlineGame, Verbose, TEXT("OnCreateSessionComplete %s bSuccess: %d"), *InSessionName.ToString(), bWasSuccessful); + + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + Sessions->ClearOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegateHandle); + } + + OnCreatePresenceSessionComplete().Broadcast(InSessionName, bWasSuccessful); +} + +/** + * Delegate fired when a destroying an online session has completed + * + * @param SessionName the name of the session this callback is for + * @param bWasSuccessful true if the async action completed without error, false if there was an error + */ +void AShooterGameSession::OnDestroySessionComplete(FName InSessionName, bool bWasSuccessful) +{ + UE_LOG(LogOnlineGame, Verbose, TEXT("OnDestroySessionComplete %s bSuccess: %d"), *InSessionName.ToString(), bWasSuccessful); + + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + Sessions->ClearOnDestroySessionCompleteDelegate_Handle(OnDestroySessionCompleteDelegateHandle); + HostSettings = NULL; + } +} + +bool AShooterGameSession::HostSession(TSharedPtr UserId, FName InSessionName, const FString& GameType, const FString& MapName, bool bIsLAN, bool bIsPresence, int32 MaxNumPlayers) +{ + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + CurrentSessionParams.SessionName = InSessionName; + CurrentSessionParams.bIsLAN = bIsLAN; + CurrentSessionParams.bIsPresence = bIsPresence; + CurrentSessionParams.UserId = UserId; + MaxPlayers = MaxNumPlayers; + + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && CurrentSessionParams.UserId.IsValid()) + { + HostSettings = MakeShareable(new FShooterOnlineSessionSettings(bIsLAN, bIsPresence, MaxPlayers)); + HostSettings->Set(SETTING_GAMEMODE, GameType, EOnlineDataAdvertisementType::ViaOnlineService); + HostSettings->Set(SETTING_MAPNAME, MapName, EOnlineDataAdvertisementType::ViaOnlineService); + 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("GameSession"), EOnlineDataAdvertisementType::DontAdvertise); + if (UserId->IsValid()) + { + FSessionSettings & UserSettings = HostSettings->MemberSettings.Add(UserId.ToSharedRef(), FSessionSettings()); + UserSettings.Add(SETTING_GAMEMODE, FOnlineSessionSetting(FString("GameSession"), EOnlineDataAdvertisementType::ViaOnlineService)); + } + +#if !PLATFORM_SWITCH + // On Switch, we don't have room for this in the session data (and it's not used anyway when searching), so there's no need to add it. + // Can be readded if the buffer size increases. + HostSettings->Set(SEARCH_KEYWORDS, CustomMatchKeyword, EOnlineDataAdvertisementType::ViaOnlineService); +#endif + + OnCreateSessionCompleteDelegateHandle = Sessions->AddOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegate); + return Sessions->CreateSession(*CurrentSessionParams.UserId, CurrentSessionParams.SessionName, *HostSettings); + } + else + { + OnCreateSessionComplete(InSessionName, false); + } + } +#if !UE_BUILD_SHIPPING + else + { + // Hack workflow in development + OnCreatePresenceSessionComplete().Broadcast(NAME_GameSession, true); + return true; + } +#endif + + return false; +} + +bool AShooterGameSession::HostSession(const TSharedPtr UserId, const FName InSessionName, const FOnlineSessionSettings& SessionSettings) +{ + bool bResult = false; + + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + CurrentSessionParams.SessionName = InSessionName; + CurrentSessionParams.bIsLAN = SessionSettings.bIsLANMatch; + CurrentSessionParams.bIsPresence = SessionSettings.bUsesPresence; + CurrentSessionParams.UserId = UserId; + MaxPlayers = SessionSettings.NumPrivateConnections + SessionSettings.NumPublicConnections; + + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && CurrentSessionParams.UserId.IsValid()) + { + OnCreateSessionCompleteDelegateHandle = Sessions->AddOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegate); + bResult = Sessions->CreateSession(*UserId, InSessionName, SessionSettings); + } + else + { + OnCreateSessionComplete(InSessionName, false); + } + } + + return bResult; +} + +void AShooterGameSession::OnFindSessionsComplete(bool bWasSuccessful) +{ + UE_LOG(LogOnlineGame, Verbose, TEXT("OnFindSessionsComplete bSuccess: %d"), bWasSuccessful); + + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid()) + { + Sessions->ClearOnFindSessionsCompleteDelegate_Handle(OnFindSessionsCompleteDelegateHandle); + + UE_LOG(LogOnlineGame, Verbose, TEXT("Num Search Results: %d"), SearchSettings->SearchResults.Num()); + for (int32 SearchIdx=0; SearchIdx < SearchSettings->SearchResults.Num(); SearchIdx++) + { + const FOnlineSessionSearchResult& SearchResult = SearchSettings->SearchResults[SearchIdx]; + DumpSession(&SearchResult.Session); + } + + OnFindSessionsComplete().Broadcast(bWasSuccessful); + } + } +} + +void AShooterGameSession::ResetBestSessionVars() +{ + CurrentSessionParams.BestSessionIdx = -1; +} + +void AShooterGameSession::ChooseBestSession() +{ + // Start searching from where we left off + for (int32 SessionIndex = CurrentSessionParams.BestSessionIdx+1; SessionIndex < SearchSettings->SearchResults.Num(); SessionIndex++) + { + // Found the match that we want + CurrentSessionParams.BestSessionIdx = SessionIndex; + return; + } + + CurrentSessionParams.BestSessionIdx = -1; +} + +void AShooterGameSession::StartMatchmaking() +{ + ResetBestSessionVars(); + ContinueMatchmaking(); +} + +void AShooterGameSession::ContinueMatchmaking() +{ + ChooseBestSession(); + if (CurrentSessionParams.BestSessionIdx >= 0 && CurrentSessionParams.BestSessionIdx < SearchSettings->SearchResults.Num()) + { + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && CurrentSessionParams.UserId.IsValid()) + { + OnJoinSessionCompleteDelegateHandle = Sessions->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate); + Sessions->JoinSession(*CurrentSessionParams.UserId, CurrentSessionParams.SessionName, SearchSettings->SearchResults[CurrentSessionParams.BestSessionIdx]); + } + } + } + else + { + OnNoMatchesAvailable(); + } +} + +void AShooterGameSession::OnNoMatchesAvailable() +{ + UE_LOG(LogOnlineGame, Verbose, TEXT("Matchmaking complete, no sessions available.")); + SearchSettings = NULL; +} + +void AShooterGameSession::FindSessions(TSharedPtr UserId, FName InSessionName, bool bIsLAN, bool bIsPresence) +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + CurrentSessionParams.SessionName = InSessionName; + CurrentSessionParams.bIsLAN = bIsLAN; + CurrentSessionParams.bIsPresence = bIsPresence; + CurrentSessionParams.UserId = UserId; + + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && CurrentSessionParams.UserId.IsValid()) + { + SearchSettings = MakeShareable(new FShooterOnlineSearchSettings(bIsLAN, bIsPresence)); + SearchSettings->QuerySettings.Set(SEARCH_KEYWORDS, CustomMatchKeyword, EOnlineComparisonOp::Equals); + + TSharedRef SearchSettingsRef = SearchSettings.ToSharedRef(); + + OnFindSessionsCompleteDelegateHandle = Sessions->AddOnFindSessionsCompleteDelegate_Handle(OnFindSessionsCompleteDelegate); + Sessions->FindSessions(*CurrentSessionParams.UserId, SearchSettingsRef); + } + } + else + { + OnFindSessionsComplete(false); + } +} + +bool AShooterGameSession::JoinSession(TSharedPtr UserId, FName InSessionName, int32 SessionIndexInSearchResults) +{ + bool bResult = false; + + if (SessionIndexInSearchResults >= 0 && SessionIndexInSearchResults < SearchSettings->SearchResults.Num()) + { + bResult = JoinSession(UserId, InSessionName, SearchSettings->SearchResults[SessionIndexInSearchResults]); + } + + return bResult; +} + +bool AShooterGameSession::JoinSession(TSharedPtr UserId, FName InSessionName, const FOnlineSessionSearchResult& SearchResult) +{ + bool bResult = false; + + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && UserId.IsValid()) + { + OnJoinSessionCompleteDelegateHandle = Sessions->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate); + bResult = Sessions->JoinSession(*UserId, InSessionName, SearchResult); + } + } + + return bResult; +} + +/** + * Delegate fired when the joining process for an online session has completed + * + * @param SessionName the name of the session this callback is for + * @param bWasSuccessful true if the async action completed without error, false if there was an error + */ +void AShooterGameSession::OnJoinSessionComplete(FName InSessionName, EOnJoinSessionCompleteResult::Type Result) +{ + bool bWillTravel = false; + + UE_LOG(LogOnlineGame, Verbose, TEXT("OnJoinSessionComplete %s bSuccess: %d"), *InSessionName.ToString(), static_cast(Result)); + + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid()) + { + Sessions->ClearOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegateHandle); + } + } + + OnJoinSessionComplete().Broadcast(Result); +} + +bool AShooterGameSession::TravelToSession(int32 ControllerId, FName InSessionName) +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + FString URL; + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && Sessions->GetResolvedConnectString(InSessionName, URL)) + { + APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), ControllerId); + if (PC) + { + PC->ClientTravel(URL, TRAVEL_Absolute); + return true; + } + } + else + { + UE_LOG(LogOnlineGame, Warning, TEXT("Failed to join session %s"), *SessionName.ToString()); + } + } +#if !UE_BUILD_SHIPPING + else + { + APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), ControllerId); + if (PC) + { + FString LocalURL(TEXT("127.0.0.1")); + PC->ClientTravel(LocalURL, TRAVEL_Absolute); + return true; + } + } +#endif //!UE_BUILD_SHIPPING + + return false; +} + +void AShooterGameSession::RegisterServer() +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr SessionInt = OnlineSub->GetSessionInterface(); + if (SessionInt.IsValid()) + { + TSharedPtr ShooterHostSettings = MakeShareable(new FShooterOnlineSessionSettings(false, false, 16)); + ShooterHostSettings->Set(SETTING_MATCHING_HOPPER, FString("TeamDeathmatch"), EOnlineDataAdvertisementType::DontAdvertise); + ShooterHostSettings->Set(SETTING_MATCHING_TIMEOUT, 120.0f, EOnlineDataAdvertisementType::ViaOnlineService); + ShooterHostSettings->Set(SETTING_SESSION_TEMPLATE_NAME, FString("GameSession"), EOnlineDataAdvertisementType::DontAdvertise); + ShooterHostSettings->Set(SETTING_GAMEMODE, FString("TeamDeathmatch"), EOnlineDataAdvertisementType::ViaOnlineService); + ShooterHostSettings->Set(SETTING_MAPNAME, GetWorld()->GetMapName(), EOnlineDataAdvertisementType::ViaOnlineService); + ShooterHostSettings->bAllowInvites = true; + ShooterHostSettings->bIsDedicated = true; + if (FParse::Param(FCommandLine::Get(), TEXT("forcelan"))) + { + UE_LOG(LogOnlineGame, Log, TEXT("Registering server as a LAN server")); + ShooterHostSettings->bIsLANMatch = true; + } + HostSettings = ShooterHostSettings; + OnCreateSessionCompleteDelegateHandle = SessionInt->AddOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegate); + SessionInt->CreateSession(0, NAME_GameSession, *HostSettings); + } + } +} diff --git a/Source/ShooterGame/Private/Online/ShooterGameState.cpp b/Source/ShooterGame/Private/Online/ShooterGameState.cpp new file mode 100644 index 0000000..10a2bf6 --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterGameState.cpp @@ -0,0 +1,99 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Online/ShooterPlayerState.h" +#include "ShooterGameInstance.h" +#include "OnlineSubsystemUtils.h" +#include "OnlineGameMatchesInterface.h" + +AShooterGameState::AShooterGameState(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + NumTeams = 0; + RemainingTime = 0; + bTimerPaused = false; + + UShooterGameInstance* GameInstance = GetWorld() != nullptr ? Cast(GetWorld()->GetGameInstance()) : nullptr; + + GameMatches.Initialize(this, GameInstance); +} + +void AShooterGameState::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const +{ + Super::GetLifetimeReplicatedProps( OutLifetimeProps ); + + DOREPLIFETIME( AShooterGameState, NumTeams ); + DOREPLIFETIME( AShooterGameState, RemainingTime ); + DOREPLIFETIME( AShooterGameState, bTimerPaused ); + DOREPLIFETIME( AShooterGameState, TeamScores ); +} + +void AShooterGameState::GetRankedMap(int32 TeamIndex, RankedPlayerMap& OutRankedMap) const +{ + OutRankedMap.Empty(); + + //first, we need to go over all the PlayerStates, grab their score, and rank them + TMultiMap SortedMap; + for(int32 i = 0; i < PlayerArray.Num(); ++i) + { + int32 Score = 0; + AShooterPlayerState* CurPlayerState = Cast(PlayerArray[i]); + if (CurPlayerState && (CurPlayerState->GetTeamNum() == TeamIndex)) + { + SortedMap.Add(FMath::TruncToInt(CurPlayerState->GetScore()), CurPlayerState); + } + } + + //sort by the keys + SortedMap.KeySort(TGreater()); + + //now, add them back to the ranked map + OutRankedMap.Empty(); + + int32 Rank = 0; + for(TMultiMap::TIterator It(SortedMap); It; ++It) + { + OutRankedMap.Add(Rank++, It.Value()); + } + +} + + +void AShooterGameState::RequestFinishAndExitToMainMenu() +{ + if (AuthorityGameMode) + { + // we are server, tell the gamemode + AShooterGameMode* const GameMode = Cast(AuthorityGameMode); + if (GameMode) + { + GameMode->RequestFinishAndExitToMainMenu(); + } + } + else + { + // we are client, handle our own business + UShooterGameInstance* GameInstance = Cast(GetGameInstance()); + if (GameInstance) + { + GameInstance->RemoveSplitScreenPlayers(); + } + + AShooterPlayerController* const PrimaryPC = Cast(GetGameInstance()->GetFirstLocalPlayerController()); + if (PrimaryPC) + { + PrimaryPC->HandleReturnToMainMenu(); + } + } +} + +void AShooterGameState::HandleMatchHasStarted() +{ + Super::HandleMatchHasStarted(); + GameMatches.HandleMatchHasStarted(ActivityId, NumTeams); +} + +void AShooterGameState::HandleMatchHasEnded() +{ + Super::HandleMatchHasEnded(); + GameMatches.HandleMatchHasEnded(bEnableGameFeedback, NumTeams, MakeArrayView(TeamScores)); +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Online/ShooterGame_FreeForAll.cpp b/Source/ShooterGame/Private/Online/ShooterGame_FreeForAll.cpp new file mode 100644 index 0000000..5a9760b --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterGame_FreeForAll.cpp @@ -0,0 +1,40 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterGame_FreeForAll.h" +#include "ShooterPlayerState.h" + +AShooterGame_FreeForAll::AShooterGame_FreeForAll(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + bDelayedStart = true; +} + +void AShooterGame_FreeForAll::DetermineMatchWinner() +{ + AShooterGameState const* const MyGameState = CastChecked(GameState); + float BestScore = MIN_flt; + int32 BestPlayer = -1; + int32 NumBestPlayers = 0; + + for (int32 i = 0; i < MyGameState->PlayerArray.Num(); i++) + { + const float PlayerScore = MyGameState->PlayerArray[i]->GetScore(); + if (BestScore < PlayerScore) + { + BestScore = PlayerScore; + BestPlayer = i; + NumBestPlayers = 1; + } + else if (BestScore == PlayerScore) + { + NumBestPlayers++; + } + } + + WinnerPlayerState = (NumBestPlayers == 1) ? Cast(MyGameState->PlayerArray[BestPlayer]) : NULL; +} + +bool AShooterGame_FreeForAll::IsWinner(AShooterPlayerState* PlayerState) const +{ + return PlayerState && !PlayerState->IsQuitter() && PlayerState == WinnerPlayerState; +} diff --git a/Source/ShooterGame/Private/Online/ShooterGame_Menu.cpp b/Source/ShooterGame/Private/Online/ShooterGame_Menu.cpp new file mode 100644 index 0000000..ebbf00f --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterGame_Menu.cpp @@ -0,0 +1,25 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterGame_Menu.h" +#include "ShooterMainMenu.h" +#include "ShooterWelcomeMenu.h" +#include "ShooterMessageMenu.h" +#include "ShooterPlayerController_Menu.h" +#include "Online/ShooterGameSession.h" + +AShooterGame_Menu::AShooterGame_Menu(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + PlayerControllerClass = AShooterPlayerController_Menu::StaticClass(); +} + +void AShooterGame_Menu::RestartPlayer(class AController* NewPlayer) +{ + // don't restart +} + +/** Returns game session class to use */ +TSubclassOf AShooterGame_Menu::GetGameSessionClass() const +{ + return AShooterGameSession::StaticClass(); +} diff --git a/Source/ShooterGame/Private/Online/ShooterGame_TeamDeathMatch.cpp b/Source/ShooterGame/Private/Online/ShooterGame_TeamDeathMatch.cpp new file mode 100644 index 0000000..9a96703 --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterGame_TeamDeathMatch.cpp @@ -0,0 +1,134 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterTeamStart.h" +#include "Online/ShooterGame_TeamDeathMatch.h" +#include "Online/ShooterPlayerState.h" +#include "Bots/ShooterAIController.h" + +AShooterGame_TeamDeathMatch::AShooterGame_TeamDeathMatch(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + NumTeams = 2; + bDelayedStart = true; +} + +void AShooterGame_TeamDeathMatch::PostLogin(APlayerController* NewPlayer) +{ + // Place player on a team before Super (VoIP team based init, findplayerstart, etc) + AShooterPlayerState* NewPlayerState = CastChecked(NewPlayer->PlayerState); + const int32 TeamNum = ChooseTeam(NewPlayerState); + NewPlayerState->SetTeamNum(TeamNum); + + Super::PostLogin(NewPlayer); +} + +void AShooterGame_TeamDeathMatch::InitGameState() +{ + Super::InitGameState(); + + AShooterGameState* const MyGameState = Cast(GameState); + if (MyGameState) + { + MyGameState->NumTeams = NumTeams; + } +} + +bool AShooterGame_TeamDeathMatch::CanDealDamage(AShooterPlayerState* DamageInstigator, class AShooterPlayerState* DamagedPlayer) const +{ + return DamageInstigator && DamagedPlayer && (DamagedPlayer == DamageInstigator || DamagedPlayer->GetTeamNum() != DamageInstigator->GetTeamNum()); +} + +int32 AShooterGame_TeamDeathMatch::ChooseTeam(AShooterPlayerState* ForPlayerState) const +{ + TArray TeamBalance; + TeamBalance.AddZeroed(NumTeams); + + // get current team balance + for (int32 i = 0; i < GameState->PlayerArray.Num(); i++) + { + AShooterPlayerState const* const TestPlayerState = Cast(GameState->PlayerArray[i]); + if (TestPlayerState && TestPlayerState != ForPlayerState && TeamBalance.IsValidIndex(TestPlayerState->GetTeamNum())) + { + TeamBalance[TestPlayerState->GetTeamNum()]++; + } + } + + // find least populated one + int32 BestTeamScore = TeamBalance[0]; + for (int32 i = 1; i < TeamBalance.Num(); i++) + { + if (BestTeamScore > TeamBalance[i]) + { + BestTeamScore = TeamBalance[i]; + } + } + + // there could be more than one... + TArray BestTeams; + for (int32 i = 0; i < TeamBalance.Num(); i++) + { + if (TeamBalance[i] == BestTeamScore) + { + BestTeams.Add(i); + } + } + + // get random from best list + const int32 RandomBestTeam = BestTeams[FMath::RandHelper(BestTeams.Num())]; + return RandomBestTeam; +} + +void AShooterGame_TeamDeathMatch::DetermineMatchWinner() +{ + AShooterGameState const* const MyGameState = Cast(GameState); + int32 BestScore = MIN_uint32; + int32 BestTeam = -1; + int32 NumBestTeams = 1; + + for (int32 i = 0; i < MyGameState->TeamScores.Num(); i++) + { + const int32 TeamScore = MyGameState->TeamScores[i]; + if (BestScore < TeamScore) + { + BestScore = TeamScore; + BestTeam = i; + NumBestTeams = 1; + } + else if (BestScore == TeamScore) + { + NumBestTeams++; + } + } + + WinnerTeam = (NumBestTeams == 1) ? BestTeam : NumTeams; +} + +bool AShooterGame_TeamDeathMatch::IsWinner(AShooterPlayerState* PlayerState) const +{ + return PlayerState && !PlayerState->IsQuitter() && PlayerState->GetTeamNum() == WinnerTeam; +} + +bool AShooterGame_TeamDeathMatch::IsSpawnpointAllowed(APlayerStart* SpawnPoint, AController* Player) const +{ + if (Player) + { + AShooterTeamStart* TeamStart = Cast(SpawnPoint); + AShooterPlayerState* PlayerState = Cast(Player->PlayerState); + + if (PlayerState && TeamStart && TeamStart->SpawnTeam != PlayerState->GetTeamNum()) + { + return false; + } + } + + return Super::IsSpawnpointAllowed(SpawnPoint, Player); +} + +void AShooterGame_TeamDeathMatch::InitBot(AShooterAIController* AIC, int32 BotNum) +{ + AShooterPlayerState* BotPlayerState = CastChecked(AIC->PlayerState); + const int32 TeamNum = ChooseTeam(BotPlayerState); + BotPlayerState->SetTeamNum(TeamNum); + + Super::InitBot(AIC, BotNum); +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Online/ShooterOnlineGameMatches.cpp b/Source/ShooterGame/Private/Online/ShooterOnlineGameMatches.cpp new file mode 100644 index 0000000..ede2a3e --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterOnlineGameMatches.cpp @@ -0,0 +1,597 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterOnlineGameMatches.h" +#include "OnlineSubsystemUtils.h" +#include "ShooterGameInstance.h" +#include "ShooterPlayerState.h" + +void FShooterOnlineGameMatches::Initialize(AShooterGameState* InGameState, UShooterGameInstance* InGameInstance) +{ + if (InGameInstance != nullptr) + { + GameInstance = InGameInstance; + } + + if (InGameState != nullptr) + { + GameState = InGameState; + } +} +void FShooterOnlineGameMatches::CreateMatch(const FUniqueNetId& LocalOwnerId, const FString& ActivitId, const TArray& Players, const TArray& Teams) +{ + if (IOnlineGameMatchesPtr MatchesInterface = IOnlineSubsystem::Get()->GetGameMatchesInterface()) + { + FGameMatchesData MatchData; + FGameMatchRoster Roaster; + Roaster.Players = Players; + Roaster.Teams = Teams; + MatchData.MatchesRoster = Roaster; + MatchData.ActivityId = ActivitId; + + TWeakObjectPtr WeakGameState(GameState); + FOnCreateGameMatchComplete CreateMatchCompleteDelegate = FOnCreateGameMatchComplete::CreateLambda( + [this, WeakGameState](const FUniqueNetId& LambdaLocalUserId, const FString& LambdaMatchId, const FOnlineError& LambdaResult) + { + if (WeakGameState.IsValid()) + { + OnCreateMatchComplete(LambdaLocalUserId, LambdaMatchId, LambdaResult); + } + }); + MatchesInterface->CreateGameMatch(LocalOwnerId, MatchData, CreateMatchCompleteDelegate); + } + else + { + // No valid matches interface + return; + } +} + +void FShooterOnlineGameMatches::OnCreateMatchComplete(const FUniqueNetId& LocalUserId, const FString& MatchId, const FOnlineError& Result) +{ + if (Result.WasSuccessful()) + { + if (IOnlineSessionPtr Sessions = IOnlineSubsystem::Get()->GetSessionInterface()) + { + FNamedOnlineSession* NamedSession = Sessions->GetNamedSession(NAME_GameSession); + if (NamedSession != nullptr) + { + NamedSession->SessionSettings.Set(TEXT("SETTING_MATCHID"), MatchId); + Sessions->UpdateSession(NAME_GameSession, NamedSession->SessionSettings, true); + + StartMatch(LocalUserId, MatchId); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("Namedsession is invalid")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::OnCreateMatchComplete: Create game match failed: [%s]"), *Result.ErrorCode); + } +} + +void FShooterOnlineGameMatches::StartMatch(const FUniqueNetId& LocalUserId, const FString& MatchId) +{ + if (IOnlineGameMatchesPtr Matches = IOnlineSubsystem::Get()->GetGameMatchesInterface()) + { + TWeakObjectPtr WeakGameState(GameState); + FOnGameMatchStatusUpdateComplete MatchStatusUpdateCompleteDelegate = FOnGameMatchStatusUpdateComplete::CreateLambda( + [this, WeakGameState](const FUniqueNetId& LambdaLocalUserId, const EUpdateGameMatchStatus& LambdaStatus, const FOnlineError& LambdaResult) + { + if (WeakGameState.IsValid()) + { + OnMatchStatusUpdateComplete(LambdaLocalUserId, LambdaStatus, LambdaResult); + } + }); + + Matches->UpdateGameMatchStatus(LocalUserId, MatchId, EUpdateGameMatchStatus::InProgress, MatchStatusUpdateCompleteDelegate); + } + else + { + // No valid matches interface + return; + } +} + +void FShooterOnlineGameMatches::OnMatchStatusUpdateComplete(const FUniqueNetId& LocalUserId, const EUpdateGameMatchStatus& Status, const FOnlineError& Result) +{ + if (Result.WasSuccessful()) + { + UE_LOG(LogOnline, Log, TEXT("Match status updated to [%d]"), Status); + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::OnMatchStatusUpdateComplete: Could not set new status: [%s]"), *Result.ToLogString()); + } +} + +void FShooterOnlineGameMatches::EndMatch(const FUniqueNetId& LocalOwnerId, const FString& MatchId, const FFinalGameMatchReport& FinalReport) +{ + if(IOnlineGameMatchesPtr MatchesInterface = IOnlineSubsystem::Get()->GetGameMatchesInterface()) + { + TWeakObjectPtr WeakGameState(GameState); + FOnGameMatchReportComplete MatchReportCompleteDelegate = FOnGameMatchReportComplete::CreateLambda( + [this, WeakGameState, MatchId, FinalReport](const FUniqueNetId& LambdaLocalUserId, const FOnlineError& LambdaResult) + { + if (WeakGameState.IsValid()) + { + OnEndMatchComplete(LambdaLocalUserId, LambdaResult, MatchId, FinalReport.bLeaveGameFeedback); + } + }); + + MatchesInterface->ReportGameMatchResults(LocalOwnerId, MatchId, FinalReport, MatchReportCompleteDelegate); + } + else + { + // No valid matches interface + return; + } +} + +void FShooterOnlineGameMatches::OnEndMatchComplete(const FUniqueNetId& LocalUserId, const FOnlineError& Result, FString MatchId, bool bRequestReview) +{ + if (Result.WasSuccessful()) + { + UE_LOG(LogOnlineGame, Log, TEXT("FShooterOnlineGameMatches::OnEndMatchComplete: Report end of game match [%s] was successful"), *MatchId); + } + else + { + UE_LOG(LogOnlineGame, Warning, TEXT("FShooterOnlineGameMatches::OnEndMatchComplete: Report end of game match failed: [%s]"), *Result.ErrorCode); + } +} + +void FShooterOnlineGameMatches::LeaveGameMatchFeedback(const FUniqueNetId& LocalUserId, const FString& MatchId, const bool bRequestReview) +{ + if (bRequestReview) + { + if (IOnlineGameMatchesPtr Matches = IOnlineSubsystem::Get()->GetGameMatchesInterface()) + { + bool bTeamReviewOnly = true; + + TWeakObjectPtr WeakGameState(GameState); + FOnGameMatchFeedbackComplete GameMatchFeedbackCompleteDelegate = FOnGameMatchFeedbackComplete::CreateLambda( + [this, WeakGameState](const FUniqueNetId& LambdaLocalUserId, const FOnlineError& LambdaResult) + { + if (WeakGameState.IsValid()) + { + OnLeaveGameMatchFeedbackComplete(LambdaLocalUserId, LambdaResult); + } + }); + + Matches->ProvideGameMatchFeedback(LocalUserId, MatchId, bTeamReviewOnly, GameMatchFeedbackCompleteDelegate); + } + else + { + // No valid matches interface + return; + } + } +} + +void FShooterOnlineGameMatches::OnLeaveGameMatchFeedbackComplete(const FUniqueNetId& LocalUserId, const FOnlineError& Result) +{ + if (Result.WasSuccessful()) + { + UE_LOG(LogOnline, Log, TEXT("FShooterOnlineGameMatches::OnLeaveGameMatchFeedbackComplete: Feedback results process complete for [%s]"), *LocalUserId.ToDebugString()); + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::OnLeaveGameMatchFeedbackComplete: Feedback results process failed for [%s]: [%s]"), *LocalUserId.ToDebugString(), *Result.ErrorCode); + } +} + +void FShooterOnlineGameMatches::BuildTeamPlayerGameMatchStats(const TSharedRef PlayerNetId, const int32 TeamId, const FString& StatKey, const FString& StatValue) +{ + FGameMatchPlayerStats PlayerStat; + PlayerStat.PlayerId = PlayerNetId; + + FGameMatchStatsData MatchStat; + MatchStat.StatsKey = StatKey; + MatchStat.StatsValue = StatValue; + PlayerStat.PlayerStats.Emplace(MatchStat); + TArray* PlayerStatsArray = TeamToPlayerStatsMap.Find(TeamId); + if (PlayerStatsArray != nullptr) + { + PlayerStatsArray->Emplace(PlayerStat); + } + else + { + TArray PlayerStats; + PlayerStats.Emplace(PlayerStat); + TeamToPlayerStatsMap.Emplace(TeamId, PlayerStats); + } +} + +void FShooterOnlineGameMatches::BuildTeamGameMatchStats(TArray& TeamStats) +{ + TeamStats.Reserve(TeamToPlayerStatsMap.Num()); + for (const TPair>& Team : TeamToPlayerStatsMap) + { + FGameMatchTeamStats TeamStat; + TeamStat.TeamId = FString::FromInt(Team.Key); + TeamStat.TeamMemberStats = Team.Value; + TeamStats.Emplace(TeamStat); + } +} + +void FShooterOnlineGameMatches::BuildPlayerGameMatchResults(const TSharedRef PlayerNetId, const int32 TeamId, FGameMatchPlayerResult PlayerResult) +{ + + TArray* PlayerResultArray = TeamPlayerResultsMap.Find(TeamId); + if (PlayerResultArray != nullptr) + { + PlayerResultArray->Emplace(PlayerResult); + } + else + { + TArray MemberResult; + MemberResult.Emplace(PlayerResult); + TeamPlayerResultsMap.Emplace(TeamId, MemberResult); + } +} + +void FShooterOnlineGameMatches::BuildTeamGameMatchResults(const TArrayView TeamScores, const int32& NumTeams, TArray& TeamResults) +{ + // Determine team rank + int32 BestScore = MIN_uint32; + + for (int32 TeamIndex = 0; TeamIndex < TeamScores.Num(); ++TeamIndex) + { + const int32 TeamScore = TeamScores[TeamIndex]; + if (TeamScore > BestScore) + { + BestScore = TeamScore; + } + } + + // Create the team structure with rank and score + for (int32 TeamIndex = 0; TeamIndex < NumTeams; ++TeamIndex) + { + FGameMatchTeamResult TeamResult; + if (TeamScores.Num() > 0) + { + TeamResult.Score = TeamScores[TeamIndex]; + TeamResult.Rank = (TeamResult.Score == BestScore) ? 1 : 2; + } + else + { + TeamResult.Score = 0; + TeamResult.Rank = 0; + } + + TeamResult.TeamId = FString::FromInt(TeamIndex); + + TArray* MemberResults = TeamPlayerResultsMap.Find(TeamIndex); + if (MemberResults != nullptr) + { + TeamResult.MembersResult = *MemberResults; + } + + TeamResults.Emplace(TeamResult); + } +} + +bool FShooterOnlineGameMatches::GetMatchOwnerAndId(TSharedPtr& OwnerId, FString& MatchId) +{ + FNamedOnlineSession* NamedSession; + if (GetNamedSessionInfo(NamedSession)) + { + TSharedPtr OwningUserId = NamedSession->OwningUserId; + if (OwningUserId.IsValid()) + { + OwnerId = OwningUserId; + NamedSession->SessionSettings.Get(TEXT("SETTING_MATCHID"), MatchId); + + if (MatchId.IsEmpty()) + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::GetMatchOwnerAndId: MatchId was not found.")); + return false; + } + return true; + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::GetMatchOwnerAndId: OwningUserId is invalid")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::GetMatchOwnerAndId: GetNamedSessionInfo failed")); + } + + return false; +} + +bool FShooterOnlineGameMatches::GetNamedSessionInfo(FNamedOnlineSession*& NamedSession) +{ + if (IOnlineSessionPtr Sessions = IOnlineSubsystem::Get()->GetSessionInterface()) + { + NamedSession = Sessions->GetNamedSession(NAME_GameSession); + if (NamedSession != nullptr) + { + return true; + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::GetNamedSessionInfo: Namedsession is invalid")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::GetNamedSessionInfo: session interface is invalid")); + } + + return false; +} + +TSharedPtr FShooterOnlineGameMatches::GetMatchOwnerId() +{ + TSharedPtr OwnerId; + FNamedOnlineSession* NamedSession; + if (GetNamedSessionInfo(NamedSession)) + { + TSharedPtr OwningUserId = NamedSession->OwningUserId; + if (OwningUserId.IsValid()) + { + OwnerId = OwningUserId; + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::GetMatchOwnerId: OwningUserId is invalid")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::GetMatchOwnerId: GetNamedSessionInfo failed")); + } + + return OwnerId; +} + +bool FShooterOnlineGameMatches::GetMatchId(FString& MatchId) +{ + FNamedOnlineSession* NamedSession; + if (GetNamedSessionInfo(NamedSession)) + { + NamedSession->SessionSettings.Get(TEXT("SETTING_MATCHID"), MatchId); + + if (MatchId.IsEmpty()) + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::GetMatchId: MatchId was not found.")); + return false; + } + return true; + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::GetMatchId: GetNamedSessionInfo failed")); + } + + return false; +} + +bool FShooterOnlineGameMatches::IsClientRepresentative(const TSharedPtr& RepresentativeId) +{ + TWeakObjectPtr WeakGameInstance(GameInstance); + if (WeakGameInstance.IsValid()) + { + const TArray& LocalPlayers = GameInstance->GetLocalPlayers(); + for (int i = 0; i < LocalPlayers.Num(); ++i) + { + FUniqueNetIdRepl LocalPlayer = LocalPlayers[i]->GetUniqueNetIdFromCachedControllerId(); + if (LocalPlayer != nullptr) + { + if (*RepresentativeId == *LocalPlayer) + { + UE_LOG(LogOnline, Verbose, TEXT("LocalOwnerId [%s] is our representative"), *RepresentativeId->ToDebugString()); + return true; + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::IsClientRepresentative: LocalPlayer is not valid")); + } + } + } + else + { + // Game instance is not valid + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::IsClientRepresentative: Game instance is not valid")); + } + return false; +} + +void FShooterOnlineGameMatches::HandleMatchHasStarted(const FString& ActivityId, int32& NumTeams) +{ + IOnlineGameMatchesPtr Matches = IOnlineSubsystem::Get()->GetGameMatchesInterface(); + if(!Matches.IsValid()) + { + // No valid matches interface + return; + } + + TWeakObjectPtr WeakGameInstance(GameInstance); + if (!WeakGameInstance.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::HandleMatchHasStarted: Game instance is invalid")); + return; + } + + TSharedPtr OwnerId = GetMatchOwnerId(); + if (OwnerId.IsValid()) + { + if (IsClientRepresentative(OwnerId)) + { + TArray Players; + //AccountId, TeamId Map + TMap>> TeamMembershipMap; + int32 PlayerIndex = 0; + // Iterate all players to build out the match + for (TActorIterator It(WeakGameInstance->GetWorld()); It; ++It) + { + bool bIsABot = (*It)->IsABot(); + + const TSharedPtr& PlayerId = (*It)->GetUniqueId().GetUniqueNetId(); + if (!PlayerId.IsValid() && !bIsABot) + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::HandleMatchHasStarted: Player is invalid")); + return; + } + TSharedPtr PlayerNetId = bIsABot ? MakeShared(FString::FromInt((*It)->GetPlayerId())) : PlayerId; + FGameMatchPlayer Player; + Player.bIsNpc = bIsABot; + FString PlayerName((*It)->GetPlayerName()); + Player.PlayerName = PlayerName; + Player.PlayerId = PlayerNetId; + Players.Emplace(Player); + + //Save our id for after the match is over. + FString PlayerIdString(FString::FromInt((*It)->GetPlayerId())); + PlayerIdToNetIdIndexMap.Emplace(PlayerIdString, PlayerIndex); + PlayerNetIds.Emplace(PlayerNetId.ToSharedRef()); + PlayerIndex++; + + FString TeamId = FString::FromInt((*It)->GetTeamNum()); + + //Build out the team membership mapping + TArray>& TeamMemberArray = TeamMembershipMap.FindOrAdd(TeamId); + TeamMemberArray.Emplace(PlayerNetId.ToSharedRef()); + } + + NumTeams = TeamMembershipMap.Num(); + + TArray Teams; + for (const TPair>>& CurrentTeam : TeamMembershipMap) + { + FGameMatchTeam Team; + Team.TeamId = CurrentTeam.Key; + Team.TeamMemberIds = CurrentTeam.Value; + Teams.Emplace(Team); + } + + if (ActivityId.IsEmpty()) + { + // An activity Id must be present in game ini or we should fail. + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::HandleMatchHasStarted: Activity id is not set. Please add it to the game ini")); + return; + } + + CreateMatch(*OwnerId, ActivityId, Players, Teams); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("FShooterOnlineGameMatches::HandleMatchHasStarted: Failed to get a valid session owner")); + } +} + +void FShooterOnlineGameMatches::HandleMatchHasEnded(const bool bEnableGameFeedback, const int32 NumTeams, TArrayView TeamScores) +{ + IOnlineGameMatchesPtr Matches = IOnlineSubsystem::Get()->GetGameMatchesInterface(); + if (!Matches.IsValid()) + { + // No valid matches interface + return; + } + + TSharedPtr OwnerId; + FString MatchId; + + if (GetMatchOwnerAndId(OwnerId, MatchId)) + { + if (IsClientRepresentative(OwnerId)) + { + TArray TeamsResult; + + if (NumTeams > 0) + { + + TWeakObjectPtr WeakGameState(GameState); + if (!WeakGameState.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("GameState is not valid")); + return; + } + TArray TeamRankedPlayers; + TeamRankedPlayers.AddZeroed(NumTeams); + for (int32 TeamIndex = 0; TeamIndex < NumTeams; ++TeamIndex) + { + WeakGameState->GetRankedMap(TeamIndex, TeamRankedPlayers[TeamIndex]); + } + + for (int32 TeamIndex = 0; TeamIndex < TeamRankedPlayers.Num(); ++TeamIndex) + { + + for (const TPair>& RankedPlayer : TeamRankedPlayers[TeamIndex]) + { + + FString PlayerIdString(FString::FromInt((*RankedPlayer.Value).GetPlayerId())); + const int32* NetIdIndex = PlayerIdToNetIdIndexMap.Find(PlayerIdString); + if (NetIdIndex == nullptr) + { + UE_LOG(LogOnline, Warning, TEXT("AShooterGameState::HandleMatchHasEnded: Could not find player net id index in map: [%s]"), *PlayerIdString); + return; + } + + const TSharedRef PlayerNetId = PlayerNetIds[*NetIdIndex]; + + FGameMatchPlayerResult PlayerResult; + PlayerResult.PlayerId = PlayerNetId; + PlayerResult.Rank = RankedPlayer.Key; + PlayerResult.Score = (*RankedPlayer.Value).GetScore(); + + int32 TeamId = (*RankedPlayer.Value).GetTeamNum(); + + BuildPlayerGameMatchResults(PlayerNetId, TeamId, PlayerResult); + + // Setup match stats for deaths and kills + BuildTeamPlayerGameMatchStats(PlayerNetId, TeamId, TEXT("Deaths"), FString::FromInt((*RankedPlayer.Value).GetDeaths())); + BuildTeamPlayerGameMatchStats(PlayerNetId, TeamId, TEXT("Kills"), FString::FromInt((*RankedPlayer.Value).GetKills())); + + // Set the match id on the player for UI access + (*RankedPlayer.Value).SetMatchId(MatchId); + + } + } + + //Now split out by teams. + TArray TeamResults; + BuildTeamGameMatchResults(TeamScores, NumTeams, TeamResults); + + FGameMatchCompetitiveResults CompetitiveMatch; + CompetitiveMatch.TeamsResult = TeamResults; + + FGameMatchResult Results; + Results.CompetitiveResult = CompetitiveMatch; + + // Build the final match structure + FFinalGameMatchReport MatchReport; + MatchReport.Results = Results; + + // If we have extra stats recorded we need to provide them as well + TArray TeamStats; + BuildTeamGameMatchStats(TeamStats); + + if (TeamStats.Num() > 0) + { + FGameMatchStats MatchStats; + MatchStats.TeamStats = TeamStats; + MatchReport.Stats = MatchStats; + } + + MatchReport.bLeaveGameFeedback = bEnableGameFeedback; + MatchReport.MatchType = GAME_MATCH_TYPE_COMPETITIVE; + MatchReport.GroupType = EMatchGroupType::Teams; + + EndMatch(*OwnerId, MatchId, MatchReport); + } + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("Failed to get named session info")); + return; + } +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Online/ShooterOnlineGameSettings.cpp b/Source/ShooterGame/Private/Online/ShooterOnlineGameSettings.cpp new file mode 100644 index 0000000..4604e34 --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterOnlineGameSettings.cpp @@ -0,0 +1,41 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterOnlineGameSettings.h" + + +FShooterOnlineSessionSettings::FShooterOnlineSessionSettings(bool bIsLAN, bool bIsPresence, int32 MaxNumPlayers) +{ + NumPublicConnections = MaxNumPlayers; + if (NumPublicConnections < 0) + { + NumPublicConnections = 0; + } + NumPrivateConnections = 0; + bIsLANMatch = bIsLAN; + bShouldAdvertise = true; + bAllowJoinInProgress = true; + bAllowInvites = true; + bUsesPresence = bIsPresence; + bAllowJoinViaPresence = true; + bAllowJoinViaPresenceFriendsOnly = false; +} + +FShooterOnlineSearchSettings::FShooterOnlineSearchSettings(bool bSearchingLAN, bool bSearchingPresence) +{ + bIsLanQuery = bSearchingLAN; + MaxSearchResults = 10; + PingBucketSize = 50; + + if (bSearchingPresence) + { + QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); + } +} + +FShooterOnlineSearchSettingsEmptyDedicated::FShooterOnlineSearchSettingsEmptyDedicated(bool bSearchingLAN, bool bSearchingPresence) : + FShooterOnlineSearchSettings(bSearchingLAN, bSearchingPresence) +{ + QuerySettings.Set(SEARCH_DEDICATED_ONLY, true, EOnlineComparisonOp::Equals); + QuerySettings.Set(SEARCH_EMPTY_SERVERS_ONLY, true, EOnlineComparisonOp::Equals); +} diff --git a/Source/ShooterGame/Private/Online/ShooterOnlineGameSettings.h b/Source/ShooterGame/Private/Online/ShooterOnlineGameSettings.h new file mode 100644 index 0000000..58fcd40 --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterOnlineGameSettings.h @@ -0,0 +1,36 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +/** + * General session settings for a Shooter game + */ +class FShooterOnlineSessionSettings : public FOnlineSessionSettings +{ +public: + + FShooterOnlineSessionSettings(bool bIsLAN = false, bool bIsPresence = false, int32 MaxNumPlayers = 4); + virtual ~FShooterOnlineSessionSettings() {} +}; + +/** + * General search setting for a Shooter game + */ +class FShooterOnlineSearchSettings : public FOnlineSessionSearch +{ +public: + FShooterOnlineSearchSettings(bool bSearchingLAN = false, bool bSearchingPresence = false); + + virtual ~FShooterOnlineSearchSettings() {} +}; + +/** + * Search settings for an empty dedicated server to host a match + */ +class FShooterOnlineSearchSettingsEmptyDedicated : public FShooterOnlineSearchSettings +{ +public: + FShooterOnlineSearchSettingsEmptyDedicated(bool bSearchingLAN = false, bool bSearchingPresence = false); + + virtual ~FShooterOnlineSearchSettingsEmptyDedicated() {} +}; diff --git a/Source/ShooterGame/Private/Online/ShooterOnlineSessionClient.cpp b/Source/ShooterGame/Private/Online/ShooterOnlineSessionClient.cpp new file mode 100644 index 0000000..817fe92 --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterOnlineSessionClient.cpp @@ -0,0 +1,52 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterOnlineSessionClient.h" +#include "ShooterGameInstance.h" + +UShooterOnlineSessionClient::UShooterOnlineSessionClient() +{ +} + +void UShooterOnlineSessionClient::OnSessionUserInviteAccepted( + const bool bWasSuccess, + const int32 ControllerId, + TSharedPtr< const FUniqueNetId > UserId, + const FOnlineSessionSearchResult & InviteResult +) +{ + UE_LOG(LogOnline, Verbose, TEXT("HandleSessionUserInviteAccepted: bSuccess: %d, ControllerId: %d, User: %s"), bWasSuccess, ControllerId, UserId.IsValid() ? *UserId->ToString() : TEXT("NULL")); + + if (!bWasSuccess) + { + return; + } + + if (!InviteResult.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("Invite accept returned no search result.")); + return; + } + + if (!UserId.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("Invite accept returned no user.")); + return; + } + + UShooterGameInstance* ShooterGameInstance = Cast(GetGameInstance()); + + if (ShooterGameInstance) + { + FShooterPendingInvite PendingInvite; + + // Set the pending invite, and then go to the initial screen, which is where we will process it + PendingInvite.ControllerId = ControllerId; + PendingInvite.UserId = UserId; + PendingInvite.InviteResult = InviteResult; + PendingInvite.bPrivilegesCheckedAndAllowed = false; + + ShooterGameInstance->SetPendingInvite(PendingInvite); + ShooterGameInstance->GotoState(ShooterGameInstanceState::PendingInvite); + } +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Online/ShooterPlayerState.cpp b/Source/ShooterGame/Private/Online/ShooterPlayerState.cpp new file mode 100644 index 0000000..6e6424b --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterPlayerState.cpp @@ -0,0 +1,224 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterPlayerState.h" +#include "Net/OnlineEngineInterface.h" + +AShooterPlayerState::AShooterPlayerState(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + TeamNumber = 0; + NumKills = 0; + NumDeaths = 0; + NumBulletsFired = 0; + NumRocketsFired = 0; + bQuitter = false; +} + +void AShooterPlayerState::Reset() +{ + Super::Reset(); + + //PlayerStates persist across seamless travel. Keep the same teams as previous match. + //SetTeamNum(0); + NumKills = 0; + NumDeaths = 0; + NumBulletsFired = 0; + NumRocketsFired = 0; + bQuitter = false; +} + +void AShooterPlayerState::RegisterPlayerWithSession(bool bWasFromInvite) +{ + if (UOnlineEngineInterface::Get()->DoesSessionExist(GetWorld(), NAME_GameSession)) + { + Super::RegisterPlayerWithSession(bWasFromInvite); + } +} + +void AShooterPlayerState::UnregisterPlayerWithSession() +{ + if (!IsFromPreviousLevel() && UOnlineEngineInterface::Get()->DoesSessionExist(GetWorld(), NAME_GameSession)) + { + Super::UnregisterPlayerWithSession(); + } +} + +void AShooterPlayerState::ClientInitialize(AController* InController) +{ + Super::ClientInitialize(InController); + + UpdateTeamColors(); +} + +void AShooterPlayerState::SetTeamNum(int32 NewTeamNumber) +{ + TeamNumber = NewTeamNumber; + + UpdateTeamColors(); +} + +void AShooterPlayerState::OnRep_TeamColor() +{ + UpdateTeamColors(); +} + +void AShooterPlayerState::AddBulletsFired(int32 NumBullets) +{ + NumBulletsFired += NumBullets; +} + +void AShooterPlayerState::AddRocketsFired(int32 NumRockets) +{ + NumRocketsFired += NumRockets; +} + +void AShooterPlayerState::SetQuitter(bool bInQuitter) +{ + bQuitter = bInQuitter; +} + +void AShooterPlayerState::SetMatchId(const FString& CurrentMatchId) +{ + MatchId = CurrentMatchId; +} + +void AShooterPlayerState::CopyProperties(APlayerState* PlayerState) +{ + Super::CopyProperties(PlayerState); + + AShooterPlayerState* ShooterPlayer = Cast(PlayerState); + if (ShooterPlayer) + { + ShooterPlayer->TeamNumber = TeamNumber; + } +} + +void AShooterPlayerState::UpdateTeamColors() +{ + AController* OwnerController = Cast(GetOwner()); + if (OwnerController != NULL) + { + AShooterCharacter* ShooterCharacter = Cast(OwnerController->GetCharacter()); + if (ShooterCharacter != NULL) + { + ShooterCharacter->UpdateTeamColorsAllMIDs(); + } + } +} + +int32 AShooterPlayerState::GetTeamNum() const +{ + return TeamNumber; +} + +int32 AShooterPlayerState::GetKills() const +{ + return NumKills; +} + +int32 AShooterPlayerState::GetDeaths() const +{ + return NumDeaths; +} + +int32 AShooterPlayerState::GetNumBulletsFired() const +{ + return NumBulletsFired; +} + +int32 AShooterPlayerState::GetNumRocketsFired() const +{ + return NumRocketsFired; +} + +bool AShooterPlayerState::IsQuitter() const +{ + return bQuitter; +} + +FString AShooterPlayerState::GetMatchId() const +{ + return MatchId; +} + +void AShooterPlayerState::ScoreKill(AShooterPlayerState* Victim, int32 Points) +{ + NumKills++; + ScorePoints(Points); +} + +void AShooterPlayerState::ScoreDeath(AShooterPlayerState* KilledBy, int32 Points) +{ + NumDeaths++; + ScorePoints(Points); +} + +void AShooterPlayerState::ScorePoints(int32 Points) +{ + AShooterGameState* const MyGameState = GetWorld()->GetGameState(); + if (MyGameState && TeamNumber >= 0) + { + if (TeamNumber >= MyGameState->TeamScores.Num()) + { + MyGameState->TeamScores.AddZeroed(TeamNumber - MyGameState->TeamScores.Num() + 1); + } + + MyGameState->TeamScores[TeamNumber] += Points; + } + + SetScore(GetScore() + Points); +} + +void AShooterPlayerState::InformAboutKill_Implementation(class AShooterPlayerState* KillerPlayerState, const UDamageType* KillerDamageType, class AShooterPlayerState* KilledPlayerState) +{ + //id can be null for bots + if (KillerPlayerState->GetUniqueId().IsValid()) + { + //search for the actual killer before calling OnKill() + for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It) + { + AShooterPlayerController* TestPC = Cast(*It); + if (TestPC && TestPC->IsLocalController()) + { + // a local player might not have an ID if it was created with CreateDebugPlayer. + ULocalPlayer* LocalPlayer = Cast(TestPC->Player); + FUniqueNetIdRepl LocalID = LocalPlayer->GetCachedUniqueNetId(); + if (LocalID.IsValid() && *LocalPlayer->GetCachedUniqueNetId() == *KillerPlayerState->GetUniqueId()) + { + TestPC->OnKill(); + } + } + } + } +} + +void AShooterPlayerState::BroadcastDeath_Implementation(class AShooterPlayerState* KillerPlayerState, const UDamageType* KillerDamageType, class AShooterPlayerState* KilledPlayerState) +{ + for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It) + { + // all local players get death messages so they can update their huds. + AShooterPlayerController* TestPC = Cast(*It); + if (TestPC && TestPC->IsLocalController()) + { + TestPC->OnDeathMessage(KillerPlayerState, this, KillerDamageType); + } + } +} + +void AShooterPlayerState::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const +{ + Super::GetLifetimeReplicatedProps( OutLifetimeProps ); + + DOREPLIFETIME( AShooterPlayerState, TeamNumber ); + DOREPLIFETIME( AShooterPlayerState, NumKills ); + DOREPLIFETIME( AShooterPlayerState, NumDeaths ); +} + +FString AShooterPlayerState::GetShortPlayerName() const +{ + if( GetPlayerName().Len() > MAX_PLAYER_NAME_LENGTH ) + { + return GetPlayerName().Left(MAX_PLAYER_NAME_LENGTH) + "..."; + } + return GetPlayerName(); +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Online/ShooterReplicationGraph.cpp b/Source/ShooterGame/Private/Online/ShooterReplicationGraph.cpp new file mode 100644 index 0000000..96d344a --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterReplicationGraph.cpp @@ -0,0 +1,873 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +/** +* +* ===================== ShooterReplicationGraph Replication ===================== +* +* Overview +* +* This changes the way actor relevancy works. AActor::IsNetRelevantFor is NOT used in this system! +* +* Instead, The UShooterReplicationGraph contains UReplicationGraphNodes. These nodes are responsible for generating lists of actors to replicate for each connection. +* Most of these lists are persistent across frames. This enables most of the gathering work ("which actors should be considered for replication) to be shared/reused. +* Nodes may be global (used by all connections), connection specific (each connection gets its own node), or shared (e.g, teams: all connections on the same team share). +* Actors can be in multiple nodes! For example a pawn may be in the spatialization node but also in the always-relevant-for-team node. It will be returned twice for +* teammates. This is ok though should be minimized when possible. +* +* UShooterReplicationGraph is intended to not be directly used by the game code. That is, you should not have to include ShooterReplicationGraph.h anywhere else. +* Rather, UShooterReplicationGraph depends on the game code and registers for events that the game code broadcasts (e.g., events for players joining/leaving teams). +* This choice was made because it gives UShooterReplicationGraph a complete holistic view of actor replication. Rather than exposing generic public functions that any +* place in game code can invoke, all notifications are explicitly registered in UShooterReplicationGraph::InitGlobalActorClassSettings. +* +* ShooterGame Nodes +* +* These are the top level nodes currently used: +* +* UReplicationGraphNode_GridSpatialization2D: +* This is the spatialization node. All "distance based relevant" actors will be routed here. This node divides the map into a 2D grid. Each cell in the grid contains +* children nodes that hold lists of actors based on how they update/go dormant. Actors are put in multiple cells. Connections pull from the single cell they are in. +* +* UReplicationGraphNode_ActorList +* This is an actor list node that contains the always relevant actors. These actors are always relevant to every connection. +* +* UShooterReplicationGraphNode_AlwaysRelevant_ForConnection +* This is the node for connection specific always relevant actors. This node does not maintain a persistent list but builds it each frame. This is possible because (currently) +* these actors are all easily accessed from the PlayerController. A persistent list would require notifications to be broadcast when these actors change, which would be possible +* but currently not necessary. +* +* UShooterReplicationGraphNode_PlayerStateFrequencyLimiter +* A custom node for handling player state replication. This replicates a small rolling set of player states (currently 2/frame). This is so player states replicate +* to simulated connections at a low, steady frequency, and to take advantage of serialization sharing. Auto proxy player states are replicated at higher frequency (to the +* owning connection only) via UShooterReplicationGraphNode_AlwaysRelevant_ForConnection. +* +* UReplicationGraphNode_TearOff_ForConnection +* Connection specific node for handling tear off actors. This is created and managed in the base implementation of Replication Graph. +* +* Dependent Actors (AShooterWeapon) +* +* Replication Graph introduces a concept of dependent actor replication. This is an actor (AShooterWeapon) that only replicates when another actor replicates (Pawn). I.e, the weapon +* actor itself never goes into the Replication Graph. It is never gathered on its own and never prioritized. It just has a chance to replicate when the Pawn replicates. This keeps +* the graph leaner since no extra work has to be done for the weapon actors. +* +* See UShooterReplicationGraph::OnCharacterWeaponChange: this is how actors are added/removed from the dependent actor list. +* +* How To Use +* +* Making something always relevant: Please avoid if you can :) If you must, just setting AActor::bAlwaysRelevant = true in the class defaults will do it. +* +* Making something always relevant to connection: You will need to modify UShooterReplicationGraphNode_AlwaysRelevant_ForConnection::GatherActorListsForConnection. You will also want +* to make sure the actor does not get put in one of the other nodes. The safest way to do this is by setting its EClassRepNodeMapping to NotRouted in UShooterReplicationGraph::InitGlobalActorClassSettings. +* +* How To Debug +* +* Its a good idea to just disable rep graph to see if your problem is specific to this system or just general replication/game play problem. +* +* If it is replication graph related, there are several useful commands that can be used: see ReplicationGraph_Debugging.cpp. The most useful are below. Use the 'cheat' command to run these on the server from a client. +* +* "Net.RepGraph.PrintGraph" - this will print the graph to the log: each node and actor. +* "Net.RepGraph.PrintGraph class" - same as above but will group by class. +* "Net.RepGraph.PrintGraph nclass" - same as above but will group by native classes (hides blueprint noise) +* +* Net.RepGraph.PrintAll <"Class"/"Nclass"> - will print the entire graph, the gathered actors, and how they were prioritized for a given connection for X amount of frames. +* +* Net.RepGraph.PrintAllActorInfo - will print the class, global, and connection replication info associated with an actor/class. If MatchString is empty will print everything. Call directly from client. +* +* ShooterRepGraph.PrintRouting - will print the EClassRepNodeMapping for each class. That is, how a given actor class is routed (or not) in the Replication Graph. +* +*/ + +#include "ShooterGame.h" +#include "ShooterReplicationGraph.h" + +#include "Net/UnrealNetwork.h" +#include "Engine/LevelStreaming.h" +#include "EngineUtils.h" +#include "CoreGlobals.h" + +#if WITH_GAMEPLAY_DEBUGGER +#include "GameplayDebuggerCategoryReplicator.h" +#endif + +#include "GameFramework/GameModeBase.h" +#include "GameFramework/GameState.h" +#include "GameFramework/PlayerState.h" +#include "GameFramework/Pawn.h" +#include "Engine/LevelScriptActor.h" +#include "Player/ShooterCharacter.h" +#include "Online/ShooterPlayerState.h" +#include "Weapons/ShooterWeapon.h" +#include "Pickups/ShooterPickup.h" + +DEFINE_LOG_CATEGORY( LogShooterReplicationGraph ); + +float CVar_ShooterRepGraph_DestructionInfoMaxDist = 30000.f; +static FAutoConsoleVariableRef CVarShooterRepGraphDestructMaxDist(TEXT("ShooterRepGraph.DestructInfo.MaxDist"), CVar_ShooterRepGraph_DestructionInfoMaxDist, TEXT("Max distance (not squared) to rep destruct infos at"), ECVF_Default ); + +int32 CVar_ShooterRepGraph_DisplayClientLevelStreaming = 0; +static FAutoConsoleVariableRef CVarShooterRepGraphDisplayClientLevelStreaming(TEXT("ShooterRepGraph.DisplayClientLevelStreaming"), CVar_ShooterRepGraph_DisplayClientLevelStreaming, TEXT(""), ECVF_Default ); + +float CVar_ShooterRepGraph_CellSize = 10000.f; +static FAutoConsoleVariableRef CVarShooterRepGraphCellSize(TEXT("ShooterRepGraph.CellSize"), CVar_ShooterRepGraph_CellSize, TEXT(""), ECVF_Default ); + +// Essentially "Min X" for replication. This is just an initial value. The system will reset itself if actors appears outside of this. +float CVar_ShooterRepGraph_SpatialBiasX = -150000.f; +static FAutoConsoleVariableRef CVarShooterRepGraphSpatialBiasX(TEXT("ShooterRepGraph.SpatialBiasX"), CVar_ShooterRepGraph_SpatialBiasX, TEXT(""), ECVF_Default ); + +// Essentially "Min Y" for replication. This is just an initial value. The system will reset itself if actors appears outside of this. +float CVar_ShooterRepGraph_SpatialBiasY = -200000.f; +static FAutoConsoleVariableRef CVarShooterRepSpatialBiasY(TEXT("ShooterRepGraph.SpatialBiasY"), CVar_ShooterRepGraph_SpatialBiasY, TEXT(""), ECVF_Default ); + +// How many buckets to spread dynamic, spatialized actors across. High number = more buckets = smaller effective replication frequency. This happens before individual actors do their own NetUpdateFrequency check. +int32 CVar_ShooterRepGraph_DynamicActorFrequencyBuckets = 3; +static FAutoConsoleVariableRef CVarShooterRepDynamicActorFrequencyBuckets(TEXT("ShooterRepGraph.DynamicActorFrequencyBuckets"), CVar_ShooterRepGraph_DynamicActorFrequencyBuckets, TEXT(""), ECVF_Default ); + +int32 CVar_ShooterRepGraph_DisableSpatialRebuilds = 1; +static FAutoConsoleVariableRef CVarShooterRepDisableSpatialRebuilds(TEXT("ShooterRepGraph.DisableSpatialRebuilds"), CVar_ShooterRepGraph_DisableSpatialRebuilds, TEXT(""), ECVF_Default ); + +// ---------------------------------------------------------------------------------------------------------- + + +UShooterReplicationGraph::UShooterReplicationGraph() +{ +} + +void InitClassReplicationInfo(FClassReplicationInfo& Info, UClass* Class, bool bSpatialize, float ServerMaxTickRate) +{ + AActor* CDO = Class->GetDefaultObject(); + if (bSpatialize) + { + Info.SetCullDistanceSquared(CDO->NetCullDistanceSquared); + UE_LOG(LogShooterReplicationGraph, Log, TEXT("Setting cull distance for %s to %f (%f)"), *Class->GetName(), Info.GetCullDistanceSquared(), Info.GetCullDistance()); + } + + Info.ReplicationPeriodFrame = FMath::Max( (uint32)FMath::RoundToFloat(ServerMaxTickRate / CDO->NetUpdateFrequency), 1); + + UClass* NativeClass = Class; + while(!NativeClass->IsNative() && NativeClass->GetSuperClass() && NativeClass->GetSuperClass() != AActor::StaticClass()) + { + NativeClass = NativeClass->GetSuperClass(); + } + + UE_LOG(LogShooterReplicationGraph, Log, TEXT("Setting replication period for %s (%s) to %d frames (%.2f)"), *Class->GetName(), *NativeClass->GetName(), Info.ReplicationPeriodFrame, CDO->NetUpdateFrequency); +} + +void UShooterReplicationGraph::ResetGameWorldState() +{ + Super::ResetGameWorldState(); + + AlwaysRelevantStreamingLevelActors.Empty(); + + for (UNetReplicationGraphConnection* ConnManager : Connections) + { + for (UReplicationGraphNode* ConnectionNode : ConnManager->GetConnectionGraphNodes()) + { + if (UShooterReplicationGraphNode_AlwaysRelevant_ForConnection* AlwaysRelevantConnectionNode = Cast(ConnectionNode)) + { + AlwaysRelevantConnectionNode->ResetGameWorldState(); + } + } + } + + for (UNetReplicationGraphConnection* ConnManager : PendingConnections) + { + for (UReplicationGraphNode* ConnectionNode : ConnManager->GetConnectionGraphNodes()) + { + if (UShooterReplicationGraphNode_AlwaysRelevant_ForConnection* AlwaysRelevantConnectionNode = Cast(ConnectionNode)) + { + AlwaysRelevantConnectionNode->ResetGameWorldState(); + } + } + } +} + +void UShooterReplicationGraph::InitGlobalActorClassSettings() +{ + Super::InitGlobalActorClassSettings(); + + // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + // Programatically build the rules. + // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + auto AddInfo = [&]( UClass* Class, EClassRepNodeMapping Mapping) { ClassRepNodePolicies.Set(Class, Mapping); }; + + AddInfo( AShooterWeapon::StaticClass(), EClassRepNodeMapping::NotRouted); // Handled via DependantActor replication (Pawn) + AddInfo( ALevelScriptActor::StaticClass(), EClassRepNodeMapping::NotRouted); // Not needed + AddInfo( APlayerState::StaticClass(), EClassRepNodeMapping::NotRouted); // Special cased via UShooterReplicationGraphNode_PlayerStateFrequencyLimiter + AddInfo( AReplicationGraphDebugActor::StaticClass(), EClassRepNodeMapping::NotRouted); // Not needed. Replicated special case inside RepGraph + AddInfo( AInfo::StaticClass(), EClassRepNodeMapping::RelevantAllConnections); // Non spatialized, relevant to all + AddInfo( AShooterPickup::StaticClass(), EClassRepNodeMapping::Spatialize_Static); // Spatialized and never moves. Routes to GridNode. + +#if WITH_GAMEPLAY_DEBUGGER + AddInfo( AGameplayDebuggerCategoryReplicator::StaticClass(), EClassRepNodeMapping::NotRouted); // Replicated via UShooterReplicationGraphNode_AlwaysRelevant_ForConnection +#endif + + TArray AllReplicatedClasses; + + for (TObjectIterator It; It; ++It) + { + UClass* Class = *It; + AActor* ActorCDO = Cast(Class->GetDefaultObject()); + if (!ActorCDO || !ActorCDO->GetIsReplicated()) + { + continue; + } + + // Skip SKEL and REINST classes. + if (Class->GetName().StartsWith(TEXT("SKEL_")) || Class->GetName().StartsWith(TEXT("REINST_"))) + { + continue; + } + + // -------------------------------------------------------------------- + // This is a replicated class. Save this off for the second pass below + // -------------------------------------------------------------------- + + AllReplicatedClasses.Add(Class); + + // Skip if already in the map (added explicitly) + if (ClassRepNodePolicies.Contains(Class, false)) + { + continue; + } + + auto ShouldSpatialize = [](const AActor* CDO) + { + return CDO->GetIsReplicated() && (!(CDO->bAlwaysRelevant || CDO->bOnlyRelevantToOwner || CDO->bNetUseOwnerRelevancy)); + }; + + auto GetLegacyDebugStr = [](const AActor* CDO) + { + return FString::Printf(TEXT("%s [%d/%d/%d]"), *CDO->GetClass()->GetName(), CDO->bAlwaysRelevant, CDO->bOnlyRelevantToOwner, CDO->bNetUseOwnerRelevancy); + }; + + // Only handle this class if it differs from its super. There is no need to put every child class explicitly in the graph class mapping + UClass* SuperClass = Class->GetSuperClass(); + if (AActor* SuperCDO = Cast(SuperClass->GetDefaultObject())) + { + if ( SuperCDO->GetIsReplicated() == ActorCDO->GetIsReplicated() + && SuperCDO->bAlwaysRelevant == ActorCDO->bAlwaysRelevant + && SuperCDO->bOnlyRelevantToOwner == ActorCDO->bOnlyRelevantToOwner + && SuperCDO->bNetUseOwnerRelevancy == ActorCDO->bNetUseOwnerRelevancy + ) + { + continue; + } + + if (ShouldSpatialize(ActorCDO) == false && ShouldSpatialize(SuperCDO) == true) + { + UE_LOG(LogShooterReplicationGraph, Log, TEXT("Adding %s to NonSpatializedChildClasses. (Parent: %s)"), *GetLegacyDebugStr(ActorCDO), *GetLegacyDebugStr(SuperCDO)); + NonSpatializedChildClasses.Add(Class); + } + } + + if (ShouldSpatialize(ActorCDO)) + { + AddInfo(Class, EClassRepNodeMapping::Spatialize_Dynamic); + } + else if (ActorCDO->bAlwaysRelevant && !ActorCDO->bOnlyRelevantToOwner) + { + AddInfo(Class, EClassRepNodeMapping::RelevantAllConnections); + } + } + + // ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + // Setup FClassReplicationInfo. This is essentially the per class replication settings. Some we set explicitly, the rest we are setting via looking at the legacy settings on AActor. + // ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + TArray ExplicitlySetClasses; + auto SetClassInfo = [&](UClass* Class, const FClassReplicationInfo& Info) { GlobalActorReplicationInfoMap.SetClassInfo(Class, Info); ExplicitlySetClasses.Add(Class); }; + + FClassReplicationInfo PawnClassRepInfo; + PawnClassRepInfo.DistancePriorityScale = 1.f; + PawnClassRepInfo.StarvationPriorityScale = 1.f; + PawnClassRepInfo.ActorChannelFrameTimeout = 4; + PawnClassRepInfo.SetCullDistanceSquared(15000.f * 15000.f); // Yuck + SetClassInfo( APawn::StaticClass(), PawnClassRepInfo ); + + FClassReplicationInfo PlayerStateRepInfo; + PlayerStateRepInfo.DistancePriorityScale = 0.f; + PlayerStateRepInfo.ActorChannelFrameTimeout = 0; + SetClassInfo( APlayerState::StaticClass(), PlayerStateRepInfo ); + + UReplicationGraphNode_ActorListFrequencyBuckets::DefaultSettings.ListSize = 12; + + // Set FClassReplicationInfo based on legacy settings from all replicated classes + for (UClass* ReplicatedClass : AllReplicatedClasses) + { + if (ExplicitlySetClasses.FindByPredicate([&](const UClass* SetClass) { return ReplicatedClass->IsChildOf(SetClass); }) != nullptr) + { + continue; + } + + const bool bClassIsSpatialized = IsSpatialized(ClassRepNodePolicies.GetChecked(ReplicatedClass)); + + FClassReplicationInfo ClassInfo; + InitClassReplicationInfo(ClassInfo, ReplicatedClass, bClassIsSpatialized, NetDriver->NetServerMaxTickRate); + GlobalActorReplicationInfoMap.SetClassInfo( ReplicatedClass, ClassInfo ); + } + + + // Print out what we came up with + UE_LOG(LogShooterReplicationGraph, Log, TEXT("")); + UE_LOG(LogShooterReplicationGraph, Log, TEXT("Class Routing Map: ")); + UEnum* Enum = StaticEnum(); + for (auto ClassMapIt = ClassRepNodePolicies.CreateIterator(); ClassMapIt; ++ClassMapIt) + { + UClass* Class = CastChecked(ClassMapIt.Key().ResolveObjectPtr()); + const EClassRepNodeMapping Mapping = ClassMapIt.Value(); + + // Only print if different than native class + UClass* ParentNativeClass = GetParentNativeClass(Class); + const EClassRepNodeMapping* ParentMapping = ClassRepNodePolicies.Get(ParentNativeClass); + if (ParentMapping && Class != ParentNativeClass && Mapping == *ParentMapping) + { + continue; + } + + UE_LOG(LogShooterReplicationGraph, Log, TEXT(" %s (%s) -> %s"), *Class->GetName(), *GetNameSafe(ParentNativeClass), *Enum->GetNameStringByValue(static_cast(Mapping))); + } + + UE_LOG(LogShooterReplicationGraph, Log, TEXT("")); + UE_LOG(LogShooterReplicationGraph, Log, TEXT("Class Settings Map: ")); + FClassReplicationInfo DefaultValues; + for (auto ClassRepInfoIt = GlobalActorReplicationInfoMap.CreateClassMapIterator(); ClassRepInfoIt; ++ClassRepInfoIt) + { + UClass* Class = CastChecked(ClassRepInfoIt.Key().ResolveObjectPtr()); + const FClassReplicationInfo& ClassInfo = ClassRepInfoIt.Value(); + UE_LOG(LogShooterReplicationGraph, Log, TEXT(" %s (%s) -> %s"), *Class->GetName(), *GetNameSafe(GetParentNativeClass(Class)), *ClassInfo.BuildDebugStringDelta()); + } + + + // Rep destruct infos based on CVar value + DestructInfoMaxDistanceSquared = CVar_ShooterRepGraph_DestructionInfoMaxDist * CVar_ShooterRepGraph_DestructionInfoMaxDist; + + // ------------------------------------------------------- + // Register for game code callbacks. + // This could have been done the other way: E.g, AMyGameActor could do GetNetDriver()->GetReplicationDriver()->OnMyGameEvent etc. + // This way at least keeps the rep graph out of game code directly and allows rep graph to exist in its own module + // So for now, erring on the side of a cleaning dependencies between classes. + // ------------------------------------------------------- + + AShooterCharacter::NotifyEquipWeapon.AddUObject(this, &UShooterReplicationGraph::OnCharacterEquipWeapon); + AShooterCharacter::NotifyUnEquipWeapon.AddUObject(this, &UShooterReplicationGraph::OnCharacterUnEquipWeapon); + +#if WITH_GAMEPLAY_DEBUGGER + AGameplayDebuggerCategoryReplicator::NotifyDebuggerOwnerChange.AddUObject(this, &UShooterReplicationGraph::OnGameplayDebuggerOwnerChange); +#endif +} + +void UShooterReplicationGraph::InitGlobalGraphNodes() +{ + // Preallocate some replication lists. + PreAllocateRepList(3, 12); + PreAllocateRepList(6, 12); + PreAllocateRepList(128, 64); + PreAllocateRepList(512, 16); + + // ----------------------------------------------- + // Spatial Actors + // ----------------------------------------------- + + GridNode = CreateNewNode(); + GridNode->CellSize = CVar_ShooterRepGraph_CellSize; + GridNode->SpatialBias = FVector2D(CVar_ShooterRepGraph_SpatialBiasX, CVar_ShooterRepGraph_SpatialBiasY); + + if (CVar_ShooterRepGraph_DisableSpatialRebuilds) + { + GridNode->AddSpatialRebuildBlacklistClass(AActor::StaticClass()); // Disable All spatial rebuilding + } + + AddGlobalGraphNode(GridNode); + + // ----------------------------------------------- + // Always Relevant (to everyone) Actors + // ----------------------------------------------- + AlwaysRelevantNode = CreateNewNode(); + AddGlobalGraphNode(AlwaysRelevantNode); + + // ----------------------------------------------- + // Player State specialization. This will return a rolling subset of the player states to replicate + // ----------------------------------------------- + UShooterReplicationGraphNode_PlayerStateFrequencyLimiter* PlayerStateNode = CreateNewNode(); + AddGlobalGraphNode(PlayerStateNode); +} + +void UShooterReplicationGraph::InitConnectionGraphNodes(UNetReplicationGraphConnection* RepGraphConnection) +{ + Super::InitConnectionGraphNodes(RepGraphConnection); + + UShooterReplicationGraphNode_AlwaysRelevant_ForConnection* AlwaysRelevantConnectionNode = CreateNewNode(); + + // This node needs to know when client levels go in and out of visibility + RepGraphConnection->OnClientVisibleLevelNameAdd.AddUObject(AlwaysRelevantConnectionNode, &UShooterReplicationGraphNode_AlwaysRelevant_ForConnection::OnClientLevelVisibilityAdd); + RepGraphConnection->OnClientVisibleLevelNameRemove.AddUObject(AlwaysRelevantConnectionNode, &UShooterReplicationGraphNode_AlwaysRelevant_ForConnection::OnClientLevelVisibilityRemove); + + AddConnectionGraphNode(AlwaysRelevantConnectionNode, RepGraphConnection); +} + +EClassRepNodeMapping UShooterReplicationGraph::GetMappingPolicy(UClass* Class) +{ + EClassRepNodeMapping* PolicyPtr = ClassRepNodePolicies.Get(Class); + EClassRepNodeMapping Policy = PolicyPtr ? *PolicyPtr : EClassRepNodeMapping::NotRouted; + return Policy; +} + +void UShooterReplicationGraph::RouteAddNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo, FGlobalActorReplicationInfo& GlobalInfo) +{ + EClassRepNodeMapping Policy = GetMappingPolicy(ActorInfo.Class); + switch(Policy) + { + case EClassRepNodeMapping::NotRouted: + { + break; + } + + case EClassRepNodeMapping::RelevantAllConnections: + { + if (ActorInfo.StreamingLevelName == NAME_None) + { + AlwaysRelevantNode->NotifyAddNetworkActor(ActorInfo); + } + else + { + FActorRepListRefView& RepList = AlwaysRelevantStreamingLevelActors.FindOrAdd(ActorInfo.StreamingLevelName); + RepList.PrepareForWrite(); + RepList.ConditionalAdd(ActorInfo.Actor); + } + break; + } + + case EClassRepNodeMapping::Spatialize_Static: + { + GridNode->AddActor_Static(ActorInfo, GlobalInfo); + break; + } + + case EClassRepNodeMapping::Spatialize_Dynamic: + { + GridNode->AddActor_Dynamic(ActorInfo, GlobalInfo); + break; + } + + case EClassRepNodeMapping::Spatialize_Dormancy: + { + GridNode->AddActor_Dormancy(ActorInfo, GlobalInfo); + break; + } + }; +} + +void UShooterReplicationGraph::RouteRemoveNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo) +{ + EClassRepNodeMapping Policy = GetMappingPolicy(ActorInfo.Class); + switch(Policy) + { + case EClassRepNodeMapping::NotRouted: + { + break; + } + + case EClassRepNodeMapping::RelevantAllConnections: + { + if (ActorInfo.StreamingLevelName == NAME_None) + { + AlwaysRelevantNode->NotifyRemoveNetworkActor(ActorInfo); + } + else + { + FActorRepListRefView& RepList = AlwaysRelevantStreamingLevelActors.FindChecked(ActorInfo.StreamingLevelName); + if (RepList.Remove(ActorInfo.Actor) == false) + { + UE_LOG(LogShooterReplicationGraph, Warning, TEXT("Actor %s was not found in AlwaysRelevantStreamingLevelActors list. LevelName: %s"), *GetActorRepListTypeDebugString(ActorInfo.Actor), *ActorInfo.StreamingLevelName.ToString()); + } + } + break; + } + + case EClassRepNodeMapping::Spatialize_Static: + { + GridNode->RemoveActor_Static(ActorInfo); + break; + } + + case EClassRepNodeMapping::Spatialize_Dynamic: + { + GridNode->RemoveActor_Dynamic(ActorInfo); + break; + } + + case EClassRepNodeMapping::Spatialize_Dormancy: + { + GridNode->RemoveActor_Dormancy(ActorInfo); + break; + } + }; +} + +// Since we listen to global (static) events, we need to watch out for cross world broadcasts (PIE) +#if WITH_EDITOR +#define CHECK_WORLDS(X) if(X->GetWorld() != GetWorld()) return; +#else +#define CHECK_WORLDS(X) +#endif + +void UShooterReplicationGraph::OnCharacterEquipWeapon(AShooterCharacter* Character, AShooterWeapon* NewWeapon) +{ + if (Character && NewWeapon) + { + CHECK_WORLDS(Character); + + GlobalActorReplicationInfoMap.AddDependentActor(Character, NewWeapon); + } +} + +void UShooterReplicationGraph::OnCharacterUnEquipWeapon(AShooterCharacter* Character, AShooterWeapon* OldWeapon) +{ + if (Character && OldWeapon) + { + CHECK_WORLDS(Character); + + GlobalActorReplicationInfoMap.RemoveDependentActor(Character, OldWeapon); + } +} + +#if WITH_GAMEPLAY_DEBUGGER +void UShooterReplicationGraph::OnGameplayDebuggerOwnerChange(AGameplayDebuggerCategoryReplicator* Debugger, APlayerController* OldOwner) +{ + auto GetAlwaysRelevantForConnectionNode = [&](APlayerController* Controller) -> UShooterReplicationGraphNode_AlwaysRelevant_ForConnection* + { + if (OldOwner) + { + if (UNetConnection* NetConnection = OldOwner->GetNetConnection()) + { + if (UNetReplicationGraphConnection* GraphConnection = FindOrAddConnectionManager(NetConnection)) + { + for (UReplicationGraphNode* ConnectionNode : GraphConnection->GetConnectionGraphNodes()) + { + if (UShooterReplicationGraphNode_AlwaysRelevant_ForConnection* AlwaysRelevantConnectionNode = Cast(ConnectionNode)) + { + return AlwaysRelevantConnectionNode; + } + } + + } + } + } + + return nullptr; + }; + + if (UShooterReplicationGraphNode_AlwaysRelevant_ForConnection* AlwaysRelevantConnectionNode = GetAlwaysRelevantForConnectionNode(OldOwner)) + { + AlwaysRelevantConnectionNode->GameplayDebugger = nullptr; + } + + if (UShooterReplicationGraphNode_AlwaysRelevant_ForConnection* AlwaysRelevantConnectionNode = GetAlwaysRelevantForConnectionNode(Debugger->GetReplicationOwner())) + { + AlwaysRelevantConnectionNode->GameplayDebugger = Debugger; + } +} +#endif + +// ------------------------------------------------------------------------------ + +void UShooterReplicationGraphNode_AlwaysRelevant_ForConnection::ResetGameWorldState() +{ + AlwaysRelevantStreamingLevelsNeedingReplication.Empty(); +} + +void UShooterReplicationGraphNode_AlwaysRelevant_ForConnection::GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) +{ + QUICK_SCOPE_CYCLE_COUNTER( UShooterReplicationGraphNode_AlwaysRelevant_ForConnection_GatherActorListsForConnection ); + + UShooterReplicationGraph* ShooterGraph = CastChecked(GetOuter()); + + ReplicationActorList.Reset(); + + auto ResetActorCullDistance = [&](AActor* ActorToSet, AActor*& LastActor) { + + if (ActorToSet != LastActor) + { + LastActor = ActorToSet; + + UE_LOG(LogShooterReplicationGraph, Verbose, TEXT("Setting pawn cull distance to 0. %s"), *ActorToSet->GetName()); + FConnectionReplicationActorInfo& ConnectionActorInfo = Params.ConnectionManager.ActorInfoMap.FindOrAdd(ActorToSet); + ConnectionActorInfo.SetCullDistanceSquared(0.f); + } + }; + + for (const FNetViewer& CurViewer : Params.Viewers) + { + ReplicationActorList.ConditionalAdd(CurViewer.InViewer); + ReplicationActorList.ConditionalAdd(CurViewer.ViewTarget); + + if (AShooterPlayerController* PC = Cast(CurViewer.InViewer)) + { + // 50% throttling of PlayerStates. + const bool bReplicatePS = (Params.ConnectionManager.ConnectionOrderNum % 2) == (Params.ReplicationFrameNum % 2); + if (bReplicatePS) + { + // Always return the player state to the owning player. Simulated proxy player states are handled by UShooterReplicationGraphNode_PlayerStateFrequencyLimiter + if (APlayerState* PS = PC->PlayerState) + { + if (!bInitializedPlayerState) + { + bInitializedPlayerState = true; + FConnectionReplicationActorInfo& ConnectionActorInfo = Params.ConnectionManager.ActorInfoMap.FindOrAdd(PS); + ConnectionActorInfo.ReplicationPeriodFrame = 1; + } + + ReplicationActorList.ConditionalAdd(PS); + } + } + + FAlwaysRelevantActorInfo* LastData = PastRelevantActors.FindByKey(CurViewer.Connection); + + // We've not seen this actor before, go ahead and add them. + if (LastData == nullptr) + { + FAlwaysRelevantActorInfo NewActorInfo; + NewActorInfo.Connection = CurViewer.Connection; + LastData = &(PastRelevantActors[PastRelevantActors.Add(NewActorInfo)]); + } + + check(LastData != nullptr); + + if (AShooterCharacter* Pawn = Cast(PC->GetPawn())) + { + ResetActorCullDistance(Pawn, LastData->LastViewer); + + if (Pawn != CurViewer.ViewTarget) + { + ReplicationActorList.ConditionalAdd(Pawn); + } + + int32 InventoryCount = Pawn->GetInventoryCount(); + for (int32 i = 0; i < InventoryCount; ++i) + { + AShooterWeapon* Weapon = Pawn->GetInventoryWeapon(i); + if (Weapon) + { + ReplicationActorList.ConditionalAdd(Weapon); + } + } + } + + if (AShooterCharacter* ViewTargetPawn = Cast(CurViewer.ViewTarget)) + { + ResetActorCullDistance(ViewTargetPawn, LastData->LastViewTarget); + } + } + } + + PastRelevantActors.RemoveAll([&](FAlwaysRelevantActorInfo& RelActorInfo) { + return RelActorInfo.Connection == nullptr; + }); + + Params.OutGatheredReplicationLists.AddReplicationActorList(ReplicationActorList); + + // Always relevant streaming level actors. + FPerConnectionActorInfoMap& ConnectionActorInfoMap = Params.ConnectionManager.ActorInfoMap; + + TMap& AlwaysRelevantStreamingLevelActors = ShooterGraph->AlwaysRelevantStreamingLevelActors; + + for (int32 Idx=AlwaysRelevantStreamingLevelsNeedingReplication.Num()-1; Idx >= 0; --Idx) + { + const FName& StreamingLevel = AlwaysRelevantStreamingLevelsNeedingReplication[Idx]; + + FActorRepListRefView* Ptr = AlwaysRelevantStreamingLevelActors.Find(StreamingLevel); + if (Ptr == nullptr) + { + // No always relevant lists for that level + UE_CLOG(CVar_ShooterRepGraph_DisplayClientLevelStreaming > 0, LogShooterReplicationGraph, Display, TEXT("CLIENTSTREAMING Removing %s from AlwaysRelevantStreamingLevelActors because FActorRepListRefView is null. %s "), *StreamingLevel.ToString(), *Params.ConnectionManager.GetName()); + AlwaysRelevantStreamingLevelsNeedingReplication.RemoveAtSwap(Idx, 1, false); + continue; + } + + FActorRepListRefView& RepList = *Ptr; + + if (RepList.Num() > 0) + { + bool bAllDormant = true; + for (FActorRepListType Actor : RepList) + { + FConnectionReplicationActorInfo& ConnectionActorInfo = ConnectionActorInfoMap.FindOrAdd(Actor); + if (ConnectionActorInfo.bDormantOnConnection == false) + { + bAllDormant = false; + break; + } + } + + if (bAllDormant) + { + UE_CLOG(CVar_ShooterRepGraph_DisplayClientLevelStreaming > 0, LogShooterReplicationGraph, Display, TEXT("CLIENTSTREAMING All AlwaysRelevant Actors Dormant on StreamingLevel %s for %s. Removing list."), *StreamingLevel.ToString(), *Params.ConnectionManager.GetName()); + AlwaysRelevantStreamingLevelsNeedingReplication.RemoveAtSwap(Idx, 1, false); + } + else + { + UE_CLOG(CVar_ShooterRepGraph_DisplayClientLevelStreaming > 0, LogShooterReplicationGraph, Display, TEXT("CLIENTSTREAMING Adding always Actors on StreamingLevel %s for %s because it has at least one non dormant actor"), *StreamingLevel.ToString(), *Params.ConnectionManager.GetName()); + Params.OutGatheredReplicationLists.AddReplicationActorList(RepList); + } + } + else + { + UE_LOG(LogShooterReplicationGraph, Warning, TEXT("UShooterReplicationGraphNode_AlwaysRelevant_ForConnection::GatherActorListsForConnection - empty RepList %s"), *Params.ConnectionManager.GetName()); + } + + } + +#if WITH_GAMEPLAY_DEBUGGER + if (GameplayDebugger) + { + ReplicationActorList.ConditionalAdd(GameplayDebugger); + } +#endif +} + +void UShooterReplicationGraphNode_AlwaysRelevant_ForConnection::OnClientLevelVisibilityAdd(FName LevelName, UWorld* StreamingWorld) +{ + UE_CLOG(CVar_ShooterRepGraph_DisplayClientLevelStreaming > 0, LogShooterReplicationGraph, Display, TEXT("CLIENTSTREAMING ::OnClientLevelVisibilityAdd - %s"), *LevelName.ToString()); + AlwaysRelevantStreamingLevelsNeedingReplication.Add(LevelName); +} + +void UShooterReplicationGraphNode_AlwaysRelevant_ForConnection::OnClientLevelVisibilityRemove(FName LevelName) +{ + UE_CLOG(CVar_ShooterRepGraph_DisplayClientLevelStreaming > 0, LogShooterReplicationGraph, Display, TEXT("CLIENTSTREAMING ::OnClientLevelVisibilityRemove - %s"), *LevelName.ToString()); + AlwaysRelevantStreamingLevelsNeedingReplication.Remove(LevelName); +} + +void UShooterReplicationGraphNode_AlwaysRelevant_ForConnection::LogNode(FReplicationGraphDebugInfo& DebugInfo, const FString& NodeName) const +{ + DebugInfo.Log(NodeName); + DebugInfo.PushIndent(); + LogActorRepList(DebugInfo, NodeName, ReplicationActorList); + + for (const FName& LevelName : AlwaysRelevantStreamingLevelsNeedingReplication) + { + UShooterReplicationGraph* ShooterGraph = CastChecked(GetOuter()); + if (FActorRepListRefView* RepList = ShooterGraph->AlwaysRelevantStreamingLevelActors.Find(LevelName)) + { + LogActorRepList(DebugInfo, FString::Printf(TEXT("AlwaysRelevant StreamingLevel List: %s"), *LevelName.ToString()), *RepList); + } + } + + DebugInfo.PopIndent(); +} + +// ------------------------------------------------------------------------------ + +UShooterReplicationGraphNode_PlayerStateFrequencyLimiter::UShooterReplicationGraphNode_PlayerStateFrequencyLimiter() +{ + bRequiresPrepareForReplicationCall = true; +} + +void UShooterReplicationGraphNode_PlayerStateFrequencyLimiter::PrepareForReplication() +{ + QUICK_SCOPE_CYCLE_COUNTER( UShooterReplicationGraphNode_PlayerStateFrequencyLimiter_GlobalPrepareForReplication ); + + ReplicationActorLists.Reset(); + ForceNetUpdateReplicationActorList.Reset(); + + ReplicationActorLists.AddDefaulted(); + FActorRepListRefView* CurrentList = &ReplicationActorLists[0]; + CurrentList->PrepareForWrite(); + + // We rebuild our lists of player states each frame. This is not as efficient as it could be but its the simplest way + // to handle players disconnecting and keeping the lists compact. If the lists were persistent we would need to defrag them as players left. + + for (TActorIterator It(GetWorld()); It; ++It) + { + APlayerState* PS = *It; + if (IsActorValidForReplicationGather(PS) == false) + { + continue; + } + + if (CurrentList->Num() >= TargetActorsPerFrame) + { + ReplicationActorLists.AddDefaulted(); + CurrentList = &ReplicationActorLists.Last(); + CurrentList->PrepareForWrite(); + } + + CurrentList->Add(PS); + } +} + +void UShooterReplicationGraphNode_PlayerStateFrequencyLimiter::GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) +{ + const int32 ListIdx = Params.ReplicationFrameNum % ReplicationActorLists.Num(); + Params.OutGatheredReplicationLists.AddReplicationActorList(ReplicationActorLists[ListIdx]); + + if (ForceNetUpdateReplicationActorList.Num() > 0) + { + Params.OutGatheredReplicationLists.AddReplicationActorList(ForceNetUpdateReplicationActorList); + } +} + +void UShooterReplicationGraphNode_PlayerStateFrequencyLimiter::LogNode(FReplicationGraphDebugInfo& DebugInfo, const FString& NodeName) const +{ + DebugInfo.Log(NodeName); + DebugInfo.PushIndent(); + + int32 i=0; + for (const FActorRepListRefView& List : ReplicationActorLists) + { + LogActorRepList(DebugInfo, FString::Printf(TEXT("Bucket[%d]"), i++), List); + } + + DebugInfo.PopIndent(); +} + +// ------------------------------------------------------------------------------ + +void UShooterReplicationGraph::PrintRepNodePolicies() +{ + UEnum* Enum = StaticEnum(); + if (!Enum) + { + return; + } + + GLog->Logf(TEXT("====================================")); + GLog->Logf(TEXT("Shooter Replication Routing Policies")); + GLog->Logf(TEXT("====================================")); + + for (auto It = ClassRepNodePolicies.CreateIterator(); It; ++It) + { + FObjectKey ObjKey = It.Key(); + + EClassRepNodeMapping Mapping = It.Value(); + + GLog->Logf(TEXT("%-40s --> %s"), *GetNameSafe(ObjKey.ResolveObjectPtr()), *Enum->GetNameStringByValue(static_cast(Mapping))); + } +} + +FAutoConsoleCommandWithWorldAndArgs ShooterPrintRepNodePoliciesCmd(TEXT("ShooterRepGraph.PrintRouting"),TEXT("Prints how actor classes are routed to RepGraph nodes"), + FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray& Args, UWorld* World) + { + for (TObjectIterator It; It; ++It) + { + It->PrintRepNodePolicies(); + } + }) +); + +// ------------------------------------------------------------------------------ + +FAutoConsoleCommandWithWorldAndArgs ChangeFrequencyBucketsCmd(TEXT("ShooterRepGraph.FrequencyBuckets"), TEXT("Resets frequency bucket count."), FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray< FString >& Args, UWorld* World) +{ + int32 Buckets = 1; + if (Args.Num() > 0) + { + LexTryParseString(Buckets, *Args[0]); + } + + UE_LOG(LogShooterReplicationGraph, Display, TEXT("Setting Frequency Buckets to %d"), Buckets); + for (TObjectIterator It; It; ++It) + { + UReplicationGraphNode_ActorListFrequencyBuckets* Node = *It; + Node->SetNonStreamingCollectionSize(Buckets); + } +})); diff --git a/Source/ShooterGame/Private/Online/ShooterReplicationGraph.h b/Source/ShooterGame/Private/Online/ShooterReplicationGraph.h new file mode 100644 index 0000000..904d525 --- /dev/null +++ b/Source/ShooterGame/Private/Online/ShooterReplicationGraph.h @@ -0,0 +1,147 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "ReplicationGraph.h" +#include "ShooterReplicationGraph.generated.h" + +class AShooterCharacter; +class AShooterWeapon; +class UReplicationGraphNode_GridSpatialization2D; +class AGameplayDebuggerCategoryReplicator; + +DECLARE_LOG_CATEGORY_EXTERN( LogShooterReplicationGraph, Display, All ); + +// This is the main enum we use to route actors to the right replication node. Each class maps to one enum. +UENUM() +enum class EClassRepNodeMapping : uint32 +{ + NotRouted, // Doesn't map to any node. Used for special case actors that handled by special case nodes (UShooterReplicationGraphNode_PlayerStateFrequencyLimiter) + RelevantAllConnections, // Routes to an AlwaysRelevantNode or AlwaysRelevantStreamingLevelNode node + + // ONLY SPATIALIZED Enums below here! See UShooterReplicationGraph::IsSpatialized + + Spatialize_Static, // Routes to GridNode: these actors don't move and don't need to be updated every frame. + Spatialize_Dynamic, // Routes to GridNode: these actors mode frequently and are updated once per frame. + Spatialize_Dormancy, // Routes to GridNode: While dormant we treat as static. When flushed/not dormant dynamic. Note this is for things that "move while not dormant". +}; + +/** ShooterGame Replication Graph implementation. See additional notes in ShooterReplicationGraph.cpp! */ +UCLASS(transient, config=Engine) +class UShooterReplicationGraph :public UReplicationGraph +{ + GENERATED_BODY() + +public: + + UShooterReplicationGraph(); + + virtual void ResetGameWorldState() override; + + virtual void InitGlobalActorClassSettings() override; + virtual void InitGlobalGraphNodes() override; + virtual void InitConnectionGraphNodes(UNetReplicationGraphConnection* RepGraphConnection) override; + virtual void RouteAddNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo, FGlobalActorReplicationInfo& GlobalInfo) override; + virtual void RouteRemoveNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo) override; + + UPROPERTY() + TArray SpatializedClasses; + + UPROPERTY() + TArray NonSpatializedChildClasses; + + UPROPERTY() + TArray AlwaysRelevantClasses; + + UPROPERTY() + UReplicationGraphNode_GridSpatialization2D* GridNode; + + UPROPERTY() + UReplicationGraphNode_ActorList* AlwaysRelevantNode; + + TMap AlwaysRelevantStreamingLevelActors; + + void OnCharacterEquipWeapon(AShooterCharacter* Character, AShooterWeapon* NewWeapon); + void OnCharacterUnEquipWeapon(AShooterCharacter* Character, AShooterWeapon* OldWeapon); + +#if WITH_GAMEPLAY_DEBUGGER + void OnGameplayDebuggerOwnerChange(AGameplayDebuggerCategoryReplicator* Debugger, APlayerController* OldOwner); +#endif + + void PrintRepNodePolicies(); + +private: + + EClassRepNodeMapping GetMappingPolicy(UClass* Class); + + bool IsSpatialized(EClassRepNodeMapping Mapping) const { return Mapping >= EClassRepNodeMapping::Spatialize_Static; } + + TClassMap ClassRepNodePolicies; +}; + +UCLASS() +class UShooterReplicationGraphNode_AlwaysRelevant_ForConnection : public UReplicationGraphNode +{ + GENERATED_BODY() + +public: + + virtual void NotifyAddNetworkActor(const FNewReplicatedActorInfo& Actor) override { } + virtual bool NotifyRemoveNetworkActor(const FNewReplicatedActorInfo& ActorInfo, bool bWarnIfNotFound=true) override { return false; } + virtual void NotifyResetAllNetworkActors() override { } + + virtual void GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) override; + + virtual void LogNode(FReplicationGraphDebugInfo& DebugInfo, const FString& NodeName) const override; + + void OnClientLevelVisibilityAdd(FName LevelName, UWorld* StreamingWorld); + void OnClientLevelVisibilityRemove(FName LevelName); + + void ResetGameWorldState(); + +#if WITH_GAMEPLAY_DEBUGGER + AGameplayDebuggerCategoryReplicator* GameplayDebugger = nullptr; +#endif + +private: + + TArray > AlwaysRelevantStreamingLevelsNeedingReplication; + + FActorRepListRefView ReplicationActorList; + + UPROPERTY() + AActor* LastPawn = nullptr; + + /** List of previously (or currently if nothing changed last tick) focused actor data per connection */ + UPROPERTY() + TArray PastRelevantActors; + + bool bInitializedPlayerState = false; +}; + +/** This is a specialized node for handling PlayerState replication in a frequency limited fashion. It tracks all player states but only returns a subset of them to the replication driver each frame. */ +UCLASS() +class UShooterReplicationGraphNode_PlayerStateFrequencyLimiter : public UReplicationGraphNode +{ + GENERATED_BODY() + + UShooterReplicationGraphNode_PlayerStateFrequencyLimiter(); + + virtual void NotifyAddNetworkActor(const FNewReplicatedActorInfo& Actor) override { } + virtual bool NotifyRemoveNetworkActor(const FNewReplicatedActorInfo& ActorInfo, bool bWarnIfNotFound=true) override { return false; } + + virtual void GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) override; + + virtual void PrepareForReplication() override; + + virtual void LogNode(FReplicationGraphDebugInfo& DebugInfo, const FString& NodeName) const override; + + /** How many actors we want to return to the replication driver per frame. Will not suppress ForceNetUpdate. */ + int32 TargetActorsPerFrame = 2; + +private: + + TArray ReplicationActorLists; + FActorRepListRefView ForceNetUpdateReplicationActorList; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Private/Pickups/ShooterPickup.cpp b/Source/ShooterGame/Private/Pickups/ShooterPickup.cpp new file mode 100644 index 0000000..4ce7af3 --- /dev/null +++ b/Source/ShooterGame/Private/Pickups/ShooterPickup.cpp @@ -0,0 +1,156 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Pickups/ShooterPickup.h" +#include "Particles/ParticleSystemComponent.h" + +AShooterPickup::AShooterPickup(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + UCapsuleComponent* CollisionComp = ObjectInitializer.CreateDefaultSubobject(this, TEXT("CollisionComp")); + CollisionComp->InitCapsuleSize(40.0f, 50.0f); + CollisionComp->SetCollisionObjectType(COLLISION_PICKUP); + CollisionComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + CollisionComp->SetCollisionResponseToAllChannels(ECR_Ignore); + CollisionComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); + RootComponent = CollisionComp; + + PickupPSC = ObjectInitializer.CreateDefaultSubobject(this, TEXT("PickupFX")); + PickupPSC->bAutoActivate = false; + PickupPSC->bAutoDestroy = false; + PickupPSC->SetupAttachment(RootComponent); + + RespawnTime = 10.0f; + bIsActive = false; + PickedUpBy = NULL; + + SetRemoteRoleForBackwardsCompat(ROLE_SimulatedProxy); + bReplicates = true; +} + +void AShooterPickup::BeginPlay() +{ + Super::BeginPlay(); + + RespawnPickup(); + + // register on pickup list (server only), don't care about unregistering (in FinishDestroy) - no streaming + AShooterGameMode* GameMode = GetWorld()->GetAuthGameMode(); + if (GameMode) + { + GameMode->LevelPickups.Add(this); + } +} + +void AShooterPickup::NotifyActorBeginOverlap(class AActor* Other) +{ + Super::NotifyActorBeginOverlap(Other); + PickupOnTouch(Cast(Other)); +} + +bool AShooterPickup::CanBePickedUp(class AShooterCharacter* TestPawn) const +{ + return TestPawn && TestPawn->IsAlive(); +} + +void AShooterPickup::GivePickupTo(class AShooterCharacter* Pawn) +{ +} + +void AShooterPickup::PickupOnTouch(class AShooterCharacter* Pawn) +{ + if (bIsActive && Pawn && Pawn->IsAlive() && !IsPendingKill()) + { + if (CanBePickedUp(Pawn)) + { + GivePickupTo(Pawn); + PickedUpBy = Pawn; + + if (!IsPendingKill()) + { + bIsActive = false; + OnPickedUp(); + + if (RespawnTime > 0.0f) + { + GetWorldTimerManager().SetTimer(TimerHandle_RespawnPickup, this, &AShooterPickup::RespawnPickup, RespawnTime, false); + } + } + } + } +} + +void AShooterPickup::RespawnPickup() +{ + bIsActive = true; + PickedUpBy = NULL; + OnRespawned(); + + TSet OverlappingPawns; + GetOverlappingActors(OverlappingPawns, AShooterCharacter::StaticClass()); + + for (AActor* OverlappingPawn : OverlappingPawns) + { + PickupOnTouch(CastChecked(OverlappingPawn)); + } +} + +void AShooterPickup::OnPickedUp() +{ + if (RespawningFX) + { + PickupPSC->SetTemplate(RespawningFX); + PickupPSC->ActivateSystem(); + } + else + { + PickupPSC->DeactivateSystem(); + } + + if (PickupSound && PickedUpBy) + { + UGameplayStatics::SpawnSoundAttached(PickupSound, PickedUpBy->GetRootComponent()); + } + + OnPickedUpEvent(); +} + +void AShooterPickup::OnRespawned() +{ + if (ActiveFX) + { + PickupPSC->SetTemplate(ActiveFX); + PickupPSC->ActivateSystem(); + } + else + { + PickupPSC->DeactivateSystem(); + } + + const bool bJustSpawned = CreationTime <= (GetWorld()->GetTimeSeconds() + 5.0f); + if (RespawnSound && !bJustSpawned) + { + UGameplayStatics::PlaySoundAtLocation(this, RespawnSound, GetActorLocation()); + } + + OnRespawnEvent(); +} + +void AShooterPickup::OnRep_IsActive() +{ + if (bIsActive) + { + OnRespawned(); + } + else + { + OnPickedUp(); + } +} + +void AShooterPickup::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const +{ + Super::GetLifetimeReplicatedProps( OutLifetimeProps ); + + DOREPLIFETIME( AShooterPickup, bIsActive ); + DOREPLIFETIME( AShooterPickup, PickedUpBy ); +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Pickups/ShooterPickup_Ammo.cpp b/Source/ShooterGame/Private/Pickups/ShooterPickup_Ammo.cpp new file mode 100644 index 0000000..3bf639e --- /dev/null +++ b/Source/ShooterGame/Private/Pickups/ShooterPickup_Ammo.cpp @@ -0,0 +1,79 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Pickups/ShooterPickup_Ammo.h" +#include "Weapons/ShooterWeapon.h" +#include "OnlineSubsystemUtils.h" + +AShooterPickup_Ammo::AShooterPickup_Ammo(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + AmmoClips = 2; +} + +bool AShooterPickup_Ammo::IsForWeapon(UClass* WeaponClass) +{ + return WeaponType->IsChildOf(WeaponClass); +} + +bool AShooterPickup_Ammo::CanBePickedUp(AShooterCharacter* TestPawn) const +{ + AShooterWeapon* TestWeapon = (TestPawn ? TestPawn->FindWeapon(WeaponType) : NULL); + if (bIsActive && TestWeapon) + { + return TestWeapon->GetCurrentAmmo() < TestWeapon->GetMaxAmmo(); + } + + return false; +} + +void AShooterPickup_Ammo::GivePickupTo(class AShooterCharacter* Pawn) +{ + AShooterWeapon* Weapon = (Pawn ? Pawn->FindWeapon(WeaponType) : NULL); + if (Weapon) + { + int32 Qty = AmmoClips * Weapon->GetAmmoPerClip(); + Weapon->GiveAmmo(Qty); + + // Fire event for collected ammo + if (Pawn) + { + const UWorld* World = GetWorld(); + const IOnlineEventsPtr Events = Online::GetEventsInterface(World); + const IOnlineIdentityPtr Identity = Online::GetIdentityInterface(World); + + if (Events.IsValid() && Identity.IsValid()) + { + AShooterPlayerController* PC = Cast(Pawn->Controller); + if (PC) + { + ULocalPlayer* LocalPlayer = Cast(PC->Player); + + if (LocalPlayer) + { + const int32 UserIndex = LocalPlayer->GetControllerId(); + TSharedPtr UniqueID = Identity->GetUniquePlayerId(UserIndex); + if (UniqueID.IsValid()) + { + FVector Location = Pawn->GetActorLocation(); + + FOnlineEventParms Params; + + Params.Add( TEXT( "SectionId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "GameplayModeId" ), FVariantData( (int32)1 ) ); // @todo determine game mode (ffa v tdm) + Params.Add( TEXT( "DifficultyLevelId" ), FVariantData( (int32)0 ) ); // unused + + Params.Add( TEXT( "ItemId" ), FVariantData( (int32)Weapon->GetAmmoType() + 1 ) ); // @todo come up with a better way to determine item id, currently health is 0 and ammo counts from 1 + Params.Add( TEXT( "AcquisitionMethodId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "LocationX" ), FVariantData( Location.X ) ); + Params.Add( TEXT( "LocationY" ), FVariantData( Location.Y ) ); + Params.Add( TEXT( "LocationZ" ), FVariantData( Location.Z ) ); + Params.Add( TEXT( "ItemQty" ), FVariantData( (int32)Qty ) ); + + Events->TriggerEvent(*UniqueID, TEXT("CollectPowerup"), Params); + } + } + } + } + } + } +} diff --git a/Source/ShooterGame/Private/Pickups/ShooterPickup_Health.cpp b/Source/ShooterGame/Private/Pickups/ShooterPickup_Health.cpp new file mode 100644 index 0000000..fcbec54 --- /dev/null +++ b/Source/ShooterGame/Private/Pickups/ShooterPickup_Health.cpp @@ -0,0 +1,62 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Pickups/ShooterPickup_Health.h" +#include "OnlineSubsystemUtils.h" + +AShooterPickup_Health::AShooterPickup_Health(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + Health = 50; +} + +bool AShooterPickup_Health::CanBePickedUp(class AShooterCharacter* TestPawn) const +{ + return TestPawn && (TestPawn->Health < TestPawn->GetMaxHealth()); +} + +void AShooterPickup_Health::GivePickupTo(class AShooterCharacter* Pawn) +{ + if (Pawn) + { + Pawn->Health = FMath::Min(FMath::TruncToInt(Pawn->Health) + Health, Pawn->GetMaxHealth()); + + // Fire event for collected health + const UWorld* World = GetWorld(); + const IOnlineEventsPtr Events = Online::GetEventsInterface(World); + const IOnlineIdentityPtr Identity = Online::GetIdentityInterface(World); + + if (Events.IsValid() && Identity.IsValid()) + { + AShooterPlayerController* PC = Cast(Pawn->Controller); + if (PC) + { + ULocalPlayer* LocalPlayer = Cast(PC->Player); + + if (LocalPlayer) + { + const int32 UserIndex = LocalPlayer->GetControllerId(); + TSharedPtr UniqueID = Identity->GetUniquePlayerId(UserIndex); + if (UniqueID.IsValid()) + { + FVector Location = Pawn->GetActorLocation(); + + FOnlineEventParms Params; + + Params.Add( TEXT( "SectionId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "GameplayModeId" ), FVariantData( (int32)1 ) ); // @todo determine game mode (ffa v tdm) + Params.Add( TEXT( "DifficultyLevelId" ), FVariantData( (int32)0 ) ); // unused + + Params.Add( TEXT( "ItemId" ), FVariantData( (int32)0 ) ); // @todo come up with a better way to determine item id, currently health is 0 and ammo counts from 1 + Params.Add( TEXT( "AcquisitionMethodId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "LocationX" ), FVariantData( Location.X ) ); + Params.Add( TEXT( "LocationY" ), FVariantData( Location.Y ) ); + Params.Add( TEXT( "LocationZ" ), FVariantData( Location.Z ) ); + Params.Add( TEXT( "ItemQty" ), FVariantData( (int32)Health ) ); + + Events->TriggerEvent(*UniqueID, TEXT("CollectPowerup"), Params); + } + } + } + } + } +} diff --git a/Source/ShooterGame/Private/Player/ShooterCharacter.cpp b/Source/ShooterGame/Private/Player/ShooterCharacter.cpp new file mode 100644 index 0000000..f00e19d --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterCharacter.cpp @@ -0,0 +1,1326 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Weapons/ShooterWeapon.h" +#include "Weapons/ShooterDamageType.h" +#include "UI/ShooterHUD.h" +#include "Online/ShooterPlayerState.h" +#include "Animation/AnimMontage.h" +#include "Animation/AnimInstance.h" +#include "Sound/SoundNodeLocalPlayer.h" +#include "AudioThread.h" + +static int32 NetVisualizeRelevancyTestPoints = 0; +FAutoConsoleVariableRef CVarNetVisualizeRelevancyTestPoints( + TEXT("p.NetVisualizeRelevancyTestPoints"), + NetVisualizeRelevancyTestPoints, + TEXT("") + TEXT("0: Disable, 1: Enable"), + ECVF_Cheat); + + +static int32 NetEnablePauseRelevancy = 1; +FAutoConsoleVariableRef CVarNetEnablePauseRelevancy( + TEXT("p.NetEnablePauseRelevancy"), + NetEnablePauseRelevancy, + TEXT("") + TEXT("0: Disable, 1: Enable"), + ECVF_Cheat); + +FOnShooterCharacterEquipWeapon AShooterCharacter::NotifyEquipWeapon; +FOnShooterCharacterUnEquipWeapon AShooterCharacter::NotifyUnEquipWeapon; + +AShooterCharacter::AShooterCharacter(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer.SetDefaultSubobjectClass(ACharacter::CharacterMovementComponentName)) +{ + Mesh1P = ObjectInitializer.CreateDefaultSubobject(this, TEXT("PawnMesh1P")); + Mesh1P->SetupAttachment(GetCapsuleComponent()); + Mesh1P->bOnlyOwnerSee = true; + Mesh1P->bOwnerNoSee = false; + Mesh1P->bCastDynamicShadow = false; + Mesh1P->bReceivesDecals = false; + Mesh1P->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered; + Mesh1P->PrimaryComponentTick.TickGroup = TG_PrePhysics; + Mesh1P->SetCollisionObjectType(ECC_Pawn); + Mesh1P->SetCollisionEnabled(ECollisionEnabled::NoCollision); + Mesh1P->SetCollisionResponseToAllChannels(ECR_Ignore); + + GetMesh()->bOnlyOwnerSee = false; + GetMesh()->bOwnerNoSee = true; + GetMesh()->bReceivesDecals = false; + GetMesh()->SetCollisionObjectType(ECC_Pawn); + GetMesh()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + GetMesh()->SetCollisionResponseToChannel(COLLISION_WEAPON, ECR_Block); + GetMesh()->SetCollisionResponseToChannel(COLLISION_PROJECTILE, ECR_Block); + GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block); + + GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore); + GetCapsuleComponent()->SetCollisionResponseToChannel(COLLISION_PROJECTILE, ECR_Block); + GetCapsuleComponent()->SetCollisionResponseToChannel(COLLISION_WEAPON, ECR_Ignore); + + TargetingSpeedModifier = 0.5f; + bIsTargeting = false; + RunningSpeedModifier = 1.5f; + bWantsToRun = false; + bWantsToFire = false; + LowHealthPercentage = 0.5f; + + BaseTurnRate = 45.f; + BaseLookUpRate = 45.f; +} + +void AShooterCharacter::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + + if (GetLocalRole() == ROLE_Authority) + { + Health = GetMaxHealth(); + + // Needs to happen after character is added to repgraph + GetWorldTimerManager().SetTimerForNextTick(this, &AShooterCharacter::SpawnDefaultInventory); + } + + // set initial mesh visibility (3rd person view) + UpdatePawnMeshes(); + + // create material instance for setting team colors (3rd person view) + for (int32 iMat = 0; iMat < GetMesh()->GetNumMaterials(); iMat++) + { + MeshMIDs.Add(GetMesh()->CreateAndSetMaterialInstanceDynamic(iMat)); + } + + // play respawn effects + if (GetNetMode() != NM_DedicatedServer) + { + if (RespawnFX) + { + UGameplayStatics::SpawnEmitterAtLocation(this, RespawnFX, GetActorLocation(), GetActorRotation()); + } + + if (RespawnSound) + { + UGameplayStatics::PlaySoundAtLocation(this, RespawnSound, GetActorLocation()); + } + } +} + +void AShooterCharacter::Destroyed() +{ + Super::Destroyed(); + DestroyInventory(); +} + +void AShooterCharacter::PawnClientRestart() +{ + Super::PawnClientRestart(); + + // switch mesh to 1st person view + UpdatePawnMeshes(); + + // reattach weapon if needed + SetCurrentWeapon(CurrentWeapon); + + // set team colors for 1st person view + UMaterialInstanceDynamic* Mesh1PMID = Mesh1P->CreateAndSetMaterialInstanceDynamic(0); + UpdateTeamColors(Mesh1PMID); +} + +void AShooterCharacter::PossessedBy(class AController* InController) +{ + Super::PossessedBy(InController); + + // [server] as soon as PlayerState is assigned, set team colors of this pawn for local player + UpdateTeamColorsAllMIDs(); +} + +void AShooterCharacter::OnRep_PlayerState() +{ + Super::OnRep_PlayerState(); + + // [client] as soon as PlayerState is assigned, set team colors of this pawn for local player + if (GetPlayerState() != NULL) + { + UpdateTeamColorsAllMIDs(); + } +} + +FRotator AShooterCharacter::GetAimOffsets() const +{ + const FVector AimDirWS = GetBaseAimRotation().Vector(); + const FVector AimDirLS = ActorToWorld().InverseTransformVectorNoScale(AimDirWS); + const FRotator AimRotLS = AimDirLS.Rotation(); + + return AimRotLS; +} + +bool AShooterCharacter::IsEnemyFor(AController* TestPC) const +{ + if (TestPC == Controller || TestPC == NULL) + { + return false; + } + + AShooterPlayerState* TestPlayerState = Cast(TestPC->PlayerState); + AShooterPlayerState* MyPlayerState = Cast(GetPlayerState()); + + bool bIsEnemy = true; + if (GetWorld()->GetGameState()) + { + const AShooterGameMode* DefGame = GetWorld()->GetGameState()->GetDefaultGameMode(); + if (DefGame && MyPlayerState && TestPlayerState) + { + bIsEnemy = DefGame->CanDealDamage(TestPlayerState, MyPlayerState); + } + } + + return bIsEnemy; +} + +////////////////////////////////////////////////////////////////////////// +// Meshes + +void AShooterCharacter::UpdatePawnMeshes() +{ + bool const bFirstPerson = IsFirstPerson(); + + Mesh1P->VisibilityBasedAnimTickOption = !bFirstPerson ? EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered : EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones; + Mesh1P->SetOwnerNoSee(!bFirstPerson); + + GetMesh()->VisibilityBasedAnimTickOption = bFirstPerson ? EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered : EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones; + GetMesh()->SetOwnerNoSee(bFirstPerson); +} + +void AShooterCharacter::UpdateTeamColors(UMaterialInstanceDynamic* UseMID) +{ + if (UseMID) + { + AShooterPlayerState* MyPlayerState = Cast(GetPlayerState()); + if (MyPlayerState != NULL) + { + float MaterialParam = (float)MyPlayerState->GetTeamNum(); + UseMID->SetScalarParameterValue(TEXT("Team Color Index"), MaterialParam); + } + } +} + +void AShooterCharacter::OnCameraUpdate(const FVector& CameraLocation, const FRotator& CameraRotation) +{ + USkeletalMeshComponent* DefMesh1P = Cast(GetClass()->GetDefaultSubobjectByName(TEXT("PawnMesh1P"))); + const FMatrix DefMeshLS = FRotationTranslationMatrix(DefMesh1P->GetRelativeRotation(), DefMesh1P->GetRelativeLocation()); + const FMatrix LocalToWorld = ActorToWorld().ToMatrixWithScale(); + + // Mesh rotating code expect uniform scale in LocalToWorld matrix + + const FRotator RotCameraPitch(CameraRotation.Pitch, 0.0f, 0.0f); + const FRotator RotCameraYaw(0.0f, CameraRotation.Yaw, 0.0f); + + const FMatrix LeveledCameraLS = FRotationTranslationMatrix(RotCameraYaw, CameraLocation) * LocalToWorld.Inverse(); + const FMatrix PitchedCameraLS = FRotationMatrix(RotCameraPitch) * LeveledCameraLS; + const FMatrix MeshRelativeToCamera = DefMeshLS * LeveledCameraLS.Inverse(); + const FMatrix PitchedMesh = MeshRelativeToCamera * PitchedCameraLS; + + Mesh1P->SetRelativeLocationAndRotation(PitchedMesh.GetOrigin(), PitchedMesh.Rotator()); +} + + +////////////////////////////////////////////////////////////////////////// +// Damage & death + + +void AShooterCharacter::FellOutOfWorld(const class UDamageType& dmgType) +{ + Die(Health, FDamageEvent(dmgType.GetClass()), NULL, NULL); +} + +void AShooterCharacter::Suicide() +{ + KilledBy(this); +} + +void AShooterCharacter::KilledBy(APawn* EventInstigator) +{ + if (GetLocalRole() == ROLE_Authority && !bIsDying) + { + AController* Killer = NULL; + if (EventInstigator != NULL) + { + Killer = EventInstigator->Controller; + LastHitBy = NULL; + } + + Die(Health, FDamageEvent(UDamageType::StaticClass()), Killer, NULL); + } +} + + +float AShooterCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, class AActor* DamageCauser) +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->HasGodMode()) + { + return 0.f; + } + + if (Health <= 0.f) + { + return 0.f; + } + + // Modify based on game rules. + AShooterGameMode* const Game = GetWorld()->GetAuthGameMode(); + Damage = Game ? Game->ModifyDamage(Damage, this, DamageEvent, EventInstigator, DamageCauser) : 0.f; + + const float ActualDamage = Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser); + if (ActualDamage > 0.f) + { + Health -= ActualDamage; + if (Health <= 0) + { + Die(ActualDamage, DamageEvent, EventInstigator, DamageCauser); + } + else + { + PlayHit(ActualDamage, DamageEvent, EventInstigator ? EventInstigator->GetPawn() : NULL, DamageCauser); + } + + MakeNoise(1.0f, EventInstigator ? EventInstigator->GetPawn() : this); + } + + return ActualDamage; +} + + +bool AShooterCharacter::CanDie(float KillingDamage, FDamageEvent const& DamageEvent, AController* Killer, AActor* DamageCauser) const +{ + if (bIsDying // already dying + || IsPendingKill() // already destroyed + || GetLocalRole() != ROLE_Authority // not authority + || GetWorld()->GetAuthGameMode() == NULL + || GetWorld()->GetAuthGameMode()->GetMatchState() == MatchState::LeavingMap) // level transition occurring + { + return false; + } + + return true; +} + + +bool AShooterCharacter::Die(float KillingDamage, FDamageEvent const& DamageEvent, AController* Killer, AActor* DamageCauser) +{ + if (!CanDie(KillingDamage, DamageEvent, Killer, DamageCauser)) + { + return false; + } + + Health = FMath::Min(0.0f, Health); + + // if this is an environmental death then refer to the previous killer so that they receive credit (knocked into lava pits, etc) + UDamageType const* const DamageType = DamageEvent.DamageTypeClass ? DamageEvent.DamageTypeClass->GetDefaultObject() : GetDefault(); + Killer = GetDamageInstigator(Killer, *DamageType); + + AController* const KilledPlayer = (Controller != NULL) ? Controller : Cast(GetOwner()); + GetWorld()->GetAuthGameMode()->Killed(Killer, KilledPlayer, this, DamageType); + + NetUpdateFrequency = GetDefault()->NetUpdateFrequency; + GetCharacterMovement()->ForceReplicationUpdate(); + + OnDeath(KillingDamage, DamageEvent, Killer ? Killer->GetPawn() : NULL, DamageCauser); + return true; +} + + +void AShooterCharacter::OnDeath(float KillingDamage, struct FDamageEvent const& DamageEvent, class APawn* PawnInstigator, class AActor* DamageCauser) +{ + if (bIsDying) + { + return; + } + + SetReplicatingMovement(false); + TearOff(); + bIsDying = true; + + if (GetLocalRole() == ROLE_Authority) + { + ReplicateHit(KillingDamage, DamageEvent, PawnInstigator, DamageCauser, true); + + // play the force feedback effect on the client player controller + AShooterPlayerController* PC = Cast(Controller); + if (PC && DamageEvent.DamageTypeClass) + { + UShooterDamageType *DamageType = Cast(DamageEvent.DamageTypeClass->GetDefaultObject()); + if (DamageType && DamageType->KilledForceFeedback && PC->IsVibrationEnabled()) + { + FForceFeedbackParameters FFParams; + FFParams.Tag = "Damage"; + PC->ClientPlayForceFeedback(DamageType->KilledForceFeedback, FFParams); + } + } + } + + // cannot use IsLocallyControlled here, because even local client's controller may be NULL here + if (GetNetMode() != NM_DedicatedServer && DeathSound && Mesh1P && Mesh1P->IsVisible()) + { + UGameplayStatics::PlaySoundAtLocation(this, DeathSound, GetActorLocation()); + } + + // remove all weapons + DestroyInventory(); + + // switch back to 3rd person view + UpdatePawnMeshes(); + + DetachFromControllerPendingDestroy(); + StopAllAnimMontages(); + + if (LowHealthWarningPlayer && LowHealthWarningPlayer->IsPlaying()) + { + LowHealthWarningPlayer->Stop(); + } + + if (RunLoopAC) + { + RunLoopAC->Stop(); + } + + if (GetMesh()) + { + static FName CollisionProfileName(TEXT("Ragdoll")); + GetMesh()->SetCollisionProfileName(CollisionProfileName); + } + SetActorEnableCollision(true); + + // Death anim + float DeathAnimDuration = PlayAnimMontage(DeathAnim); + + // Ragdoll + if (DeathAnimDuration > 0.f) + { + // Trigger ragdoll a little before the animation early so the character doesn't + // blend back to its normal position. + const float TriggerRagdollTime = DeathAnimDuration - 0.7f; + + // Enable blend physics so the bones are properly blending against the montage. + GetMesh()->bBlendPhysics = true; + + // Use a local timer handle as we don't need to store it for later but we don't need to look for something to clear + FTimerHandle TimerHandle; + GetWorldTimerManager().SetTimer(TimerHandle, this, &AShooterCharacter::SetRagdollPhysics, FMath::Max(0.1f, TriggerRagdollTime), false); + } + else + { + SetRagdollPhysics(); + } + + // disable collisions on capsule + GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision); + GetCapsuleComponent()->SetCollisionResponseToAllChannels(ECR_Ignore); +} + +void AShooterCharacter::PlayHit(float DamageTaken, struct FDamageEvent const& DamageEvent, class APawn* PawnInstigator, class AActor* DamageCauser) +{ + if (GetLocalRole() == ROLE_Authority) + { + ReplicateHit(DamageTaken, DamageEvent, PawnInstigator, DamageCauser, false); + + // play the force feedback effect on the client player controller + AShooterPlayerController* PC = Cast(Controller); + if (PC && DamageEvent.DamageTypeClass) + { + UShooterDamageType *DamageType = Cast(DamageEvent.DamageTypeClass->GetDefaultObject()); + if (DamageType && DamageType->HitForceFeedback && PC->IsVibrationEnabled()) + { + FForceFeedbackParameters FFParams; + FFParams.Tag = "Damage"; + PC->ClientPlayForceFeedback(DamageType->HitForceFeedback, FFParams); + } + } + } + + if (DamageTaken > 0.f) + { + ApplyDamageMomentum(DamageTaken, DamageEvent, PawnInstigator, DamageCauser); + } + + AShooterPlayerController* MyPC = Cast(Controller); + AShooterHUD* MyHUD = MyPC ? Cast(MyPC->GetHUD()) : NULL; + if (MyHUD) + { + MyHUD->NotifyWeaponHit(DamageTaken, DamageEvent, PawnInstigator); + } + + if (PawnInstigator && PawnInstigator != this && PawnInstigator->IsLocallyControlled()) + { + AShooterPlayerController* InstigatorPC = Cast(PawnInstigator->Controller); + AShooterHUD* InstigatorHUD = InstigatorPC ? Cast(InstigatorPC->GetHUD()) : NULL; + if (InstigatorHUD) + { + InstigatorHUD->NotifyEnemyHit(); + } + } +} + + +void AShooterCharacter::SetRagdollPhysics() +{ + bool bInRagdoll = false; + + if (IsPendingKill()) + { + bInRagdoll = false; + } + else if (!GetMesh() || !GetMesh()->GetPhysicsAsset()) + { + bInRagdoll = false; + } + else + { + // initialize physics/etc + GetMesh()->SetSimulatePhysics(true); + GetMesh()->WakeAllRigidBodies(); + GetMesh()->bBlendPhysics = true; + + bInRagdoll = true; + } + + GetCharacterMovement()->StopMovementImmediately(); + GetCharacterMovement()->DisableMovement(); + GetCharacterMovement()->SetComponentTickEnabled(false); + + if (!bInRagdoll) + { + // hide and set short lifespan + TurnOff(); + SetActorHiddenInGame(true); + SetLifeSpan(1.0f); + } + else + { + SetLifeSpan(10.0f); + } +} + + + +void AShooterCharacter::ReplicateHit(float Damage, struct FDamageEvent const& DamageEvent, class APawn* PawnInstigator, class AActor* DamageCauser, bool bKilled) +{ + const float TimeoutTime = GetWorld()->GetTimeSeconds() + 0.5f; + + FDamageEvent const& LastDamageEvent = LastTakeHitInfo.GetDamageEvent(); + if ((PawnInstigator == LastTakeHitInfo.PawnInstigator.Get()) && (LastDamageEvent.DamageTypeClass == LastTakeHitInfo.DamageTypeClass) && (LastTakeHitTimeTimeout == TimeoutTime)) + { + // same frame damage + if (bKilled && LastTakeHitInfo.bKilled) + { + // Redundant death take hit, just ignore it + return; + } + + // otherwise, accumulate damage done this frame + Damage += LastTakeHitInfo.ActualDamage; + } + + LastTakeHitInfo.ActualDamage = Damage; + LastTakeHitInfo.PawnInstigator = Cast(PawnInstigator); + LastTakeHitInfo.DamageCauser = DamageCauser; + LastTakeHitInfo.SetDamageEvent(DamageEvent); + LastTakeHitInfo.bKilled = bKilled; + LastTakeHitInfo.EnsureReplication(); + + LastTakeHitTimeTimeout = TimeoutTime; +} + +void AShooterCharacter::OnRep_LastTakeHitInfo() +{ + if (LastTakeHitInfo.bKilled) + { + OnDeath(LastTakeHitInfo.ActualDamage, LastTakeHitInfo.GetDamageEvent(), LastTakeHitInfo.PawnInstigator.Get(), LastTakeHitInfo.DamageCauser.Get()); + } + else + { + PlayHit(LastTakeHitInfo.ActualDamage, LastTakeHitInfo.GetDamageEvent(), LastTakeHitInfo.PawnInstigator.Get(), LastTakeHitInfo.DamageCauser.Get()); + } +} + +//Pawn::PlayDying sets this lifespan, but when that function is called on client, dead pawn's role is still SimulatedProxy despite bTearOff being true. +void AShooterCharacter::TornOff() +{ + SetLifeSpan(25.f); +} + +bool AShooterCharacter::IsMoving() +{ + return FMath::Abs(GetLastMovementInputVector().Size()) > 0.f; +} + +////////////////////////////////////////////////////////////////////////// +// Inventory + +void AShooterCharacter::SpawnDefaultInventory() +{ + if (GetLocalRole() < ROLE_Authority) + { + return; + } + + int32 NumWeaponClasses = DefaultInventoryClasses.Num(); + for (int32 i = 0; i < NumWeaponClasses; i++) + { + if (DefaultInventoryClasses[i]) + { + FActorSpawnParameters SpawnInfo; + SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + AShooterWeapon* NewWeapon = GetWorld()->SpawnActor(DefaultInventoryClasses[i], SpawnInfo); + AddWeapon(NewWeapon); + } + } + + // equip first weapon in inventory + if (Inventory.Num() > 0) + { + EquipWeapon(Inventory[0]); + } +} + +void AShooterCharacter::DestroyInventory() +{ + if (GetLocalRole() < ROLE_Authority) + { + return; + } + + // remove all weapons from inventory and destroy them + for (int32 i = Inventory.Num() - 1; i >= 0; i--) + { + AShooterWeapon* Weapon = Inventory[i]; + if (Weapon) + { + RemoveWeapon(Weapon); + Weapon->Destroy(); + } + } +} + +void AShooterCharacter::AddWeapon(AShooterWeapon* Weapon) +{ + if (Weapon && GetLocalRole() == ROLE_Authority) + { + Weapon->OnEnterInventory(this); + Inventory.AddUnique(Weapon); + } +} + +void AShooterCharacter::RemoveWeapon(AShooterWeapon* Weapon) +{ + if (Weapon && GetLocalRole() == ROLE_Authority) + { + Weapon->OnLeaveInventory(); + Inventory.RemoveSingle(Weapon); + } +} + +AShooterWeapon* AShooterCharacter::FindWeapon(TSubclassOf WeaponClass) +{ + for (int32 i = 0; i < Inventory.Num(); i++) + { + if (Inventory[i] && Inventory[i]->IsA(WeaponClass)) + { + return Inventory[i]; + } + } + + return NULL; +} + +void AShooterCharacter::EquipWeapon(AShooterWeapon* Weapon) +{ + if (Weapon) + { + if (GetLocalRole() == ROLE_Authority) + { + SetCurrentWeapon(Weapon, CurrentWeapon); + } + else + { + ServerEquipWeapon(Weapon); + } + } +} + +bool AShooterCharacter::ServerEquipWeapon_Validate(AShooterWeapon* Weapon) +{ + return true; +} + +void AShooterCharacter::ServerEquipWeapon_Implementation(AShooterWeapon* Weapon) +{ + EquipWeapon(Weapon); +} + +void AShooterCharacter::OnRep_CurrentWeapon(AShooterWeapon* LastWeapon) +{ + SetCurrentWeapon(CurrentWeapon, LastWeapon); +} + +void AShooterCharacter::SetCurrentWeapon(AShooterWeapon* NewWeapon, AShooterWeapon* LastWeapon) +{ + AShooterWeapon* LocalLastWeapon = nullptr; + + if (LastWeapon != NULL) + { + LocalLastWeapon = LastWeapon; + } + else if (NewWeapon != CurrentWeapon) + { + LocalLastWeapon = CurrentWeapon; + } + + // unequip previous + if (LocalLastWeapon) + { + LocalLastWeapon->OnUnEquip(); + } + + CurrentWeapon = NewWeapon; + + // equip new one + if (NewWeapon) + { + NewWeapon->SetOwningPawn(this); // Make sure weapon's MyPawn is pointing back to us. During replication, we can't guarantee APawn::CurrentWeapon will rep after AWeapon::MyPawn! + + NewWeapon->OnEquip(LastWeapon); + } +} + + +////////////////////////////////////////////////////////////////////////// +// Weapon usage + +void AShooterCharacter::StartWeaponFire() +{ + if (!bWantsToFire) + { + bWantsToFire = true; + if (CurrentWeapon) + { + CurrentWeapon->StartFire(); + } + } +} + +void AShooterCharacter::StopWeaponFire() +{ + if (bWantsToFire) + { + bWantsToFire = false; + if (CurrentWeapon) + { + CurrentWeapon->StopFire(); + } + } +} + +bool AShooterCharacter::CanFire() const +{ + return IsAlive(); +} + +bool AShooterCharacter::CanReload() const +{ + return true; +} + +void AShooterCharacter::SetTargeting(bool bNewTargeting) +{ + bIsTargeting = bNewTargeting; + + if (TargetingSound) + { + UGameplayStatics::SpawnSoundAttached(TargetingSound, GetRootComponent()); + } + + if (GetLocalRole() < ROLE_Authority) + { + ServerSetTargeting(bNewTargeting); + } +} + +bool AShooterCharacter::ServerSetTargeting_Validate(bool bNewTargeting) +{ + return true; +} + +void AShooterCharacter::ServerSetTargeting_Implementation(bool bNewTargeting) +{ + SetTargeting(bNewTargeting); +} + +////////////////////////////////////////////////////////////////////////// +// Movement + +void AShooterCharacter::SetRunning(bool bNewRunning, bool bToggle) +{ + bWantsToRun = bNewRunning; + bWantsToRunToggled = bNewRunning && bToggle; + + if (GetLocalRole() < ROLE_Authority) + { + ServerSetRunning(bNewRunning, bToggle); + } +} + +bool AShooterCharacter::ServerSetRunning_Validate(bool bNewRunning, bool bToggle) +{ + return true; +} + +void AShooterCharacter::ServerSetRunning_Implementation(bool bNewRunning, bool bToggle) +{ + SetRunning(bNewRunning, bToggle); +} + +void AShooterCharacter::UpdateRunSounds() +{ + const bool bIsRunSoundPlaying = RunLoopAC != nullptr && RunLoopAC->IsActive(); + const bool bWantsRunSoundPlaying = IsRunning() && IsMoving(); + + // Don't bother playing the sounds unless we're running and moving. + if (!bIsRunSoundPlaying && bWantsRunSoundPlaying) + { + if (RunLoopAC != nullptr) + { + RunLoopAC->Play(); + } + else if (RunLoopSound != nullptr) + { + RunLoopAC = UGameplayStatics::SpawnSoundAttached(RunLoopSound, GetRootComponent()); + if (RunLoopAC != nullptr) + { + RunLoopAC->bAutoDestroy = false; + } + } + } + else if (bIsRunSoundPlaying && !bWantsRunSoundPlaying) + { + RunLoopAC->Stop(); + if (RunStopSound != nullptr) + { + UGameplayStatics::SpawnSoundAttached(RunStopSound, GetRootComponent()); + } + } +} + +////////////////////////////////////////////////////////////////////////// +// Animations + +float AShooterCharacter::PlayAnimMontage(class UAnimMontage* AnimMontage, float InPlayRate, FName StartSectionName) +{ + USkeletalMeshComponent* UseMesh = GetPawnMesh(); + if (AnimMontage && UseMesh && UseMesh->AnimScriptInstance) + { + return UseMesh->AnimScriptInstance->Montage_Play(AnimMontage, InPlayRate); + } + + return 0.0f; +} + +void AShooterCharacter::StopAnimMontage(class UAnimMontage* AnimMontage) +{ + USkeletalMeshComponent* UseMesh = GetPawnMesh(); + if (AnimMontage && UseMesh && UseMesh->AnimScriptInstance && + UseMesh->AnimScriptInstance->Montage_IsPlaying(AnimMontage)) + { + UseMesh->AnimScriptInstance->Montage_Stop(AnimMontage->BlendOut.GetBlendTime(), AnimMontage); + } +} + +void AShooterCharacter::StopAllAnimMontages() +{ + USkeletalMeshComponent* UseMesh = GetPawnMesh(); + if (UseMesh && UseMesh->AnimScriptInstance) + { + UseMesh->AnimScriptInstance->Montage_Stop(0.0f); + } +} + + +////////////////////////////////////////////////////////////////////////// +// Input + +void AShooterCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) +{ + check(PlayerInputComponent); + PlayerInputComponent->BindAxis("MoveForward", this, &AShooterCharacter::MoveForward); + PlayerInputComponent->BindAxis("MoveRight", this, &AShooterCharacter::MoveRight); + PlayerInputComponent->BindAxis("MoveUp", this, &AShooterCharacter::MoveUp); + PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput); + PlayerInputComponent->BindAxis("TurnRate", this, &AShooterCharacter::TurnAtRate); + PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput); + PlayerInputComponent->BindAxis("LookUpRate", this, &AShooterCharacter::LookUpAtRate); + + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC->bAnalogFireTrigger) + { + PlayerInputComponent->BindAxis("FireTrigger", this, &AShooterCharacter::FireTrigger); + } + else + { + PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AShooterCharacter::OnStartFire); + PlayerInputComponent->BindAction("Fire", IE_Released, this, &AShooterCharacter::OnStopFire); + } + + PlayerInputComponent->BindAction("Targeting", IE_Pressed, this, &AShooterCharacter::OnStartTargeting); + PlayerInputComponent->BindAction("Targeting", IE_Released, this, &AShooterCharacter::OnStopTargeting); + + PlayerInputComponent->BindAction("NextWeapon", IE_Pressed, this, &AShooterCharacter::OnNextWeapon); + PlayerInputComponent->BindAction("PrevWeapon", IE_Pressed, this, &AShooterCharacter::OnPrevWeapon); + + PlayerInputComponent->BindAction("Reload", IE_Pressed, this, &AShooterCharacter::OnReload); + + PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AShooterCharacter::OnStartJump); + PlayerInputComponent->BindAction("Jump", IE_Released, this, &AShooterCharacter::OnStopJump); + + PlayerInputComponent->BindAction("Run", IE_Pressed, this, &AShooterCharacter::OnStartRunning); + PlayerInputComponent->BindAction("RunToggle", IE_Pressed, this, &AShooterCharacter::OnStartRunningToggle); + PlayerInputComponent->BindAction("Run", IE_Released, this, &AShooterCharacter::OnStopRunning); +} + + +void AShooterCharacter::FireTrigger(float Val) +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (bWantsToFire && Val < MyPC->FireTriggerThreshold) + { + OnStopFire(); + } + else if (!bWantsToFire && Val >= MyPC->FireTriggerThreshold) + { + OnStartFire(); + } +} + +void AShooterCharacter::MoveForward(float Val) +{ + if (Controller && Val != 0.f) + { + // Limit pitch when walking or falling + const bool bLimitRotation = (GetCharacterMovement()->IsMovingOnGround() || GetCharacterMovement()->IsFalling()); + const FRotator Rotation = bLimitRotation ? GetActorRotation() : Controller->GetControlRotation(); + const FVector Direction = FRotationMatrix(Rotation).GetScaledAxis(EAxis::X); + AddMovementInput(Direction, Val); + } +} + +void AShooterCharacter::MoveRight(float Val) +{ + if (Val != 0.f) + { + const FQuat Rotation = GetActorQuat(); + const FVector Direction = FQuatRotationMatrix(Rotation).GetScaledAxis(EAxis::Y); + AddMovementInput(Direction, Val); + } +} + +void AShooterCharacter::MoveUp(float Val) +{ + if (Val != 0.f) + { + // Not when walking or falling. + if (GetCharacterMovement()->IsMovingOnGround() || GetCharacterMovement()->IsFalling()) + { + return; + } + + AddMovementInput(FVector::UpVector, Val); + } +} + +void AShooterCharacter::TurnAtRate(float Val) +{ + // calculate delta for this frame from the rate information + AddControllerYawInput(Val * BaseTurnRate * GetWorld()->GetDeltaSeconds()); +} + +void AShooterCharacter::LookUpAtRate(float Val) +{ + // calculate delta for this frame from the rate information + AddControllerPitchInput(Val * BaseLookUpRate * GetWorld()->GetDeltaSeconds()); +} + +void AShooterCharacter::OnStartFire() +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->IsGameInputAllowed()) + { + if (IsRunning()) + { + SetRunning(false, false); + } + StartWeaponFire(); + } +} + +void AShooterCharacter::OnStopFire() +{ + StopWeaponFire(); +} + +void AShooterCharacter::OnStartTargeting() +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->IsGameInputAllowed()) + { + if (IsRunning()) + { + SetRunning(false, false); + } + SetTargeting(true); + } +} + +void AShooterCharacter::OnStopTargeting() +{ + SetTargeting(false); +} + +void AShooterCharacter::OnNextWeapon() +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->IsGameInputAllowed()) + { + if (Inventory.Num() >= 2 && (CurrentWeapon == NULL || CurrentWeapon->GetCurrentState() != EWeaponState::Equipping)) + { + const int32 CurrentWeaponIdx = Inventory.IndexOfByKey(CurrentWeapon); + AShooterWeapon* NextWeapon = Inventory[(CurrentWeaponIdx + 1) % Inventory.Num()]; + EquipWeapon(NextWeapon); + } + } +} + +void AShooterCharacter::OnPrevWeapon() +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->IsGameInputAllowed()) + { + if (Inventory.Num() >= 2 && (CurrentWeapon == NULL || CurrentWeapon->GetCurrentState() != EWeaponState::Equipping)) + { + const int32 CurrentWeaponIdx = Inventory.IndexOfByKey(CurrentWeapon); + AShooterWeapon* PrevWeapon = Inventory[(CurrentWeaponIdx - 1 + Inventory.Num()) % Inventory.Num()]; + EquipWeapon(PrevWeapon); + } + } +} + +void AShooterCharacter::OnReload() +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->IsGameInputAllowed()) + { + if (CurrentWeapon) + { + CurrentWeapon->StartReload(); + } + } +} + +void AShooterCharacter::OnStartRunning() +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->IsGameInputAllowed()) + { + if (IsTargeting()) + { + SetTargeting(false); + } + StopWeaponFire(); + SetRunning(true, false); + } +} + +void AShooterCharacter::OnStartRunningToggle() +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->IsGameInputAllowed()) + { + if (IsTargeting()) + { + SetTargeting(false); + } + StopWeaponFire(); + SetRunning(true, true); + } +} + +void AShooterCharacter::OnStopRunning() +{ + SetRunning(false, false); +} + +bool AShooterCharacter::IsRunning() const +{ + if (!GetCharacterMovement()) + { + return false; + } + + return (bWantsToRun || bWantsToRunToggled) && !GetVelocity().IsZero() && (GetVelocity().GetSafeNormal2D() | GetActorForwardVector()) > -0.1; +} + +void AShooterCharacter::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + if (bWantsToRunToggled && !IsRunning()) + { + SetRunning(false, false); + } + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->HasHealthRegen()) + { + if (this->Health < this->GetMaxHealth()) + { + this->Health += 5 * DeltaSeconds; + if (Health > this->GetMaxHealth()) + { + Health = this->GetMaxHealth(); + } + } + } + + if (GEngine->UseSound()) + { + if (LowHealthSound) + { + if ((this->Health > 0 && this->Health < this->GetMaxHealth() * LowHealthPercentage) && (!LowHealthWarningPlayer || !LowHealthWarningPlayer->IsPlaying())) + { + LowHealthWarningPlayer = UGameplayStatics::SpawnSoundAttached(LowHealthSound, GetRootComponent(), + NAME_None, FVector(ForceInit), EAttachLocation::KeepRelativeOffset, true); + if (LowHealthWarningPlayer) + { + LowHealthWarningPlayer->SetVolumeMultiplier(0.0f); + } + } + else if ((this->Health > this->GetMaxHealth() * LowHealthPercentage || this->Health < 0) && LowHealthWarningPlayer && LowHealthWarningPlayer->IsPlaying()) + { + LowHealthWarningPlayer->Stop(); + } + if (LowHealthWarningPlayer && LowHealthWarningPlayer->IsPlaying()) + { + const float MinVolume = 0.3f; + const float VolumeMultiplier = (1.0f - (this->Health / (this->GetMaxHealth() * LowHealthPercentage))); + LowHealthWarningPlayer->SetVolumeMultiplier(MinVolume + (1.0f - MinVolume) * VolumeMultiplier); + } + } + + UpdateRunSounds(); + } + + const APlayerController* PC = Cast(GetController()); + const bool bLocallyControlled = (PC ? PC->IsLocalController() : false); + const uint32 UniqueID = GetUniqueID(); + FAudioThread::RunCommandOnAudioThread([UniqueID, bLocallyControlled]() + { + USoundNodeLocalPlayer::GetLocallyControlledActorCache().Add(UniqueID, bLocallyControlled); + }); + + TArray PointsToTest; + BuildPauseReplicationCheckPoints(PointsToTest); + + if (NetVisualizeRelevancyTestPoints == 1) + { + for (FVector PointToTest : PointsToTest) + { + DrawDebugSphere(GetWorld(), PointToTest, 10.0f, 8, FColor::Red); + } + } +} + +void AShooterCharacter::BeginDestroy() +{ + Super::BeginDestroy(); + + if (!GExitPurge) + { + const uint32 UniqueID = GetUniqueID(); + FAudioThread::RunCommandOnAudioThread([UniqueID]() + { + USoundNodeLocalPlayer::GetLocallyControlledActorCache().Remove(UniqueID); + }); + } +} + +void AShooterCharacter::OnStartJump() +{ + AShooterPlayerController* MyPC = Cast(Controller); + if (MyPC && MyPC->IsGameInputAllowed()) + { + bPressedJump = true; + } +} + +void AShooterCharacter::OnStopJump() +{ + bPressedJump = false; + StopJumping(); +} + +////////////////////////////////////////////////////////////////////////// +// Replication + +void AShooterCharacter::PreReplication(IRepChangedPropertyTracker & ChangedPropertyTracker) +{ + Super::PreReplication(ChangedPropertyTracker); + + // Only replicate this property for a short duration after it changes so join in progress players don't get spammed with fx when joining late + DOREPLIFETIME_ACTIVE_OVERRIDE(AShooterCharacter, LastTakeHitInfo, GetWorld() && GetWorld()->GetTimeSeconds() < LastTakeHitTimeTimeout); +} + +void AShooterCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + // only to local owner: weapon change requests are locally instigated, other clients don't need it + DOREPLIFETIME_CONDITION(AShooterCharacter, Inventory, COND_OwnerOnly); + + // everyone except local owner: flag change is locally instigated + DOREPLIFETIME_CONDITION(AShooterCharacter, bIsTargeting, COND_SkipOwner); + DOREPLIFETIME_CONDITION(AShooterCharacter, bWantsToRun, COND_SkipOwner); + + DOREPLIFETIME_CONDITION(AShooterCharacter, LastTakeHitInfo, COND_Custom); + + // everyone + DOREPLIFETIME(AShooterCharacter, CurrentWeapon); + DOREPLIFETIME(AShooterCharacter, Health); +} + +bool AShooterCharacter::IsReplicationPausedForConnection(const FNetViewer& ConnectionOwnerNetViewer) +{ + if (NetEnablePauseRelevancy == 1) + { + APlayerController* PC = Cast(ConnectionOwnerNetViewer.InViewer); + check(PC); + + FVector ViewLocation; + FRotator ViewRotation; + PC->GetPlayerViewPoint(ViewLocation, ViewRotation); + + FCollisionQueryParams CollisionParams(SCENE_QUERY_STAT(LineOfSight), true, PC->GetPawn()); + CollisionParams.AddIgnoredActor(this); + + TArray PointsToTest; + BuildPauseReplicationCheckPoints(PointsToTest); + + for (FVector PointToTest : PointsToTest) + { + if (!GetWorld()->LineTraceTestByChannel(PointToTest, ViewLocation, ECC_Visibility, CollisionParams)) + { + return false; + } + } + + return true; + } + + return false; +} + +void AShooterCharacter::OnReplicationPausedChanged(bool bIsReplicationPaused) +{ + GetMesh()->SetHiddenInGame(bIsReplicationPaused, true); +} + +AShooterWeapon* AShooterCharacter::GetWeapon() const +{ + return CurrentWeapon; +} + +int32 AShooterCharacter::GetInventoryCount() const +{ + return Inventory.Num(); +} + +AShooterWeapon* AShooterCharacter::GetInventoryWeapon(int32 index) const +{ + return Inventory[index]; +} + +USkeletalMeshComponent* AShooterCharacter::GetPawnMesh() const +{ + return IsFirstPerson() ? Mesh1P : GetMesh(); +} + +USkeletalMeshComponent* AShooterCharacter::GetSpecifcPawnMesh(bool WantFirstPerson) const +{ + return WantFirstPerson == true ? Mesh1P : GetMesh(); +} + +FName AShooterCharacter::GetWeaponAttachPoint() const +{ + return WeaponAttachPoint; +} + +float AShooterCharacter::GetTargetingSpeedModifier() const +{ + return TargetingSpeedModifier; +} + +bool AShooterCharacter::IsTargeting() const +{ + return bIsTargeting; +} + +float AShooterCharacter::GetRunningSpeedModifier() const +{ + return RunningSpeedModifier; +} + +bool AShooterCharacter::IsFiring() const +{ + return bWantsToFire; +}; + +bool AShooterCharacter::IsFirstPerson() const +{ + return IsAlive() && Controller && Controller->IsLocalPlayerController(); +} + +int32 AShooterCharacter::GetMaxHealth() const +{ + return GetClass()->GetDefaultObject()->Health; +} + +bool AShooterCharacter::IsAlive() const +{ + return Health > 0; +} + +float AShooterCharacter::GetLowHealthPercentage() const +{ + return LowHealthPercentage; +} + +void AShooterCharacter::UpdateTeamColorsAllMIDs() +{ + for (int32 i = 0; i < MeshMIDs.Num(); ++i) + { + UpdateTeamColors(MeshMIDs[i]); + } +} + +void AShooterCharacter::BuildPauseReplicationCheckPoints(TArray& RelevancyCheckPoints) +{ + FBoxSphereBounds Bounds = GetCapsuleComponent()->CalcBounds(GetCapsuleComponent()->GetComponentTransform()); + FBox BoundingBox = Bounds.GetBox(); + float XDiff = Bounds.BoxExtent.X * 2; + float YDiff = Bounds.BoxExtent.Y * 2; + + RelevancyCheckPoints.Add(BoundingBox.Min); + RelevancyCheckPoints.Add(FVector(BoundingBox.Min.X + XDiff, BoundingBox.Min.Y, BoundingBox.Min.Z)); + RelevancyCheckPoints.Add(FVector(BoundingBox.Min.X, BoundingBox.Min.Y + YDiff, BoundingBox.Min.Z)); + RelevancyCheckPoints.Add(FVector(BoundingBox.Min.X + XDiff, BoundingBox.Min.Y + YDiff, BoundingBox.Min.Z)); + RelevancyCheckPoints.Add(FVector(BoundingBox.Max.X - XDiff, BoundingBox.Max.Y, BoundingBox.Max.Z)); + RelevancyCheckPoints.Add(FVector(BoundingBox.Max.X, BoundingBox.Max.Y - YDiff, BoundingBox.Max.Z)); + RelevancyCheckPoints.Add(FVector(BoundingBox.Max.X - XDiff, BoundingBox.Max.Y - YDiff, BoundingBox.Max.Z)); + RelevancyCheckPoints.Add(BoundingBox.Max); +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Player/ShooterCharacterMovement.cpp b/Source/ShooterGame/Private/Player/ShooterCharacterMovement.cpp new file mode 100644 index 0000000..6a9318b --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterCharacterMovement.cpp @@ -0,0 +1,33 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Player/ShooterCharacterMovement.h" + +//----------------------------------------------------------------------// +// UPawnMovementComponent +//----------------------------------------------------------------------// +UShooterCharacterMovement::UShooterCharacterMovement(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + + +float UShooterCharacterMovement::GetMaxSpeed() const +{ + float MaxSpeed = Super::GetMaxSpeed(); + + const AShooterCharacter* ShooterCharacterOwner = Cast(PawnOwner); + if (ShooterCharacterOwner) + { + if (ShooterCharacterOwner->IsTargeting()) + { + MaxSpeed *= ShooterCharacterOwner->GetTargetingSpeedModifier(); + } + if (ShooterCharacterOwner->IsRunning()) + { + MaxSpeed *= ShooterCharacterOwner->GetRunningSpeedModifier(); + } + } + + return MaxSpeed; +} diff --git a/Source/ShooterGame/Private/Player/ShooterCheatManager.cpp b/Source/ShooterGame/Private/Player/ShooterCheatManager.cpp new file mode 100644 index 0000000..0a57c3a --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterCheatManager.cpp @@ -0,0 +1,80 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Player/ShooterCheatManager.h" +#include "Online/ShooterPlayerState.h" +#include "Bots/ShooterAIController.h" + +UShooterCheatManager::UShooterCheatManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ +} + +void UShooterCheatManager::ToggleInfiniteAmmo() +{ + AShooterPlayerController* MyPC = GetOuterAShooterPlayerController(); + + MyPC->SetInfiniteAmmo(!MyPC->HasInfiniteAmmo()); + MyPC->ClientMessage(FString::Printf(TEXT("Infinite ammo: %s"), MyPC->HasInfiniteAmmo() ? TEXT("ENABLED") : TEXT("off"))); +} + +void UShooterCheatManager::ToggleInfiniteClip() +{ + AShooterPlayerController* MyPC = GetOuterAShooterPlayerController(); + + MyPC->SetInfiniteClip(!MyPC->HasInfiniteClip()); + MyPC->ClientMessage(FString::Printf(TEXT("Infinite clip: %s"), MyPC->HasInfiniteClip() ? TEXT("ENABLED") : TEXT("off"))); +} + +void UShooterCheatManager::ToggleMatchTimer() +{ + AShooterPlayerController* MyPC = GetOuterAShooterPlayerController(); + + AShooterGameState* const MyGameState = MyPC->GetWorld()->GetGameState(); + if (MyGameState && MyGameState->GetLocalRole() == ROLE_Authority) + { + MyGameState->bTimerPaused = !MyGameState->bTimerPaused; + MyPC->ClientMessage(FString::Printf(TEXT("Match timer: %s"), MyGameState->bTimerPaused ? TEXT("PAUSED") : TEXT("running"))); + } +} + +void UShooterCheatManager::ForceMatchStart() +{ + AShooterPlayerController* const MyPC = GetOuterAShooterPlayerController(); + + AShooterGameMode* const MyGame = MyPC->GetWorld()->GetAuthGameMode(); + if (MyGame && MyGame->GetMatchState() == MatchState::WaitingToStart) + { + MyGame->StartMatch(); + } +} + +void UShooterCheatManager::ChangeTeam(int32 NewTeamNumber) +{ + AShooterPlayerController* MyPC = GetOuterAShooterPlayerController(); + + AShooterPlayerState* MyPlayerState = Cast(MyPC->PlayerState); + if (MyPlayerState && MyPlayerState->GetLocalRole() == ROLE_Authority) + { + MyPlayerState->SetTeamNum(NewTeamNumber); + MyPC->ClientMessage(FString::Printf(TEXT("Team changed to: %d"), MyPlayerState->GetTeamNum())); + } +} + +void UShooterCheatManager::Cheat(const FString& Msg) +{ + GetOuterAShooterPlayerController()->ServerCheat(Msg.Left(128)); +} + +void UShooterCheatManager::SpawnBot() +{ + AShooterPlayerController* const MyPC = GetOuterAShooterPlayerController(); + APawn* const MyPawn = MyPC->GetPawn(); + AShooterGameMode* const MyGame = MyPC->GetWorld()->GetAuthGameMode(); + UWorld* World = MyPC->GetWorld(); + if (MyPawn && MyGame && World) + { + static int32 CheatBotNum = 50; + AShooterAIController* ShooterAIController = MyGame->CreateBot(CheatBotNum++); + MyGame->RestartPlayer(ShooterAIController); + } +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Player/ShooterDemoSpectator.cpp b/Source/ShooterGame/Private/Player/ShooterDemoSpectator.cpp new file mode 100644 index 0000000..82402a5 --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterDemoSpectator.cpp @@ -0,0 +1,95 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Player/ShooterDemoSpectator.h" +#include "UI/Menu/ShooterDemoPlaybackMenu.h" +#include "UI/Widgets/SShooterDemoHUD.h" +#include "Engine/DemoNetDriver.h" + +AShooterDemoSpectator::AShooterDemoSpectator(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + bShowMouseCursor = true; + PrimaryActorTick.bTickEvenWhenPaused = true; + bShouldPerformFullTickWhenPaused = true; +} + +void AShooterDemoSpectator::SetupInputComponent() +{ + Super::SetupInputComponent(); + + // UI input + InputComponent->BindAction( "InGameMenu", IE_Pressed, this, &AShooterDemoSpectator::OnToggleInGameMenu ); + + InputComponent->BindAction( "NextWeapon", IE_Pressed, this, &AShooterDemoSpectator::OnIncreasePlaybackSpeed ); + InputComponent->BindAction( "PrevWeapon", IE_Pressed, this, &AShooterDemoSpectator::OnDecreasePlaybackSpeed ); +} + +void AShooterDemoSpectator::SetPlayer( UPlayer* InPlayer ) +{ + Super::SetPlayer( InPlayer ); + + // Build menu only after game is initialized + ShooterDemoPlaybackMenu = MakeShareable( new FShooterDemoPlaybackMenu() ); + ShooterDemoPlaybackMenu->Construct( Cast< ULocalPlayer >( Player ) ); + + // Create HUD if this is playback + if (GetWorld() != nullptr && GetWorld()->GetDemoNetDriver() != nullptr && !GetWorld()->GetDemoNetDriver()->IsServer()) + { + if (GEngine != nullptr && GEngine->GameViewport != nullptr) + { + DemoHUD = SNew(SShooterDemoHUD) + .PlayerOwner(this); + + GEngine->GameViewport->AddViewportWidgetContent(DemoHUD.ToSharedRef()); + } + } + + FActorSpawnParameters SpawnInfo; + + SpawnInfo.Owner = this; + SpawnInfo.Instigator = GetInstigator(); + SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + PlaybackSpeed = 2; + + FInputModeGameAndUI InputMode; + InputMode.SetWidgetToFocus(DemoHUD); + + SetInputMode(InputMode); +} + +void AShooterDemoSpectator::OnToggleInGameMenu() +{ + // if no one's paused, pause + if ( ShooterDemoPlaybackMenu.IsValid() ) + { + ShooterDemoPlaybackMenu->ToggleGameMenu(); + } +} + +static float PlaybackSpeedLUT[5] = { 0.1f, 0.5f, 1.0f, 2.0f, 4.0f }; + +void AShooterDemoSpectator::OnIncreasePlaybackSpeed() +{ + PlaybackSpeed = FMath::Clamp( PlaybackSpeed + 1, 0, 4 ); + + GetWorldSettings()->DemoPlayTimeDilation = PlaybackSpeedLUT[ PlaybackSpeed ]; +} + +void AShooterDemoSpectator::OnDecreasePlaybackSpeed() +{ + PlaybackSpeed = FMath::Clamp( PlaybackSpeed - 1, 0, 4 ); + + GetWorldSettings()->DemoPlayTimeDilation = PlaybackSpeedLUT[ PlaybackSpeed ]; +} + +void AShooterDemoSpectator::Destroyed() +{ + if (GEngine != nullptr && GEngine->GameViewport != nullptr && DemoHUD.IsValid()) + { + // Remove HUD + GEngine->GameViewport->RemoveViewportWidgetContent(DemoHUD.ToSharedRef()); + } + + Super::Destroyed(); +} diff --git a/Source/ShooterGame/Private/Player/ShooterLocalPlayer.cpp b/Source/ShooterGame/Private/Player/ShooterLocalPlayer.cpp new file mode 100644 index 0000000..6bdde4a --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterLocalPlayer.cpp @@ -0,0 +1,126 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Player/ShooterLocalPlayer.h" +#include "OnlineSubsystemUtilsClasses.h" +#include "ShooterGameInstance.h" +#include "OnlineSubsystemUtils.h" + +UShooterLocalPlayer::UShooterLocalPlayer(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +UShooterPersistentUser* UShooterLocalPlayer::GetPersistentUser() const +{ + // if persistent data isn't loaded yet, load it + if (PersistentUser == nullptr) + { + UShooterLocalPlayer* const MutableThis = const_cast(this); + // casting away constness to enable caching implementation behavior + MutableThis->LoadPersistentUser(); + } + return PersistentUser; +} + +void UShooterLocalPlayer::LoadPersistentUser() +{ + FString SaveGameName = GetNickname(); + +#if PLATFORM_SWITCH + // on Switch, the displayable nickname can change, so we can't use it as a save ID (explicitly stated in docs, so changing for pre-cert) + FPlatformMisc::GetUniqueStringNameForControllerId(GetControllerId(), SaveGameName); +#endif + + // if we changed controllerid / user, then we need to load the appropriate persistent user. + if (PersistentUser != nullptr && ( GetControllerId() != PersistentUser->GetUserIndex() || SaveGameName != PersistentUser->GetName() ) ) + { + PersistentUser->SaveIfDirty(); + PersistentUser = nullptr; + } + + if (PersistentUser == NULL) + { + // Use the platform id here to be resilient in the face of controller swapping and similar situations. + FPlatformUserId PlatformId = GetControllerId(); + + IOnlineIdentityPtr Identity = Online::GetIdentityInterface(GetWorld()); + if (Identity.IsValid() && GetPreferredUniqueNetId().IsValid()) + { + PlatformId = Identity->GetPlatformUserIdFromUniqueNetId(*GetPreferredUniqueNetId()); + } + + PersistentUser = UShooterPersistentUser::LoadPersistentUser(SaveGameName, PlatformId ); + } +} + +void UShooterLocalPlayer::SetControllerId(int32 NewControllerId) +{ + ULocalPlayer::SetControllerId(NewControllerId); + + FString SaveGameName = GetNickname(); + +#if PLATFORM_SWITCH + // on Switch, the displayable nickname can change, so we can't use it as a save ID (explicitly stated in docs, so changing for pre-cert) + FPlatformMisc::GetUniqueStringNameForControllerId(GetControllerId(), SaveGameName); +#endif + + // if we changed controllerid / user, then we need to load the appropriate persistent user. + if (PersistentUser != nullptr && ( GetControllerId() != PersistentUser->GetUserIndex() || SaveGameName != PersistentUser->GetName() ) ) + { + PersistentUser->SaveIfDirty(); + PersistentUser = nullptr; + } + + if (!PersistentUser) + { + LoadPersistentUser(); + } +} + +FString UShooterLocalPlayer::GetNickname() const +{ + FString UserNickName = Super::GetNickname(); + + if ( UserNickName.Len() > MAX_PLAYER_NAME_LENGTH ) + { + UserNickName = UserNickName.Left( MAX_PLAYER_NAME_LENGTH ) + "..."; + } + + bool bReplace = (UserNickName.Len() == 0); + + // Check for duplicate nicknames...and prevent reentry + static bool bReentry = false; + if(!bReentry) + { + bReentry = true; + UShooterGameInstance* GameInstance = GetWorld() != NULL ? Cast(GetWorld()->GetGameInstance()) : NULL; + if(GameInstance) + { + // Check all the names that occur before ours that are the same + const TArray& LocalPlayers = GameInstance->GetLocalPlayers(); + for (int i = 0; i < LocalPlayers.Num(); ++i) + { + const ULocalPlayer* LocalPlayer = LocalPlayers[i]; + if( this == LocalPlayer) + { + break; + } + + if( UserNickName == LocalPlayer->GetNickname()) + { + bReplace = true; + break; + } + } + } + bReentry = false; + } + + if ( bReplace ) + { + UserNickName = FString::Printf( TEXT( "Player%i" ), GetControllerId() + 1 ); + } + + return UserNickName; +} diff --git a/Source/ShooterGame/Private/Player/ShooterPersistentUser.cpp b/Source/ShooterGame/Private/Player/ShooterPersistentUser.cpp new file mode 100644 index 0000000..9d30484 --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterPersistentUser.cpp @@ -0,0 +1,251 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Player/ShooterPersistentUser.h" +#include "ShooterLocalPlayer.h" + +UShooterPersistentUser::UShooterPersistentUser(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SetToDefaults(); +} + +void UShooterPersistentUser::SetToDefaults() +{ + bIsDirty = false; + + bVibrationOpt = true; + bInvertedYAxis = false; + AimSensitivity = 1.0f; + Gamma = 2.2f; + BotsCount = 1; + bIsRecordingDemos = false; +} + +bool UShooterPersistentUser::IsAimSensitivityDirty() const +{ + bool bIsAimSensitivityDirty = false; + + // Fixme: UShooterPersistentUser is not setup to work with multiple worlds. + // For now, user settings are global to all world instances. + if (GEngine) + { + TArray PlayerList; + GEngine->GetAllLocalPlayerControllers(PlayerList); + + for (auto It = PlayerList.CreateIterator(); It; ++It) + { + APlayerController* PC = *It; + if (!PC || !PC->Player || !PC->PlayerInput) + { + continue; + } + + // Update key bindings for the current user only + UShooterLocalPlayer* LocalPlayer = Cast(PC->Player); + if(!LocalPlayer || LocalPlayer->GetPersistentUser() != this) + { + continue; + } + + // check if the aim sensitivity is off anywhere + for (int32 Idx = 0; Idx < PC->PlayerInput->AxisMappings.Num(); Idx++) + { + FInputAxisKeyMapping &AxisMapping = PC->PlayerInput->AxisMappings[Idx]; + if (AxisMapping.AxisName == "Lookup" || AxisMapping.AxisName == "LookupRate" || AxisMapping.AxisName == "Turn" || AxisMapping.AxisName == "TurnRate") + { + if (FMath::Abs(AxisMapping.Scale) != GetAimSensitivity()) + { + bIsAimSensitivityDirty = true; + break; + } + } + } + } + } + + return bIsAimSensitivityDirty; +} + +bool UShooterPersistentUser::IsInvertedYAxisDirty() const +{ + bool bIsInvertedYAxisDirty = false; + if (GEngine) + { + TArray PlayerList; + GEngine->GetAllLocalPlayerControllers(PlayerList); + + for (auto It = PlayerList.CreateIterator(); It; ++It) + { + APlayerController* PC = *It; + if (!PC || !PC->Player || !PC->PlayerInput) + { + continue; + } + + // Update key bindings for the current user only + UShooterLocalPlayer* LocalPlayer = Cast(PC->Player); + if(!LocalPlayer || LocalPlayer->GetPersistentUser() != this) + { + continue; + } + + bIsInvertedYAxisDirty |= PC->PlayerInput->GetInvertAxis("Lookup") != GetInvertedYAxis(); + bIsInvertedYAxisDirty |= PC->PlayerInput->GetInvertAxis("LookupRate") != GetInvertedYAxis(); + } + } + + return bIsInvertedYAxisDirty; +} + +void UShooterPersistentUser::SavePersistentUser() +{ + UGameplayStatics::SaveGameToSlot(this, SlotName, UserIndex); + bIsDirty = false; +} + +UShooterPersistentUser* UShooterPersistentUser::LoadPersistentUser(FString SlotName, const int32 UserIndex) +{ + UShooterPersistentUser* Result = nullptr; + + // first set of player signins can happen before the UWorld exists, which means no OSS, which means no user names, which means no slotnames. + // Persistent users aren't valid in this state. + if (SlotName.Len() > 0) + { + if (!GIsBuildMachine && UGameplayStatics::DoesSaveGameExist(SlotName, UserIndex)) + { + Result = Cast(UGameplayStatics::LoadGameFromSlot(SlotName, UserIndex)); + } + + if (Result == nullptr) + { + // if failed to load, create a new one + Result = Cast( UGameplayStatics::CreateSaveGameObject(UShooterPersistentUser::StaticClass()) ); + } + check(Result != nullptr); + + Result->SlotName = SlotName; + Result->UserIndex = UserIndex; + } + + return Result; +} + +void UShooterPersistentUser::SaveIfDirty() +{ + if (bIsDirty || IsInvertedYAxisDirty() || IsAimSensitivityDirty()) + { + SavePersistentUser(); + } +} + +void UShooterPersistentUser::AddMatchResult(int32 MatchKills, int32 MatchDeaths, int32 MatchBulletsFired, int32 MatchRocketsFired, bool bIsMatchWinner) +{ + Kills += MatchKills; + Deaths += MatchDeaths; + BulletsFired += MatchBulletsFired; + RocketsFired += MatchRocketsFired; + + if (bIsMatchWinner) + { + Wins++; + } + else + { + Losses++; + } + + bIsDirty = true; +} + +void UShooterPersistentUser::TellInputAboutKeybindings() +{ + TArray PlayerList; + GEngine->GetAllLocalPlayerControllers(PlayerList); + + for (auto It = PlayerList.CreateIterator(); It; ++It) + { + APlayerController* PC = *It; + if (!PC || !PC->Player || !PC->PlayerInput) + { + continue; + } + + // Update key bindings for the current user only + UShooterLocalPlayer* LocalPlayer = Cast(PC->Player); + if(!LocalPlayer || LocalPlayer->GetPersistentUser() != this) + { + continue; + } + + //set the aim sensitivity + for (int32 Idx = 0; Idx < PC->PlayerInput->AxisMappings.Num(); Idx++) + { + FInputAxisKeyMapping &AxisMapping = PC->PlayerInput->AxisMappings[Idx]; + if (AxisMapping.AxisName == "Lookup" || AxisMapping.AxisName == "LookupRate" || AxisMapping.AxisName == "Turn" || AxisMapping.AxisName == "TurnRate") + { + AxisMapping.Scale = (AxisMapping.Scale < 0.0f) ? -GetAimSensitivity() : +GetAimSensitivity(); + } + } + PC->PlayerInput->ForceRebuildingKeyMaps(); + + //invert it, and if does not equal our bool, invert it again + if (PC->PlayerInput->GetInvertAxis("LookupRate") != GetInvertedYAxis()) + { + PC->PlayerInput->InvertAxis("LookupRate"); + } + + if (PC->PlayerInput->GetInvertAxis("Lookup") != GetInvertedYAxis()) + { + PC->PlayerInput->InvertAxis("Lookup"); + } + } +} + +int32 UShooterPersistentUser::GetUserIndex() const +{ + return UserIndex; +} + +void UShooterPersistentUser::SetVibration(bool bVibration) +{ + bIsDirty |= bVibrationOpt != bVibration; + + bVibrationOpt = bVibration; + +} + +void UShooterPersistentUser::SetInvertedYAxis(bool bInvert) +{ + bIsDirty |= bInvertedYAxis != bInvert; + + bInvertedYAxis = bInvert; +} + +void UShooterPersistentUser::SetAimSensitivity(float InSensitivity) +{ + bIsDirty |= AimSensitivity != InSensitivity; + + AimSensitivity = InSensitivity; +} + +void UShooterPersistentUser::SetGamma(float InGamma) +{ + bIsDirty |= Gamma != InGamma; + + Gamma = InGamma; +} + +void UShooterPersistentUser::SetBotsCount(int32 InCount) +{ + bIsDirty |= BotsCount != InCount; + + BotsCount = InCount; +} + +void UShooterPersistentUser::SetIsRecordingDemos(const bool InbIsRecordingDemos) +{ + bIsDirty |= bIsRecordingDemos != InbIsRecordingDemos; + + bIsRecordingDemos = InbIsRecordingDemos; +} diff --git a/Source/ShooterGame/Private/Player/ShooterPlayerCameraManager.cpp b/Source/ShooterGame/Private/Player/ShooterPlayerCameraManager.cpp new file mode 100644 index 0000000..8d63534 --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterPlayerCameraManager.cpp @@ -0,0 +1,30 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Player/ShooterPlayerCameraManager.h" + +AShooterPlayerCameraManager::AShooterPlayerCameraManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + NormalFOV = 90.0f; + TargetingFOV = 60.0f; + ViewPitchMin = -87.0f; + ViewPitchMax = 87.0f; + bAlwaysApplyModifiers = true; +} + +void AShooterPlayerCameraManager::UpdateCamera(float DeltaTime) +{ + AShooterCharacter* MyPawn = PCOwner ? Cast(PCOwner->GetPawn()) : NULL; + if (MyPawn && MyPawn->IsFirstPerson()) + { + const float TargetFOV = MyPawn->IsTargeting() ? TargetingFOV : NormalFOV; + DefaultFOV = FMath::FInterpTo(DefaultFOV, TargetFOV, DeltaTime, 20.0f); + } + + Super::UpdateCamera(DeltaTime); + + if (MyPawn && MyPawn->IsFirstPerson()) + { + MyPawn->OnCameraUpdate(GetCameraLocation(), GetCameraRotation()); + } +} diff --git a/Source/ShooterGame/Private/Player/ShooterPlayerController.cpp b/Source/ShooterGame/Private/Player/ShooterPlayerController.cpp new file mode 100644 index 0000000..e4293ad --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterPlayerController.cpp @@ -0,0 +1,1419 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Player/ShooterPlayerController.h" +#include "Player/ShooterPlayerCameraManager.h" +#include "Player/ShooterCheatManager.h" +#include "Player/ShooterLocalPlayer.h" +#include "Online/ShooterPlayerState.h" +#include "Weapons/ShooterWeapon.h" +#include "UI/Menu/ShooterIngameMenu.h" +#include "UI/Style/ShooterStyle.h" +#include "UI/ShooterHUD.h" +#include "Online.h" +#include "OnlineAchievementsInterface.h" +#include "OnlineEventsInterface.h" +#include "OnlineStatsInterface.h" +#include "OnlineIdentityInterface.h" +#include "OnlineSessionInterface.h" +#include "ShooterGameInstance.h" +#include "ShooterLeaderboards.h" +#include "ShooterGameViewportClient.h" +#include "Sound/SoundNodeLocalPlayer.h" +#include "AudioThread.h" +#include "OnlineSubsystemUtils.h" + +#define ACH_FRAG_SOMEONE TEXT("ACH_FRAG_SOMEONE") +#define ACH_SOME_KILLS TEXT("ACH_SOME_KILLS") +#define ACH_LOTS_KILLS TEXT("ACH_LOTS_KILLS") +#define ACH_FINISH_MATCH TEXT("ACH_FINISH_MATCH") +#define ACH_LOTS_MATCHES TEXT("ACH_LOTS_MATCHES") +#define ACH_FIRST_WIN TEXT("ACH_FIRST_WIN") +#define ACH_LOTS_WIN TEXT("ACH_LOTS_WIN") +#define ACH_MANY_WIN TEXT("ACH_MANY_WIN") +#define ACH_SHOOT_BULLETS TEXT("ACH_SHOOT_BULLETS") +#define ACH_SHOOT_ROCKETS TEXT("ACH_SHOOT_ROCKETS") +#define ACH_GOOD_SCORE TEXT("ACH_GOOD_SCORE") +#define ACH_GREAT_SCORE TEXT("ACH_GREAT_SCORE") +#define ACH_PLAY_SANCTUARY TEXT("ACH_PLAY_SANCTUARY") +#define ACH_PLAY_HIGHRISE TEXT("ACH_PLAY_HIGHRISE") + +static const int32 SomeKillsCount = 10; +static const int32 LotsKillsCount = 20; +static const int32 LotsMatchesCount = 5; +static const int32 LotsWinsCount = 3; +static const int32 ManyWinsCount = 5; +static const int32 LotsBulletsCount = 100; +static const int32 LotsRocketsCount = 10; +static const int32 GoodScoreCount = 10; +static const int32 GreatScoreCount = 15; + +#if !defined(TRACK_STATS_LOCALLY) +#define TRACK_STATS_LOCALLY 1 +#endif + +AShooterPlayerController::AShooterPlayerController(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + PlayerCameraManagerClass = AShooterPlayerCameraManager::StaticClass(); + CheatClass = UShooterCheatManager::StaticClass(); + bAllowGameActions = true; + bGameEndedFrame = false; + LastDeathLocation = FVector::ZeroVector; + + ServerSayString = TEXT("Say"); + ShooterFriendUpdateTimer = 0.0f; + bHasSentStartEvents = false; + + StatMatchesPlayed = 0; + StatKills = 0; + StatDeaths = 0; + bHasQueriedPlatformStats = false; + bHasQueriedPlatformAchievements = false; + bHasInitializedInputComponent = false; +} + +void AShooterPlayerController::SetupInputComponent() +{ + Super::SetupInputComponent(); + if(!bHasInitializedInputComponent) + { + // UI input + InputComponent->BindAction("InGameMenu", IE_Pressed, this, &AShooterPlayerController::OnToggleInGameMenu); + InputComponent->BindAction("Scoreboard", IE_Pressed, this, &AShooterPlayerController::OnShowScoreboard); + InputComponent->BindAction("Scoreboard", IE_Released, this, &AShooterPlayerController::OnHideScoreboard); + InputComponent->BindAction("ConditionalCloseScoreboard", IE_Pressed, this, &AShooterPlayerController::OnConditionalCloseScoreboard); + InputComponent->BindAction("ToggleScoreboard", IE_Pressed, this, &AShooterPlayerController::OnToggleScoreboard); + + // voice chat + InputComponent->BindAction("PushToTalk", IE_Pressed, this, &APlayerController::StartTalking); + InputComponent->BindAction("PushToTalk", IE_Released, this, &APlayerController::StopTalking); + + InputComponent->BindAction("ToggleChat", IE_Pressed, this, &AShooterPlayerController::ToggleChatWindow); + + bHasInitializedInputComponent = true; + } +} + + +void AShooterPlayerController::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + FShooterStyle::Initialize(); + ShooterFriendUpdateTimer = 0; +} + +void AShooterPlayerController::ClearLeaderboardDelegate() +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineLeaderboardsPtr Leaderboards = OnlineSub->GetLeaderboardsInterface(); + if (Leaderboards.IsValid()) + { + Leaderboards->ClearOnLeaderboardReadCompleteDelegate_Handle(LeaderboardReadCompleteDelegateHandle); + } + } +} + +void AShooterPlayerController::TickActor(float DeltaTime, enum ELevelTick TickType, FActorTickFunction& ThisTickFunction) +{ + Super::TickActor(DeltaTime, TickType, ThisTickFunction); + + if (IsGameMenuVisible()) + { + if (ShooterFriendUpdateTimer > 0) + { + ShooterFriendUpdateTimer -= DeltaTime; + } + else + { + TSharedPtr ShooterFriends = ShooterIngameMenu->GetShooterFriends(); + ULocalPlayer* LocalPlayer = Cast(Player); + if (ShooterFriends.IsValid() && LocalPlayer && LocalPlayer->GetControllerId() >= 0) + { + ShooterFriends->UpdateFriends(LocalPlayer->GetControllerId()); + } + + // Make sure the time between calls is long enough that we won't trigger (0x80552C81) and not exceed the web api rate limit + // That value is currently 75 requests / 15 minutes. + ShooterFriendUpdateTimer = 15; + + } + } + + // Is this the first frame after the game has ended + if(bGameEndedFrame) + { + bGameEndedFrame = false; + + // ONLY PUT CODE HERE WHICH YOU DON'T WANT TO BE DONE DUE TO HOST LOSS + + // Do we need to show the end of round scoreboard? + if (IsPrimaryPlayer()) + { + AShooterHUD* ShooterHUD = GetShooterHUD(); + if (ShooterHUD) + { + ShooterHUD->ShowScoreboard(true, true); + } + } + } + + const bool bLocallyControlled = IsLocalController(); + const uint32 UniqueID = GetUniqueID(); + FAudioThread::RunCommandOnAudioThread([UniqueID, bLocallyControlled]() + { + USoundNodeLocalPlayer::GetLocallyControlledActorCache().Add(UniqueID, bLocallyControlled); + }); +}; + +void AShooterPlayerController::BeginDestroy() +{ + Super::BeginDestroy(); + ClearLeaderboardDelegate(); + + // clear any online subsystem references + ShooterIngameMenu = nullptr; + + if (!GExitPurge) + { + const uint32 UniqueID = GetUniqueID(); + FAudioThread::RunCommandOnAudioThread([UniqueID]() + { + USoundNodeLocalPlayer::GetLocallyControlledActorCache().Remove(UniqueID); + }); + } +} + +void AShooterPlayerController::SetPlayer( UPlayer* InPlayer ) +{ + Super::SetPlayer( InPlayer ); + + if (ULocalPlayer* const LocalPlayer = Cast(Player)) + { + //Build menu only after game is initialized + ShooterIngameMenu = MakeShareable(new FShooterIngameMenu()); + ShooterIngameMenu->Construct(Cast(Player)); + + FInputModeGameOnly InputMode; + SetInputMode(InputMode); + } +} + +void AShooterPlayerController::QueryAchievements() +{ + if (bHasQueriedPlatformAchievements) + { + return; + } + bHasQueriedPlatformAchievements = true; + // precache achievements + ULocalPlayer* LocalPlayer = Cast(Player); + if (LocalPlayer && LocalPlayer->GetControllerId() != -1) + { + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if(OnlineSub) + { + IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if (Identity.IsValid()) + { + TSharedPtr UserId = Identity->GetUniquePlayerId(LocalPlayer->GetControllerId()); + + if (UserId.IsValid()) + { + IOnlineAchievementsPtr Achievements = OnlineSub->GetAchievementsInterface(); + + if (Achievements.IsValid()) + { + Achievements->QueryAchievements( *UserId.Get(), FOnQueryAchievementsCompleteDelegate::CreateUObject( this, &AShooterPlayerController::OnQueryAchievementsComplete )); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("No valid user id for this controller.")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("No valid identity interface.")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("No default online subsystem.")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("No local player, cannot read achievements.")); + } +} + +void AShooterPlayerController::OnQueryAchievementsComplete(const FUniqueNetId& PlayerId, const bool bWasSuccessful ) +{ + UE_LOG(LogOnline, Display, TEXT("AShooterPlayerController::OnQueryAchievementsComplete(bWasSuccessful = %s)"), bWasSuccessful ? TEXT("TRUE") : TEXT("FALSE")); +} + +void AShooterPlayerController::OnLeaderboardReadComplete(bool bWasSuccessful) +{ + if (ReadObject.IsValid() && ReadObject->ReadState == EOnlineAsyncTaskState::Done && !bHasQueriedPlatformStats) + { + bHasQueriedPlatformStats = true; + ClearLeaderboardDelegate(); + + // We should only have one stat. + if (bWasSuccessful && ReadObject->Rows.Num() == 1) + { + FOnlineStatsRow& RowData = ReadObject->Rows[0]; + if (const FVariantData* KillData = RowData.Columns.Find(LEADERBOARD_STAT_KILLS)) + { + KillData->GetValue(StatKills); + } + + if (const FVariantData* DeathData = RowData.Columns.Find(LEADERBOARD_STAT_DEATHS)) + { + DeathData->GetValue(StatDeaths); + } + + if (const FVariantData* MatchData = RowData.Columns.Find(LEADERBOARD_STAT_MATCHESPLAYED)) + { + MatchData->GetValue(StatMatchesPlayed); + } + + UE_LOG(LogOnline, Log, TEXT("Fetched player stat data. Kills %d Deaths %d Matches %d"), StatKills, StatDeaths, StatMatchesPlayed); + } + } +} + +void AShooterPlayerController::QueryStats() +{ + ULocalPlayer* LocalPlayer = Cast(Player); + if (LocalPlayer && LocalPlayer->GetControllerId() != -1) + { + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if (Identity.IsValid()) + { + TSharedPtr UserId = Identity->GetUniquePlayerId(LocalPlayer->GetControllerId()); + + if (UserId.IsValid()) + { + IOnlineLeaderboardsPtr Leaderboards = OnlineSub->GetLeaderboardsInterface(); + if (Leaderboards.IsValid() && !bHasQueriedPlatformStats) + { + TArray> QueryPlayers; + QueryPlayers.Add(UserId.ToSharedRef()); + + LeaderboardReadCompleteDelegateHandle = Leaderboards->OnLeaderboardReadCompleteDelegates.AddUObject(this, &AShooterPlayerController::OnLeaderboardReadComplete); + ReadObject = MakeShareable(new FShooterAllTimeMatchResultsRead()); + FOnlineLeaderboardReadRef ReadObjectRef = ReadObject.ToSharedRef(); + if (Leaderboards->ReadLeaderboards(QueryPlayers, ReadObjectRef)) + { + UE_LOG(LogOnline, Log, TEXT("Started process to fetch stats for current user.")); + } + else + { + UE_LOG(LogOnline, Warning, TEXT("Could not start leaderboard fetch process. This will affect stat writes for this session.")); + } + + } + } + } + } + } +} + +void AShooterPlayerController::UnFreeze() +{ + ServerRestartPlayer(); +} + +void AShooterPlayerController::FailedToSpawnPawn() +{ + if(StateName == NAME_Inactive) + { + BeginInactiveState(); + } + Super::FailedToSpawnPawn(); +} + +void AShooterPlayerController::PawnPendingDestroy(APawn* P) +{ + LastDeathLocation = P->GetActorLocation(); + FVector CameraLocation = LastDeathLocation + FVector(0, 0, 300.0f); + FRotator CameraRotation(-90.0f, 0.0f, 0.0f); + FindDeathCameraSpot(CameraLocation, CameraRotation); + + Super::PawnPendingDestroy(P); + + ClientSetSpectatorCamera(CameraLocation, CameraRotation); +} + +void AShooterPlayerController::GameHasEnded(class AActor* EndGameFocus, bool bIsWinner) +{ + Super::GameHasEnded(EndGameFocus, bIsWinner); +} + +void AShooterPlayerController::ClientSetSpectatorCamera_Implementation(FVector CameraLocation, FRotator CameraRotation) +{ + SetInitialLocationAndRotation(CameraLocation, CameraRotation); + SetViewTarget(this); +} + +bool AShooterPlayerController::FindDeathCameraSpot(FVector& CameraLocation, FRotator& CameraRotation) +{ + const FVector PawnLocation = GetPawn()->GetActorLocation(); + FRotator ViewDir = GetControlRotation(); + ViewDir.Pitch = -45.0f; + + const float YawOffsets[] = { 0.0f, -180.0f, 90.0f, -90.0f, 45.0f, -45.0f, 135.0f, -135.0f }; + const float CameraOffset = 600.0f; + FCollisionQueryParams TraceParams(SCENE_QUERY_STAT(DeathCamera), true, GetPawn()); + + FHitResult HitResult; + for (int32 i = 0; i < UE_ARRAY_COUNT(YawOffsets); i++) + { + FRotator CameraDir = ViewDir; + CameraDir.Yaw += YawOffsets[i]; + CameraDir.Normalize(); + + const FVector TestLocation = PawnLocation - CameraDir.Vector() * CameraOffset; + + const bool bBlocked = GetWorld()->LineTraceSingleByChannel(HitResult, PawnLocation, TestLocation, ECC_Camera, TraceParams); + + if (!bBlocked) + { + CameraLocation = TestLocation; + CameraRotation = CameraDir; + return true; + } + } + + return false; +} + +bool AShooterPlayerController::ServerCheat_Validate(const FString& Msg) +{ + return true; +} + +void AShooterPlayerController::ServerCheat_Implementation(const FString& Msg) +{ + if (CheatManager) + { + ClientMessage(ConsoleCommand(Msg)); + } +} + +void AShooterPlayerController::SimulateInputKey(FKey Key, bool bPressed) +{ + InputKey(Key, bPressed ? IE_Pressed : IE_Released, 1, false); +} + +void AShooterPlayerController::OnKill() +{ + UpdateAchievementProgress(ACH_FRAG_SOMEONE, 100.0f); + + const UWorld* World = GetWorld(); + + const IOnlineEventsPtr Events = Online::GetEventsInterface(World); + const IOnlineIdentityPtr Identity = Online::GetIdentityInterface(World); + + if (Events.IsValid() && Identity.IsValid()) + { + ULocalPlayer* LocalPlayer = Cast(Player); + if (LocalPlayer) + { + int32 UserIndex = LocalPlayer->GetControllerId(); + TSharedPtr UniqueID = Identity->GetUniquePlayerId(UserIndex); + if (UniqueID.IsValid()) + { + AShooterCharacter* ShooterChar = Cast(GetCharacter()); + // If player is dead, use location stored during pawn cleanup. + FVector Location = ShooterChar ? ShooterChar->GetActorLocation() : LastDeathLocation; + AShooterWeapon* Weapon = ShooterChar ? ShooterChar->GetWeapon() : 0; + int32 WeaponType = Weapon ? (int32)Weapon->GetAmmoType() : 0; + + FOnlineEventParms Params; + + Params.Add( TEXT( "SectionId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "GameplayModeId" ), FVariantData( (int32)1 ) ); // @todo determine game mode (ffa v tdm) + Params.Add( TEXT( "DifficultyLevelId" ), FVariantData( (int32)0 ) ); // unused + + Params.Add( TEXT( "PlayerRoleId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "PlayerWeaponId" ), FVariantData( (int32)WeaponType ) ); + Params.Add( TEXT( "EnemyRoleId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "EnemyWeaponId" ), FVariantData( (int32)0 ) ); // untracked + Params.Add( TEXT( "KillTypeId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "LocationX" ), FVariantData( Location.X ) ); + Params.Add( TEXT( "LocationY" ), FVariantData( Location.Y ) ); + Params.Add( TEXT( "LocationZ" ), FVariantData( Location.Z ) ); + + Events->TriggerEvent(*UniqueID, TEXT("KillOponent"), Params); + } + } + } +} + +void AShooterPlayerController::OnDeathMessage(class AShooterPlayerState* KillerPlayerState, class AShooterPlayerState* KilledPlayerState, const UDamageType* KillerDamageType) +{ + AShooterHUD* ShooterHUD = GetShooterHUD(); + if (ShooterHUD) + { + ShooterHUD->ShowDeathMessage(KillerPlayerState, KilledPlayerState, KillerDamageType); + } + + ULocalPlayer* LocalPlayer = Cast(Player); + if (LocalPlayer && LocalPlayer->GetCachedUniqueNetId().IsValid() && KilledPlayerState->GetUniqueId().IsValid()) + { + // if this controller is the player who died, update the hero stat. + if (*LocalPlayer->GetCachedUniqueNetId() == *KilledPlayerState->GetUniqueId()) + { + const UWorld* World = GetWorld(); + const IOnlineEventsPtr Events = Online::GetEventsInterface(World); + const IOnlineIdentityPtr Identity = Online::GetIdentityInterface(World); + + if (Events.IsValid() && Identity.IsValid()) + { + const int32 UserIndex = LocalPlayer->GetControllerId(); + TSharedPtr UniqueID = Identity->GetUniquePlayerId(UserIndex); + if (UniqueID.IsValid()) + { + AShooterCharacter* ShooterChar = Cast(GetCharacter()); + AShooterWeapon* Weapon = ShooterChar ? ShooterChar->GetWeapon() : NULL; + + FVector Location = ShooterChar ? ShooterChar->GetActorLocation() : FVector::ZeroVector; + int32 WeaponType = Weapon ? (int32)Weapon->GetAmmoType() : 0; + + FOnlineEventParms Params; + Params.Add( TEXT( "SectionId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "GameplayModeId" ), FVariantData( (int32)1 ) ); // @todo determine game mode (ffa v tdm) + Params.Add( TEXT( "DifficultyLevelId" ), FVariantData( (int32)0 ) ); // unused + + Params.Add( TEXT( "PlayerRoleId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "PlayerWeaponId" ), FVariantData( (int32)WeaponType ) ); + Params.Add( TEXT( "EnemyRoleId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "EnemyWeaponId" ), FVariantData( (int32)0 ) ); // untracked + + Params.Add( TEXT( "LocationX" ), FVariantData( Location.X ) ); + Params.Add( TEXT( "LocationY" ), FVariantData( Location.Y ) ); + Params.Add( TEXT( "LocationZ" ), FVariantData( Location.Z ) ); + + Events->TriggerEvent(*UniqueID, TEXT("PlayerDeath"), Params); + } + } + } + } +} + +void AShooterPlayerController::UpdateAchievementProgress( const FString& Id, float Percent ) +{ + ULocalPlayer* LocalPlayer = Cast(Player); + if (LocalPlayer) + { + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if(OnlineSub) + { + IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if (Identity.IsValid()) + { + FUniqueNetIdRepl UserId = LocalPlayer->GetCachedUniqueNetId(); + + if (UserId.IsValid()) + { + + IOnlineAchievementsPtr Achievements = OnlineSub->GetAchievementsInterface(); + if (Achievements.IsValid() && (!WriteObject.IsValid() || WriteObject->WriteState != EOnlineAsyncTaskState::InProgress)) + { + WriteObject = MakeShareable(new FOnlineAchievementsWrite()); + WriteObject->SetFloatStat(*Id, Percent); + + FOnlineAchievementsWriteRef WriteObjectRef = WriteObject.ToSharedRef(); + Achievements->WriteAchievements(*UserId, WriteObjectRef); + } + else + { + UE_LOG(LogOnline, Warning, TEXT("No valid achievement interface or another write is in progress.")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("No valid user id for this controller.")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("No valid identity interface.")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("No default online subsystem.")); + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("No local player, cannot update achievements.")); + } +} + +void AShooterPlayerController::OnToggleInGameMenu() +{ + if( GEngine->GameViewport == nullptr ) + { + return; + } + + // this is not ideal, but necessary to prevent both players from pausing at the same time on the same frame + UWorld* GameWorld = GEngine->GameViewport->GetWorld(); + + for(auto It = GameWorld->GetControllerIterator(); It; ++It) + { + AShooterPlayerController* Controller = Cast(*It); + if(Controller && Controller->IsPaused()) + { + return; + } + } + + // if no one's paused, pause + if (ShooterIngameMenu.IsValid()) + { + ShooterIngameMenu->ToggleGameMenu(); + } +} + +void AShooterPlayerController::OnConditionalCloseScoreboard() +{ + AShooterHUD* ShooterHUD = GetShooterHUD(); + if(ShooterHUD && ( ShooterHUD->IsMatchOver() == false )) + { + ShooterHUD->ConditionalCloseScoreboard(); + } +} + +void AShooterPlayerController::OnToggleScoreboard() +{ + AShooterHUD* ShooterHUD = GetShooterHUD(); + if(ShooterHUD && ( ShooterHUD->IsMatchOver() == false )) + { + ShooterHUD->ToggleScoreboard(); + } +} + +void AShooterPlayerController::OnShowScoreboard() +{ + AShooterHUD* ShooterHUD = GetShooterHUD(); + if(ShooterHUD) + { + ShooterHUD->ShowScoreboard(true); + } +} + +void AShooterPlayerController::OnHideScoreboard() +{ + AShooterHUD* ShooterHUD = GetShooterHUD(); + // If have a valid match and the match is over - hide the scoreboard + if( (ShooterHUD != NULL ) && ( ShooterHUD->IsMatchOver() == false ) ) + { + ShooterHUD->ShowScoreboard(false); + } +} + +bool AShooterPlayerController::IsGameMenuVisible() const +{ + bool Result = false; + if (ShooterIngameMenu.IsValid()) + { + Result = ShooterIngameMenu->GetIsGameMenuUp(); + } + + return Result; +} + +void AShooterPlayerController::SetInfiniteAmmo(bool bEnable) +{ + bInfiniteAmmo = bEnable; +} + +void AShooterPlayerController::SetInfiniteClip(bool bEnable) +{ + bInfiniteClip = bEnable; +} + +void AShooterPlayerController::SetHealthRegen(bool bEnable) +{ + bHealthRegen = bEnable; +} + +void AShooterPlayerController::SetGodMode(bool bEnable) +{ + bGodMode = bEnable; +} + +void AShooterPlayerController::SetIsVibrationEnabled(bool bEnable) +{ + bIsVibrationEnabled = bEnable; +} + +void AShooterPlayerController::ClientGameStarted_Implementation() +{ + bAllowGameActions = true; + + // Enable controls mode now the game has started + SetIgnoreMoveInput(false); + + AShooterHUD* ShooterHUD = GetShooterHUD(); + if (ShooterHUD) + { + ShooterHUD->SetMatchState(EShooterMatchState::Playing); + ShooterHUD->ShowScoreboard(false); + } + bGameEndedFrame = false; + + + QueryAchievements(); + + QueryStats(); + + + const UWorld* World = GetWorld(); + + // Send round start event + const IOnlineEventsPtr Events = Online::GetEventsInterface(World); + ULocalPlayer* LocalPlayer = Cast(Player); + + if(LocalPlayer != nullptr && World != nullptr && Events.IsValid()) + { + FUniqueNetIdRepl UniqueId = LocalPlayer->GetPreferredUniqueNetId(); + + if (UniqueId.IsValid()) + { + // Generate a new session id + Events->SetPlayerSessionId(*UniqueId, FGuid::NewGuid()); + + FString MapName = *FPackageName::GetShortName(World->PersistentLevel->GetOutermost()->GetName()); + + // Fire session start event for all cases + FOnlineEventParms Params; + Params.Add( TEXT( "GameplayModeId" ), FVariantData( (int32)1 ) ); // @todo determine game mode (ffa v tdm) + Params.Add( TEXT( "DifficultyLevelId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "MapName" ), FVariantData( MapName ) ); + + Events->TriggerEvent(*UniqueId, TEXT("PlayerSessionStart"), Params); + + // Online matches require the MultiplayerRoundStart event as well + UShooterGameInstance* SGI = Cast(World->GetGameInstance()); + + if (SGI && (SGI->GetOnlineMode() == EOnlineMode::Online)) + { + FOnlineEventParms MultiplayerParams; + + // @todo: fill in with real values + MultiplayerParams.Add( TEXT( "SectionId" ), FVariantData( (int32)0 ) ); // unused + MultiplayerParams.Add( TEXT( "GameplayModeId" ), FVariantData( (int32)1 ) ); // @todo determine game mode (ffa v tdm) + MultiplayerParams.Add( TEXT( "MatchTypeId" ), FVariantData( (int32)1 ) ); // @todo abstract the specific meaning of this value across platforms + MultiplayerParams.Add( TEXT( "DifficultyLevelId" ), FVariantData( (int32)0 ) ); // unused + + Events->TriggerEvent(*UniqueId, TEXT("MultiplayerRoundStart"), MultiplayerParams); + } + + bHasSentStartEvents = true; + } + } +} + +/** Starts the online game using the session name in the PlayerState */ +void AShooterPlayerController::ClientStartOnlineGame_Implementation() +{ + if (!IsPrimaryPlayer()) + return; + + AShooterPlayerState* ShooterPlayerState = Cast(PlayerState); + if (ShooterPlayerState) + { + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && (Sessions->GetNamedSession(ShooterPlayerState->SessionName) != nullptr)) + { + UE_LOG(LogOnline, Log, TEXT("Starting session %s on client"), *ShooterPlayerState->SessionName.ToString() ); + Sessions->StartSession(ShooterPlayerState->SessionName); + } + } + } + else + { + // Keep retrying until player state is replicated + GetWorld()->GetTimerManager().SetTimer(TimerHandle_ClientStartOnlineGame, this, &AShooterPlayerController::ClientStartOnlineGame_Implementation, 0.2f, false); + } +} + +/** Ends the online game using the session name in the PlayerState */ +void AShooterPlayerController::ClientEndOnlineGame_Implementation() +{ + if (!IsPrimaryPlayer()) + return; + + AShooterPlayerState* ShooterPlayerState = Cast(PlayerState); + if (ShooterPlayerState) + { + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid() && (Sessions->GetNamedSession(ShooterPlayerState->SessionName) != nullptr)) + { + UE_LOG(LogOnline, Log, TEXT("Ending session %s on client"), *ShooterPlayerState->SessionName.ToString() ); + Sessions->EndSession(ShooterPlayerState->SessionName); + } + } + } +} + +void AShooterPlayerController::HandleReturnToMainMenu() +{ + OnHideScoreboard(); + CleanupSessionOnReturnToMenu(); +} + +// Note: Replaced unused, deprecated ClientReturnToMainMenu_Implementation +void AShooterPlayerController::ClientReturnToMainMenuWithTextReason_Implementation(const FText& ReturnReason) +{ + const UWorld* World = GetWorld(); + UShooterGameInstance* SGI = World != NULL ? Cast(World->GetGameInstance()) : NULL; + + if ( !ensure( SGI != NULL ) ) + { + return; + } + + if ( GetNetMode() == NM_Client ) + { + const FText OKButton = NSLOCTEXT( "DialogButtons", "OKAY", "OK" ); + SGI->ShowMessageThenGotoState( ReturnReason, OKButton, FText::GetEmpty(), ShooterGameInstanceState::MainMenu ); + } + else + { + SGI->GotoState(ShooterGameInstanceState::MainMenu); + } + + // Clear the flag so we don't do normal end of round stuff next + bGameEndedFrame = false; +} + +/** Ends and/or destroys game session */ +void AShooterPlayerController::CleanupSessionOnReturnToMenu() +{ + const UWorld* World = GetWorld(); + UShooterGameInstance * SGI = World != NULL ? Cast( World->GetGameInstance() ) : NULL; + + if ( ensure( SGI != NULL ) ) + { + SGI->CleanupSessionOnReturnToMenu(); + } +} + +void AShooterPlayerController::ClientGameEnded_Implementation(class AActor* EndGameFocus, bool bIsWinner) +{ + Super::ClientGameEnded_Implementation(EndGameFocus, bIsWinner); + + // Disable controls now the game has ended + SetIgnoreMoveInput(true); + + bAllowGameActions = false; + + // Make sure that we still have valid view target + SetViewTarget(GetPawn()); + + AShooterHUD* ShooterHUD = GetShooterHUD(); + if (ShooterHUD) + { + ShooterHUD->SetMatchState(bIsWinner ? EShooterMatchState::Won : EShooterMatchState::Lost); + } + + UpdateSaveFileOnGameEnd(bIsWinner); + UpdateAchievementsOnGameEnd(); + UpdateLeaderboardsOnGameEnd(); + UpdateStatsOnGameEnd(bIsWinner); + + // Flag that the game has just ended (if it's ended due to host loss we want to wait for ClientReturnToMainMenu_Implementation first, incase we don't want to process) + bGameEndedFrame = true; +} + +void AShooterPlayerController::ClientSendRoundEndEvent_Implementation(bool bIsWinner, int32 ExpendedTimeInSeconds) +{ + const UWorld* World = GetWorld(); + const IOnlineEventsPtr Events = Online::GetEventsInterface(World); + ULocalPlayer* LocalPlayer = Cast(Player); + + if(bHasSentStartEvents && LocalPlayer != nullptr && World != nullptr && Events.IsValid()) + { + FUniqueNetIdRepl UniqueId = LocalPlayer->GetPreferredUniqueNetId(); + + if (UniqueId.IsValid()) + { + FString MapName = *FPackageName::GetShortName(World->PersistentLevel->GetOutermost()->GetName()); + AShooterPlayerState* ShooterPlayerState = Cast(PlayerState); + int32 PlayerScore = ShooterPlayerState ? ShooterPlayerState->GetScore() : 0; + int32 PlayerDeaths = ShooterPlayerState ? ShooterPlayerState->GetDeaths() : 0; + int32 PlayerKills = ShooterPlayerState ? ShooterPlayerState->GetKills() : 0; + + // Fire session end event for all cases + FOnlineEventParms Params; + Params.Add( TEXT( "GameplayModeId" ), FVariantData( (int32)1 ) ); // @todo determine game mode (ffa v tdm) + Params.Add( TEXT( "DifficultyLevelId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "ExitStatusId" ), FVariantData( (int32)0 ) ); // unused + Params.Add( TEXT( "PlayerScore" ), FVariantData( (int32)PlayerScore ) ); + Params.Add( TEXT( "PlayerWon" ), FVariantData( (bool)bIsWinner ) ); + Params.Add( TEXT( "MapName" ), FVariantData( MapName ) ); + Params.Add( TEXT( "MapNameString" ), FVariantData( MapName ) ); // @todo workaround for a bug in backend service, remove when fixed + + Events->TriggerEvent(*UniqueId, TEXT("PlayerSessionEnd"), Params); + + // Update all time results + FOnlineEventParms AllTimeMatchParams; + AllTimeMatchParams.Add(TEXT("ShooterAllTimeMatchResultsScore"), FVariantData((uint64)PlayerScore)); + AllTimeMatchParams.Add(TEXT("ShooterAllTimeMatchResultsDeaths"), FVariantData((int32)PlayerDeaths)); + AllTimeMatchParams.Add(TEXT("ShooterAllTimeMatchResultsFrags"), FVariantData((int32)PlayerKills)); + AllTimeMatchParams.Add(TEXT("ShooterAllTimeMatchResultsMatchesPlayed"), FVariantData((int32)1)); + + Events->TriggerEvent(*UniqueId, TEXT("ShooterAllTimeMatchResults"), AllTimeMatchParams); + + // Online matches require the MultiplayerRoundEnd event as well + UShooterGameInstance* SGI = Cast(World->GetGameInstance()); + if (SGI && (SGI->GetOnlineMode() == EOnlineMode::Online)) + { + FOnlineEventParms MultiplayerParams; + MultiplayerParams.Add( TEXT( "SectionId" ), FVariantData( (int32)0 ) ); // unused + MultiplayerParams.Add( TEXT( "GameplayModeId" ), FVariantData( (int32)1 ) ); // @todo determine game mode (ffa v tdm) + MultiplayerParams.Add( TEXT( "MatchTypeId" ), FVariantData( (int32)1 ) ); // @todo abstract the specific meaning of this value across platforms + MultiplayerParams.Add( TEXT( "DifficultyLevelId" ), FVariantData( (int32)0 ) ); // unused + MultiplayerParams.Add( TEXT( "TimeInSeconds" ), FVariantData( (float)ExpendedTimeInSeconds ) ); + MultiplayerParams.Add( TEXT( "ExitStatusId" ), FVariantData( (int32)0 ) ); // unused + + Events->TriggerEvent(*UniqueId, TEXT("MultiplayerRoundEnd"), MultiplayerParams); + } + } + + bHasSentStartEvents = false; + } +} + +void AShooterPlayerController::SetCinematicMode(bool bInCinematicMode, bool bHidePlayer, bool bAffectsHUD, bool bAffectsMovement, bool bAffectsTurning) +{ + Super::SetCinematicMode(bInCinematicMode, bHidePlayer, bAffectsHUD, bAffectsMovement, bAffectsTurning); + + // If we have a pawn we need to determine if we should show/hide the weapon + AShooterCharacter* MyPawn = Cast(GetPawn()); + AShooterWeapon* MyWeapon = MyPawn ? MyPawn->GetWeapon() : NULL; + if (MyWeapon) + { + if (bInCinematicMode && bHidePlayer) + { + MyWeapon->SetActorHiddenInGame(true); + } + else if (!bCinematicMode) + { + MyWeapon->SetActorHiddenInGame(false); + } + } +} + +bool AShooterPlayerController::IsMoveInputIgnored() const +{ + if (IsInState(NAME_Spectating)) + { + return false; + } + else + { + return Super::IsMoveInputIgnored(); + } +} + +bool AShooterPlayerController::IsLookInputIgnored() const +{ + if (IsInState(NAME_Spectating)) + { + return false; + } + else + { + return Super::IsLookInputIgnored(); + } +} + +void AShooterPlayerController::InitInputSystem() +{ + Super::InitInputSystem(); + + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + PersistentUser->TellInputAboutKeybindings(); + } +} + +void AShooterPlayerController::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const +{ + Super::GetLifetimeReplicatedProps( OutLifetimeProps ); + + DOREPLIFETIME_CONDITION( AShooterPlayerController, bInfiniteAmmo, COND_OwnerOnly ); + DOREPLIFETIME_CONDITION( AShooterPlayerController, bInfiniteClip, COND_OwnerOnly ); + + DOREPLIFETIME(AShooterPlayerController, bHealthRegen); +} + +void AShooterPlayerController::Suicide() +{ + if ( IsInState(NAME_Playing) ) + { + ServerSuicide(); + } +} + +bool AShooterPlayerController::ServerSuicide_Validate() +{ + return true; +} + +void AShooterPlayerController::ServerSuicide_Implementation() +{ + if ( (GetPawn() != NULL) && ((GetWorld()->TimeSeconds - GetPawn()->CreationTime > 1) || (GetNetMode() == NM_Standalone)) ) + { + AShooterCharacter* MyPawn = Cast(GetPawn()); + if (MyPawn) + { + MyPawn->Suicide(); + } + } +} + +bool AShooterPlayerController::HasInfiniteAmmo() const +{ + return bInfiniteAmmo; +} + +bool AShooterPlayerController::HasInfiniteClip() const +{ + return bInfiniteClip; +} + +bool AShooterPlayerController::HasHealthRegen() const +{ + return bHealthRegen; +} + +bool AShooterPlayerController::HasGodMode() const +{ + return bGodMode; +} + +bool AShooterPlayerController::IsVibrationEnabled() const +{ + return bIsVibrationEnabled; +} + +bool AShooterPlayerController::IsGameInputAllowed() const +{ + return bAllowGameActions && !bCinematicMode; +} + +void AShooterPlayerController::ToggleChatWindow() +{ + AShooterHUD* ShooterHUD = Cast(GetHUD()); + if (ShooterHUD) + { + ShooterHUD->ToggleChat(); + } +} + +void AShooterPlayerController::ClientTeamMessage_Implementation( APlayerState* SenderPlayerState, const FString& S, FName Type, float MsgLifeTime ) +{ + AShooterHUD* ShooterHUD = Cast(GetHUD()); + if (ShooterHUD) + { + if( Type == ServerSayString ) + { + if( SenderPlayerState != PlayerState ) + { + ShooterHUD->AddChatLine(FText::FromString(S), false); + } + } + } +} + +void AShooterPlayerController::Say( const FString& Msg ) +{ + ServerSay(Msg.Left(128)); +} + +bool AShooterPlayerController::ServerSay_Validate( const FString& Msg ) +{ + return true; +} + +void AShooterPlayerController::ServerSay_Implementation( const FString& Msg ) +{ + GetWorld()->GetAuthGameMode()->Broadcast(this, Msg, ServerSayString); +} + +AShooterHUD* AShooterPlayerController::GetShooterHUD() const +{ + return Cast(GetHUD()); +} + + +UShooterPersistentUser* AShooterPlayerController::GetPersistentUser() const +{ + UShooterLocalPlayer* const ShooterLocalPlayer = Cast(Player); + return ShooterLocalPlayer ? ShooterLocalPlayer->GetPersistentUser() : nullptr; +} + +bool AShooterPlayerController::SetPause(bool bPause, FCanUnpause CanUnpauseDelegate /*= FCanUnpause()*/) +{ + const bool Result = APlayerController::SetPause(bPause, CanUnpauseDelegate); + + // Update rich presence. + const UWorld* World = GetWorld(); + const IOnlinePresencePtr PresenceInterface = Online::GetPresenceInterface(World); + const IOnlineEventsPtr Events = Online::GetEventsInterface(World); + const ULocalPlayer* LocalPlayer = Cast(Player); + FUniqueNetIdRepl UserId = LocalPlayer ? LocalPlayer->GetCachedUniqueNetId() : FUniqueNetIdRepl(); + + // Don't send pause events while online since the game doesn't actually pause + if(GetNetMode() == NM_Standalone && Events.IsValid() && PlayerState->GetUniqueId().IsValid()) + { + FOnlineEventParms Params; + Params.Add( TEXT( "GameplayModeId" ), FVariantData( (int32)1 ) ); // @todo determine game mode (ffa v tdm) + Params.Add( TEXT( "DifficultyLevelId" ), FVariantData( (int32)0 ) ); // unused + if(Result && bPause) + { + Events->TriggerEvent(*PlayerState->GetUniqueId(), TEXT("PlayerSessionPause"), Params); + } + else + { + Events->TriggerEvent(*PlayerState->GetUniqueId(), TEXT("PlayerSessionResume"), Params); + } + } + + return Result; +} + +FVector AShooterPlayerController::GetFocalLocation() const +{ + const AShooterCharacter* ShooterCharacter = Cast(GetPawn()); + + // On death we want to use the player's death cam location rather than the location of where the pawn is at the moment + // This guarantees that the clients see their death cam correctly, as their pawns have delayed destruction. + if (ShooterCharacter && ShooterCharacter->bIsDying) + { + return GetSpawnLocation(); + } + + return Super::GetFocalLocation(); +} + +void AShooterPlayerController::ShowInGameMenu() +{ + AShooterHUD* ShooterHUD = GetShooterHUD(); + if(ShooterIngameMenu.IsValid() && !ShooterIngameMenu->GetIsGameMenuUp() && ShooterHUD && (ShooterHUD->IsMatchOver() == false)) + { + ShooterIngameMenu->ToggleGameMenu(); + } +} +void AShooterPlayerController::UpdateAchievementsOnGameEnd() +{ + ULocalPlayer* LocalPlayer = Cast(Player); + if (LocalPlayer) + { + AShooterPlayerState* ShooterPlayerState = Cast(PlayerState); + if (ShooterPlayerState) + { + const UShooterPersistentUser* PersistentUser = GetPersistentUser(); + + if (PersistentUser) + { + const int32 Wins = PersistentUser->GetWins(); + const int32 Losses = PersistentUser->GetLosses(); + const int32 Matches = Wins + Losses; + + const int32 TotalKills = PersistentUser->GetKills(); + const int32 MatchScore = (int32)ShooterPlayerState->GetScore(); + + const int32 TotalBulletsFired = PersistentUser->GetBulletsFired(); + const int32 TotalRocketsFired = PersistentUser->GetRocketsFired(); + + float TotalGameAchievement = 0; + float CurrentGameAchievement = 0; + + /////////////////////////////////////// + // Kill achievements + if (TotalKills >= 1) + { + CurrentGameAchievement += 100.0f; + } + TotalGameAchievement += 100; + + { + float fSomeKillPct = ((float)TotalKills / (float)SomeKillsCount) * 100.0f; + fSomeKillPct = FMath::RoundToFloat(fSomeKillPct); + UpdateAchievementProgress(ACH_SOME_KILLS, fSomeKillPct); + + CurrentGameAchievement += FMath::Min(fSomeKillPct, 100.0f); + TotalGameAchievement += 100; + } + + { + float fLotsKillPct = ((float)TotalKills / (float)LotsKillsCount) * 100.0f; + fLotsKillPct = FMath::RoundToFloat(fLotsKillPct); + UpdateAchievementProgress(ACH_LOTS_KILLS, fLotsKillPct); + + CurrentGameAchievement += FMath::Min(fLotsKillPct, 100.0f); + TotalGameAchievement += 100; + } + /////////////////////////////////////// + + /////////////////////////////////////// + // Match Achievements + { + UpdateAchievementProgress(ACH_FINISH_MATCH, 100.0f); + + CurrentGameAchievement += 100; + TotalGameAchievement += 100; + } + + { + float fLotsRoundsPct = ((float)Matches / (float)LotsMatchesCount) * 100.0f; + fLotsRoundsPct = FMath::RoundToFloat(fLotsRoundsPct); + UpdateAchievementProgress(ACH_LOTS_MATCHES, fLotsRoundsPct); + + CurrentGameAchievement += FMath::Min(fLotsRoundsPct, 100.0f); + TotalGameAchievement += 100; + } + /////////////////////////////////////// + + /////////////////////////////////////// + // Win Achievements + if (Wins >= 1) + { + UpdateAchievementProgress(ACH_FIRST_WIN, 100.0f); + + CurrentGameAchievement += 100.0f; + } + TotalGameAchievement += 100; + + { + float fLotsWinPct = ((float)Wins / (float)LotsWinsCount) * 100.0f; + fLotsWinPct = FMath::RoundToInt(fLotsWinPct); + UpdateAchievementProgress(ACH_LOTS_WIN, fLotsWinPct); + + CurrentGameAchievement += FMath::Min(fLotsWinPct, 100.0f); + TotalGameAchievement += 100; + } + + { + float fManyWinPct = ((float)Wins / (float)ManyWinsCount) * 100.0f; + fManyWinPct = FMath::RoundToInt(fManyWinPct); + UpdateAchievementProgress(ACH_MANY_WIN, fManyWinPct); + + CurrentGameAchievement += FMath::Min(fManyWinPct, 100.0f); + TotalGameAchievement += 100; + } + /////////////////////////////////////// + + /////////////////////////////////////// + // Ammo Achievements + { + float fLotsBulletsPct = ((float)TotalBulletsFired / (float)LotsBulletsCount) * 100.0f; + fLotsBulletsPct = FMath::RoundToFloat(fLotsBulletsPct); + UpdateAchievementProgress(ACH_SHOOT_BULLETS, fLotsBulletsPct); + + CurrentGameAchievement += FMath::Min(fLotsBulletsPct, 100.0f); + TotalGameAchievement += 100; + } + + { + float fLotsRocketsPct = ((float)TotalRocketsFired / (float)LotsRocketsCount) * 100.0f; + fLotsRocketsPct = FMath::RoundToFloat(fLotsRocketsPct); + UpdateAchievementProgress(ACH_SHOOT_ROCKETS, fLotsRocketsPct); + + CurrentGameAchievement += FMath::Min(fLotsRocketsPct, 100.0f); + TotalGameAchievement += 100; + } + /////////////////////////////////////// + + /////////////////////////////////////// + // Score Achievements + { + float fGoodScorePct = ((float)MatchScore / (float)GoodScoreCount) * 100.0f; + fGoodScorePct = FMath::RoundToFloat(fGoodScorePct); + UpdateAchievementProgress(ACH_GOOD_SCORE, fGoodScorePct); + } + + { + float fGreatScorePct = ((float)MatchScore / (float)GreatScoreCount) * 100.0f; + fGreatScorePct = FMath::RoundToFloat(fGreatScorePct); + UpdateAchievementProgress(ACH_GREAT_SCORE, fGreatScorePct); + } + /////////////////////////////////////// + + /////////////////////////////////////// + // Map Play Achievements + UWorld* World = GetWorld(); + if (World) + { + FString MapName = *FPackageName::GetShortName(World->PersistentLevel->GetOutermost()->GetName()); + if (MapName.Find(TEXT("Highrise")) != -1) + { + UpdateAchievementProgress(ACH_PLAY_HIGHRISE, 100.0f); + } + else if (MapName.Find(TEXT("Sanctuary")) != -1) + { + UpdateAchievementProgress(ACH_PLAY_SANCTUARY, 100.0f); + } + } + /////////////////////////////////////// + + const IOnlineEventsPtr Events = Online::GetEventsInterface(World); + const IOnlineIdentityPtr Identity = Online::GetIdentityInterface(World); + + if (Events.IsValid() && Identity.IsValid()) + { + const int32 UserIndex = LocalPlayer->GetControllerId(); + TSharedPtr UniqueID = Identity->GetUniquePlayerId(UserIndex); + if (UniqueID.IsValid()) + { + FOnlineEventParms Params; + + float fGamePct = (CurrentGameAchievement / TotalGameAchievement) * 100.0f; + fGamePct = FMath::RoundToFloat(fGamePct); + Params.Add( TEXT( "CompletionPercent" ), FVariantData( (float)fGamePct ) ); + if (UniqueID.IsValid()) + { + Events->TriggerEvent(*UniqueID, TEXT("GameProgress"), Params); + } + } + } + } + } + } +} + +void AShooterPlayerController::UpdateLeaderboardsOnGameEnd() +{ + UShooterLocalPlayer* LocalPlayer = Cast(Player); + if (LocalPlayer) + { + // update leaderboards - note this does not respect existing scores and overwrites them. We would first need to read the leaderboards if we wanted to do that. + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if (Identity.IsValid()) + { + TSharedPtr UserId = Identity->GetUniquePlayerId(LocalPlayer->GetControllerId()); + if (UserId.IsValid()) + { + IOnlineLeaderboardsPtr Leaderboards = OnlineSub->GetLeaderboardsInterface(); + if (Leaderboards.IsValid()) + { + AShooterPlayerState* ShooterPlayerState = Cast(PlayerState); + if (ShooterPlayerState) + { + FShooterAllTimeMatchResultsWrite ResultsWriteObject; + int32 MatchWriteData = 1; + int32 KillsWriteData = ShooterPlayerState->GetKills(); + int32 DeathsWriteData = ShooterPlayerState->GetDeaths(); + +#if TRACK_STATS_LOCALLY + StatMatchesPlayed = (MatchWriteData += StatMatchesPlayed); + StatKills = (KillsWriteData += StatKills); + StatDeaths = (DeathsWriteData += StatDeaths); +#endif + + ResultsWriteObject.SetIntStat(LEADERBOARD_STAT_SCORE, KillsWriteData); + ResultsWriteObject.SetIntStat(LEADERBOARD_STAT_KILLS, KillsWriteData); + ResultsWriteObject.SetIntStat(LEADERBOARD_STAT_DEATHS, DeathsWriteData); + ResultsWriteObject.SetIntStat(LEADERBOARD_STAT_MATCHESPLAYED, MatchWriteData); + + // the call will copy the user id and write object to its own memory + Leaderboards->WriteLeaderboards(ShooterPlayerState->SessionName, *UserId, ResultsWriteObject); + Leaderboards->FlushLeaderboards(TEXT("SHOOTERGAME")); + } + } + } + } + } + } +} + +void AShooterPlayerController::UpdateStatsOnGameEnd(bool bIsWinner) +{ + const IOnlineStatsPtr Stats = Online::GetStatsInterface(GetWorld()); + ULocalPlayer* LocalPlayer = Cast(Player); + AShooterPlayerState* ShooterPlayerState = Cast(PlayerState); + + if (Stats.IsValid() && LocalPlayer != nullptr && ShooterPlayerState != nullptr) + { + FUniqueNetIdRepl UniqueId = LocalPlayer->GetCachedUniqueNetId(); + + if (UniqueId.IsValid() ) + { + TArray UpdatedUserStats; + + FOnlineStatsUserUpdatedStats& UpdatedStats = UpdatedUserStats.Emplace_GetRef( UniqueId.GetUniqueNetId().ToSharedRef() ); + UpdatedStats.Stats.Add( TEXT("Kills"), FOnlineStatUpdate( ShooterPlayerState->GetKills(), FOnlineStatUpdate::EOnlineStatModificationType::Sum ) ); + UpdatedStats.Stats.Add( TEXT("Deaths"), FOnlineStatUpdate( ShooterPlayerState->GetDeaths(), FOnlineStatUpdate::EOnlineStatModificationType::Sum ) ); + UpdatedStats.Stats.Add( TEXT("RoundsPlayed"), FOnlineStatUpdate( 1, FOnlineStatUpdate::EOnlineStatModificationType::Sum ) ); + if (bIsWinner) + { + UpdatedStats.Stats.Add( TEXT("RoundsWon"), FOnlineStatUpdate( 1, FOnlineStatUpdate::EOnlineStatModificationType::Sum ) ); + } + + Stats->UpdateStats( UniqueId.GetUniqueNetId().ToSharedRef(), UpdatedUserStats, FOnlineStatsUpdateStatsComplete() ); + } + } +} + + +void AShooterPlayerController::UpdateSaveFileOnGameEnd(bool bIsWinner) +{ + AShooterPlayerState* ShooterPlayerState = Cast(PlayerState); + if (ShooterPlayerState) + { + // update local saved profile + UShooterPersistentUser* const PersistentUser = GetPersistentUser(); + if (PersistentUser) + { + PersistentUser->AddMatchResult(ShooterPlayerState->GetKills(), ShooterPlayerState->GetDeaths(), ShooterPlayerState->GetNumBulletsFired(), ShooterPlayerState->GetNumRocketsFired(), bIsWinner); + PersistentUser->SaveIfDirty(); + } + } +} + +void AShooterPlayerController::PreClientTravel(const FString& PendingURL, ETravelType TravelType, bool bIsSeamlessTravel) +{ + Super::PreClientTravel( PendingURL, TravelType, bIsSeamlessTravel ); + + if (const UWorld* World = GetWorld()) + { + UShooterGameViewportClient* ShooterViewport = Cast( World->GetGameViewport() ); + + if ( ShooterViewport != NULL ) + { + ShooterViewport->ShowLoadingScreen(); + } + + AShooterHUD* ShooterHUD = Cast(GetHUD()); + if (ShooterHUD != nullptr) + { + // Passing true to bFocus here ensures that focus is returned to the game viewport. + ShooterHUD->ShowScoreboard(false, true); + } + } +} diff --git a/Source/ShooterGame/Private/Player/ShooterPlayerController_Menu.cpp b/Source/ShooterGame/Private/Player/ShooterPlayerController_Menu.cpp new file mode 100644 index 0000000..8b89306 --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterPlayerController_Menu.cpp @@ -0,0 +1,17 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Player/ShooterPlayerController_Menu.h" +#include "ShooterStyle.h" + + +AShooterPlayerController_Menu::AShooterPlayerController_Menu(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ +} + +void AShooterPlayerController_Menu::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + + FShooterStyle::Initialize(); +} diff --git a/Source/ShooterGame/Private/Player/ShooterSpectatorPawn.cpp b/Source/ShooterGame/Private/Player/ShooterSpectatorPawn.cpp new file mode 100644 index 0000000..7052a67 --- /dev/null +++ b/Source/ShooterGame/Private/Player/ShooterSpectatorPawn.cpp @@ -0,0 +1,30 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "ShooterGame.h" +#include "Player/ShooterSpectatorPawn.h" + +AShooterSpectatorPawn::AShooterSpectatorPawn(const FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ + bReplicates = false; +} + +void AShooterSpectatorPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) +{ + check(PlayerInputComponent); + + PlayerInputComponent->BindAxis("MoveForward", this, &ADefaultPawn::MoveForward); + PlayerInputComponent->BindAxis("MoveRight", this, &ADefaultPawn::MoveRight); + PlayerInputComponent->BindAxis("MoveUp", this, &ADefaultPawn::MoveUp_World); + PlayerInputComponent->BindAxis("Turn", this, &ADefaultPawn::AddControllerYawInput); + PlayerInputComponent->BindAxis("TurnRate", this, &ADefaultPawn::TurnAtRate); + PlayerInputComponent->BindAxis("LookUp", this, &ADefaultPawn::AddControllerPitchInput); + PlayerInputComponent->BindAxis("LookUpRate", this, &AShooterSpectatorPawn::LookUpAtRate); +} + +void AShooterSpectatorPawn::LookUpAtRate(float Val) +{ + // calculate delta for this frame from the rate information + AddControllerPitchInput(Val * BaseLookUpRate * GetWorld()->GetDeltaSeconds() * CustomTimeDilation); +} diff --git a/Source/ShooterGame/Private/ShooterEngine.cpp b/Source/ShooterGame/Private/ShooterEngine.cpp new file mode 100644 index 0000000..2f909a3 --- /dev/null +++ b/Source/ShooterGame/Private/ShooterEngine.cpp @@ -0,0 +1,98 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +/*============================================================================= + ShooterEngine.cpp: ShooterEngine c++ code. +=============================================================================*/ + +#include "ShooterGame.h" +#include "ShooterEngine.h" +#include "ShooterGameInstance.h" + +UShooterEngine::UShooterEngine(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UShooterEngine::Init(IEngineLoop* InEngineLoop) +{ + // Note: Lots of important things happen in Super::Init(), including spawning the player pawn in-game and + // creating the renderer. + Super::Init(InEngineLoop); +} + + +void UShooterEngine::HandleNetworkFailure(UWorld *World, UNetDriver *NetDriver, ENetworkFailure::Type FailureType, const FString& ErrorString) +{ + // Determine if we need to change the King state based on network failures. + + // Only handle failure at this level for game or pending net drivers. + FName NetDriverName = NetDriver ? NetDriver->NetDriverName : NAME_None; + if (NetDriverName == NAME_GameNetDriver || NetDriverName == NAME_PendingNetDriver) + { + // If this net driver has already been unregistered with this world, then don't handle it. + //if (World) + { + //UNetDriver * NetDriver = FindNamedNetDriver(World, NetDriverName); + if (NetDriver) + { + switch (FailureType) + { + case ENetworkFailure::FailureReceived: + { + UShooterGameInstance* const ShooterInstance = Cast(GameInstance); + if (ShooterInstance && NetDriver->GetNetMode() == NM_Client) + { + const FText OKButton = NSLOCTEXT( "DialogButtons", "OKAY", "OK" ); + + // NOTE - We pass in false here to not override the message if we are already going to the main menu + // We're going to make the assumption that someone else has a better message than "Lost connection to host" if + // this is the case + ShooterInstance->ShowMessageThenGotoState( FText::FromString(ErrorString), OKButton, FText::GetEmpty(), ShooterGameInstanceState::MainMenu, false ); + } + break; + } + case ENetworkFailure::PendingConnectionFailure: + { + UShooterGameInstance* const GI = Cast(GameInstance); + if (GI && NetDriver->GetNetMode() == NM_Client) + { + const FText OKButton = NSLOCTEXT( "DialogButtons", "OKAY", "OK" ); + + // NOTE - We pass in false here to not override the message if we are already going to the main menu + // We're going to make the assumption that someone else has a better message than "Lost connection to host" if + // this is the case + GI->ShowMessageThenGotoState( FText::FromString(ErrorString), OKButton, FText::GetEmpty(), ShooterGameInstanceState::MainMenu, false ); + } + break; + } + case ENetworkFailure::ConnectionLost: + case ENetworkFailure::ConnectionTimeout: + { + UShooterGameInstance* const GI = Cast(GameInstance); + if (GI && NetDriver->GetNetMode() == NM_Client) + { + const FText ReturnReason = NSLOCTEXT( "NetworkErrors", "HostDisconnect", "Lost connection to host." ); + const FText OKButton = NSLOCTEXT( "DialogButtons", "OKAY", "OK" ); + + // NOTE - We pass in false here to not override the message if we are already going to the main menu + // We're going to make the assumption that someone else has a better message than "Lost connection to host" if + // this is the case + GI->ShowMessageThenGotoState( ReturnReason, OKButton, FText::GetEmpty(), ShooterGameInstanceState::MainMenu, false ); + } + break; + } + case ENetworkFailure::NetDriverAlreadyExists: + case ENetworkFailure::NetDriverCreateFailure: + case ENetworkFailure::OutdatedClient: + case ENetworkFailure::OutdatedServer: + default: + break; + } + } + } + } + + // standard failure handling. + Super::HandleNetworkFailure(World, NetDriver, FailureType, ErrorString); +} + diff --git a/Source/ShooterGame/Private/ShooterGameDelegates.cpp b/Source/ShooterGame/Private/ShooterGameDelegates.cpp new file mode 100644 index 0000000..248c75b --- /dev/null +++ b/Source/ShooterGame/Private/ShooterGameDelegates.cpp @@ -0,0 +1,195 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterGameState.h" +#include "Online/ShooterPlayerState.h" +#include "GameDelegates.h" +#include "IPlatformFilePak.h" + +#include "UObject/PackageReload.h" + +//#include "Runtime/RHI/Public/RHICommandlist.h" + +// Global struct for registering delegates super early +struct FShooterGameGlobalDelegateInit +{ + FShooterGameGlobalDelegateInit() + { + FPakPlatformFile::FPakSigningFailureHandlerData& HandlerData = FPakPlatformFile::GetPakSigningFailureHandlerData(); + { + FScopeLock Lock(&HandlerData.Lock); + HandlerData.ChunkSignatureCheckFailedDelegate.AddStatic(FShooterGameGlobalDelegateInit::HandlePakChunkSignatureCheckFailed); + HandlerData.MasterSignatureTableCheckFailedDelegate.AddStatic(FShooterGameGlobalDelegateInit::HandlePakMasterSignatureTableCheckFailure); + } + + FPakPlatformFile::GetPakSetIndexSettingsDelegate().BindStatic(GetPakSetIndexSettings); + } + + static void HandlePakChunkSignatureCheckFailed(const FPakChunkSignatureCheckFailedData& Data) + { + UE_LOG(LogShooter, Fatal, TEXT("Pak chunk signature check failed!")); + } + + static void HandlePakMasterSignatureTableCheckFailure(const FString& InPakFilename) + { + UE_LOG(LogShooter, Fatal, TEXT("Pak master signature table check failed for pak '%s'"), *InPakFilename); + } + + static void GetPakSetIndexSettings(bool& bKeepFullDirectory, bool& bValidatePruning, bool& bDelayPruning) + { + // Keep the full directory of filenames in PakFileIndexes, so that FindOrLoadAssetsByPath will be able to find files in a given path + bKeepFullDirectory = true; + } +} +GShooterGameGlobalDelegateInit; + + +// respond to requests from a companion app +static void WebServerDelegate(int32 UserIndex, const FString& Action, const FString& URL, const TMap& Params, TMap& Response) +{ + if (URL == TEXT("/index.html?scoreboard")) + { + FString ScoreboardStr = TEXT("{ \"scoreboard\" : [ "); + + // you shouldn't normally use this method to get a UWorld as it won't always be correct in a PIE context. + // However, the PS4 companion app server will never run in the Editor. + UGameEngine* GameEngine = CastChecked(GEngine); + if (GameEngine) + { + UWorld* World = GameEngine->GetGameWorld(); + if (World) + { + ULocalPlayer *Player = GEngine->GetFirstGamePlayer(World); + if (Player) + { + // get the shoter game + AShooterGameState* const GameState = Player->PlayerController->GetWorld()->GetGameState(); + + + RankedPlayerMap Players; + GameState->GetRankedMap(0, Players); + + bool bNeedsComma = false; + for (auto It = Players.CreateIterator(); It; ++It) + { + if (bNeedsComma) + { + ScoreboardStr += TEXT(" ,"); + } + ScoreboardStr += FString::Printf(TEXT(" { \"n\" : \"%s\" , \"k\" : \"%d\" , \"d\" : \"%d\" }"), *It.Value()->GetShortPlayerName(), It.Value()->GetKills(), It.Value()->GetDeaths()); + bNeedsComma = true; + } + } + + ScoreboardStr += TEXT(" ] }"); + + Response.Add(TEXT("Content-Type"), TEXT("text/html; charset=utf-8")); + Response.Add(TEXT("Body"), ScoreboardStr); + } + } + } +} + +static void ExtendedSaveGameInfoDelegate(const TCHAR* SaveName, const EGameDelegates_SaveGame Key, FString& Value) +{ + static const int32 MAX_SAVEGAME_SIZE = 100 * 1024; + switch(Key) + { + case EGameDelegates_SaveGame::MaxSize: + Value = FString::Printf(TEXT("%i"), MAX_SAVEGAME_SIZE); + break; + case EGameDelegates_SaveGame::Title: + Value = TEXT("ShooterGame"); + break; + case EGameDelegates_SaveGame::SubTitle: + Value = TEXT("The Shootening"); + break; + case EGameDelegates_SaveGame::Detail: + Value = TEXT("ShooterGame User Settings"); + break; + default: + break; + } +} + +static void ReloadHandler( EPackageReloadPhase ReloadPhase, FPackageReloadedEvent* Event) +{ + if ( ReloadPhase == EPackageReloadPhase::PostPackageFixup) + { + // reinitialize allthe material instances + + + /*{ + // fixup uniform expressions + UMaterialInterface::RecacheAllMaterialUniformExpressions(); + }*/ + + /*for (TObjectIterator It; It; ++It) + { + UMaterialInstance* Material = *It; + //Material->InitResources(); + Material->RebuildResource(); + }*/ + } +} + +#define EXPERIMENTAL_ENABLEHOTRELOAD 0 +static void ReloadPackagesCallback( const TArray& PackageNames) +{ +#if EXPERIMENTAL_ENABLEHOTRELOAD + TArray PackagesToReload; + TArray MaterialPackagesToReload; + for (const FString& PackageName : PackageNames) + { + UPackage* Package = FindPackage(nullptr, *PackageName); + + if (Package == nullptr) + { + // UE_LOG(, Log, TEXT("Unable to find package in memory %s"), *PackageName); + } + else + { + if ( Package->HasAnyPackageFlags(PKG_ContainsMap || PKG_ContainsMap) ) + { + continue; + } + PackagesToReload.Add(Package); + } + } + + + // see what's in these packages + + if (PackagesToReload.Num()) + { + SortPackagesForReload(PackagesToReload); + + TArray PackagesToReloadData; + PackagesToReloadData.Empty(PackagesToReload.Num()); + for (UPackage* PackageToReload : PackagesToReload) + { + PackagesToReloadData.Emplace(PackageToReload, LOAD_None); + } + + TArray ReloadedPackages; + + FDelegateHandle Handle = FCoreUObjectDelegates::OnPackageReloaded.AddStatic(&ReloadHandler); + + FText ErrorMessage; + GShouldVerifyGCAssumptions = false; + GUObjectArray.DisableDisregardForGC(); + + ::ReloadPackages(PackagesToReloadData, ReloadedPackages, 500); + + FCoreUObjectDelegates::OnPackageReloaded.Remove(Handle); + } +#endif +} + +void InitializeShooterGameDelegates() +{ + FGameDelegates::Get().GetWebServerActionDelegate() = FWebServerActionDelegate::CreateStatic(WebServerDelegate); + FGameDelegates::Get().GetExtendedSaveGameInfoDelegate() = FExtendedSaveGameInfoDelegate::CreateStatic(ExtendedSaveGameInfoDelegate); + + FCoreUObjectDelegates::NetworkFileRequestPackageReload.BindStatic(&ReloadPackagesCallback); +} diff --git a/Source/ShooterGame/Private/ShooterGameDelegates.h b/Source/ShooterGame/Private/ShooterGameDelegates.h new file mode 100644 index 0000000..5af3fc1 --- /dev/null +++ b/Source/ShooterGame/Private/ShooterGameDelegates.h @@ -0,0 +1,7 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once + +/** + * Called at startup to setup the FGameDelegates this game needs + */ +void InitializeShooterGameDelegates(); diff --git a/Source/ShooterGame/Private/ShooterGameInstance.cpp b/Source/ShooterGame/Private/ShooterGameInstance.cpp new file mode 100644 index 0000000..38da93b --- /dev/null +++ b/Source/ShooterGame/Private/ShooterGameInstance.cpp @@ -0,0 +1,2144 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +/*============================================================================= + ShooterGameInstance.cpp +=============================================================================*/ + +#include "ShooterGame.h" +#include "ShooterGameInstance.h" +#include "ShooterMainMenu.h" +#include "ShooterWelcomeMenu.h" +#include "ShooterMessageMenu.h" +#include "ShooterGameLoadingScreen.h" +#include "OnlineKeyValuePair.h" +#include "ShooterStyle.h" +#include "ShooterMenuItemWidgetStyle.h" +#include "ShooterGameViewportClient.h" +#include "Player/ShooterPlayerController_Menu.h" +#include "Online/ShooterPlayerState.h" +#include "Online/ShooterGameSession.h" +#include "Online/ShooterOnlineSessionClient.h" +#include "OnlineSubsystemUtils.h" + +#if !defined(CONTROLLER_SWAPPING) + #define CONTROLLER_SWAPPING 0 +#endif + +#if !defined(NEED_XBOX_LIVE_FOR_ONLINE) + #define NEED_XBOX_LIVE_FOR_ONLINE 0 +#endif + +FAutoConsoleVariable CVarShooterGameTestEncryption(TEXT("ShooterGame.TestEncryption"), 0, TEXT("If true, clients will send an encryption token with their request to join the server and attempt to encrypt the connection using a debug key. This is NOT SECURE and for demonstration purposes only.")); + +void SShooterWaitDialog::Construct(const FArguments& InArgs) +{ + const FShooterMenuItemStyle* ItemStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterMenuItemStyle"); + const FButtonStyle* ButtonStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterButtonStyle"); + ChildSlot + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(20.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(SBorder) + .Padding(50.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .BorderImage(&ItemStyle->BackgroundBrush) + .BorderBackgroundColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) + [ + SNew(STextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + .ColorAndOpacity(this, &SShooterWaitDialog::GetTextColor) + .Text(InArgs._MessageText) + .WrapTextAt(500.0f) + ] + ] + ]; + + //Setup a curve + const float StartDelay = 0.0f; + const float SecondDelay = 0.0f; + const float AnimDuration = 2.0f; + + WidgetAnimation = FCurveSequence(); + TextColorCurve = WidgetAnimation.AddCurve(StartDelay + SecondDelay, AnimDuration, ECurveEaseFunction::QuadInOut); + WidgetAnimation.Play(this->AsShared(), true); +} + +FSlateColor SShooterWaitDialog::GetTextColor() const +{ + //instead of going from black -> white, go from white -> grey. + float fAlpha = 1.0f - TextColorCurve.GetLerp(); + fAlpha = fAlpha * 0.5f + 0.5f; + return FLinearColor(FColor(155, 164, 182, FMath::Clamp((int32)(fAlpha * 255.0f), 0, 255))); +} + +namespace ShooterGameInstanceState +{ + const FName None = FName(TEXT("None")); + const FName PendingInvite = FName(TEXT("PendingInvite")); + const FName WelcomeScreen = FName(TEXT("WelcomeScreen")); + const FName MainMenu = FName(TEXT("MainMenu")); + const FName MessageMenu = FName(TEXT("MessageMenu")); + const FName Playing = FName(TEXT("Playing")); +} + + +UShooterGameInstance::UShooterGameInstance(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , OnlineMode(EOnlineMode::Online) // Default to online + , bIsLicensed(true) // Default to licensed (should have been checked by OS on boot) +{ + CurrentState = ShooterGameInstanceState::None; +} + +void UShooterGameInstance::Init() +{ + Super::Init(); + + IgnorePairingChangeForControllerId = -1; + CurrentConnectionStatus = EOnlineServerConnectionStatus::Connected; + + LocalPlayerOnlineStatus.InsertDefaulted(0, MAX_LOCAL_PLAYERS); + + // game requires the ability to ID users. + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + check(IdentityInterface.IsValid()); + + const IOnlineSessionPtr SessionInterface = OnlineSub->GetSessionInterface(); + check(SessionInterface.IsValid()); + + // bind any OSS delegates we needs to handle + for (int i = 0; i < MAX_LOCAL_PLAYERS; ++i) + { + IdentityInterface->AddOnLoginStatusChangedDelegate_Handle(i, FOnLoginStatusChangedDelegate::CreateUObject(this, &UShooterGameInstance::HandleUserLoginChanged)); + } + + IdentityInterface->AddOnControllerPairingChangedDelegate_Handle(FOnControllerPairingChangedDelegate::CreateUObject(this, &UShooterGameInstance::HandleControllerPairingChanged)); + + FCoreDelegates::ApplicationWillDeactivateDelegate.AddUObject(this, &UShooterGameInstance::HandleAppWillDeactivate); + + FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddUObject(this, &UShooterGameInstance::HandleAppSuspend); + FCoreDelegates::ApplicationHasEnteredForegroundDelegate.AddUObject(this, &UShooterGameInstance::HandleAppResume); + + FCoreDelegates::OnSafeFrameChangedEvent.AddUObject(this, &UShooterGameInstance::HandleSafeFrameChanged); + FCoreDelegates::OnControllerConnectionChange.AddUObject(this, &UShooterGameInstance::HandleControllerConnectionChange); + FCoreDelegates::ApplicationLicenseChange.AddUObject(this, &UShooterGameInstance::HandleAppLicenseUpdate); + + FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &UShooterGameInstance::OnPreLoadMap); + FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UShooterGameInstance::OnPostLoadMap); + + FCoreUObjectDelegates::PostDemoPlay.AddUObject(this, &UShooterGameInstance::OnPostDemoPlay); + + bPendingEnableSplitscreen = false; + + OnlineSub->AddOnConnectionStatusChangedDelegate_Handle( FOnConnectionStatusChangedDelegate::CreateUObject( this, &UShooterGameInstance::HandleNetworkConnectionStatusChanged ) ); + + if (SessionInterface.IsValid()) + { + SessionInterface->AddOnSessionFailureDelegate_Handle( FOnSessionFailureDelegate::CreateUObject( this, &UShooterGameInstance::HandleSessionFailure ) ); + } + + OnEndSessionCompleteDelegate = FOnEndSessionCompleteDelegate::CreateUObject(this, &UShooterGameInstance::OnEndSessionComplete); + + // Register delegate for ticker callback + TickDelegate = FTickerDelegate::CreateUObject(this, &UShooterGameInstance::Tick); + TickDelegateHandle = FTicker::GetCoreTicker().AddTicker(TickDelegate); + + // Register activities delegate callback + OnGameActivityActivationRequestedDelegate = FOnGameActivityActivationRequestedDelegate::CreateUObject(this, &UShooterGameInstance::OnGameActivityActivationRequestComplete); + + const IOnlineGameActivityPtr ActivityInterface = OnlineSub->GetGameActivityInterface(); + if (ActivityInterface.IsValid()) + { + OnGameActivityActivationRequestedDelegateHandle = ActivityInterface->AddOnGameActivityActivationRequestedDelegate_Handle(OnGameActivityActivationRequestedDelegate); + } + + // Initialize the debug key with a set value for AES256. This is not secure and for example purposes only. + DebugTestEncryptionKey.SetNum(32); + + for (int32 i = 0; i < DebugTestEncryptionKey.Num(); ++i) + { + DebugTestEncryptionKey[i] = uint8(i); + } +} + +void UShooterGameInstance::Shutdown() +{ + Super::Shutdown(); + + // Clear the activities delegate + if (IOnlineGameActivityPtr ActivityInterface = IOnlineSubsystem::Get()->GetGameActivityInterface()) + { + ActivityInterface->ClearOnGameActivityActivationRequestedDelegate_Handle(OnGameActivityActivationRequestedDelegateHandle); + } + + // Unregister ticker delegate + FTicker::GetCoreTicker().RemoveTicker(TickDelegateHandle); +} + +void UShooterGameInstance::HandleNetworkConnectionStatusChanged( const FString& ServiceName, EOnlineServerConnectionStatus::Type LastConnectionStatus, EOnlineServerConnectionStatus::Type ConnectionStatus ) +{ + UE_LOG( LogOnlineGame, Log, TEXT( "UShooterGameInstance::HandleNetworkConnectionStatusChanged: %s" ), EOnlineServerConnectionStatus::ToString( ConnectionStatus ) ); + +#if SHOOTER_CONSOLE_UI + // If we are disconnected from server, and not currently at (or heading to) the welcome screen + // then display a message on consoles + if ( OnlineMode != EOnlineMode::Offline && + PendingState != ShooterGameInstanceState::WelcomeScreen && + CurrentState != ShooterGameInstanceState::WelcomeScreen && + ConnectionStatus != EOnlineServerConnectionStatus::Connected && + ConnectionStatus != EOnlineServerConnectionStatus::Normal) + { + UE_LOG( LogOnlineGame, Log, TEXT( "UShooterGameInstance::HandleNetworkConnectionStatusChanged: Going to main menu" ) ); + + // Display message on consoles +#if SHOOTER_XBOX_STRINGS + const FText ReturnReason = NSLOCTEXT( "NetworkFailures", "ServiceUnavailable", "Connection to Xbox LIVE has been lost." ); +#elif PLATFORM_PS4 + const FText ReturnReason = NSLOCTEXT( "NetworkFailures", "ServiceUnavailable", "Connection to \"PSN\" has been lost." ); +#else + const FText ReturnReason = NSLOCTEXT( "NetworkFailures", "ServiceUnavailable", "Connection has been lost." ); +#endif + const FText OKButton = NSLOCTEXT( "DialogButtons", "OKAY", "OK" ); + + UWorld* const World = GetWorld(); + AShooterGameMode* const GameMode = World != NULL ? Cast(World->GetAuthGameMode()) : NULL; + if (GameMode) + { + GameMode->AbortMatch(); + } + + ShowMessageThenGotoState( ReturnReason, OKButton, FText::GetEmpty(), ShooterGameInstanceState::MainMenu ); + } + + CurrentConnectionStatus = ConnectionStatus; +#endif +} + +void UShooterGameInstance::HandleSessionFailure( const FUniqueNetId& NetId, ESessionFailure::Type FailureType ) +{ + UE_LOG( LogOnlineGame, Warning, TEXT( "UShooterGameInstance::HandleSessionFailure: %u" ), (uint32)FailureType ); + +#if SHOOTER_CONSOLE_UI + // If we are not currently at (or heading to) the welcome screen then display a message on consoles + if ( OnlineMode != EOnlineMode::Offline && + PendingState != ShooterGameInstanceState::WelcomeScreen && + CurrentState != ShooterGameInstanceState::WelcomeScreen ) + { + UE_LOG( LogOnlineGame, Log, TEXT( "UShooterGameInstance::HandleSessionFailure: Going to main menu" ) ); + + // Display message on consoles +#if SHOOTER_XBOX_STRINGS + const FText ReturnReason = NSLOCTEXT( "NetworkFailures", "ServiceUnavailable", "Connection to Xbox LIVE has been lost." ); +#elif PLATFORM_PS4 + const FText ReturnReason = NSLOCTEXT( "NetworkFailures", "ServiceUnavailable", "Connection to PSN has been lost." ); +#else + const FText ReturnReason = NSLOCTEXT( "NetworkFailures", "ServiceUnavailable", "Connection has been lost." ); +#endif + const FText OKButton = NSLOCTEXT( "DialogButtons", "OKAY", "OK" ); + + ShowMessageThenGotoState( ReturnReason, OKButton, FText::GetEmpty(), ShooterGameInstanceState::MainMenu ); + } +#endif +} + +void UShooterGameInstance::OnPreLoadMap(const FString& MapName) +{ + if (bPendingEnableSplitscreen) + { + // Allow splitscreen + UGameViewportClient* GameViewportClient = GetGameViewportClient(); + if (GameViewportClient != nullptr) + { + GameViewportClient->SetForceDisableSplitscreen(false); + + bPendingEnableSplitscreen = false; + } + } +} + +void UShooterGameInstance::OnPostLoadMap(UWorld*) +{ + // Make sure we hide the loading screen when the level is done loading + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + if (ShooterViewport != nullptr) + { + ShooterViewport->HideLoadingScreen(); + } +} + +void UShooterGameInstance::OnUserCanPlayInvite(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + CleanupOnlinePrivilegeTask(); + if (WelcomeMenuUI.IsValid()) + { + WelcomeMenuUI->LockControls(false); + } + + if (PrivilegeResults == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + if (UserId == *PendingInvite.UserId) + { + PendingInvite.bPrivilegesCheckedAndAllowed = true; + } + } + else + { + DisplayOnlinePrivilegeFailureDialogs(UserId, Privilege, PrivilegeResults); + GotoState(ShooterGameInstanceState::WelcomeScreen); + } +} + +void UShooterGameInstance::OnPostDemoPlay() +{ + GotoState( ShooterGameInstanceState::Playing ); +} + +void UShooterGameInstance::HandleDemoPlaybackFailure( EDemoPlayFailure::Type FailureType, const FString& ErrorString ) +{ + if (GetWorld() != nullptr && GetWorld()->WorldType == EWorldType::PIE) + { + UE_LOG(LogEngine, Warning, TEXT("Demo failed to play back correctly, got error %s"), *ErrorString); + return; + } + + ShowMessageThenGotoState(FText::Format(NSLOCTEXT("UShooterGameInstance", "DemoPlaybackFailedFmt", "Demo playback failed: {0}"), FText::FromString(ErrorString)), NSLOCTEXT("DialogButtons", "OKAY", "OK"), FText::GetEmpty(), ShooterGameInstanceState::MainMenu); +} + +void UShooterGameInstance::StartGameInstance() +{ +#if PLATFORM_PS4 == 0 + TCHAR Parm[4096] = TEXT(""); + + const TCHAR* Cmd = FCommandLine::Get(); + + // Catch the case where we want to override the map name on startup (used for connecting to other MP instances) + if (FParse::Token(Cmd, Parm, UE_ARRAY_COUNT(Parm), 0) && Parm[0] != '-') + { + // if we're 'overriding' with the default map anyway, don't set a bogus 'playing' state. + if (!MainMenuMap.Contains(Parm)) + { + FURL DefaultURL; + DefaultURL.LoadURLConfig(TEXT("DefaultPlayer"), GGameIni); + + FURL URL(&DefaultURL, Parm, TRAVEL_Partial); + + // If forcelan is set, we need to make sure to add the LAN flag to the travel url + if (FParse::Param(Cmd, TEXT("forcelan"))) + { + URL.AddOption(TEXT("bIsLanMatch")); + } + + if (URL.Valid) + { + UEngine* const Engine = GetEngine(); + + FString Error; + + const EBrowseReturnVal::Type BrowseRet = Engine->Browse(*WorldContext, URL, Error); + + if (BrowseRet == EBrowseReturnVal::Success) + { + // Success, we loaded the map, go directly to playing state + GotoState(ShooterGameInstanceState::Playing); + return; + } + else if (BrowseRet == EBrowseReturnVal::Pending) + { + // Assume network connection + LoadFrontEndMap(MainMenuMap); + AddNetworkFailureHandlers(); + ShowLoadingScreen(); + GotoState(ShooterGameInstanceState::Playing); + return; + } + } + } + } +#endif + + GotoInitialState(); +} + +#if WITH_EDITOR + +FGameInstancePIEResult UShooterGameInstance::StartPlayInEditorGameInstance(ULocalPlayer* LocalPlayer, const FGameInstancePIEParameters& Params) +{ + FWorldContext* PlayWorldContext = GetWorldContext(); + check(PlayWorldContext); + + UWorld* PlayWorld = PlayWorldContext->World(); + check(PlayWorld); + + FString CurrentMapName = PlayWorld->GetOutermost()->GetName(); + if (!PlayWorldContext->PIEPrefix.IsEmpty()) + { + CurrentMapName.ReplaceInline(*PlayWorldContext->PIEPrefix, TEXT("")); + } + +#if SHOOTER_CONSOLE_UI + if (CurrentMapName == WelcomeScreenMap) + { + GotoState(ShooterGameInstanceState::WelcomeScreen); + } + else +#endif // SHOOTER_CONSOLE_UI + if (CurrentMapName == MainMenuMap) + { + GotoState(ShooterGameInstanceState::MainMenu); + } + else + { + GotoState(ShooterGameInstanceState::Playing); + } + + return Super::StartPlayInEditorGameInstance(LocalPlayer, Params); +} + +#endif // WITH_EDITOR + +FName UShooterGameInstance::GetInitialState() +{ +#if SHOOTER_CONSOLE_UI + // Start in the welcome screen state on consoles + return ShooterGameInstanceState::WelcomeScreen; +#else + // On PC, go directly to the main menu + return ShooterGameInstanceState::MainMenu; +#endif +} + +void UShooterGameInstance::GotoInitialState() +{ + GotoState(GetInitialState()); +} + +const FName UShooterGameInstance::GetCurrentState() const +{ + return CurrentState; +} + +void UShooterGameInstance::ShowMessageThenGotoState( const FText& Message, const FText& OKButtonString, const FText& CancelButtonString, const FName& NewState, const bool OverrideExisting, TWeakObjectPtr< ULocalPlayer > PlayerOwner ) +{ + UE_LOG( LogOnline, Log, TEXT( "ShowMessageThenGotoState: Message: %s, NewState: %s" ), *Message.ToString(), *NewState.ToString() ); + + const bool bAtWelcomeScreen = PendingState == ShooterGameInstanceState::WelcomeScreen || CurrentState == ShooterGameInstanceState::WelcomeScreen; + + // Never override the welcome screen + if ( bAtWelcomeScreen ) + { + UE_LOG( LogOnline, Log, TEXT( "ShowMessageThenGotoState: Ignoring due to higher message priority in queue (at welcome screen)." ) ); + return; + } + + const bool bAlreadyAtMessageMenu = PendingState == ShooterGameInstanceState::MessageMenu || CurrentState == ShooterGameInstanceState::MessageMenu; + const bool bAlreadyAtDestState = PendingState == NewState || CurrentState == NewState; + + // If we are already going to the message menu, don't override unless asked to + if ( bAlreadyAtMessageMenu && PendingMessage.NextState == NewState && !OverrideExisting ) + { + UE_LOG( LogOnline, Log, TEXT( "ShowMessageThenGotoState: Ignoring due to higher message priority in queue (check 1)." ) ); + return; + } + + // If we are already going to the message menu, and the next dest is welcome screen, don't override + if ( bAlreadyAtMessageMenu && PendingMessage.NextState == ShooterGameInstanceState::WelcomeScreen ) + { + UE_LOG( LogOnline, Log, TEXT( "ShowMessageThenGotoState: Ignoring due to higher message priority in queue (check 2)." ) ); + return; + } + + // If we are already at the dest state, don't override unless asked + if ( bAlreadyAtDestState && !OverrideExisting ) + { + UE_LOG( LogOnline, Log, TEXT( "ShowMessageThenGotoState: Ignoring due to higher message priority in queue (check 3)" ) ); + return; + } + + PendingMessage.DisplayString = Message; + PendingMessage.OKButtonString = OKButtonString; + PendingMessage.CancelButtonString = CancelButtonString; + PendingMessage.NextState = NewState; + PendingMessage.PlayerOwner = PlayerOwner; + + if ( CurrentState == ShooterGameInstanceState::MessageMenu ) + { + UE_LOG( LogOnline, Log, TEXT( "ShowMessageThenGotoState: Forcing new message" ) ); + EndMessageMenuState(); + BeginMessageMenuState(); + } + else + { + GotoState(ShooterGameInstanceState::MessageMenu); + } +} + +void UShooterGameInstance::ShowLoadingScreen() +{ + // This can be confusing, so here is what is happening: + // For LoadMap, we use the IShooterGameLoadingScreenModule interface to show the load screen + // This is necessary since this is a blocking call, and our viewport loading screen won't get updated. + // We can't use IShooterGameLoadingScreenModule for seamless travel though + // In this case, we just add a widget to the viewport, and have it update on the main thread + // To simplify things, we just do both, and you can't tell, one will cover the other if they both show at the same time + IShooterGameLoadingScreenModule* const LoadingScreenModule = FModuleManager::LoadModulePtr("ShooterGameLoadingScreen"); + if (LoadingScreenModule != nullptr) + { + LoadingScreenModule->StartInGameLoadingScreen(); + } + + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + + if ( ShooterViewport != NULL ) + { + ShooterViewport->ShowLoadingScreen(); + } +} + +bool UShooterGameInstance::LoadFrontEndMap(const FString& MapName) +{ + bool bSuccess = true; + + // if already loaded, do nothing + UWorld* const World = GetWorld(); + if (World) + { + FString const CurrentMapName = *World->PersistentLevel->GetOutermost()->GetName(); + //if (MapName.Find(TEXT("Highrise")) != -1) + if (CurrentMapName == MapName) + { + return bSuccess; + } + } + + FString Error; + EBrowseReturnVal::Type BrowseRet = EBrowseReturnVal::Failure; + FURL URL( + *FString::Printf(TEXT("%s"), *MapName) + ); + + if (URL.Valid && !HasAnyFlags(RF_ClassDefaultObject)) //CastChecked() will fail if using Default__ShooterGameInstance, so make sure that we're not default + { + BrowseRet = GetEngine()->Browse(*WorldContext, URL, Error); + + // Handle failure. + if (BrowseRet != EBrowseReturnVal::Success) + { + UE_LOG(LogLoad, Fatal, TEXT("%s"), *FString::Printf(TEXT("Failed to enter %s: %s. Please check the log for errors."), *MapName, *Error)); + bSuccess = false; + } + } + return bSuccess; +} + +AShooterGameSession* UShooterGameInstance::GetGameSession() const +{ + UWorld* const World = GetWorld(); + if (World) + { + AGameModeBase* const Game = World->GetAuthGameMode(); + if (Game) + { + return Cast(Game->GameSession); + } + } + + return nullptr; +} + +void UShooterGameInstance::TravelLocalSessionFailure(UWorld *World, ETravelFailure::Type FailureType, const FString& ReasonString) +{ + AShooterPlayerController_Menu* const FirstPC = Cast(UGameplayStatics::GetPlayerController(GetWorld(), 0)); + if (FirstPC != nullptr) + { + FText ReturnReason = NSLOCTEXT("NetworkErrors", "JoinSessionFailed", "Join Session failed."); + if (ReasonString.IsEmpty() == false) + { + ReturnReason = FText::Format(NSLOCTEXT("NetworkErrors", "JoinSessionFailedReasonFmt", "Join Session failed. {0}"), FText::FromString(ReasonString)); + } + + FText OKButton = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + ShowMessageThenGoMain(ReturnReason, OKButton, FText::GetEmpty()); + } +} + +void UShooterGameInstance::ShowMessageThenGoMain(const FText& Message, const FText& OKButtonString, const FText& CancelButtonString) +{ + ShowMessageThenGotoState(Message, OKButtonString, CancelButtonString, ShooterGameInstanceState::MainMenu); +} + +void UShooterGameInstance::SetPendingInvite(const FShooterPendingInvite& InPendingInvite) +{ + PendingInvite = InPendingInvite; +} + +void UShooterGameInstance::GotoState(FName NewState) +{ + UE_LOG( LogOnline, Log, TEXT( "GotoState: NewState: %s" ), *NewState.ToString() ); + + PendingState = NewState; +} + +void UShooterGameInstance::MaybeChangeState() +{ + if ( (PendingState != CurrentState) && (PendingState != ShooterGameInstanceState::None) ) + { + FName const OldState = CurrentState; + + // end current state + EndCurrentState(PendingState); + + // begin new state + BeginNewState(PendingState, OldState); + + // clear pending change + PendingState = ShooterGameInstanceState::None; + } +} + +void UShooterGameInstance::EndCurrentState(FName NextState) +{ + // per-state custom ending code here + if (CurrentState == ShooterGameInstanceState::PendingInvite) + { + EndPendingInviteState(); + } + else if (CurrentState == ShooterGameInstanceState::WelcomeScreen) + { + EndWelcomeScreenState(); + } + else if (CurrentState == ShooterGameInstanceState::MainMenu) + { + EndMainMenuState(); + } + else if (CurrentState == ShooterGameInstanceState::MessageMenu) + { + EndMessageMenuState(); + } + else if (CurrentState == ShooterGameInstanceState::Playing) + { + EndPlayingState(); + } + + CurrentState = ShooterGameInstanceState::None; +} + +void UShooterGameInstance::BeginNewState(FName NewState, FName PrevState) +{ + // per-state custom starting code here + + if (NewState == ShooterGameInstanceState::PendingInvite) + { + BeginPendingInviteState(); + } + else if (NewState == ShooterGameInstanceState::WelcomeScreen) + { + BeginWelcomeScreenState(); + } + else if (NewState == ShooterGameInstanceState::MainMenu) + { + BeginMainMenuState(); + } + else if (NewState == ShooterGameInstanceState::MessageMenu) + { + BeginMessageMenuState(); + } + else if (NewState == ShooterGameInstanceState::Playing) + { + BeginPlayingState(); + } + + CurrentState = NewState; +} + +void UShooterGameInstance::BeginPendingInviteState() +{ + if (LoadFrontEndMap(MainMenuMap)) + { + StartOnlinePrivilegeTask(IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateUObject(this, &UShooterGameInstance::OnUserCanPlayInvite), EUserPrivileges::CanPlayOnline, PendingInvite.UserId); + } + else + { + GotoState(ShooterGameInstanceState::WelcomeScreen); + } +} + +void UShooterGameInstance::EndPendingInviteState() +{ + // cleanup in case the state changed before the pending invite was handled. + CleanupOnlinePrivilegeTask(); +} + +void UShooterGameInstance::BeginWelcomeScreenState() +{ + //this must come before split screen player removal so that the OSS sets all players to not using online features. + SetOnlineMode(EOnlineMode::Offline); + + // Remove any possible splitscren players + RemoveSplitScreenPlayers(); + + LoadFrontEndMap(WelcomeScreenMap); + + ULocalPlayer* const LocalPlayer = GetFirstGamePlayer(); + LocalPlayer->SetCachedUniqueNetId(nullptr); + check(!WelcomeMenuUI.IsValid()); + WelcomeMenuUI = MakeShareable(new FShooterWelcomeMenu); + WelcomeMenuUI->Construct( this ); + WelcomeMenuUI->AddToGameViewport(); + + // Disallow splitscreen (we will allow while in the playing state) + GetGameViewportClient()->SetForceDisableSplitscreen( true ); +} + +void UShooterGameInstance::EndWelcomeScreenState() +{ + if (WelcomeMenuUI.IsValid()) + { + WelcomeMenuUI->RemoveFromGameViewport(); + WelcomeMenuUI = nullptr; + } +} + +void UShooterGameInstance::SetPresenceForLocalPlayers(const FString& StatusStr, const FVariantData& PresenceData) +{ + const IOnlinePresencePtr Presence = Online::GetPresenceInterface(GetWorld()); + if (Presence.IsValid()) + { + for (int i = 0; i < LocalPlayers.Num(); ++i) + { + FUniqueNetIdRepl UserId = LocalPlayers[i]->GetPreferredUniqueNetId(); + + if (UserId.IsValid()) + { + FOnlineUserPresenceStatus PresenceStatus; + PresenceStatus.StatusStr = StatusStr; + PresenceStatus.State = EOnlinePresenceState::Online; + PresenceStatus.Properties.Add(DefaultPresenceKey, PresenceData); + + Presence->SetPresence(*UserId, PresenceStatus); + } + } + } +} + +void UShooterGameInstance::BeginMainMenuState() +{ + // Make sure we're not showing the loadscreen + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + + if ( ShooterViewport != NULL ) + { + ShooterViewport->HideLoadingScreen(); + } + + SetOnlineMode(EOnlineMode::Offline); + + // Disallow splitscreen + UGameViewportClient* GameViewportClient = GetGameViewportClient(); + + if (GameViewportClient) + { + GetGameViewportClient()->SetForceDisableSplitscreen(true); + } + + // Remove any possible splitscren players + RemoveSplitScreenPlayers(); + + // Set presence to menu state for the owning player + SetPresenceForLocalPlayers(FString(TEXT("In Menu")), FVariantData(FString(TEXT("OnMenu")))); + + // load startup map + LoadFrontEndMap(MainMenuMap); + + // player 0 gets to own the UI + ULocalPlayer* const Player = GetFirstGamePlayer(); + + MainMenuUI = MakeShared(MatchmakerEndpoint); + MainMenuUI->Construct(this, Player); + MainMenuUI->AddMenuToGameViewport(); + +#if !SHOOTER_CONSOLE_UI + // The cached unique net ID is usually set on the welcome screen, but there isn't + // one on PC/Mac, so do it here. + if (Player != nullptr) + { + Player->SetControllerId(0); + Player->SetCachedUniqueNetId(Player->GetUniqueNetIdFromCachedControllerId().GetUniqueNetId()); + } +#endif + + RemoveNetworkFailureHandlers(); +} + +void UShooterGameInstance::EndMainMenuState() +{ + if (MainMenuUI.IsValid()) + { + MainMenuUI->RemoveMenuFromGameViewport(); + MainMenuUI = nullptr; + } +} + +void UShooterGameInstance::BeginMessageMenuState() +{ + if (PendingMessage.DisplayString.IsEmpty()) + { + UE_LOG(LogOnlineGame, Warning, TEXT("UShooterGameInstance::BeginMessageMenuState: Display string is empty")); + GotoInitialState(); + return; + } + + // Make sure we're not showing the loadscreen + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + + if ( ShooterViewport != NULL ) + { + ShooterViewport->HideLoadingScreen(); + } + + check(!MessageMenuUI.IsValid()); + MessageMenuUI = MakeShareable(new FShooterMessageMenu); + MessageMenuUI->Construct(this, PendingMessage.PlayerOwner, PendingMessage.DisplayString, PendingMessage.OKButtonString, PendingMessage.CancelButtonString, PendingMessage.NextState); + + PendingMessage.DisplayString = FText::GetEmpty(); +} + +void UShooterGameInstance::EndMessageMenuState() +{ + if (MessageMenuUI.IsValid()) + { + MessageMenuUI->RemoveFromGameViewport(); + MessageMenuUI = nullptr; + } +} + +void UShooterGameInstance::BeginPlayingState() +{ + bPendingEnableSplitscreen = true; + + // Set presence for playing in a map + SetPresenceForLocalPlayers(FString(TEXT("In Game")), FVariantData(FString(TEXT("InGame")))); + + // Make sure viewport has focus + FSlateApplication::Get().SetAllUserFocusToGameViewport(); +} + +void UShooterGameInstance::EndPlayingState() +{ + // Disallow splitscreen + GetGameViewportClient()->SetForceDisableSplitscreen( true ); + + // Clear the players' presence information + SetPresenceForLocalPlayers(FString(TEXT("In Menu")), FVariantData(FString(TEXT("OnMenu")))); + + UWorld* const World = GetWorld(); + AShooterGameState* const GameState = World != NULL ? World->GetGameState() : NULL; + + if (GameState) + { + // Send round end events for local players + for (int i = 0; i < LocalPlayers.Num(); ++i) + { + AShooterPlayerController* ShooterPC = Cast(LocalPlayers[i]->PlayerController); + if (ShooterPC) + { + // Assuming you can't win if you quit early + ShooterPC->ClientSendRoundEndEvent(false, GameState->ElapsedTime); + } + } + + // Give the game state a chance to cleanup first + GameState->RequestFinishAndExitToMainMenu(); + } + else + { + // If there is no game state, make sure the session is in a good state + CleanupSessionOnReturnToMenu(); + } +} + +void UShooterGameInstance::OnEndSessionComplete( FName SessionName, bool bWasSuccessful ) +{ + UE_LOG(LogOnline, Log, TEXT("UShooterGameInstance::OnEndSessionComplete: Session=%s bWasSuccessful=%s"), *SessionName.ToString(), bWasSuccessful ? TEXT("true") : TEXT("false") ); + + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + if (Sessions.IsValid()) + { + Sessions->ClearOnStartSessionCompleteDelegate_Handle (OnStartSessionCompleteDelegateHandle); + Sessions->ClearOnEndSessionCompleteDelegate_Handle (OnEndSessionCompleteDelegateHandle); + Sessions->ClearOnDestroySessionCompleteDelegate_Handle(OnDestroySessionCompleteDelegateHandle); + } + } + + // continue + CleanupSessionOnReturnToMenu(); +} + +void UShooterGameInstance::CleanupSessionOnReturnToMenu() +{ + bool bPendingOnlineOp = false; + + // end online game and then destroy it + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + IOnlineSessionPtr Sessions = ( OnlineSub != NULL ) ? OnlineSub->GetSessionInterface() : NULL; + + if ( Sessions.IsValid() ) + { + FName GameSession(NAME_GameSession); + EOnlineSessionState::Type SessionState = Sessions->GetSessionState(NAME_GameSession); + UE_LOG(LogOnline, Log, TEXT("Session %s is '%s'"), *GameSession.ToString(), EOnlineSessionState::ToString(SessionState)); + + if ( EOnlineSessionState::InProgress == SessionState ) + { + UE_LOG(LogOnline, Log, TEXT("Ending session %s on return to main menu"), *GameSession.ToString() ); + OnEndSessionCompleteDelegateHandle = Sessions->AddOnEndSessionCompleteDelegate_Handle(OnEndSessionCompleteDelegate); + Sessions->EndSession(NAME_GameSession); + bPendingOnlineOp = true; + } + else if ( EOnlineSessionState::Ending == SessionState ) + { + UE_LOG(LogOnline, Log, TEXT("Waiting for session %s to end on return to main menu"), *GameSession.ToString() ); + OnEndSessionCompleteDelegateHandle = Sessions->AddOnEndSessionCompleteDelegate_Handle(OnEndSessionCompleteDelegate); + bPendingOnlineOp = true; + } + else if ( EOnlineSessionState::Ended == SessionState || EOnlineSessionState::Pending == SessionState ) + { + UE_LOG(LogOnline, Log, TEXT("Destroying session %s on return to main menu"), *GameSession.ToString() ); + OnDestroySessionCompleteDelegateHandle = Sessions->AddOnDestroySessionCompleteDelegate_Handle(OnEndSessionCompleteDelegate); + Sessions->DestroySession(NAME_GameSession); + bPendingOnlineOp = true; + } + else if ( EOnlineSessionState::Starting == SessionState || EOnlineSessionState::Creating == SessionState) + { + UE_LOG(LogOnline, Log, TEXT("Waiting for session %s to start, and then we will end it to return to main menu"), *GameSession.ToString() ); + OnStartSessionCompleteDelegateHandle = Sessions->AddOnStartSessionCompleteDelegate_Handle(OnEndSessionCompleteDelegate); + bPendingOnlineOp = true; + } + } + + if ( !bPendingOnlineOp ) + { + //GEngine->HandleDisconnect( GetWorld(), GetWorld()->GetNetDriver() ); + } +} + +void UShooterGameInstance::LabelPlayerAsQuitter(ULocalPlayer* LocalPlayer) const +{ + AShooterPlayerState* const PlayerState = LocalPlayer && LocalPlayer->PlayerController ? Cast(LocalPlayer->PlayerController->PlayerState) : nullptr; + if(PlayerState) + { + PlayerState->SetQuitter(true); + } +} + +void UShooterGameInstance::RemoveNetworkFailureHandlers() +{ + // Remove the local session/travel failure bindings if they exist + if (GEngine->OnTravelFailure().IsBoundToObject(this) == true) + { + GEngine->OnTravelFailure().Remove(TravelLocalSessionFailureDelegateHandle); + } +} + +void UShooterGameInstance::AddNetworkFailureHandlers() +{ + // Add network/travel error handlers (if they are not already there) + if (GEngine->OnTravelFailure().IsBoundToObject(this) == false) + { + TravelLocalSessionFailureDelegateHandle = GEngine->OnTravelFailure().AddUObject(this, &UShooterGameInstance::TravelLocalSessionFailure); + } +} + +TSubclassOf UShooterGameInstance::GetOnlineSessionClass() +{ + return UShooterOnlineSessionClient::StaticClass(); +} + +bool UShooterGameInstance::HostQuickSession(ULocalPlayer& LocalPlayer, const FOnlineSessionSettings& SessionSettings) +{ + // This function is different from BeginHostingQuickMatch in that it creates a session and then starts a quick match, + // while BeginHostingQuickMatch assumes a session already exists + + if (AShooterGameSession* const GameSession = GetGameSession()) + { + // Add callback delegate for completion + OnCreatePresenceSessionCompleteDelegateHandle = GameSession->OnCreatePresenceSessionComplete().AddUObject(this, &UShooterGameInstance::OnCreatePresenceSessionComplete); + + TravelURL = GetQuickMatchUrl(); + + FOnlineSessionSettings HostSettings = SessionSettings; + + const FString GameType = UGameplayStatics::ParseOption(TravelURL, TEXT("game")); + + // Determine the map name from the travelURL + const FString MapNameSubStr = "/Game/Maps/"; + const FString ChoppedMapName = TravelURL.RightChop(MapNameSubStr.Len()); + const FString MapName = ChoppedMapName.LeftChop(ChoppedMapName.Len() - ChoppedMapName.Find("?game")); + + HostSettings.Set(SETTING_GAMEMODE, GameType, EOnlineDataAdvertisementType::ViaOnlineService); + HostSettings.Set(SETTING_MAPNAME, MapName, EOnlineDataAdvertisementType::ViaOnlineService); + HostSettings.NumPublicConnections = 16; + + if (GameSession->HostSession(LocalPlayer.GetPreferredUniqueNetId().GetUniqueNetId(), NAME_GameSession, SessionSettings)) + { + // If any error occurred in the above, pending state would be set + if (PendingState == CurrentState || PendingState == ShooterGameInstanceState::None) + { + // Go ahead and go into loading state now + // If we fail, the delegate will handle showing the proper messaging and move to the correct state + ShowLoadingScreen(); + GotoState(ShooterGameInstanceState::Playing); + return true; + } + } + } + + return false; +} + +bool UShooterGameInstance::HostGame(ULocalPlayer* LocalPlayer, const FString& GameType, const FString& InTravelURL) +{ + if (GetOnlineMode() == EOnlineMode::Offline) + { + // + // Offline game, just go straight to map + // + + ShowLoadingScreen(); + GotoState(ShooterGameInstanceState::Playing); + + // Travel to the specified match URL + TravelURL = InTravelURL; + GetWorld()->ServerTravel(TravelURL); + return true; + } + + // + // Online game + // + + AShooterGameSession* const GameSession = GetGameSession(); + if (GameSession) + { + // add callback delegate for completion + OnCreatePresenceSessionCompleteDelegateHandle = GameSession->OnCreatePresenceSessionComplete().AddUObject(this, &UShooterGameInstance::OnCreatePresenceSessionComplete); + + TravelURL = InTravelURL; + bool const bIsLanMatch = InTravelURL.Contains(TEXT("?bIsLanMatch")); + + //determine the map name from the travelURL + const FString& MapNameSubStr = "/Game/Maps/"; + const FString& ChoppedMapName = TravelURL.RightChop(MapNameSubStr.Len()); + const FString& MapName = ChoppedMapName.LeftChop(ChoppedMapName.Len() - ChoppedMapName.Find("?game")); + + if (GameSession->HostSession(LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), NAME_GameSession, GameType, MapName, bIsLanMatch, true, AShooterGameSession::DEFAULT_NUM_PLAYERS)) + { + // If any error occurred in the above, pending state would be set + if ( (PendingState == CurrentState) || (PendingState == ShooterGameInstanceState::None) ) + { + // Go ahead and go into loading state now + // If we fail, the delegate will handle showing the proper messaging and move to the correct state + ShowLoadingScreen(); + GotoState(ShooterGameInstanceState::Playing); + return true; + } + } + } + + return false; +} + +bool UShooterGameInstance::JoinSession(ULocalPlayer* LocalPlayer, int32 SessionIndexInSearchResults) +{ + // needs to tear anything down based on current state? + + AShooterGameSession* const GameSession = GetGameSession(); + if (GameSession) + { + AddNetworkFailureHandlers(); + + OnJoinSessionCompleteDelegateHandle = GameSession->OnJoinSessionComplete().AddUObject(this, &UShooterGameInstance::OnJoinSessionComplete); + if (GameSession->JoinSession(LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), NAME_GameSession, SessionIndexInSearchResults)) + { + // If any error occured in the above, pending state would be set + if ( (PendingState == CurrentState) || (PendingState == ShooterGameInstanceState::None) ) + { + // Go ahead and go into loading state now + // If we fail, the delegate will handle showing the proper messaging and move to the correct state + ShowLoadingScreen(); + GotoState(ShooterGameInstanceState::Playing); + return true; + } + } + } + + return false; +} + +bool UShooterGameInstance::JoinSession(ULocalPlayer* LocalPlayer, const FOnlineSessionSearchResult& SearchResult) +{ + // needs to tear anything down based on current state? + AShooterGameSession* const GameSession = GetGameSession(); + if (GameSession) + { + AddNetworkFailureHandlers(); + + OnJoinSessionCompleteDelegateHandle = GameSession->OnJoinSessionComplete().AddUObject(this, &UShooterGameInstance::OnJoinSessionComplete); + if (GameSession->JoinSession(LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), NAME_GameSession, SearchResult)) + { + // If any error occured in the above, pending state would be set + if ( (PendingState == CurrentState) || (PendingState == ShooterGameInstanceState::None) ) + { + // Go ahead and go into loading state now + // If we fail, the delegate will handle showing the proper messaging and move to the correct state + ShowLoadingScreen(); + GotoState(ShooterGameInstanceState::Playing); + return true; + } + } + } + + return false; +} + +bool UShooterGameInstance::PlayDemo(ULocalPlayer* LocalPlayer, const FString& DemoName) +{ + ShowLoadingScreen(); + + // Play the demo + PlayReplay(DemoName); + + return true; +} + +/** Callback which is intended to be called upon finding sessions */ +void UShooterGameInstance::OnJoinSessionComplete(EOnJoinSessionCompleteResult::Type Result) +{ + // unhook the delegate + AShooterGameSession* const GameSession = GetGameSession(); + if (GameSession) + { + GameSession->OnJoinSessionComplete().Remove(OnJoinSessionCompleteDelegateHandle); + } + + // Add the splitscreen player if one exists + 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 + { + // We either failed or there is only a single local user + FinishJoinSession(Result); + } +} + +void UShooterGameInstance::FinishJoinSession(EOnJoinSessionCompleteResult::Type Result) +{ + if (Result != EOnJoinSessionCompleteResult::Success) + { + FText ReturnReason; + switch (Result) + { + case EOnJoinSessionCompleteResult::SessionIsFull: + ReturnReason = NSLOCTEXT("NetworkErrors", "JoinSessionFailed", "Game is full."); + break; + case EOnJoinSessionCompleteResult::SessionDoesNotExist: + ReturnReason = NSLOCTEXT("NetworkErrors", "JoinSessionFailed", "Game no longer exists."); + break; + default: + ReturnReason = NSLOCTEXT("NetworkErrors", "JoinSessionFailed", "Join failed."); + break; + } + + FText OKButton = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + RemoveNetworkFailureHandlers(); + ShowMessageThenGoMain(ReturnReason, OKButton, FText::GetEmpty()); + return; + } + + InternalTravelToSession(NAME_GameSession); +} + +void UShooterGameInstance::OnRegisterJoiningLocalPlayerComplete(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result) +{ + FinishJoinSession(Result); +} + +void UShooterGameInstance::InternalTravelToSession(const FName& SessionName) +{ + APlayerController * const PlayerController = GetFirstLocalPlayerController(); + + if ( PlayerController == nullptr ) + { + FText ReturnReason = NSLOCTEXT("NetworkErrors", "InvalidPlayerController", "Invalid Player Controller"); + FText OKButton = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + RemoveNetworkFailureHandlers(); + ShowMessageThenGoMain(ReturnReason, OKButton, FText::GetEmpty()); + return; + } + + // travel to session + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + + if ( OnlineSub == nullptr ) + { + FText ReturnReason = NSLOCTEXT("NetworkErrors", "OSSMissing", "OSS missing"); + FText OKButton = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + RemoveNetworkFailureHandlers(); + ShowMessageThenGoMain(ReturnReason, OKButton, FText::GetEmpty()); + return; + } + + FString URL; + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + + if ( !Sessions.IsValid() || !Sessions->GetResolvedConnectString( SessionName, URL ) ) + { + FText FailReason = NSLOCTEXT("NetworkErrors", "TravelSessionFailed", "Travel to Session failed."); + FText OKButton = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + ShowMessageThenGoMain(FailReason, OKButton, FText::GetEmpty()); + UE_LOG(LogOnlineGame, Warning, TEXT("Failed to travel to session upon joining it")); + return; + } + + // Add debug encryption token if desired. + if (CVarShooterGameTestEncryption->GetInt() != 0) + { + // This is just a value for testing/debugging, the server will use the same key regardless of the token value. + // But the token could be a user ID and/or session ID that would be used to generate a unique key per user and/or session, if desired. + URL += TEXT("?EncryptionToken=1"); + } + + PlayerController->ClientTravel(URL, TRAVEL_Absolute); +} + +/** Callback which is intended to be called upon session creation */ +void UShooterGameInstance::OnCreatePresenceSessionComplete(FName SessionName, bool bWasSuccessful) +{ + AShooterGameSession* const GameSession = GetGameSession(); + if (GameSession) + { + GameSession->OnCreatePresenceSessionComplete().Remove(OnCreatePresenceSessionCompleteDelegateHandle); + + // Add the splitscreen player if one exists + 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, &UShooterGameInstance::OnRegisterLocalPlayerComplete)); + } + } + else + { + // We either failed or there is only a single local user + FinishSessionCreation(bWasSuccessful ? EOnJoinSessionCompleteResult::Success : EOnJoinSessionCompleteResult::UnknownError); + } + } +} + +/** Initiates the session searching */ +bool UShooterGameInstance::FindSessions(ULocalPlayer* PlayerOwner, bool bIsDedicatedServer, bool bFindLAN) +{ + bool bResult = false; + + check(PlayerOwner != nullptr); + if (PlayerOwner) + { + AShooterGameSession* const GameSession = GetGameSession(); + if (GameSession) + { + GameSession->OnFindSessionsComplete().RemoveAll(this); + OnSearchSessionsCompleteDelegateHandle = GameSession->OnFindSessionsComplete().AddUObject(this, &UShooterGameInstance::OnSearchSessionsComplete); + + GameSession->FindSessions(PlayerOwner->GetPreferredUniqueNetId().GetUniqueNetId(), NAME_GameSession, bFindLAN, !bIsDedicatedServer); + + bResult = true; + } + } + + return bResult; +} + +/** Callback which is intended to be called upon finding sessions */ +void UShooterGameInstance::OnSearchSessionsComplete(bool bWasSuccessful) +{ + AShooterGameSession* const Session = GetGameSession(); + if (Session) + { + Session->OnFindSessionsComplete().Remove(OnSearchSessionsCompleteDelegateHandle); + } +} + +bool UShooterGameInstance::Tick(float DeltaSeconds) +{ + // Dedicated server doesn't need to worry about game state + if (IsDedicatedServerInstance() == true) + { + return true; + } + + UShooterGameViewportClient* ShooterViewport = Cast(GetGameViewportClient()); + if (FSlateApplication::IsInitialized() && ShooterViewport != nullptr) + { + if (FSlateApplication::Get().GetGameViewport() != ShooterViewport->GetGameViewportWidget()) + { + return true; + } + } + + // Because this takes place outside the normal UWorld tick, we need to register what world we're ticking/modifying here to avoid issues in the editor + FScopedConditionalWorldSwitcher WorldSwitcher(GetWorld()); + + MaybeChangeState(); + + if (CurrentState != ShooterGameInstanceState::WelcomeScreen && ShooterViewport != nullptr) + { + // If at any point we aren't licensed (but we are after welcome screen) bounce them back to the welcome screen + if (!bIsLicensed && CurrentState != ShooterGameInstanceState::None && ShooterViewport != nullptr && !ShooterViewport->IsShowingDialog()) + { + const FText ReturnReason = NSLOCTEXT( "ProfileMessages", "NeedLicense", "The signed in users do not have a license for this game. Please purchase ShooterGame from the Xbox Marketplace or sign in a user with a valid license." ); + const FText OKButton = NSLOCTEXT( "DialogButtons", "OKAY", "OK" ); + + ShowMessageThenGotoState( ReturnReason, OKButton, FText::GetEmpty(), ShooterGameInstanceState::WelcomeScreen ); + } + + // Show controller disconnected dialog if any local players have an invalid controller + if (!ShooterViewport->IsShowingDialog()) + { + for (int i = 0; i < LocalPlayers.Num(); ++i) + { + if (LocalPlayers[i] && LocalPlayers[i]->GetControllerId() == -1) + { + ShooterViewport->ShowDialog( + LocalPlayers[i], + EShooterDialogType::ControllerDisconnected, + FText::Format(NSLOCTEXT("ProfileMessages", "PlayerReconnectControllerFmt", "Player {0}, please reconnect your controller."), FText::AsNumber(i + 1)), +#if PLATFORM_PS4 + NSLOCTEXT("DialogButtons", "PS4_CrossButtonContinue", "Cross Button - Continue"), +#elif SHOOTER_XBOX_STRINGS + NSLOCTEXT("DialogButtons", "AButtonContinue", "A - Continue"), +#else + NSLOCTEXT("DialogButtons", "EnterContinue", "Enter - Continue"), +#endif + FText::GetEmpty(), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnControllerReconnectConfirm), + FOnClicked() + ); + } + } + } + } + + // If we have a pending invite, and we are at the welcome screen, and the session is properly shut down, accept it + if (PendingInvite.UserId.IsValid() && PendingInvite.bPrivilegesCheckedAndAllowed && CurrentState == ShooterGameInstanceState::PendingInvite) + { + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + IOnlineSessionPtr Sessions = (OnlineSub != NULL) ? OnlineSub->GetSessionInterface() : NULL; + + if (Sessions.IsValid()) + { + EOnlineSessionState::Type SessionState = Sessions->GetSessionState(NAME_GameSession); + + if (SessionState == EOnlineSessionState::NoSession) + { + ULocalPlayer * NewPlayerOwner = GetFirstGamePlayer(); + + if (NewPlayerOwner != nullptr) + { + NewPlayerOwner->SetControllerId(PendingInvite.ControllerId); + NewPlayerOwner->SetCachedUniqueNetId(PendingInvite.UserId); + SetOnlineMode(EOnlineMode::Online); + + const bool bIsLocalPlayerHost = PendingInvite.UserId.IsValid() && PendingInvite.InviteResult.Session.OwningUserId.IsValid() && *PendingInvite.UserId == *PendingInvite.InviteResult.Session.OwningUserId; + if (bIsLocalPlayerHost) + { + HostQuickSession(*NewPlayerOwner, PendingInvite.InviteResult.Session.SessionSettings); + } + else + { + JoinSession(NewPlayerOwner, PendingInvite.InviteResult); + } + } + + PendingInvite.UserId.Reset(); + } + } + } + + return true; +} + +bool UShooterGameInstance::HandleOpenCommand(const TCHAR* Cmd, FOutputDevice& Ar, UWorld* InWorld) +{ + bool const bOpenSuccessful = Super::HandleOpenCommand(Cmd, Ar, InWorld); + if (bOpenSuccessful) + { + GotoState(ShooterGameInstanceState::Playing); + } + + return bOpenSuccessful; +} + +bool UShooterGameInstance::HandleDisconnectCommand(const TCHAR* Cmd, FOutputDevice& Ar, UWorld* InWorld) +{ + bool const bDisconnectSuccessful = Super::HandleDisconnectCommand(Cmd, Ar, InWorld); + if (bDisconnectSuccessful) + { + GotoState(ShooterGameInstanceState::MainMenu); + } + + return bDisconnectSuccessful; +} + +bool UShooterGameInstance::HandleTravelCommand(const TCHAR* Cmd, FOutputDevice& Ar, UWorld* InWorld) +{ + bool const bTravelSuccessful = Super::HandleTravelCommand(Cmd, Ar, InWorld); + if (bTravelSuccessful) + { + GotoState(ShooterGameInstanceState::Playing); + } + + return bTravelSuccessful; +} + + +void UShooterGameInstance::HandleSignInChangeMessaging() +{ + // Master user signed out, go to initial state (if we aren't there already) + if ( CurrentState != GetInitialState() ) + { +#if SHOOTER_CONSOLE_UI + // Display message on consoles + const FText ReturnReason = NSLOCTEXT( "ProfileMessages", "SignInChange", "Sign in status change occurred." ); + const FText OKButton = NSLOCTEXT( "DialogButtons", "OKAY", "OK" ); + + ShowMessageThenGotoState(ReturnReason, OKButton, FText::GetEmpty(), GetInitialState()); +#else + GotoInitialState(); +#endif + } +} + +void UShooterGameInstance::HandleUserLoginChanged(int32 GameUserIndex, ELoginStatus::Type PreviousLoginStatus, ELoginStatus::Type LoginStatus, const FUniqueNetId& UserId) +{ + // On Switch, accounts can play in LAN games whether they are signed in online or not. +#if PLATFORM_SWITCH + const bool bDowngraded = LoginStatus == ELoginStatus::NotLoggedIn || (GetOnlineMode() == EOnlineMode::Online && LoginStatus == ELoginStatus::UsingLocalProfile); +#else + const bool bDowngraded = (LoginStatus == ELoginStatus::NotLoggedIn && GetOnlineMode() == EOnlineMode::Offline) || (LoginStatus != ELoginStatus::LoggedIn && GetOnlineMode() != EOnlineMode::Offline); +#endif + + UE_LOG( LogOnline, Log, TEXT( "HandleUserLoginChanged: bDownGraded: %i" ), (int)bDowngraded ); + + TSharedPtr GenericApplication = FSlateApplication::Get().GetPlatformApplication(); + bIsLicensed = GenericApplication->ApplicationLicenseValid(); + + // Find the local player associated with this unique net id + ULocalPlayer * LocalPlayer = FindLocalPlayerFromUniqueNetId( UserId ); + + LocalPlayerOnlineStatus[GameUserIndex] = LoginStatus; + + // If this user is signed out, but was previously signed in, punt to welcome (or remove splitscreen if that makes sense) + if ( LocalPlayer != NULL ) + { + if (bDowngraded) + { + UE_LOG( LogOnline, Log, TEXT( "HandleUserLoginChanged: Player logged out: %s" ), *UserId.ToString() ); + + LabelPlayerAsQuitter(LocalPlayer); + + // Check to see if this was the master, or if this was a split-screen player on the client + if ( LocalPlayer == GetFirstGamePlayer() || GetOnlineMode() != EOnlineMode::Offline ) + { + HandleSignInChangeMessaging(); + } + else + { + // Remove local split-screen players from the list + RemoveExistingLocalPlayer( LocalPlayer ); + } + } + } +} + +void UShooterGameInstance::HandleAppWillDeactivate() +{ + if (CurrentState == ShooterGameInstanceState::Playing) + { + // Just have the first player controller pause the game. + UWorld* const GameWorld = GetWorld(); + if (GameWorld) + { + // protect against a second pause menu loading on top of an existing one if someone presses the Jewel / PS buttons. + bool bNeedsPause = true; + for (FConstControllerIterator It = GameWorld->GetControllerIterator(); It; ++It) + { + AShooterPlayerController* Controller = Cast(*It); + if (Controller && (Controller->IsPaused() || Controller->IsGameMenuVisible())) + { + bNeedsPause = false; + break; + } + } + + if (bNeedsPause) + { + AShooterPlayerController* const Controller = Cast(GameWorld->GetFirstPlayerController()); + if (Controller) + { + Controller->ShowInGameMenu(); + } + } + } + } +} + +void UShooterGameInstance::HandleAppSuspend() +{ + // Players will lose connection on resume. However it is possible the game will exit before we get a resume, so we must kick off round end events here. + UE_LOG( LogOnline, Warning, TEXT( "UShooterGameInstance::HandleAppSuspend" ) ); + UWorld* const World = GetWorld(); + AShooterGameState* const GameState = World != NULL ? World->GetGameState() : NULL; + + if ( CurrentState != ShooterGameInstanceState::None && CurrentState != GetInitialState() ) + { + UE_LOG( LogOnline, Warning, TEXT( "UShooterGameInstance::HandleAppSuspend: Sending round end event for players" ) ); + + // Send round end events for local players + for (int i = 0; i < LocalPlayers.Num(); ++i) + { + AShooterPlayerController* ShooterPC = Cast(LocalPlayers[i]->PlayerController); + if (ShooterPC && GameState) + { + // Assuming you can't win if you quit early + ShooterPC->ClientSendRoundEndEvent(false, GameState->ElapsedTime); + } + } + } +} + +void UShooterGameInstance::HandleAppResume() +{ + UE_LOG( LogOnline, Log, TEXT( "UShooterGameInstance::HandleAppResume" ) ); + + if ( CurrentState != ShooterGameInstanceState::None && CurrentState != GetInitialState() ) + { + UE_LOG( LogOnline, Warning, TEXT( "UShooterGameInstance::HandleAppResume: Attempting to sign out players" ) ); + + for ( int32 i = 0; i < LocalPlayers.Num(); ++i ) + { + if ( LocalPlayers[i]->GetCachedUniqueNetId().IsValid() && LocalPlayerOnlineStatus[i] == ELoginStatus::LoggedIn && !IsLocalPlayerOnline( LocalPlayers[i] ) ) + { + UE_LOG( LogOnline, Log, TEXT( "UShooterGameInstance::HandleAppResume: Signed out during resume." ) ); + HandleSignInChangeMessaging(); + break; + } + } + } +} + +void UShooterGameInstance::HandleAppLicenseUpdate() +{ + TSharedPtr GenericApplication = FSlateApplication::Get().GetPlatformApplication(); + bIsLicensed = GenericApplication->ApplicationLicenseValid(); +} + +void UShooterGameInstance::HandleSafeFrameChanged() +{ + UCanvas::UpdateAllCanvasSafeZoneData(); +} + +void UShooterGameInstance::RemoveExistingLocalPlayer(ULocalPlayer* ExistingPlayer) +{ + check(ExistingPlayer); + if (ExistingPlayer->PlayerController != NULL) + { + // Kill the player + AShooterCharacter* MyPawn = Cast(ExistingPlayer->PlayerController->GetPawn()); + if ( MyPawn ) + { + MyPawn->KilledBy(NULL); + } + } + + // Remove local split-screen players from the list + RemoveLocalPlayer( ExistingPlayer ); +} + +void UShooterGameInstance::RemoveSplitScreenPlayers() +{ + // if we had been split screen, toss the extra players now + // remove every player, back to front, except the first one + while (LocalPlayers.Num() > 1) + { + ULocalPlayer* const PlayerToRemove = LocalPlayers.Last(); + RemoveExistingLocalPlayer(PlayerToRemove); + } +} + +FReply UShooterGameInstance::OnPairingUsePreviousProfile() +{ + // Do nothing (except hide the message) if they want to continue using previous profile + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + + if ( ShooterViewport != nullptr ) + { + ShooterViewport->HideDialog(); + } + + return FReply::Handled(); +} + +FReply UShooterGameInstance::OnPairingUseNewProfile() +{ + HandleSignInChangeMessaging(); + return FReply::Handled(); +} + +void UShooterGameInstance::HandleControllerPairingChanged(int GameUserIndex, FControllerPairingChangedUserInfo PreviousUserInfo, FControllerPairingChangedUserInfo NewUserInfo) +{ + UE_LOG(LogOnlineGame, Log, TEXT("UShooterGameInstance::HandleControllerPairingChanged GameUserIndex %d PreviousUser '%s' NewUser '%s'"), + GameUserIndex, *PreviousUserInfo.User.ToDebugString(), *NewUserInfo.User.ToDebugString()); + + if ( CurrentState == ShooterGameInstanceState::WelcomeScreen ) + { + // Don't care about pairing changes at welcome screen + return; + } + +#if SHOOTER_CONSOLE_UI && CONTROLLER_SWAPPING + const FUniqueNetId& PreviousUser = PreviousUserInfo.User; + const FUniqueNetId& NewUser = NewUserInfo.User; + if ( IgnorePairingChangeForControllerId != -1 && GameUserIndex == IgnorePairingChangeForControllerId ) + { + // We were told to ignore + IgnorePairingChangeForControllerId = -1; // Reset now so there there is no chance this remains in a bad state + return; + } + + if ( PreviousUser.IsValid() && !NewUser.IsValid() && SHOOTER_CONTROLLER_PAIRING_ON_DISCONNECT ) + { + // Treat this as a disconnect or signout, which is handled somewhere else + return; + } + + if ( !PreviousUser.IsValid() && NewUser.IsValid() ) + { + // Treat this as a signin + ULocalPlayer * ControlledLocalPlayer = FindLocalPlayerFromControllerId( GameUserIndex ); + + if ( ControlledLocalPlayer != NULL && !ControlledLocalPlayer->GetCachedUniqueNetId().IsValid() ) + { + // If a player that previously selected "continue without saving" signs into this controller, move them back to welcome screen + HandleSignInChangeMessaging(); + } + + return; + } + + // Find the local player currently being controlled by this controller + ULocalPlayer * ControlledLocalPlayer = FindLocalPlayerFromControllerId( GameUserIndex ); + + // See if the newly assigned profile is in our local player list + ULocalPlayer * NewLocalPlayer = FindLocalPlayerFromUniqueNetId( NewUser ); + + // If the local player being controlled is not the target of the pairing change, then give them a chance + // to continue controlling the old player with this controller + if ( ControlledLocalPlayer != nullptr && ControlledLocalPlayer != NewLocalPlayer ) + { + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + + if ( ShooterViewport != nullptr ) + { + ShooterViewport->ShowDialog( + nullptr, + EShooterDialogType::Generic, + NSLOCTEXT("ProfileMessages", "PairingChanged", "Your controller has been paired to another profile, would you like to switch to this new profile now? Selecting YES will sign out of the previous profile."), + NSLOCTEXT("DialogButtons", "YES", "A - YES"), + NSLOCTEXT("DialogButtons", "NO", "B - NO"), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnPairingUseNewProfile), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnPairingUsePreviousProfile) + ); + } + } +#endif +} + +void UShooterGameInstance::HandleControllerConnectionChange( bool bIsConnection, FPlatformUserId Unused, int32 GameUserIndex ) +{ + UE_LOG(LogOnlineGame, Log, TEXT("UShooterGameInstance::HandleControllerConnectionChange bIsConnection %d GameUserIndex %d"), + bIsConnection, GameUserIndex); + + if(!bIsConnection) + { + // Controller was disconnected + + // Find the local player associated with this user index + ULocalPlayer * LocalPlayer = FindLocalPlayerFromControllerId( GameUserIndex ); + + if ( LocalPlayer == NULL ) + { + return; // We don't care about players we aren't tracking + } + + // Invalidate this local player's controller id. + LocalPlayer->SetControllerId(-1); + } +} + +FReply UShooterGameInstance::OnControllerReconnectConfirm() +{ + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + if(ShooterViewport) + { + ShooterViewport->HideDialog(); + } + + return FReply::Handled(); +} + +TSharedPtr< const FUniqueNetId > UShooterGameInstance::GetUniqueNetIdFromControllerId( const int ControllerId ) +{ + IOnlineIdentityPtr OnlineIdentityInt = Online::GetIdentityInterface(GetWorld()); + + if ( OnlineIdentityInt.IsValid() ) + { + TSharedPtr UniqueId = OnlineIdentityInt->GetUniquePlayerId( ControllerId ); + + if ( UniqueId.IsValid() ) + { + return UniqueId; + } + } + + return nullptr; +} + +void UShooterGameInstance::SetOnlineMode(EOnlineMode InOnlineMode) +{ + OnlineMode = InOnlineMode; + UpdateUsingMultiplayerFeatures(InOnlineMode == EOnlineMode::Online); +} + +void UShooterGameInstance::UpdateUsingMultiplayerFeatures(bool bIsUsingMultiplayerFeatures) +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + + if (OnlineSub) + { + for (int32 i = 0; i < LocalPlayers.Num(); ++i) + { + ULocalPlayer* LocalPlayer = LocalPlayers[i]; + + FUniqueNetIdRepl PlayerId = LocalPlayer->GetPreferredUniqueNetId(); + if (PlayerId.IsValid()) + { + OnlineSub->SetUsingMultiplayerFeatures(*PlayerId, bIsUsingMultiplayerFeatures); + } + } + } +} + +void UShooterGameInstance::TravelToSession(const FName& SessionName) +{ + // Added to handle failures when joining using quickmatch (handles issue of joining a game that just ended, i.e. during game ending timer) + AddNetworkFailureHandlers(); + ShowLoadingScreen(); + GotoState(ShooterGameInstanceState::Playing); + InternalTravelToSession(SessionName); +} + +void UShooterGameInstance::DirectConnectToSession(const FString& Address) +{ + AddNetworkFailureHandlers(); + ShowLoadingScreen(); + GotoState(ShooterGameInstanceState::Playing); + GetPrimaryPlayerController()->ClientTravel(Address, TRAVEL_Absolute); +} + +void UShooterGameInstance::SetIgnorePairingChangeForControllerId( const int32 ControllerId ) +{ + IgnorePairingChangeForControllerId = ControllerId; +} + +bool UShooterGameInstance::IsLocalPlayerOnline(ULocalPlayer* LocalPlayer) +{ + if (LocalPlayer == NULL) + { + return false; + } + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if(OnlineSub) + { + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + if(IdentityInterface.IsValid()) + { + FUniqueNetIdRepl UniqueId = LocalPlayer->GetCachedUniqueNetId(); + if (UniqueId.IsValid()) + { + const ELoginStatus::Type LoginStatus = IdentityInterface->GetLoginStatus(*UniqueId); + if(LoginStatus == ELoginStatus::LoggedIn) + { + return true; + } + } + } + } + + return false; +} + +bool UShooterGameInstance::IsLocalPlayerSignedIn(ULocalPlayer* LocalPlayer) +{ + if (LocalPlayer == NULL) + { + return false; + } + + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + if (IdentityInterface.IsValid()) + { + FUniqueNetIdRepl UniqueId = LocalPlayer->GetCachedUniqueNetId(); + if (UniqueId.IsValid()) + { + return true; + } + } + } + + return false; +} + +bool UShooterGameInstance::ValidatePlayerForOnlinePlay(ULocalPlayer* LocalPlayer) +{ + // Get the viewport + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + +#if NEED_XBOX_LIVE_FOR_ONLINE + if (CurrentConnectionStatus != EOnlineServerConnectionStatus::Connected) + { + // Don't let them play online if they aren't connected to Xbox LIVE + if (ShooterViewport != NULL) + { + const FText Msg = NSLOCTEXT("NetworkFailures", "ServiceDisconnected", "You must be connected to the Xbox LIVE service to play online."); + const FText OKButtonString = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + + ShooterViewport->ShowDialog( + NULL, + EShooterDialogType::Generic, + Msg, + OKButtonString, + FText::GetEmpty(), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric) + ); + } + + return false; + } +#endif + + if (!IsLocalPlayerOnline(LocalPlayer)) + { + // Don't let them play online if they aren't online + if (ShooterViewport != NULL) + { + const FText Msg = NSLOCTEXT("NetworkFailures", "MustBeSignedIn", "You must be signed in to play online"); + const FText OKButtonString = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + + ShooterViewport->ShowDialog( + NULL, + EShooterDialogType::Generic, + Msg, + OKButtonString, + FText::GetEmpty(), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric) + ); + } + + return false; + } + + return true; +} + +bool UShooterGameInstance::ValidatePlayerIsSignedIn(ULocalPlayer* LocalPlayer) +{ + // Get the viewport + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + + if (!IsLocalPlayerSignedIn(LocalPlayer)) + { + // Don't let them play online if they aren't online + if (ShooterViewport != NULL) + { + const FText Msg = NSLOCTEXT("NetworkFailures", "MustBeSignedIn", "You must be signed in to play online"); + const FText OKButtonString = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + + ShooterViewport->ShowDialog( + NULL, + EShooterDialogType::Generic, + Msg, + OKButtonString, + FText::GetEmpty(), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric) + ); + } + + return false; + } + + return true; +} + + +FReply UShooterGameInstance::OnConfirmGeneric() +{ + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + if(ShooterViewport) + { + ShooterViewport->HideDialog(); + } + + return FReply::Handled(); +} + +void UShooterGameInstance::StartOnlinePrivilegeTask(const IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate& Delegate, EUserPrivileges::Type Privilege, TSharedPtr< const FUniqueNetId > UserId) +{ + WaitMessageWidget = SNew(SShooterWaitDialog) + .MessageText(NSLOCTEXT("NetworkStatus", "CheckingPrivilegesWithServer", "Checking privileges with server. Please wait...")); + + if (GEngine && GEngine->GameViewport) + { + UGameViewportClient* const GVC = GEngine->GameViewport; + GVC->AddViewportWidgetContent(WaitMessageWidget.ToSharedRef()); + } + + IOnlineIdentityPtr Identity = Online::GetIdentityInterface(GetWorld()); + if (Identity.IsValid() && UserId.IsValid()) + { + Identity->GetUserPrivilege(*UserId, Privilege, Delegate); + } + else + { + // Can only get away with faking the UniqueNetId here because the delegates don't use it + Delegate.ExecuteIfBound(FUniqueNetIdString(), Privilege, (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures); + } +} + +void UShooterGameInstance::CleanupOnlinePrivilegeTask() +{ + if (GEngine && GEngine->GameViewport && WaitMessageWidget.IsValid()) + { + UGameViewportClient* const GVC = GEngine->GameViewport; + GVC->RemoveViewportWidgetContent(WaitMessageWidget.ToSharedRef()); + } +} + +void UShooterGameInstance::DisplayOnlinePrivilegeFailureDialogs(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + // Show warning that the user cannot play due to age restrictions + UShooterGameViewportClient * ShooterViewport = Cast(GetGameViewportClient()); + TWeakObjectPtr OwningPlayer; + if (GEngine) + { + for (auto It = GEngine->GetLocalPlayerIterator(GetWorld()); It; ++It) + { + FUniqueNetIdRepl OtherId = (*It)->GetPreferredUniqueNetId(); + if (OtherId.IsValid()) + { + if (UserId == (*OtherId)) + { + OwningPlayer = *It; + } + } + } + } + + if (ShooterViewport != NULL && OwningPlayer.IsValid()) + { + if ((PrivilegeResults & (uint32)IOnlineIdentity::EPrivilegeResults::AccountTypeFailure) != 0) + { + IOnlineExternalUIPtr ExternalUI = Online::GetExternalUIInterface(GetWorld()); + if (ExternalUI.IsValid()) + { + ExternalUI->ShowAccountUpgradeUI(UserId); + } + } + else if ((PrivilegeResults & (uint32)IOnlineIdentity::EPrivilegeResults::RequiredSystemUpdate) != 0) + { + ShooterViewport->ShowDialog( + OwningPlayer.Get(), + EShooterDialogType::Generic, + NSLOCTEXT("OnlinePrivilegeResult", "RequiredSystemUpdate", "A required system update is available. Please upgrade to access online features."), + NSLOCTEXT("DialogButtons", "OKAY", "OK"), + FText::GetEmpty(), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric) + ); + } + else if ((PrivilegeResults & (uint32)IOnlineIdentity::EPrivilegeResults::RequiredPatchAvailable) != 0) + { + ShooterViewport->ShowDialog( + OwningPlayer.Get(), + EShooterDialogType::Generic, + NSLOCTEXT("OnlinePrivilegeResult", "RequiredPatchAvailable", "A required game patch is available. Please upgrade to access online features."), + NSLOCTEXT("DialogButtons", "OKAY", "OK"), + FText::GetEmpty(), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric) + ); + } + else if ((PrivilegeResults & (uint32)IOnlineIdentity::EPrivilegeResults::AgeRestrictionFailure) != 0) + { + ShooterViewport->ShowDialog( + OwningPlayer.Get(), + EShooterDialogType::Generic, + NSLOCTEXT("OnlinePrivilegeResult", "AgeRestrictionFailure", "Cannot play due to age restrictions!"), + NSLOCTEXT("DialogButtons", "OKAY", "OK"), + FText::GetEmpty(), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric) + ); + } + else if ((PrivilegeResults & (uint32)IOnlineIdentity::EPrivilegeResults::UserNotFound) != 0) + { + ShooterViewport->ShowDialog( + OwningPlayer.Get(), + EShooterDialogType::Generic, + NSLOCTEXT("OnlinePrivilegeResult", "UserNotFound", "Cannot play due invalid user!"), + NSLOCTEXT("DialogButtons", "OKAY", "OK"), + FText::GetEmpty(), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric) + ); + } + else if ((PrivilegeResults & (uint32)IOnlineIdentity::EPrivilegeResults::GenericFailure) != 0) + { + ShooterViewport->ShowDialog( + OwningPlayer.Get(), + EShooterDialogType::Generic, + NSLOCTEXT("OnlinePrivilegeResult", "GenericFailure", "Cannot play online. Check your network connection."), + NSLOCTEXT("DialogButtons", "OKAY", "OK"), + FText::GetEmpty(), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric), + FOnClicked::CreateUObject(this, &UShooterGameInstance::OnConfirmGeneric) + ); + } + } +} + +void UShooterGameInstance::OnRegisterLocalPlayerComplete(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result) +{ + FinishSessionCreation(Result); +} + +void UShooterGameInstance::FinishSessionCreation(EOnJoinSessionCompleteResult::Type Result) +{ + if (Result == EOnJoinSessionCompleteResult::Success) + { + // Travel to the specified match URL + GetWorld()->ServerTravel(TravelURL); + } + else + { + FText ReturnReason = NSLOCTEXT("NetworkErrors", "CreateSessionFailed", "Failed to create session."); + FText OKButton = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + ShowMessageThenGoMain(ReturnReason, OKButton, FText::GetEmpty()); + } +} + +FString UShooterGameInstance::GetQuickMatchUrl() +{ + static const FString QuickMatchUrl(TEXT("/Game/Maps/Highrise?game=TDM?listen")); + return QuickMatchUrl; +} + +void UShooterGameInstance::BeginHostingQuickMatch() +{ + ShowLoadingScreen(); + GotoState(ShooterGameInstanceState::Playing); + + // Travel to the specified match URL + GetWorld()->ServerTravel(GetQuickMatchUrl()); +} + +void UShooterGameInstance::ReceivedNetworkEncryptionToken(const FString& EncryptionToken, const FOnEncryptionKeyResponse& Delegate) +{ + // This is a simple implementation to demonstrate using encryption for game traffic using a hardcoded key. + // For a complete implementation, you would likely want to retrieve the encryption key from a secure source, + // such as from a web service over HTTPS. This could be done in this function, even asynchronously - just + // call the response delegate passed in once the key is known. The contents of the EncryptionToken is up to the user, + // but it will generally contain information used to generate a unique encryption key, such as a user and/or session ID. + + FEncryptionKeyResponse Response(EEncryptionResponse::Failure, TEXT("Unknown encryption failure")); + + if (EncryptionToken.IsEmpty()) + { + Response.Response = EEncryptionResponse::InvalidToken; + Response.ErrorMsg = TEXT("Encryption token is empty."); + } + else + { + Response.Response = EEncryptionResponse::Success; + Response.EncryptionData.Key = DebugTestEncryptionKey; + } + + Delegate.ExecuteIfBound(Response); + +} + +void UShooterGameInstance::ReceivedNetworkEncryptionAck(const FOnEncryptionKeyResponse& Delegate) +{ + // This is a simple implementation to demonstrate using encryption for game traffic using a hardcoded key. + // For a complete implementation, you would likely want to retrieve the encryption key from a secure source, + // such as from a web service over HTTPS. This could be done in this function, even asynchronously - just + // call the response delegate passed in once the key is known. + + FEncryptionKeyResponse Response; + Response.Response = EEncryptionResponse::Success; + Response.EncryptionData.Key = DebugTestEncryptionKey; + + Delegate.ExecuteIfBound(Response); +} + +void UShooterGameInstance::OnGameActivityActivationRequestComplete(const FUniqueNetId& PlayerId, const FString& ActivityId, const FOnlineSessionSearchResult* SessionInfo) +{ + if (WelcomeMenuUI.IsValid()) + { + WelcomeMenuUI->LockControls(false); + + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + check(IdentityInterface.IsValid()); + int32 LocalPlayerNum = IdentityInterface->GetPlatformUserIdFromUniqueNetId(PlayerId); + WelcomeMenuUI->SetControllerAndAdvanceToMainMenu(LocalPlayerNum); + } + +} diff --git a/Source/ShooterGame/Private/ShooterGameModule.cpp b/Source/ShooterGame/Private/ShooterGameModule.cpp new file mode 100644 index 0000000..85797aa --- /dev/null +++ b/Source/ShooterGame/Private/ShooterGameModule.cpp @@ -0,0 +1,41 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterGameDelegates.h" + +#include "ShooterMenuSoundsWidgetStyle.h" +#include "ShooterMenuWidgetStyle.h" +#include "ShooterMenuItemWidgetStyle.h" +#include "ShooterOptionsWidgetStyle.h" +#include "ShooterScoreboardWidgetStyle.h" +#include "ShooterChatWidgetStyle.h" +#include "AssetRegistryModule.h" +#include "IAssetRegistry.h" + + + +#include "UI/Style/ShooterStyle.h" + + +class FShooterGameModule : public FDefaultGameModuleImpl +{ + virtual void StartupModule() override + { + InitializeShooterGameDelegates(); + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + + //Hot reload hack + FSlateStyleRegistry::UnRegisterSlateStyle(FShooterStyle::GetStyleSetName()); + FShooterStyle::Initialize(); + } + + virtual void ShutdownModule() override + { + FShooterStyle::Shutdown(); + } +}; + +IMPLEMENT_PRIMARY_GAME_MODULE(FShooterGameModule, ShooterGame, "ShooterGame"); + +DEFINE_LOG_CATEGORY(LogShooter) +DEFINE_LOG_CATEGORY(LogShooterWeapon) diff --git a/Source/ShooterGame/Private/ShooterGameUserSettings.cpp b/Source/ShooterGame/Private/ShooterGameUserSettings.cpp new file mode 100644 index 0000000..1fb4d84 --- /dev/null +++ b/Source/ShooterGame/Private/ShooterGameUserSettings.cpp @@ -0,0 +1,34 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterGameUserSettings.h" + +UShooterGameUserSettings::UShooterGameUserSettings(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SetToDefaults(); +} + +void UShooterGameUserSettings::SetToDefaults() +{ + Super::SetToDefaults(); + + GraphicsQuality = 1; + bIsLanMatch = true; + bIsDedicatedServer = false; + bIsForceSystemResolution = false; +} + +void UShooterGameUserSettings::ApplySettings(bool bCheckForCommandLineOverrides) +{ + if (GraphicsQuality == 0) + { + ScalabilityQuality.SetFromSingleQualityLevel(1); + } + else + { + ScalabilityQuality.SetFromSingleQualityLevel(3); + } + + Super::ApplySettings(bCheckForCommandLineOverrides); +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/ShooterGameViewportClient.cpp b/Source/ShooterGame/Private/ShooterGameViewportClient.cpp new file mode 100644 index 0000000..3416c8e --- /dev/null +++ b/Source/ShooterGame/Private/ShooterGameViewportClient.cpp @@ -0,0 +1,309 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterGameViewportClient.h" +#include "SShooterConfirmationDialog.h" +#include "SSafeZone.h" +#include "SThrobber.h" +#include "Player/ShooterLocalPlayer.h" + +UShooterGameViewportClient::UShooterGameViewportClient(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SetSuppressTransitionMessage(true); +} + +void UShooterGameViewportClient::NotifyPlayerAdded(int32 PlayerIndex, ULocalPlayer* AddedPlayer) +{ + Super::NotifyPlayerAdded(PlayerIndex, AddedPlayer); + + UShooterLocalPlayer* const ShooterLP = Cast(AddedPlayer); + if (ShooterLP) + { + ShooterLP->LoadPersistentUser(); + } +} + +void UShooterGameViewportClient::AddViewportWidgetContent( TSharedRef ViewportContent, const int32 ZOrder ) +{ + UE_LOG( LogPlayerManagement, Log, TEXT( "UShooterGameViewportClient::AddViewportWidgetContent: %p" ), &ViewportContent.Get() ); + + if ( ( DialogWidget.IsValid() || LoadingScreenWidget.IsValid() ) && ViewportContent != DialogWidget && ViewportContent != LoadingScreenWidget ) + { + // Add to hidden list, and don't show until we hide the dialog widget + HiddenViewportContentStack.AddUnique( ViewportContent ); + return; + } + + if ( ViewportContentStack.Contains( ViewportContent ) ) + { + return; + } + + ViewportContentStack.AddUnique( ViewportContent ); + + Super::AddViewportWidgetContent( ViewportContent, 0 ); +} + +void UShooterGameViewportClient::RemoveViewportWidgetContent( TSharedRef ViewportContent ) +{ + UE_LOG( LogPlayerManagement, Log, TEXT( "UShooterGameViewportClient::RemoveViewportWidgetContent: %p" ), &ViewportContent.Get() ); + + ViewportContentStack.Remove( ViewportContent ); + HiddenViewportContentStack.Remove( ViewportContent ); + + Super::RemoveViewportWidgetContent( ViewportContent ); +} + +void UShooterGameViewportClient::HideExistingWidgets() +{ + check( HiddenViewportContentStack.Num() == 0 ); + + TArray> CopyOfViewportContentStack = ViewportContentStack; + + for ( int32 i = ViewportContentStack.Num() - 1; i >= 0; i-- ) + { + RemoveViewportWidgetContent( ViewportContentStack[i] ); + } + + HiddenViewportContentStack = CopyOfViewportContentStack; +} + +void UShooterGameViewportClient::ShowExistingWidgets() +{ + // We shouldn't have any visible widgets at this point + check( ViewportContentStack.Num() == 0 ); + + // Unhide all of the previously hidden widgets + for ( int32 i = 0; i < HiddenViewportContentStack.Num(); i++ ) + { + AddViewportWidgetContent( HiddenViewportContentStack[i] ); + } + + check( ViewportContentStack.Num() == HiddenViewportContentStack.Num() ); + + // Done with these + HiddenViewportContentStack.Empty(); +} + +void UShooterGameViewportClient::ShowDialog(TWeakObjectPtr PlayerOwner, EShooterDialogType::Type DialogType, const FText& Message, const FText& Confirm, const FText& Cancel, const FOnClicked& OnConfirm, const FOnClicked& OnCancel) +{ + UE_LOG( LogPlayerManagement, Log, TEXT( "UShooterGameViewportClient::ShowDialog..." ) ); + + if ( DialogWidget.IsValid() ) + { + return; // Already showing a dialog box + } + + // Hide all existing widgets + if ( !LoadingScreenWidget.IsValid() ) + { + HideExistingWidgets(); + } + + DialogWidget = SNew( SShooterConfirmationDialog ) + .PlayerOwner(PlayerOwner) + .DialogType(DialogType) + .MessageText(Message) + .ConfirmText(Confirm) + .CancelText(Cancel) + .OnConfirmClicked(OnConfirm) + .OnCancelClicked(OnCancel); + + if ( LoadingScreenWidget.IsValid() ) + { + // Can't show dialog while loading screen is visible + HiddenViewportContentStack.Add( DialogWidget.ToSharedRef() ); + } + else + { + AddViewportWidgetContent( DialogWidget.ToSharedRef() ); + + // Remember what widget currently has focus + OldFocusWidget = FSlateApplication::Get().GetKeyboardFocusedWidget(); + + // Force focus to the dialog widget + FSlateApplication::Get().SetKeyboardFocus( DialogWidget, EFocusCause::SetDirectly ); + } +} + +void UShooterGameViewportClient::HideDialog() +{ + UE_LOG( LogPlayerManagement, Log, TEXT( "UShooterGameViewportClient::HideDialog. DialogWidget: %p, OldFocusWidget: %p" ), DialogWidget.Get(), OldFocusWidget.Get() ); + + if ( DialogWidget.IsValid() ) + { + const bool bRestoreOldFocus = OldFocusWidget.IsValid() && FSlateApplication::Get().GetKeyboardFocusedWidget() == DialogWidget; + + // Hide the dialog widget + RemoveViewportWidgetContent( DialogWidget.ToSharedRef() ); + + // Destroy the dialog widget + DialogWidget = NULL; + + if ( !LoadingScreenWidget.IsValid() ) + { + ShowExistingWidgets(); + } + + // Restore focus to last widget (but only if the dialog currently has focus still) + if ( bRestoreOldFocus ) + { + FSlateApplication::Get().SetKeyboardFocus( OldFocusWidget, EFocusCause::SetDirectly ); + } + + OldFocusWidget = NULL; + } +} + +void UShooterGameViewportClient::ShowLoadingScreen() +{ + if ( LoadingScreenWidget.IsValid() ) + { + return; + } + + if ( DialogWidget.IsValid() ) + { + // Hide the dialog widget (loading screen takes priority) + check( !HiddenViewportContentStack.Contains( DialogWidget.ToSharedRef() ) ); + check( ViewportContentStack.Contains( DialogWidget.ToSharedRef() ) ); + RemoveViewportWidgetContent( DialogWidget.ToSharedRef() ); + HiddenViewportContentStack.Add( DialogWidget.ToSharedRef() ); + } + else + { + // Hide all existing widgets + HideExistingWidgets(); + } + + LoadingScreenWidget = SNew( SShooterLoadingScreen ); + + AddViewportWidgetContent( LoadingScreenWidget.ToSharedRef() ); +} + +void UShooterGameViewportClient::HideLoadingScreen() +{ + if ( !LoadingScreenWidget.IsValid() ) + { + return; + } + + RemoveViewportWidgetContent( LoadingScreenWidget.ToSharedRef() ); + + LoadingScreenWidget = NULL; + + // Show the dialog widget if we need to + if ( DialogWidget.IsValid() ) + { + check( HiddenViewportContentStack.Contains( DialogWidget.ToSharedRef() ) ); + check( !ViewportContentStack.Contains( DialogWidget.ToSharedRef() ) ); + HiddenViewportContentStack.Remove( DialogWidget.ToSharedRef() ); + AddViewportWidgetContent( DialogWidget.ToSharedRef() ); + } + else + { + ShowExistingWidgets(); + } +} + +EShooterDialogType::Type UShooterGameViewportClient::GetDialogType() const +{ + return (DialogWidget.IsValid() ? DialogWidget->DialogType : EShooterDialogType::None); +} + +TWeakObjectPtr UShooterGameViewportClient::GetDialogOwner() const +{ + return (DialogWidget.IsValid() ? DialogWidget->PlayerOwner : nullptr); +} + +void UShooterGameViewportClient::Tick(float DeltaSeconds) +{ + if ( DialogWidget.IsValid() && !LoadingScreenWidget.IsValid() ) + { + // Make sure the dialog widget always has focus + if ( FSlateApplication::Get().GetKeyboardFocusedWidget() != DialogWidget ) + { + // Remember which widget had focus before we override it + OldFocusWidget = FSlateApplication::Get().GetKeyboardFocusedWidget(); + + // Force focus back to dialog + FSlateApplication::Get().SetKeyboardFocus( DialogWidget, EFocusCause::SetDirectly ); + } + } +} + +#if WITH_EDITOR +void UShooterGameViewportClient::DrawTransition(UCanvas* Canvas) +{ + if (GetOuterUEngine() != NULL) + { + ETransitionType Type = GetOuterUEngine()->TransitionType; + switch (Type) + { + case ETransitionType::Connecting: + DrawTransitionMessage(Canvas, NSLOCTEXT("GameViewportClient", "ConnectingMessage", "CONNECTING").ToString()); + break; + case ETransitionType::WaitingToConnect: + DrawTransitionMessage(Canvas, NSLOCTEXT("GameViewportClient", "Waitingtoconnect", "Waiting to connect...").ToString()); + break; + } + } +} +#endif //WITH_EDITOR + +void UShooterGameViewportClient::BeginDestroy() +{ + ReleaseSlateResources(); + + Super::BeginDestroy(); +} + +void UShooterGameViewportClient::DetachViewportClient() +{ + Super::DetachViewportClient(); + + ReleaseSlateResources(); +} + +void UShooterGameViewportClient::ReleaseSlateResources() +{ + OldFocusWidget.Reset(); + LoadingScreenWidget.Reset(); + ViewportContentStack.Empty(); + HiddenViewportContentStack.Empty(); +} + +void SShooterLoadingScreen::Construct(const FArguments& InArgs) +{ + static const FName LoadingScreenName(TEXT("/Game/UI/Menu/LoadingScreen.LoadingScreen")); + + //since we are not using game styles here, just load one image + LoadingScreenBrush = MakeShareable( new FShooterGameLoadingScreenBrush( LoadingScreenName, FVector2D(1920,1080) ) ); + + ChildSlot + [ + SNew(SOverlay) + +SOverlay::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + [ + SNew(SImage) + .Image(LoadingScreenBrush.Get()) + ] + +SOverlay::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + [ + SNew(SSafeZone) + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .Padding(10.0f) + .IsTitleSafe(true) + [ + SNew(SThrobber) + .Visibility(this, &SShooterLoadingScreen::GetLoadIndicatorVisibility) + ] + ] + ]; +} diff --git a/Source/ShooterGame/Private/ShooterLeaderboards.h b/Source/ShooterGame/Private/ShooterLeaderboards.h new file mode 100644 index 0000000..e3dbc5a --- /dev/null +++ b/Source/ShooterGame/Private/ShooterLeaderboards.h @@ -0,0 +1,48 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once + +#include "ShooterTypes.h" +#include "OnlineLeaderboardInterface.h" + +// these are normally exported from platform-specific tools +#define LEADERBOARD_STAT_SCORE "ShooterAllTimeMatchResultsScore" +#define LEADERBOARD_STAT_KILLS "ShooterAllTimeMatchResultsFrags" +#define LEADERBOARD_STAT_DEATHS "ShooterAllTimeMatchResultsDeaths" +#define LEADERBOARD_STAT_MATCHESPLAYED "ShooterAllTimeMatchResultsMatchesPlayed" + +/** + * 'AllTime' leaderboard read object + */ +class FShooterAllTimeMatchResultsRead : public FOnlineLeaderboardRead +{ +public: + + FShooterAllTimeMatchResultsRead() + { + // Default properties + LeaderboardName = FName(TEXT("ShooterAllTimeMatchResults")); + SortedColumn = LEADERBOARD_STAT_SCORE; + + // Define default columns + new (ColumnMetadata) FColumnMetaData(LEADERBOARD_STAT_SCORE, EOnlineKeyValuePairDataType::Int32); + } +}; + +/** + * 'AllTime' leaderboard write object + */ +class FShooterAllTimeMatchResultsWrite : public FOnlineLeaderboardWrite +{ +public: + + FShooterAllTimeMatchResultsWrite() + { + // Default properties + new (LeaderboardNames) FName(TEXT("ShooterAllTimeMatchResults")); + RatedStat = LEADERBOARD_STAT_SCORE; + DisplayFormat = ELeaderboardFormat::Number; + SortMethod = ELeaderboardSort::Descending; + UpdateMethod = ELeaderboardUpdateMethod::KeepBest; + } +}; + diff --git a/Source/ShooterGame/Private/ShooterTeamStart.cpp b/Source/ShooterGame/Private/ShooterTeamStart.cpp new file mode 100644 index 0000000..0aec21f --- /dev/null +++ b/Source/ShooterGame/Private/ShooterTeamStart.cpp @@ -0,0 +1,10 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterTeamStart.h" + +AShooterTeamStart::AShooterTeamStart(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} diff --git a/Source/ShooterGame/Private/Sound/SoundNodeLocalPlayer.cpp b/Source/ShooterGame/Private/Sound/SoundNodeLocalPlayer.cpp new file mode 100644 index 0000000..0d502f1 --- /dev/null +++ b/Source/ShooterGame/Private/Sound/SoundNodeLocalPlayer.cpp @@ -0,0 +1,48 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Sound/SoundNodeLocalPlayer.h" +#include "SoundDefinitions.h" + +#define LOCTEXT_NAMESPACE "SoundNodeLocalPlayer" + +TMap USoundNodeLocalPlayer::LocallyControlledActorCache; + +USoundNodeLocalPlayer::USoundNodeLocalPlayer(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ +} + +void USoundNodeLocalPlayer::ParseNodes(FAudioDevice* AudioDevice, const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound, const FSoundParseParameters& ParseParams, TArray& WaveInstances) +{ + bool bLocallyControlled = false; + if (bool* LocallyControlledPtr = LocallyControlledActorCache.Find(ActiveSound.GetOwnerID())) + { + bLocallyControlled = *LocallyControlledPtr; + } + + const int32 PlayIndex = bLocallyControlled ? 0 : 1; + + if (PlayIndex < ChildNodes.Num() && ChildNodes[PlayIndex]) + { + ChildNodes[PlayIndex]->ParseNodes(AudioDevice, GetNodeWaveInstanceHash(NodeWaveInstanceHash, ChildNodes[PlayIndex], PlayIndex), ActiveSound, ParseParams, WaveInstances); + } +} + +#if WITH_EDITOR +FText USoundNodeLocalPlayer::GetInputPinName(int32 PinIndex) const +{ + return (PinIndex == 0) ? LOCTEXT("LocalPlayerLabel", "Local") : LOCTEXT("RemotePlayerLabel","Remote"); +} +#endif + +int32 USoundNodeLocalPlayer::GetMaxChildNodes() const +{ + return 2; +} + +int32 USoundNodeLocalPlayer::GetMinChildNodes() const +{ + return 2; +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/ShooterGame/Private/TakeHitInfo.cpp b/Source/ShooterGame/Private/TakeHitInfo.cpp new file mode 100644 index 0000000..9958673 --- /dev/null +++ b/Source/ShooterGame/Private/TakeHitInfo.cpp @@ -0,0 +1,65 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterTypes.h" +#include "ShooterCharacter.h" + +FTakeHitInfo::FTakeHitInfo() + : ActualDamage(0) + , DamageTypeClass(NULL) + , PawnInstigator(NULL) + , DamageCauser(NULL) + , DamageEventClassID(0) + , bKilled(false) + , EnsureReplicationByte(0) +{} + +FDamageEvent& FTakeHitInfo::GetDamageEvent() +{ + switch (DamageEventClassID) + { + case FPointDamageEvent::ClassID: + if (PointDamageEvent.DamageTypeClass == NULL) + { + PointDamageEvent.DamageTypeClass = DamageTypeClass ? DamageTypeClass : UDamageType::StaticClass(); + } + return PointDamageEvent; + + case FRadialDamageEvent::ClassID: + if (RadialDamageEvent.DamageTypeClass == NULL) + { + RadialDamageEvent.DamageTypeClass = DamageTypeClass ? DamageTypeClass : UDamageType::StaticClass(); + } + return RadialDamageEvent; + + default: + if (GeneralDamageEvent.DamageTypeClass == NULL) + { + GeneralDamageEvent.DamageTypeClass = DamageTypeClass ? DamageTypeClass : UDamageType::StaticClass(); + } + return GeneralDamageEvent; + } +} + +void FTakeHitInfo::SetDamageEvent(const FDamageEvent& DamageEvent) +{ + DamageEventClassID = DamageEvent.GetTypeID(); + switch (DamageEventClassID) + { + case FPointDamageEvent::ClassID: + PointDamageEvent = *((FPointDamageEvent const*)(&DamageEvent)); + break; + case FRadialDamageEvent::ClassID: + RadialDamageEvent = *((FRadialDamageEvent const*)(&DamageEvent)); + break; + default: + GeneralDamageEvent = DamageEvent; + } + + DamageTypeClass = DamageEvent.DamageTypeClass; +} + +void FTakeHitInfo::EnsureReplication() +{ + EnsureReplicationByte++; +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Tests/ShooterTestControllerBase.cpp b/Source/ShooterGame/Private/Tests/ShooterTestControllerBase.cpp new file mode 100644 index 0000000..b14645d --- /dev/null +++ b/Source/ShooterGame/Private/Tests/ShooterTestControllerBase.cpp @@ -0,0 +1,497 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#include "ShooterTestControllerBase.h" +#include "ShooterGame.h" +#include "ShooterGameSession.h" +#include "Online/ShooterOnlineGameSettings.h" +#include "OnlineSubsystemSessionSettings.h" +#include "OnlineSubsystemUtils.h" + +void UShooterTestControllerBase::OnInit() +{ + bIsLoggedIn = false; + bIsLoggingIn = false; + bInQuickMatchSearch = false; + bFoundQuickMatchGame = false; + bIsSearchingForGame = false; + bFoundGame = false; + NumOfCycledMatches = 0; + + if (!FParse::Value(FCommandLine::Get(), TEXT("TargetNumOfCycledMatches"), TargetNumOfCycledMatches)) + { + TargetNumOfCycledMatches = 2; + } +} + +void UShooterTestControllerBase::OnTick(float TimeDelta) +{ + const FName GameInstanceState = GetGameInstanceState(); + + if (GameInstanceState == ShooterGameInstanceState::WelcomeScreen) + { + if (!bIsLoggedIn && !bIsLoggingIn) + { + bIsLoggingIn = true; + StartPlayerLoginProcess(); + } + } + else if (GameInstanceState == ShooterGameInstanceState::MainMenu) + { + if (!bIsLoggedIn && !bIsLoggingIn) + { + ULocalPlayer* LP = GetFirstLocalPlayer(); + + if (LP) + { + bIsLoggingIn = true; + TSharedPtr UserId = LP->GetPreferredUniqueNetId().GetUniqueNetId(); + OnUserCanPlay(*UserId, EUserPrivileges::CanPlay, (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures); + } + } + } + else if (GameInstanceState == ShooterGameInstanceState::MessageMenu) + { + UE_LOG(LogGauntlet, Error, TEXT("Failing due to MessageMenu!")); + EndTest(-1); + return; + } +} + +void UShooterTestControllerBase::OnPostMapChange(UWorld* World) +{ + if (IsInGame()) + { + if (++NumOfCycledMatches >= TargetNumOfCycledMatches) + { + EndTest(0); + } + } + else if (NumOfCycledMatches > 0) + { + UE_LOG(LogGauntlet, Error, TEXT("Failed to cycle match TargetNumOfCycledMatches(%i)! NumOfCycledMatches = %i"), TargetNumOfCycledMatches, NumOfCycledMatches); + EndTest(-1); + } +} + +void UShooterTestControllerBase::StartPlayerLoginProcess() +{ + const IOnlineIdentityPtr IdentityInterface = Online::GetIdentityInterface(GetWorld()); + + if (IdentityInterface.IsValid()) + { + const ELoginStatus::Type LoginStatus = IdentityInterface->GetLoginStatus(0); + if (LoginStatus == ELoginStatus::NotLoggedIn) + { + // Show the account picker. + const IOnlineExternalUIPtr ExternalUI = Online::GetExternalUIInterface(GetWorld()); + if (ExternalUI.IsValid()) + { + ExternalUI->ShowLoginUI(0, false, true, FOnLoginUIClosedDelegate::CreateUObject(this, &UShooterTestControllerBase::OnLoginUIClosed)); + } + return; + } + + CheckApplicationLicenseValid(); + + TSharedPtr UserId = IdentityInterface->GetUniquePlayerId(0); + + if (UserId.IsValid()) + { + IdentityInterface->GetUserPrivilege(*UserId, EUserPrivileges::CanPlay, + IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateUObject(this, &UShooterTestControllerBase::OnUserCanPlay)); + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Player has invalid UniqueNetId!")); + EndTest(-1); + } + } +} + +void UShooterTestControllerBase::OnLoginUIClosed(TSharedPtr UniqueId, const int ControllerIndex, const FOnlineError& Error) +{ + CheckApplicationLicenseValid(); + + const IOnlineIdentityPtr IdentityInterface = Online::GetIdentityInterface(GetWorld()); + + if (IdentityInterface.IsValid()) + { + if (UniqueId.IsValid()) + { + IdentityInterface->GetUserPrivilege(*UniqueId, EUserPrivileges::CanPlay, + IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateUObject(this, &UShooterTestControllerBase::OnUserCanPlay)); + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Player has invalid UniqueNetId!")); + EndTest(-1); + } + } +} + +void UShooterTestControllerBase::OnUserCanPlay(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + if (PrivilegeResults == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + ULocalPlayer* NewPlayerOwner = GetFirstLocalPlayer(); + + if (NewPlayerOwner) + { + NewPlayerOwner->SetControllerId(0); + NewPlayerOwner->SetCachedUniqueNetId(NewPlayerOwner->GetUniqueNetIdFromCachedControllerId().GetUniqueNetId()); + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Could not find LocalPlayer in OnUserCanPlay!")); + EndTest(-1); + } + +#if SHOOTER_CONSOLE_UI +#if LOGIN_REQUIRED_FOR_ONLINE_PLAY + StartLoginTask(); +#else + StartOnlinePrivilegeTask(); +#endif //LOGIN_REQUIRED_FOR_ONLINE_PLAY +#else + OnUserCanPlayOnline(*NewPlayerOwner->GetCachedUniqueNetId().GetUniqueNetId(), EUserPrivileges::CanPlayOnline, (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures); +#endif //SHOOTER_CONSOLE_UI + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Player does not have appropiate privileges to play!")); + EndTest(-1); + } +} + +void UShooterTestControllerBase::StartLoginTask() +{ + const IOnlineIdentityPtr IdentityInterface = Online::GetIdentityInterface(GetWorld()); + + if (IdentityInterface.IsValid()) + { + OnLoginCompleteDelegateHandle = IdentityInterface->AddOnLoginCompleteDelegate_Handle(0, + FOnLoginCompleteDelegate::CreateUObject(this, &UShooterTestControllerBase::OnLoginTaskComplete)); + + IdentityInterface->Login(0, FOnlineAccountCredentials()); + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! IdentityInterface is not valid in StartLoginTask!")); + EndTest(-1); + } +} + +void UShooterTestControllerBase::OnLoginTaskComplete(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error) +{ + const IOnlineIdentityPtr IdentityInterface = Online::GetIdentityInterface(GetWorld()); + + if (IdentityInterface.IsValid()) + { + IdentityInterface->ClearOnLoginCompleteDelegate_Handle(LocalUserNum, OnLoginCompleteDelegateHandle); + + if (bWasSuccessful) + { + StartOnlinePrivilegeTask(); + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Player failed to login!")); + EndTest(-1); + } + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! IdentityInterface is not valid in OnLoginTaskComplete!")); + EndTest(-1); + } + + +} + +void UShooterTestControllerBase::StartOnlinePrivilegeTask() +{ + const ULocalPlayer* PlayerOwner = GetFirstLocalPlayer(); + const IOnlineIdentityPtr IdentityInterface = Online::GetIdentityInterface(GetWorld()); + + if (PlayerOwner && IdentityInterface.IsValid()) + { + IdentityInterface->GetUserPrivilege(*PlayerOwner->GetCachedUniqueNetId().GetUniqueNetId(), EUserPrivileges::CanPlayOnline, + IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateUObject(this, &UShooterTestControllerBase::OnUserCanPlayOnline)); + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Could not find LocalPlayer or IdentityInterface is null in OnlinePrivilegeTask!")); + EndTest(-1); + } +} + +void UShooterTestControllerBase::OnUserCanPlayOnline(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + if (PrivilegeResults == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + bIsLoggingIn = false; + bIsLoggedIn = true; + + if (UShooterGameInstance* GameInstance = GetGameInstance()) + { + GameInstance->SetOnlineMode(EOnlineMode::Online); + } + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Player does not have appropiate privileges to play online!")); + EndTest(-1); + } +} + +void UShooterTestControllerBase::CheckApplicationLicenseValid() +{ + if (FSlateApplication::IsInitialized()) + { + TSharedPtr GenericApplication = FSlateApplication::Get().GetPlatformApplication(); + const bool bIsLicensed = GenericApplication->ApplicationLicenseValid(); + + if (!bIsLicensed) + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! The signed in user(s) do not have a license for this game!")); + EndTest(-1); + } + } +} + +void UShooterTestControllerBase::HostGame() +{ + UShooterGameInstance* GameInstance = GetGameInstance(); + ULocalPlayer* PlayerOwner = GameInstance ? GameInstance->GetFirstGamePlayer() : nullptr; + + if (PlayerOwner) + { + const FString GameType = TEXT("FFA"); + const FString StartURL = FString::Printf(TEXT("/Game/Maps/%s?game=%s%s"), TEXT("Highrise"), *GameType, TEXT("?listen")); + + GameInstance->HostGame(PlayerOwner, GameType, StartURL); + } + else + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Could not find LocalPlayer or GameInstance is null!")); + EndTest(-1); + } +} + +void UShooterTestControllerBase::StartQuickMatch() +{ + IOnlineSessionPtr Sessions = Online::GetSessionInterface(GetWorld()); + UShooterGameInstance* GameInstance = GetGameInstance(); + if (!Sessions.IsValid() || GameInstance == nullptr) + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Could not find online session interface or GameInstance is null!")); + EndTest(-1); + return; + } + + QuickMatchSearchSettings = MakeShareable(new FShooterOnlineSearchSettings(false, true)); + QuickMatchSearchSettings->QuerySettings.Set(SEARCH_XBOX_LIVE_HOPPER_NAME, FString("FreeForAll"), EOnlineComparisonOp::Equals); + QuickMatchSearchSettings->QuerySettings.Set(SEARCH_XBOX_LIVE_SESSION_TEMPLATE_NAME, FString("MatchSession"), EOnlineComparisonOp::Equals); + QuickMatchSearchSettings->TimeoutInSeconds = 120.0f; + + FShooterOnlineSessionSettings SessionSettings(false, true, 8); + SessionSettings.Set(SETTING_GAMEMODE, FString("FFA"), EOnlineDataAdvertisementType::ViaOnlineService); + SessionSettings.Set(SETTING_MATCHING_HOPPER, FString("FreeForAll"), EOnlineDataAdvertisementType::DontAdvertise); + SessionSettings.Set(SETTING_MATCHING_TIMEOUT, 120.0f, EOnlineDataAdvertisementType::ViaOnlineService); + SessionSettings.Set(SETTING_SESSION_TEMPLATE_NAME, FString("GameSession"), EOnlineDataAdvertisementType::DontAdvertise); + + TSharedRef QuickMatchSearchSettingsRef = QuickMatchSearchSettings.ToSharedRef(); + + // Perform matchmaking with all local players + TArray LocalPlayers; + for (auto It = GameInstance->GetLocalPlayerIterator(); It; ++It) + { + FUniqueNetIdRepl PlayerId = (*It)->GetPreferredUniqueNetId(); + if (PlayerId.IsValid()) + { + FSessionMatchmakingUser LocalPlayer = { (*PlayerId).AsShared() }; + LocalPlayers.Emplace(LocalPlayer); + } + } + + bInQuickMatchSearch = true; + + FOnStartMatchmakingComplete CompletionDelegate; + CompletionDelegate.BindUObject(this, &UShooterTestControllerBase::OnMatchmakingComplete); + if (!Sessions->StartMatchmaking(LocalPlayers, NAME_GameSession, SessionSettings, QuickMatchSearchSettingsRef, CompletionDelegate)) + { + OnMatchmakingComplete(NAME_GameSession, FOnlineError(false), FSessionMatchmakingResults()); + } +} + +void UShooterTestControllerBase::OnMatchmakingComplete(FName SessionName, const FOnlineError& ErrorDetails, const FSessionMatchmakingResults& Results) +{ + const bool bWasSuccessful = ErrorDetails.WasSuccessful(); + IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(GetWorld()); + if (!SessionInterface.IsValid()) + { + UE_LOG(LogGauntlet, Error, TEXT("Failed! Could not find online session interface!")); + EndTest(-1); + return; + } + + bInQuickMatchSearch = false; + + if (!bWasSuccessful) + { + UE_LOG(LogGauntlet, Warning, TEXT("Matchmaking was unsuccessful.")); + return; + } + + UE_LOG(LogGauntlet, Log, TEXT("Matchmaking successful! Session name is %s."), *SessionName.ToString()); + + FNamedOnlineSession* MatchmadeSession = SessionInterface->GetNamedSession(SessionName); + + if (!MatchmadeSession) + { + UE_LOG(LogGauntlet, Warning, TEXT("OnMatchmakingComplete: No session.")); + return; + } + + if (!MatchmadeSession->OwningUserId.IsValid()) + { + UE_LOG(LogGauntlet, Warning, TEXT("OnMatchmakingComplete: No session owner/host.")); + return; + } + + UE_LOG(LogGauntlet, Log, TEXT("OnMatchmakingComplete: Session host is %d."), *MatchmadeSession->OwningUserId->ToString()); + + if (UShooterGameInstance* GameInstance = GetGameInstance()) + { + IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld()); + + // We only care about hosted games + if (Subsystem && Subsystem->IsLocalPlayer(*MatchmadeSession->OwningUserId)) + { + SessionInterface->EndSession(SessionName); + } + else + { + bFoundQuickMatchGame = true; + GameInstance->TravelToSession(SessionName); + } + } +} + +void UShooterTestControllerBase::StartSearchingForGame() +{ + if (UShooterGameInstance* GameInstance = GetGameInstance()) + { + bIsSearchingForGame = true; + GameInstance->FindSessions(GameInstance->GetFirstGamePlayer(), false, false); + } +} + +void UShooterTestControllerBase::UpdateSearchStatus() +{ + AShooterGameSession* ShooterSession = GetGameSession(); + + if (ShooterSession) + { + int32 CurrentSearchIdx, NumSearchResults; + EOnlineAsyncTaskState::Type SearchState = ShooterSession->GetSearchResultStatus(CurrentSearchIdx, NumSearchResults); + + UE_LOG(LogGauntlet, VeryVerbose, TEXT("ShooterSession->GetSearchResultStatus: %s"), EOnlineAsyncTaskState::ToString(SearchState)); + + switch (SearchState) + { + case EOnlineAsyncTaskState::InProgress: + { + break; + } + case EOnlineAsyncTaskState::Done: + { + const TArray & SearchResults = ShooterSession->GetSearchResults(); + check(SearchResults.Num() == NumSearchResults); + + if (NumSearchResults > 0) + { + for (int i = 0; i < SearchResults.Num(); ++i) + { + const FOnlineSessionSearchResult& Result = SearchResults[i]; + + FString GameType; + FString MapName; + + Result.Session.SessionSettings.Get(SETTING_GAMEMODE, GameType); + Result.Session.SessionSettings.Get(SETTING_MAPNAME, MapName); + + if (GameType == "FFA" && MapName == "Highrise") + { + bFoundGame = true; + + UShooterGameInstance* GameInstance = GetGameInstance(); + ULocalPlayer* PlayerOwner = GameInstance ? GameInstance->GetFirstGamePlayer() : nullptr; + + if (PlayerOwner) + { + GameInstance->JoinSession(PlayerOwner, i); + } + } + } + } + + bIsSearchingForGame = false; + + break; + } + + case EOnlineAsyncTaskState::Failed: + case EOnlineAsyncTaskState::NotStarted: + default: + { + bIsSearchingForGame = false; + break; + } + } + } +} + +UShooterGameInstance* UShooterTestControllerBase::GetGameInstance() const +{ + if (const UWorld* World = GetWorld()) + { + return Cast(GetWorld()->GetGameInstance()); + } + + return nullptr; +} + +const FName UShooterTestControllerBase::GetGameInstanceState() const +{ + if (const UShooterGameInstance* GameInstance = GetGameInstance()) + { + return GameInstance->GetCurrentState(); + } + + return ""; +} + +AShooterGameSession* UShooterTestControllerBase::GetGameSession() const +{ + if (const UShooterGameInstance* GameInstance = GetGameInstance()) + { + return GameInstance->GetGameSession(); + } + + return nullptr; +} + +bool UShooterTestControllerBase::IsInGame() const +{ + return GetGameInstanceState() == ShooterGameInstanceState::Playing; +} + +ULocalPlayer* UShooterTestControllerBase::GetFirstLocalPlayer() const +{ + if (const UShooterGameInstance* GameInstance = GetGameInstance()) + { + return GameInstance->GetFirstGamePlayer(); + } + + return nullptr; +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Tests/ShooterTestControllerBasicDedicatedServerTest.cpp b/Source/ShooterGame/Private/Tests/ShooterTestControllerBasicDedicatedServerTest.cpp new file mode 100644 index 0000000..d5ddedc --- /dev/null +++ b/Source/ShooterGame/Private/Tests/ShooterTestControllerBasicDedicatedServerTest.cpp @@ -0,0 +1,20 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#include "ShooterTestControllerBasicDedicatedServerTest.h" +#include "ShooterGameInstance.h" + +void UShooterTestControllerBasicDedicatedServerTest::OnTick(float TimeDelta) +{ + if (GetTimeInCurrentState() > 300) + { + UE_LOG(LogGauntlet, Error, TEXT("Failing boot test after 300 secs!")); + EndTest(-1); + } +} + +void UShooterTestControllerBasicDedicatedServerTest::OnPostMapChange(UWorld* World) +{ + if (IsInGame()) + { + EndTest(0); + } +} diff --git a/Source/ShooterGame/Private/Tests/ShooterTestControllerBootTest.cpp b/Source/ShooterGame/Private/Tests/ShooterTestControllerBootTest.cpp new file mode 100644 index 0000000..af3cf65 --- /dev/null +++ b/Source/ShooterGame/Private/Tests/ShooterTestControllerBootTest.cpp @@ -0,0 +1,26 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#include "Tests/ShooterTestControllerBootTest.h" +#include "ShooterGameInstance.h" + +bool UShooterTestControllerBootTest::IsBootProcessComplete() const +{ + static double StartTime = FPlatformTime::Seconds(); + const double TimeSinceStart = FPlatformTime::Seconds() - StartTime; + + if (TimeSinceStart >= TestDelay) + { + if (const UWorld* World = GetWorld()) + { + if (const UShooterGameInstance* GameInstance = Cast(GetWorld()->GetGameInstance())) + { + if (GameInstance->GetCurrentState() == ShooterGameInstanceState::WelcomeScreen || + GameInstance->GetCurrentState() == ShooterGameInstanceState::MainMenu) + { + return true; + } + } + } + } + + return false; +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Tests/ShooterTestControllerDedicatedServerTest.cpp b/Source/ShooterGame/Private/Tests/ShooterTestControllerDedicatedServerTest.cpp new file mode 100644 index 0000000..b1dac84 --- /dev/null +++ b/Source/ShooterGame/Private/Tests/ShooterTestControllerDedicatedServerTest.cpp @@ -0,0 +1,18 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#include "ShooterTestControllerDedicatedServerTest.h" +#include "ShooterGameSession.h" + +void UShooterTestControllerDedicatedServerTest::OnTick(float TimeDelta) +{ + Super::OnTick(TimeDelta); + + if (bIsLoggedIn && !bIsSearchingForGame && !bFoundGame) + { + StartSearchingForGame(); + } + + if (bIsSearchingForGame && !bFoundGame) + { + UpdateSearchStatus(); + } +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Tests/ShooterTestControllerListenServerClient.cpp b/Source/ShooterGame/Private/Tests/ShooterTestControllerListenServerClient.cpp new file mode 100644 index 0000000..b077117 --- /dev/null +++ b/Source/ShooterGame/Private/Tests/ShooterTestControllerListenServerClient.cpp @@ -0,0 +1,18 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#include "ShooterTestControllerListenServerClient.h" +#include "ShooterGameSession.h" + +void UShooterTestControllerListenServerClient::OnTick(float TimeDelta) +{ + Super::OnTick(TimeDelta); + + if (bIsLoggedIn && !bIsSearchingForGame && !bFoundGame) + { + StartSearchingForGame(); + } + + if (bIsSearchingForGame && !bFoundGame) + { + UpdateSearchStatus(); + } +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Tests/ShooterTestControllerListenServerHost.cpp b/Source/ShooterGame/Private/Tests/ShooterTestControllerListenServerHost.cpp new file mode 100644 index 0000000..3556c1f --- /dev/null +++ b/Source/ShooterGame/Private/Tests/ShooterTestControllerListenServerHost.cpp @@ -0,0 +1,12 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#include "ShooterTestControllerListenServerHost.h" + +void UShooterTestControllerListenServerHost::OnUserCanPlayOnline(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + Super::OnUserCanPlayOnline(UserId, Privilege, PrivilegeResults); + + if (PrivilegeResults == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + HostGame(); + } +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Tests/ShooterTestControllerListenServerQuickMatchClient.cpp b/Source/ShooterGame/Private/Tests/ShooterTestControllerListenServerQuickMatchClient.cpp new file mode 100644 index 0000000..db9d20d --- /dev/null +++ b/Source/ShooterGame/Private/Tests/ShooterTestControllerListenServerQuickMatchClient.cpp @@ -0,0 +1,13 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#include "ShooterTestControllerListenServerQuickMatchClient.h" +#include "ShooterGameSession.h" + +void UShooterTestControllerListenServerQuickMatchClient::OnTick(float TimeDelta) +{ + Super::OnTick(TimeDelta); + + if (bIsLoggedIn && !bInQuickMatchSearch && !bFoundQuickMatchGame) + { + StartQuickMatch(); + } +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterDemoPlaybackMenu.cpp b/Source/ShooterGame/Private/UI/Menu/ShooterDemoPlaybackMenu.cpp new file mode 100644 index 0000000..fe0b48c --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterDemoPlaybackMenu.cpp @@ -0,0 +1,147 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterDemoPlaybackMenu.h" +#include "ShooterStyle.h" +#include "ShooterMenuSoundsWidgetStyle.h" +#include "ShooterGameInstance.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +void FShooterDemoPlaybackMenu::Construct( ULocalPlayer* _PlayerOwner ) +{ + PlayerOwner = _PlayerOwner; + bIsAddedToViewport = false; + + if ( !GEngine || !GEngine->GameViewport ) + { + return; + } + + if ( !GameMenuWidget.IsValid() ) + { + SAssignNew( GameMenuWidget, SShooterMenuWidget ) + .PlayerOwner( MakeWeakObjectPtr( PlayerOwner ) ) + .Cursor( EMouseCursor::Default ) + .IsGameMenu( true ); + + TSharedPtr MainMenuRoot = FShooterMenuItem::CreateRoot(); + MainMenuItem = MenuHelper::AddMenuItem(MainMenuRoot,LOCTEXT( "Main Menu", "MAIN MENU" ) ); + + MenuHelper::AddMenuItemSP( MainMenuItem, LOCTEXT( "No", "NO" ), this, &FShooterDemoPlaybackMenu::OnCancelExitToMain ); + MenuHelper::AddMenuItemSP( MainMenuItem, LOCTEXT( "Yes", "YES" ), this, &FShooterDemoPlaybackMenu::OnConfirmExitToMain ); + + MenuHelper::AddExistingMenuItem( RootMenuItem, MainMenuItem.ToSharedRef() ); + +#if !SHOOTER_CONSOLE_UI + MenuHelper::AddMenuItemSP( RootMenuItem, LOCTEXT("Quit", "QUIT"), this, &FShooterDemoPlaybackMenu::OnUIQuit ); +#endif + + GameMenuWidget->MainMenu = GameMenuWidget->CurrentMenu = RootMenuItem->SubMenu; + GameMenuWidget->OnMenuHidden.BindSP( this, &FShooterDemoPlaybackMenu::DetachGameMenu ); + GameMenuWidget->OnToggleMenu.BindSP( this, &FShooterDemoPlaybackMenu::ToggleGameMenu ); + GameMenuWidget->OnGoBack.BindSP( this, &FShooterDemoPlaybackMenu::OnMenuGoBack ); + } +} + +void FShooterDemoPlaybackMenu::CloseSubMenu() +{ + GameMenuWidget->MenuGoBack(); +} + +void FShooterDemoPlaybackMenu::OnMenuGoBack(MenuPtr Menu) +{ +} + +void FShooterDemoPlaybackMenu::DetachGameMenu() +{ + if ( GEngine && GEngine->GameViewport ) + { + GEngine->GameViewport->RemoveViewportWidgetContent( GameMenuContainer.ToSharedRef() ); + } + + bIsAddedToViewport = false; +} + +void FShooterDemoPlaybackMenu::ToggleGameMenu() +{ + if ( !GameMenuWidget.IsValid( )) + { + return; + } + + if ( bIsAddedToViewport && GameMenuWidget->CurrentMenu != RootMenuItem->SubMenu ) + { + GameMenuWidget->MenuGoBack(); + return; + } + + if ( !bIsAddedToViewport ) + { + GEngine->GameViewport->AddViewportWidgetContent( SAssignNew( GameMenuContainer, SWeakWidget ).PossiblyNullContent( GameMenuWidget.ToSharedRef() ) ); + + GameMenuWidget->BuildAndShowMenu(); + + bIsAddedToViewport = true; + } + else + { + // Start hiding animation + GameMenuWidget->HideMenu(); + + AShooterPlayerController* const PCOwner = PlayerOwner ? Cast(PlayerOwner->PlayerController) : nullptr; + + if ( PCOwner ) + { + // Make sure viewport has focus + FSlateApplication::Get().SetAllUserFocusToGameViewport(); + } + } +} + +void FShooterDemoPlaybackMenu::OnCancelExitToMain() +{ + CloseSubMenu(); +} + +void FShooterDemoPlaybackMenu::OnConfirmExitToMain() +{ + UShooterGameInstance* const GameInstance = Cast( PlayerOwner->GetGameInstance() ); + + if ( GameInstance ) + { + // tell game instance to go back to main menu state + GameInstance->GotoState( ShooterGameInstanceState::MainMenu ); + } +} + +void FShooterDemoPlaybackMenu::OnUIQuit() +{ + UShooterGameInstance* const GameInstance = Cast( PlayerOwner->GetGameInstance() ); + + GameMenuWidget->LockControls( true ); + GameMenuWidget->HideMenu(); + + UWorld* const World = PlayerOwner ? PlayerOwner->GetWorld() : nullptr; + if ( World ) + { + const FShooterMenuSoundsStyle& MenuSounds = FShooterStyle::Get().GetWidgetStyle< FShooterMenuSoundsStyle >( "DefaultShooterMenuSoundsStyle" ); + MenuHelper::PlaySoundAndCall( World, MenuSounds.ExitGameSound, GetOwnerUserIndex(), this, &FShooterDemoPlaybackMenu::Quit ); + } +} + +void FShooterDemoPlaybackMenu::Quit() +{ + APlayerController* const PCOwner = PlayerOwner ? PlayerOwner->PlayerController : nullptr; + + if ( PCOwner ) + { + PCOwner->ConsoleCommand( "quit" ); + } +} + +int32 FShooterDemoPlaybackMenu::GetOwnerUserIndex() const +{ + return PlayerOwner ? PlayerOwner->GetControllerId() : 0; +} +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterDemoPlaybackMenu.h b/Source/ShooterGame/Private/UI/Menu/ShooterDemoPlaybackMenu.h new file mode 100644 index 0000000..7266d39 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterDemoPlaybackMenu.h @@ -0,0 +1,60 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "Widgets/ShooterMenuItem.h" +#include "Widgets/SShooterMenuWidget.h" + +class FShooterDemoPlaybackMenu : public TSharedFromThis +{ +public: + /** sets owning player controller */ + void Construct( ULocalPlayer* PlayerOwner ); + + /** toggles in game menu */ + void ToggleGameMenu(); + +protected: + + /** Owning player controller */ + ULocalPlayer* PlayerOwner; + + /** game menu container widget - used for removing */ + TSharedPtr GameMenuContainer; + + /** root menu item pointer */ + TSharedPtr RootMenuItem; + + /** main menu item pointer */ + TSharedPtr MainMenuItem; + + /** HUD menu widget */ + TSharedPtr GameMenuWidget; + + /** if game menu is currently added to the viewport */ + bool bIsAddedToViewport; + + /** get current user index out of PlayerOwner */ + int32 GetOwnerUserIndex() const; + + /** called when going back to previous menu */ + void OnMenuGoBack(MenuPtr Menu); + + /** goes back in menu structure */ + void CloseSubMenu(); + + /** removes widget from viewport */ + void DetachGameMenu(); + + /** Delegate called when user cancels confirmation dialog to exit to main menu */ + void OnCancelExitToMain(); + + /** Delegate called when user confirms confirmation dialog to exit to main menu */ + void OnConfirmExitToMain(); + + /** Plays sound and calls Quit */ + void OnUIQuit(); + + /** Quits the game */ + void Quit(); +}; diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterFriends.cpp b/Source/ShooterGame/Private/UI/Menu/ShooterFriends.cpp new file mode 100644 index 0000000..1343f94 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterFriends.cpp @@ -0,0 +1,177 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterFriends.h" +#include "ShooterTypes.h" +#include "ShooterStyle.h" +#include "ShooterOptionsWidgetStyle.h" +#include "Player/ShooterPersistentUser.h" +#include "ShooterGameUserSettings.h" +#include "ShooterLocalPlayer.h" +#include "OnlineSubsystemUtils.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +void FShooterFriends::Construct(ULocalPlayer* _PlayerOwner, int32 LocalUserNum_) +{ + FriendsStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterOptionsStyle"); + + PlayerOwner = _PlayerOwner; + LocalUserNum = LocalUserNum_; + CurrFriendIndex = 0; + MinFriendIndex = 0; + MaxFriendIndex = 0; //initialized after the friends list is read in + + /** Friends menu root item */ + TSharedPtr FriendsRoot = FShooterMenuItem::CreateRoot(); + + //Populate the friends list + FriendsItem = MenuHelper::AddMenuItem(FriendsRoot, LOCTEXT("Friends", "FRIENDS")); + + if (PlayerOwner) + { + OnlineSub = Online::GetSubsystem(PlayerOwner->GetWorld()); + OnlineFriendsPtr = OnlineSub->GetFriendsInterface(); + } + + UpdateFriends(LocalUserNum); + + UserSettings = CastChecked(GEngine->GetGameUserSettings()); +} + +void FShooterFriends::OnApplySettings() +{ + ApplySettings(); +} + +void FShooterFriends::ApplySettings() +{ + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + PersistentUser->TellInputAboutKeybindings(); + + PersistentUser->SaveIfDirty(); + } + + UserSettings->ApplySettings(false); + + OnApplyChanges.ExecuteIfBound(); +} + +void FShooterFriends::TellInputAboutKeybindings() +{ + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + PersistentUser->TellInputAboutKeybindings(); + } +} + +UShooterPersistentUser* FShooterFriends::GetPersistentUser() const +{ + UShooterLocalPlayer* const ShooterLocalPlayer = Cast(PlayerOwner); + return ShooterLocalPlayer ? ShooterLocalPlayer->GetPersistentUser() : nullptr; + //// Main Menu + //AShooterPlayerController_Menu* ShooterPCM = Cast(PCOwner); + //if(ShooterPCM) + //{ + // return ShooterPCM->GetPersistentUser(); + //} + + //// In-game Menu + //AShooterPlayerController* ShooterPC = Cast(PCOwner); + //if(ShooterPC) + //{ + // return ShooterPC->GetPersistentUser(); + //} + + //return nullptr; +} + +void FShooterFriends::UpdateFriends(int32 NewOwnerIndex) +{ + if (!OnlineFriendsPtr.IsValid()) + { + return; + } + + LocalUserNum = NewOwnerIndex; + OnlineFriendsPtr->ReadFriendsList(LocalUserNum, EFriendsLists::ToString(EFriendsLists::OnlinePlayers), FOnReadFriendsListComplete::CreateSP(this, &FShooterFriends::OnFriendsUpdated)); +} + +void FShooterFriends::OnFriendsUpdated(int32 /*unused*/, bool bWasSuccessful, const FString& FriendListName, const FString& ErrorString) +{ + if (!bWasSuccessful) + { + UE_LOG(LogOnline, Warning, TEXT("Unable to update friendslist %s due to error=[%s]"), *FriendListName, *ErrorString); + return; + } + + MenuHelper::ClearSubMenu(FriendsItem); + + Friends.Reset(); + if (OnlineFriendsPtr->GetFriendsList(LocalUserNum, EFriendsLists::ToString(EFriendsLists::OnlinePlayers), Friends)) + { + for (const TSharedRef& Friend : Friends) + { + TSharedRef FriendItem = MenuHelper::AddMenuItem(FriendsItem, FText::FromString(Friend->GetDisplayName())); + FriendItem->OnControllerFacebuttonDownPressed.BindSP(this, &FShooterFriends::ViewSelectedFriendProfile); + FriendItem->OnControllerDownInputPressed.BindSP(this, &FShooterFriends::IncrementFriendsCounter); + FriendItem->OnControllerUpInputPressed.BindSP(this, &FShooterFriends::DecrementFriendsCounter); + } + + MaxFriendIndex = Friends.Num() - 1; + } + + MenuHelper::AddMenuItemSP(FriendsItem, LOCTEXT("Close", "CLOSE"), this, &FShooterFriends::OnApplySettings); +} + +void FShooterFriends::IncrementFriendsCounter() +{ + if (CurrFriendIndex + 1 <= MaxFriendIndex) + { + ++CurrFriendIndex; + } +} +void FShooterFriends::DecrementFriendsCounter() +{ + if (CurrFriendIndex - 1 >= MinFriendIndex) + { + --CurrFriendIndex; + } +} +void FShooterFriends::ViewSelectedFriendProfile() +{ + if (OnlineSub) + { + IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if (Identity.IsValid() && Friends.IsValidIndex(CurrFriendIndex)) + { + TSharedPtr Requestor = Identity->GetUniquePlayerId(LocalUserNum); + TSharedPtr Requestee = Friends[CurrFriendIndex]->GetUserId(); + + IOnlineExternalUIPtr ExternalUI = OnlineSub->GetExternalUIInterface(); + if (ExternalUI.IsValid() && Requestor.IsValid() && Requestee.IsValid()) + { + ExternalUI->ShowProfileUI(*Requestor, *Requestee, FOnProfileUIClosedDelegate()); + } + } +} +} +void FShooterFriends::InviteSelectedFriendToGame() +{ + // invite the user to the current gamesession + if (OnlineSub) + { + IOnlineSessionPtr OnlineSessionInterface = OnlineSub->GetSessionInterface(); + if (OnlineSessionInterface.IsValid()) + { + OnlineSessionInterface->SendSessionInviteToFriend(LocalUserNum, NAME_GameSession, *Friends[CurrFriendIndex]->GetUserId()); + } + } +} + + +#undef LOCTEXT_NAMESPACE + diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterFriends.h b/Source/ShooterGame/Private/UI/Menu/ShooterFriends.h new file mode 100644 index 0000000..c2a5fc5 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterFriends.h @@ -0,0 +1,104 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "Widgets/ShooterMenuItem.h" +#include "Widgets/SShooterMenuWidget.h" + +class UShooterGameUserSettings; +class UShooterPersistentUser; + +/** delegate called when changes are applied */ +DECLARE_DELEGATE(FOnApplyChanges); + +/** delegate called when a menu item is confirmed */ +DECLARE_DELEGATE(FOnConfirmMenuItem); + +/** delegate called when facebutton_left is pressed */ +DECLARE_DELEGATE(FOnControllerFacebuttonLeftPressed); + +/** delegate called down on the gamepad is pressed*/ +DECLARE_DELEGATE(FOnControllerDownInputPressed); + +/** delegate called up on the gamepad is pressed*/ +DECLARE_DELEGATE(FOnControllerUpInputPressed); + +/** delegate called when facebutton_down is pressed */ +DECLARE_DELEGATE(FOnOnControllerFacebuttonDownPressed); + +class FShooterFriends : public TSharedFromThis +{ +public: + /** sets owning player controller */ + void Construct(ULocalPlayer* _PlayerOwner, int32 LocalUserNum); + + /** get current Friends values for display */ + void UpdateFriends(int32 NewOwnerIndex); + + /** UI callback for applying settings, plays sound */ + void OnApplySettings(); + + /** applies changes in game settings */ + void ApplySettings(); + + /** needed because we can recreate the subsystem that stores it */ + void TellInputAboutKeybindings(); + + /** increment the counter keeping track of which user we're looking at */ + void IncrementFriendsCounter(); + + /** decrement the counter keeping track of which user we're looking at */ + void DecrementFriendsCounter(); + + /** view the profile of the selected user */ + void ViewSelectedFriendProfile(); + + /** send an invite to the selected user */ + void InviteSelectedFriendToGame(); + + /** holds Friends menu item */ + TSharedPtr FriendsItem; + + /** called when changes were applied - can be used to close submenu */ + FOnApplyChanges OnApplyChanges; + + /** delegate, which is executed by SShooterMenuWidget if user confirms this menu item */ + FOnConfirmMenuItem OnConfirmMenuItem; + + /** delegate, which is executed by SShooterMenuWidget if facebutton_left is pressed */ + FOnControllerFacebuttonLeftPressed OnControllerFacebuttonLeftPressed; + + /** delegate, which is executed by SShooterMenuWidget if down input is pressed */ + FOnControllerDownInputPressed OnControllerDownInputPressed; + + /** delegate, which is executed by SShooterMenuWidget if up input is pressed */ + FOnControllerUpInputPressed OnControllerUpInputPressed; + + /** delegate, which is executed by SShooterMenuWidget if facebutton_down is pressed */ + FOnOnControllerFacebuttonDownPressed OnControllerFacebuttonDownPressed; + + int32 LocalUserNum; + int32 CurrFriendIndex; + int32 MinFriendIndex; + int32 MaxFriendIndex; + + TArray< TSharedRef > Friends; + + IOnlineSubsystem* OnlineSub; + IOnlineFriendsPtr OnlineFriendsPtr; + +protected: + void OnFriendsUpdated(int32 UpdatedPlayerIndex, bool bWasSuccessful, const FString& FriendListName, const FString& ErrorString); + + /** User settings pointer */ + UShooterGameUserSettings* UserSettings; + + /** Get the persistence user associated with PCOwner*/ + UShooterPersistentUser* GetPersistentUser() const; + + /** Owning player controller */ + ULocalPlayer* PlayerOwner; + + /** style used for the shooter Friends */ + const struct FShooterOptionsStyle *FriendsStyle; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterIngameMenu.cpp b/Source/ShooterGame/Private/UI/Menu/ShooterIngameMenu.cpp new file mode 100644 index 0000000..1fe4c59 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterIngameMenu.cpp @@ -0,0 +1,333 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterIngameMenu.h" +#include "ShooterStyle.h" +#include "ShooterMenuSoundsWidgetStyle.h" +#include "Online.h" +#include "OnlineExternalUIInterface.h" +#include "ShooterGameInstance.h" +#include "UI/ShooterHUD.h" +#include "OnlineSubsystemUtils.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +#if PLATFORM_SWITCH +# define FRIENDS_SUPPORTED 0 +#else +# define FRIENDS_SUPPORTED 1 +#endif + +#if !defined(FRIENDS_IN_INGAME_MENU) + #define FRIENDS_IN_INGAME_MENU 1 +#endif + +void FShooterIngameMenu::Construct(ULocalPlayer* _PlayerOwner) +{ + PlayerOwner = _PlayerOwner; + bIsGameMenuUp = false; + + if (!GEngine || !GEngine->GameViewport) + { + return; + } + + //todo: don't create ingame menus for remote players. + const UShooterGameInstance* GameInstance = nullptr; + if (PlayerOwner) + { + GameInstance = Cast(PlayerOwner->GetGameInstance()); + } + + if (!GameMenuWidget.IsValid()) + { + SAssignNew(GameMenuWidget, SShooterMenuWidget) + .PlayerOwner(MakeWeakObjectPtr(PlayerOwner)) + .Cursor(EMouseCursor::Default) + .IsGameMenu(true); + + + int32 const OwnerUserIndex = GetOwnerUserIndex(); + + // setup the exit to main menu submenu. We wanted a confirmation to avoid a potential TRC violation. + // fixes TTP: 322267 + TSharedPtr MainMenuRoot = FShooterMenuItem::CreateRoot(); + MainMenuItem = MenuHelper::AddMenuItem(MainMenuRoot,LOCTEXT("Main Menu", "MAIN MENU")); + MenuHelper::AddMenuItemSP(MainMenuItem,LOCTEXT("No", "NO"), this, &FShooterIngameMenu::OnCancelExitToMain); + MenuHelper::AddMenuItemSP(MainMenuItem,LOCTEXT("Yes", "YES"), this, &FShooterIngameMenu::OnConfirmExitToMain); + + ShooterOptions = MakeShareable(new FShooterOptions()); + ShooterOptions->Construct(PlayerOwner); + ShooterOptions->TellInputAboutKeybindings(); + ShooterOptions->OnApplyChanges.BindSP(this, &FShooterIngameMenu::CloseSubMenu); + + MenuHelper::AddExistingMenuItem(RootMenuItem, ShooterOptions->CheatsItem.ToSharedRef()); + MenuHelper::AddExistingMenuItem(RootMenuItem, ShooterOptions->OptionsItem.ToSharedRef()); + +#if FRIENDS_SUPPORTED + if (GameInstance && GameInstance->GetOnlineMode() == EOnlineMode::Online) + { +#if !FRIENDS_IN_INGAME_MENU + ShooterFriends = MakeShareable(new FShooterFriends()); + ShooterFriends->Construct(PlayerOwner, OwnerUserIndex); + ShooterFriends->TellInputAboutKeybindings(); + ShooterFriends->OnApplyChanges.BindSP(this, &FShooterIngameMenu::CloseSubMenu); + + MenuHelper::AddExistingMenuItem(RootMenuItem, ShooterFriends->FriendsItem.ToSharedRef()); + + ShooterRecentlyMet = MakeShareable(new FShooterRecentlyMet()); + ShooterRecentlyMet->Construct(PlayerOwner, OwnerUserIndex); + ShooterRecentlyMet->TellInputAboutKeybindings(); + ShooterRecentlyMet->OnApplyChanges.BindSP(this, &FShooterIngameMenu::CloseSubMenu); + + MenuHelper::AddExistingMenuItem(RootMenuItem, ShooterRecentlyMet->RecentlyMetItem.ToSharedRef()); +#endif + +#if SHOOTER_CONSOLE_UI && INVITE_ONLINE_GAME_ENABLED + TSharedPtr ShowInvitesItem = MenuHelper::AddMenuItem(RootMenuItem, LOCTEXT("Invite Players", "INVITE PLAYERS (via System UI)")); + ShowInvitesItem->OnConfirmMenuItem.BindRaw(this, &FShooterIngameMenu::OnShowInviteUI); +#endif + } +#endif + + if (FSlateApplication::Get().SupportsSystemHelp()) + { + TSharedPtr HelpSubMenu = MenuHelper::AddMenuItem(RootMenuItem, LOCTEXT("Help", "HELP")); + HelpSubMenu->OnConfirmMenuItem.BindStatic([](){ FSlateApplication::Get().ShowSystemHelp(); }); + } + + MenuHelper::AddExistingMenuItem(RootMenuItem, MainMenuItem.ToSharedRef()); + +#if !SHOOTER_CONSOLE_UI + MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("Quit", "QUIT"), this, &FShooterIngameMenu::OnUIQuit); +#endif + + GameMenuWidget->MainMenu = GameMenuWidget->CurrentMenu = RootMenuItem->SubMenu; + GameMenuWidget->OnMenuHidden.BindSP(this,&FShooterIngameMenu::DetachGameMenu); + GameMenuWidget->OnToggleMenu.BindSP(this,&FShooterIngameMenu::ToggleGameMenu); + GameMenuWidget->OnGoBack.BindSP(this, &FShooterIngameMenu::OnMenuGoBack); + } +} + +void FShooterIngameMenu::CloseSubMenu() +{ + GameMenuWidget->MenuGoBack(); +} + +void FShooterIngameMenu::OnMenuGoBack(MenuPtr Menu) +{ + // if we are going back from options menu + if (ShooterOptions.IsValid() && ShooterOptions->OptionsItem->SubMenu == Menu) + { + ShooterOptions->RevertChanges(); + } +} + +bool FShooterIngameMenu::GetIsGameMenuUp() const +{ + return bIsGameMenuUp; +} + +void FShooterIngameMenu::UpdateFriendsList() +{ + if (PlayerOwner) + { + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(PlayerOwner->GetWorld()); + if (OnlineSub) + { + IOnlineFriendsPtr OnlineFriendsPtr = OnlineSub->GetFriendsInterface(); + if (OnlineFriendsPtr.IsValid()) + { + OnlineFriendsPtr->ReadFriendsList(GetOwnerUserIndex(), EFriendsLists::ToString(EFriendsLists::OnlinePlayers)); + } + } + } +} + +void FShooterIngameMenu::DetachGameMenu() +{ + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(GameMenuContainer.ToSharedRef()); + } + bIsGameMenuUp = false; + + AShooterPlayerController* const PCOwner = PlayerOwner ? Cast(PlayerOwner->PlayerController) : nullptr; + if (PCOwner) + { + PCOwner->SetPause(false); + + // If the game is over enable the scoreboard + AShooterHUD* const ShooterHUD = PCOwner->GetShooterHUD(); + if( ( ShooterHUD != NULL ) && ( ShooterHUD->IsMatchOver() == true ) && ( PCOwner->IsPrimaryPlayer() == true ) ) + { + ShooterHUD->ShowScoreboard( true, true ); + } + } +} + +void FShooterIngameMenu::ToggleGameMenu() +{ + //Update the owner in case the menu was opened by another controller + //UpdateMenuOwner(); + + if (!GameMenuWidget.IsValid()) + { + return; + } + + // check for a valid user index. could be invalid if the user signed out, in which case the 'please connect your control' ui should be up anyway. + // in-game menu needs a valid userindex for many OSS calls. + if (GetOwnerUserIndex() == -1) + { + UE_LOG(LogShooter, Log, TEXT("Trying to toggle in-game menu for invalid userid")); + return; + } + + if (bIsGameMenuUp && GameMenuWidget->CurrentMenu != RootMenuItem->SubMenu) + { + GameMenuWidget->MenuGoBack(); + return; + } + + AShooterPlayerController* const PCOwner = PlayerOwner ? Cast(PlayerOwner->PlayerController) : nullptr; + if (!bIsGameMenuUp) + { + // Hide the scoreboard + if (PCOwner) + { + AShooterHUD* const ShooterHUD = PCOwner->GetShooterHUD(); + if( ShooterHUD != NULL ) + { + ShooterHUD->ShowScoreboard( false ); + } + } + + GEngine->GameViewport->AddViewportWidgetContent( + SAssignNew(GameMenuContainer,SWeakWidget) + .PossiblyNullContent(GameMenuWidget.ToSharedRef()) + ); + + int32 const OwnerUserIndex = GetOwnerUserIndex(); + if(ShooterOptions.IsValid()) + { + ShooterOptions->UpdateOptions(); + } + if(ShooterRecentlyMet.IsValid()) + { + ShooterRecentlyMet->UpdateRecentlyMet(OwnerUserIndex); + } + GameMenuWidget->BuildAndShowMenu(); + bIsGameMenuUp = true; + + if (PCOwner) + { + // Disable controls while paused + PCOwner->SetCinematicMode(true, false, false, true, true); + + if (PCOwner->SetPause(true)) + { + UShooterGameInstance* GameInstance = Cast(PlayerOwner->GetGameInstance()); + GameInstance->SetPresenceForLocalPlayers(FString(TEXT("On Pause")), FVariantData(FString(TEXT("Paused")))); + } + + FInputModeGameAndUI InputMode; + PCOwner->SetInputMode(InputMode); + } + } + else + { + //Start hiding animation + GameMenuWidget->HideMenu(); + if (PCOwner) + { + // Make sure viewport has focus + FSlateApplication::Get().SetAllUserFocusToGameViewport(); + + if (PCOwner->SetPause(false)) + { + UShooterGameInstance* GameInstance = Cast(PlayerOwner->GetGameInstance()); + GameInstance->SetPresenceForLocalPlayers(FString(TEXT("In Game")), FVariantData(FString(TEXT("InGame")))); + } + + // Don't renable controls if the match is over + AShooterHUD* const ShooterHUD = PCOwner->GetShooterHUD(); + if( ( ShooterHUD != NULL ) && ( ShooterHUD->IsMatchOver() == false ) ) + { + PCOwner->SetCinematicMode(false,false,false,true,true); + + FInputModeGameOnly InputMode; + PCOwner->SetInputMode(InputMode); + } + } + } +} + +void FShooterIngameMenu::OnCancelExitToMain() +{ + CloseSubMenu(); +} + +void FShooterIngameMenu::OnConfirmExitToMain() +{ + UShooterGameInstance* const GameInstance = Cast(PlayerOwner->GetGameInstance()); + if (GameInstance) + { + GameInstance->LabelPlayerAsQuitter(PlayerOwner); + + // tell game instance to go back to main menu state + GameInstance->GotoState(ShooterGameInstanceState::MainMenu); + } +} + +void FShooterIngameMenu::OnUIQuit() +{ + UShooterGameInstance* const GI = Cast(PlayerOwner->GetGameInstance()); + if (GI) + { + GI->LabelPlayerAsQuitter(PlayerOwner); + } + + GameMenuWidget->LockControls(true); + GameMenuWidget->HideMenu(); + + UWorld* const World = PlayerOwner ? PlayerOwner->GetWorld() : nullptr; + if (World) + { + const FShooterMenuSoundsStyle& MenuSounds = FShooterStyle::Get().GetWidgetStyle("DefaultShooterMenuSoundsStyle"); + MenuHelper::PlaySoundAndCall(World, MenuSounds.ExitGameSound, GetOwnerUserIndex(), this, &FShooterIngameMenu::Quit); + } +} + +void FShooterIngameMenu::Quit() +{ + APlayerController* const PCOwner = PlayerOwner ? PlayerOwner->PlayerController : nullptr; + if (PCOwner) + { + PCOwner->ConsoleCommand("quit"); + } +} + +void FShooterIngameMenu::OnShowInviteUI() +{ + if (PlayerOwner) + { + const IOnlineExternalUIPtr ExternalUI = Online::GetExternalUIInterface(PlayerOwner->GetWorld()); + + if (!ExternalUI.IsValid()) + { + UE_LOG(LogShooter, Warning, TEXT("OnShowInviteUI: External UI interface is not supported on this platform.")); + return; + } + + ExternalUI->ShowInviteUI(GetOwnerUserIndex()); + } +} + +int32 FShooterIngameMenu::GetOwnerUserIndex() const +{ + return PlayerOwner ? PlayerOwner->GetControllerId() : 0; +} + + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterIngameMenu.h b/Source/ShooterGame/Private/UI/Menu/ShooterIngameMenu.h new file mode 100644 index 0000000..772c79a --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterIngameMenu.h @@ -0,0 +1,86 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "Widgets/ShooterMenuItem.h" +#include "Widgets/SShooterMenuWidget.h" +#include "ShooterOptions.h" +#include "ShooterFriends.h" +#include "ShooterRecentlyMet.h" + +class FShooterIngameMenu : public TSharedFromThis +{ +public: + /** sets owning player controller */ + void Construct(ULocalPlayer* PlayerOwner); + + /** toggles in game menu */ + void ToggleGameMenu(); + + /** is game menu currently active? */ + bool GetIsGameMenuUp() const; + + /* updates the friends list of the current owner*/ + void UpdateFriendsList(); + + /* Getter for the ShooterFriends interface/pointer*/ + TSharedPtr GetShooterFriends(){ return ShooterFriends; } + +protected: + + /** Owning player controller */ + ULocalPlayer* PlayerOwner; + + /** game menu container widget - used for removing */ + TSharedPtr GameMenuContainer; + + /** root menu item pointer */ + TSharedPtr RootMenuItem; + + /** main menu item pointer */ + TSharedPtr MainMenuItem; + + /** HUD menu widget */ + TSharedPtr GameMenuWidget; + + /** if game menu is currently opened*/ + bool bIsGameMenuUp; + + /** holds cheats menu item to toggle it's visibility */ + TSharedPtr CheatsMenu; + + /** Shooter options */ + TSharedPtr ShooterOptions; + + /** get current user index out of PlayerOwner */ + int32 GetOwnerUserIndex() const; + /** Shooter friends */ + TSharedPtr ShooterFriends; + + /** Shooter recently met users*/ + TSharedPtr ShooterRecentlyMet; + + /** called when going back to previous menu */ + void OnMenuGoBack(MenuPtr Menu); + + /** goes back in menu structure */ + void CloseSubMenu(); + + /** removes widget from viewport */ + void DetachGameMenu(); + + /** Delegate called when user cancels confirmation dialog to exit to main menu */ + void OnCancelExitToMain(); + + /** Delegate called when user confirms confirmation dialog to exit to main menu */ + void OnConfirmExitToMain(); + + /** Plays sound and calls Quit */ + void OnUIQuit(); + + /** Quits the game */ + void Quit(); + + /** Shows the system UI to invite friends to the game */ + void OnShowInviteUI(); +}; diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterMainMenu.cpp b/Source/ShooterGame/Private/UI/Menu/ShooterMainMenu.cpp new file mode 100644 index 0000000..6cab0d7 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterMainMenu.cpp @@ -0,0 +1,1484 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterMainMenu.h" +#include "ShooterGameLoadingScreen.h" +#include "ShooterStyle.h" +#include "ShooterMenuSoundsWidgetStyle.h" +#include "ShooterGameInstance.h" +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "GenericPlatformChunkInstall.h" +#include "IHttpResponse.h" +#include "Online/ShooterOnlineGameSettings.h" +#include "OnlineSubsystemSessionSettings.h" +#include "SShooterConfirmationDialog.h" +#include "ShooterMenuItemWidgetStyle.h" +#include "ShooterGameUserSettings.h" +#include "ShooterGameViewportClient.h" +#include "ShooterPersistentUser.h" +#include "Player/ShooterLocalPlayer.h" +#include "OnlineSubsystemUtils.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +#define MAX_BOT_COUNT 8 + +static const FString MapNames[] = { TEXT("Sanctuary"), TEXT("Highrise") }; +static const FString JoinMapNames[] = { TEXT("Any"), TEXT("Sanctuary"), TEXT("Highrise") }; +static const FName PackageNames[] = { TEXT("Sanctuary.umap"), TEXT("Highrise.umap") }; +static const int DefaultTDMMap = 1; +static const int DefaultFFAMap = 0; +static const float QuickmatchUIAnimationTimeDuration = 30.f; + +//use an EMap index, get back the ChunkIndex that map should be part of. +//Instead of this mapping we should really use the AssetRegistry to query for chunk mappings, but maps aren't members of the AssetRegistry yet. +static const int ChunkMapping[] = { 1, 2 }; + +#if PLATFORM_SWITCH +# define LOGIN_REQUIRED_FOR_ONLINE_PLAY 1 +#else +# define LOGIN_REQUIRED_FOR_ONLINE_PLAY 0 +#endif + +#if PLATFORM_SWITCH +# define CONSOLE_LAN_SUPPORTED 1 +#else +# define CONSOLE_LAN_SUPPORTED 0 +#endif + +#if !defined(SHOOTER_XBOX_MENU) + #define SHOOTER_XBOX_MENU 0 +#endif + +FShooterMainMenu::FShooterMainMenu(FString& MatchmakerEndpoint) : MatchmakerEndpoint(MatchmakerEndpoint) {} + +FShooterMainMenu::~FShooterMainMenu() +{ + CleanupOnlinePrivilegeTask(); +} + +void FShooterMainMenu::Construct(TWeakObjectPtr _GameInstance, TWeakObjectPtr _PlayerOwner) +{ + bShowingDownloadPct = false; + bAnimateQuickmatchSearchingUI = false; + bUsedInputToCancelQuickmatchSearch = false; + bQuickmatchSearchRequestCanceled = false; + bIncQuickMAlpha = false; + PlayerOwner = _PlayerOwner; + MatchType = EMatchType::Custom; + + check(_GameInstance.IsValid()); + + GameInstance = _GameInstance; + PlayerOwner = _PlayerOwner; + + OnCancelMatchmakingCompleteDelegate = FOnCancelMatchmakingCompleteDelegate::CreateSP(this, &FShooterMainMenu::OnCancelMatchmakingComplete); + + // read user settings +#if SHOOTER_CONSOLE_UI + bIsLanMatch = FParse::Param(FCommandLine::Get(), TEXT("forcelan")); +#else + UShooterGameUserSettings* const UserSettings = CastChecked(GEngine->GetGameUserSettings()); + bIsLanMatch = UserSettings->IsLanMatch(); + bIsDedicatedServer = UserSettings->IsDedicatedServer(); +#endif + + BotsCountOpt = 1; + bIsRecordingDemo = false; + bIsQuitting = false; + + if(GetPersistentUser()) + { + BotsCountOpt = GetPersistentUser()->GetBotsCount(); + bIsRecordingDemo = GetPersistentUser()->IsRecordingDemos(); + } + + // number entries 0 up to MAX_BOX_COUNT + TArray BotsCountList; + for (int32 i = 0; i <= MAX_BOT_COUNT; i++) + { + BotsCountList.Add(FText::AsNumber(i)); + } + + TArray MapList; + for (int32 i = 0; i < UE_ARRAY_COUNT(MapNames); ++i) + { + MapList.Add(FText::FromString(MapNames[i])); + } + + TArray JoinMapList; + for (int32 i = 0; i < UE_ARRAY_COUNT(JoinMapNames); ++i) + { + JoinMapList.Add(FText::FromString(JoinMapNames[i])); + } + + TArray OnOffList; + OnOffList.Add( LOCTEXT("Off","OFF") ); + OnOffList.Add( LOCTEXT("On","ON") ); + + ShooterOptions = MakeShareable(new FShooterOptions()); + ShooterOptions->Construct(GetPlayerOwner()); + ShooterOptions->TellInputAboutKeybindings(); + ShooterOptions->OnApplyChanges.BindSP(this, &FShooterMainMenu::CloseSubMenu); + + //Now that we are here, build our menu + MenuWidget.Reset(); + MenuWidgetContainer.Reset(); + + TArray Keys; + GConfig->GetSingleLineArray(TEXT("/Script/SwitchRuntimeSettings.SwitchRuntimeSettings"), TEXT("LeaderboardMap"), Keys, GEngineIni); + + if (GEngine && GEngine->GameViewport) + { + SAssignNew(MenuWidget, SShooterMenuWidget) + .Cursor(EMouseCursor::Default) + .PlayerOwner(GetPlayerOwner()) + .IsGameMenu(false); + + SAssignNew(MenuWidgetContainer, SWeakWidget) + .PossiblyNullContent(MenuWidget); + + TSharedPtr RootMenuItem; + + + SAssignNew(SplitScreenLobbyWidget, SShooterSplitScreenLobby) + .PlayerOwner(GetPlayerOwner()) + .OnCancelClicked(FOnClicked::CreateSP(this, &FShooterMainMenu::OnSplitScreenBackedOut)) + .OnPlayClicked(FOnClicked::CreateSP(this, &FShooterMainMenu::OnSplitScreenPlay)); + + FText Msg = LOCTEXT("No matches could be found", "No matches could be found"); + FText OKButtonString = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + QuickMatchFailureWidget = SNew(SShooterConfirmationDialog).PlayerOwner(PlayerOwner) + .MessageText(Msg) + .ConfirmText(OKButtonString) + .CancelText(FText()) + .OnConfirmClicked(FOnClicked::CreateRaw(this, &FShooterMainMenu::OnQuickMatchFailureUICancel)) + .OnCancelClicked(FOnClicked::CreateRaw(this, &FShooterMainMenu::OnQuickMatchFailureUICancel)); + + Msg = LOCTEXT("Searching for Match...", "SEARCHING FOR MATCH..."); + OKButtonString = LOCTEXT("Stop", "STOP"); + QuickMatchSearchingWidget = SNew(SShooterConfirmationDialog).PlayerOwner(PlayerOwner) + .MessageText(Msg) + .ConfirmText(OKButtonString) + .CancelText(FText()) + .OnConfirmClicked(FOnClicked::CreateRaw(this, &FShooterMainMenu::OnQuickMatchSearchingUICancel)) + .OnCancelClicked(FOnClicked::CreateRaw(this, &FShooterMainMenu::OnQuickMatchSearchingUICancel)); + + SAssignNew(SplitScreenLobbyWidgetContainer, SWeakWidget) + .PossiblyNullContent(SplitScreenLobbyWidget); + + SAssignNew(QuickMatchFailureWidgetContainer, SWeakWidget) + .PossiblyNullContent(QuickMatchFailureWidget); + + SAssignNew(QuickMatchSearchingWidgetContainer, SWeakWidget) + .PossiblyNullContent(QuickMatchSearchingWidget); + + FText StoppingOKButtonString = LOCTEXT("Stopping", "STOPPING..."); + QuickMatchStoppingWidget = SNew(SShooterConfirmationDialog).PlayerOwner(PlayerOwner) + .MessageText(Msg) + .ConfirmText(StoppingOKButtonString) + .CancelText(FText()) + .OnConfirmClicked(FOnClicked()) + .OnCancelClicked(FOnClicked()); + + SAssignNew(QuickMatchStoppingWidgetContainer, SWeakWidget) + .PossiblyNullContent(QuickMatchStoppingWidget); + +#if SHOOTER_XBOX_MENU + TSharedPtr MenuItem; + + // HOST ONLINE menu option + { + MenuItem = MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("HostCustom", "HOST CUSTOM"), this, &FShooterMainMenu::OnHostOnlineSelected); + + // submenu under "HOST ONLINE" + MenuHelper::AddMenuItemSP(MenuItem, LOCTEXT("TDMLong", "TEAM DEATHMATCH"), this, &FShooterMainMenu::OnSplitScreenSelected); + + TSharedPtr NumberOfBotsOption = MenuHelper::AddMenuOptionSP(MenuItem, LOCTEXT("NumberOfBots", "NUMBER OF BOTS"), BotsCountList, this, &FShooterMainMenu::BotCountOptionChanged); + NumberOfBotsOption->SelectedMultiChoice = BotsCountOpt; + + HostOnlineMapOption = MenuHelper::AddMenuOption(MenuItem, LOCTEXT("SELECTED_LEVEL", "Map"), MapList); + } + + // JOIN menu option + { + // JOIN menu option + MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("FindCustom", "FIND CUSTOM"), this, &FShooterMainMenu::OnJoinServer); + + // Server list widget that will be called up if appropriate + MenuHelper::AddCustomMenuItem(JoinServerItem,SAssignNew(ServerListWidget,SShooterServerList).OwnerWidget(MenuWidget).PlayerOwner(GetPlayerOwner())); + } + + // QUICK MATCH menu option + { + MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("QuickMatch", "QUICK MATCH"), this, &FShooterMainMenu::OnQuickMatchSelected); + } + + // HOST OFFLINE menu option + { + MenuItem = MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("PlayOffline", "PLAY OFFLINE"),this, &FShooterMainMenu::OnHostOfflineSelected); + + // submenu under "HOST OFFLINE" + MenuHelper::AddMenuItemSP(MenuItem, LOCTEXT("TDMLong", "TEAM DEATHMATCH"), this, &FShooterMainMenu::OnSplitScreenSelected); + + TSharedPtr NumberOfBotsOption = MenuHelper::AddMenuOptionSP(MenuItem, LOCTEXT("NumberOfBots", "NUMBER OF BOTS"), BotsCountList, this, &FShooterMainMenu::BotCountOptionChanged); + NumberOfBotsOption->SelectedMultiChoice = BotsCountOpt; + + HostOfflineMapOption = MenuHelper::AddMenuOption(MenuItem, LOCTEXT("SELECTED_LEVEL", "Map"), MapList); + } +#elif SHOOTER_CONSOLE_UI + TSharedPtr MenuItem; + +#if HOST_ONLINE_GAMEMODE_ENABLED + // HOST ONLINE menu option + { + HostOnlineMenuItem = MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("HostOnline", "HOST ONLINE"), this, &FShooterMainMenu::OnHostOnlineSelected); + + // submenu under "HOST ONLINE" +#if LOGIN_REQUIRED_FOR_ONLINE_PLAY + MenuHelper::AddMenuItemSP(HostOnlineMenuItem, LOCTEXT("TDMLong", "TEAM DEATHMATCH"), this, &FShooterMainMenu::OnSplitScreenSelectedHostOnlineLoginRequired); +#else + MenuHelper::AddMenuItemSP(HostOnlineMenuItem, LOCTEXT("TDMLong", "TEAM DEATHMATCH"), this, &FShooterMainMenu::OnSplitScreenSelectedHostOnline); +#endif + + TSharedPtr NumberOfBotsOption = MenuHelper::AddMenuOptionSP(HostOnlineMenuItem, LOCTEXT("NumberOfBots", "NUMBER OF BOTS"), BotsCountList, this, &FShooterMainMenu::BotCountOptionChanged); + NumberOfBotsOption->SelectedMultiChoice = BotsCountOpt; + + HostOnlineMapOption = MenuHelper::AddMenuOption(HostOnlineMenuItem, LOCTEXT("SELECTED_LEVEL", "Map"), MapList); +#if CONSOLE_LAN_SUPPORTED + HostLANItem = MenuHelper::AddMenuOptionSP(HostOnlineMenuItem, LOCTEXT("LanMatch", "LAN"), OnOffList, this, &FShooterMainMenu::LanMatchChanged); + HostLANItem->SelectedMultiChoice = bIsLanMatch; +#endif + } +#endif //HOST_ONLINE_GAMEMODE_ENABLED + // HOST OFFLINE menu option + { + MenuItem = MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("HostOffline", "HOST OFFLINE"),this, &FShooterMainMenu::OnHostOfflineSelected); + + // submenu under "HOST OFFLINE" + MenuHelper::AddMenuItemSP(MenuItem, LOCTEXT("TDMLong", "TEAM DEATHMATCH"), this, &FShooterMainMenu::OnSplitScreenSelected); + + TSharedPtr NumberOfBotsOption = MenuHelper::AddMenuOptionSP(MenuItem, LOCTEXT("NumberOfBots", "NUMBER OF BOTS"), BotsCountList, this, &FShooterMainMenu::BotCountOptionChanged); + NumberOfBotsOption->SelectedMultiChoice = BotsCountOpt; + + HostOfflineMapOption = MenuHelper::AddMenuOption(MenuItem, LOCTEXT("SELECTED_LEVEL", "Map"), MapList); + } + + // QUICK MATCH menu option + { +#if LOGIN_REQUIRED_FOR_ONLINE_PLAY + MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("QuickMatch", "QUICK MATCH"), this, &FShooterMainMenu::OnQuickMatchSelectedLoginRequired); +#else + MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("QuickMatch", "QUICK MATCH"), this, &FShooterMainMenu::OnQuickMatchSelected); +#endif + } + +#if JOIN_ONLINE_GAME_ENABLED + // JOIN menu option + { + // JOIN menu option + MenuItem = MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("Join", "JOIN"), this, &FShooterMainMenu::OnJoinSelected); + + // submenu under "join" +#if LOGIN_REQUIRED_FOR_ONLINE_PLAY + MenuHelper::AddMenuItemSP(MenuItem, LOCTEXT("Server", "SERVER"), this, &FShooterMainMenu::OnJoinServerLoginRequired); +#else + MenuHelper::AddMenuItemSP(MenuItem, LOCTEXT("Server", "SERVER"), this, &FShooterMainMenu::OnJoinServer); +#endif + JoinMapOption = MenuHelper::AddMenuOption(MenuItem, LOCTEXT("SELECTED_LEVEL", "Map"), JoinMapList); + + // Server list widget that will be called up if appropriate + MenuHelper::AddCustomMenuItem(JoinServerItem,SAssignNew(ServerListWidget,SShooterServerList).OwnerWidget(MenuWidget).PlayerOwner(GetPlayerOwner())); + +#if CONSOLE_LAN_SUPPORTED + JoinLANItem = MenuHelper::AddMenuOptionSP(MenuItem, LOCTEXT("LanMatch", "LAN"), OnOffList, this, &FShooterMainMenu::LanMatchChanged); + JoinLANItem->SelectedMultiChoice = bIsLanMatch; +#endif + } +#endif //JOIN_ONLINE_GAME_ENABLED + +#else + + // JOIN menu option - only add this if a matchmaker endpoint is defined + if (!MatchmakerEndpoint.IsEmpty()) + { + MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("Join", "JOIN"), this, &FShooterMainMenu::OnJoinClicked); + } + + // Direct Connect + MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("DirectConnect", "DIRECT CONNECT"), this, &FShooterMainMenu::OnShowDirectConnect); + MenuHelper::AddCustomMenuItem(DirectConnectItem,SAssignNew(DirectConnectWidget,SShooterDirectConnect).OwnerWidget(MenuWidget).PlayerOwner(GetPlayerOwner())); + +#endif + + // Options + MenuHelper::AddExistingMenuItem(RootMenuItem, ShooterOptions->OptionsItem.ToSharedRef()); + + if(FSlateApplication::Get().SupportsSystemHelp()) + { + TSharedPtr HelpSubMenu = MenuHelper::AddMenuItem(RootMenuItem, LOCTEXT("Help", "HELP")); + HelpSubMenu->OnConfirmMenuItem.BindStatic([](){ FSlateApplication::Get().ShowSystemHelp(); }); + } + + // QUIT option (for PC) +#if SHOOTER_SHOW_QUIT_MENU_ITEM + MenuHelper::AddMenuItemSP(RootMenuItem, LOCTEXT("Quit", "QUIT"), this, &FShooterMainMenu::OnUIQuit); +#endif + + MenuWidget->CurrentMenuTitle = LOCTEXT("MainMenu","MAIN MENU"); + MenuWidget->OnGoBack.BindSP(this, &FShooterMainMenu::OnMenuGoBack); + MenuWidget->MainMenu = MenuWidget->CurrentMenu = RootMenuItem->SubMenu; + MenuWidget->OnMenuHidden.BindSP(this, &FShooterMainMenu::OnMenuHidden); + + ShooterOptions->UpdateOptions(); + MenuWidget->BuildAndShowMenu(); + } +} + +void FShooterMainMenu::AddMenuToGameViewport() +{ + if (GEngine && GEngine->GameViewport) + { + UGameViewportClient* GVC = GEngine->GameViewport; + GVC->AddViewportWidgetContent(MenuWidgetContainer.ToSharedRef()); + GVC->SetMouseCaptureMode(EMouseCaptureMode::NoCapture); + } +} + +void FShooterMainMenu::RemoveMenuFromGameViewport() +{ + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(MenuWidgetContainer.ToSharedRef()); + } +} + +void FShooterMainMenu::Tick(float DeltaSeconds) +{ + if (bAnimateQuickmatchSearchingUI) + { + FLinearColor QuickMColor = QuickMatchSearchingWidget->GetColorAndOpacity(); + if (bIncQuickMAlpha) + { + if (QuickMColor.A >= 1.f) + { + bIncQuickMAlpha = false; + } + else + { + QuickMColor.A += DeltaSeconds; + } + } + else + { + if (QuickMColor.A <= .1f) + { + bIncQuickMAlpha = true; + } + else + { + QuickMColor.A -= DeltaSeconds; + } + } + QuickMatchSearchingWidget->SetColorAndOpacity(QuickMColor); + QuickMatchStoppingWidget->SetColorAndOpacity(QuickMColor); + } + + IPlatformChunkInstall* ChunkInstaller = FPlatformMisc::GetPlatformChunkInstall(); + if (ChunkInstaller) + { + EMap SelectedMap = GetSelectedMap(); + // use assetregistry when maps are added to it. + int32 MapChunk = ChunkMapping[(int)SelectedMap]; + EChunkLocation::Type ChunkLocation = ChunkInstaller->GetPakchunkLocation(MapChunk); + + FText UpdatedText; + bool bUpdateText = false; + if (ChunkLocation == EChunkLocation::NotAvailable) + { + float PercentComplete = FMath::Min(ChunkInstaller->GetChunkProgress(MapChunk, EChunkProgressReportingType::PercentageComplete), 100.0f); + UpdatedText = FText::FromString(FString::Printf(TEXT("%s %4.0f%%"),*LOCTEXT("SELECTED_LEVEL", "Map").ToString(), PercentComplete)); + bUpdateText = true; + bShowingDownloadPct = true; + } + else if (bShowingDownloadPct) + { + UpdatedText = LOCTEXT("SELECTED_LEVEL", "Map"); + bUpdateText = true; + bShowingDownloadPct = false; + } + + if (bUpdateText) + { + if (GameInstance.IsValid() && GameInstance->GetOnlineMode() != EOnlineMode::Offline && HostOnlineMapOption.IsValid()) + { + HostOnlineMapOption->SetText(UpdatedText); + } + else if (HostOfflineMapOption.IsValid()) + { + HostOfflineMapOption->SetText(UpdatedText); + } + } + } +} + +TStatId FShooterMainMenu::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(FShooterMainMenu, STATGROUP_Tickables); +} + +void FShooterMainMenu::OnMenuHidden() +{ +#if SHOOTER_CONSOLE_UI + if (bIsQuitting) + { + RemoveMenuFromGameViewport(); + } + // Menu was hidden from the top-level main menu, on consoles show the welcome screen again. + else if ( ensure(GameInstance.IsValid())) + { + GameInstance->GotoState(ShooterGameInstanceState::WelcomeScreen); + } +#else + RemoveMenuFromGameViewport(); +#endif +} + +void FShooterMainMenu::OnQuickMatchSelectedLoginRequired() +{ + IOnlineIdentityPtr Identity = Online::GetIdentityInterface(GetTickableGameObjectWorld()); + if (Identity.IsValid()) + { + int32 ControllerId = GetPlayerOwner()->GetControllerId(); + + OnLoginCompleteDelegateHandle = Identity->AddOnLoginCompleteDelegate_Handle(ControllerId, FOnLoginCompleteDelegate::CreateRaw(this, &FShooterMainMenu::OnLoginCompleteQuickmatch)); + Identity->Login(ControllerId, FOnlineAccountCredentials()); + } +} + +void FShooterMainMenu::OnLoginCompleteQuickmatch(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error) +{ + IOnlineIdentityPtr Identity = Online::GetIdentityInterface(GetTickableGameObjectWorld()); + if (Identity.IsValid()) + { + Identity->ClearOnLoginCompleteDelegate_Handle(LocalUserNum, OnLoginCompleteDelegateHandle); + } + + OnQuickMatchSelected(); +} + +void FShooterMainMenu::OnQuickMatchSelected() +{ + bQuickmatchSearchRequestCanceled = false; +#if SHOOTER_CONSOLE_UI + if ( !ValidatePlayerForOnlinePlay(GetPlayerOwner()) ) + { + return; + } +#endif + + StartOnlinePrivilegeTask(IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateSP(this, &FShooterMainMenu::OnUserCanPlayOnlineQuickMatch)); +} + +void FShooterMainMenu::OnUserCanPlayOnlineQuickMatch(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + CleanupOnlinePrivilegeTask(); + MenuWidget->LockControls(false); + if (PrivilegeResults == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + if (GameInstance.IsValid()) + { + GameInstance->SetOnlineMode(EOnlineMode::Online); + } + + MatchType = EMatchType::Quick; + + SplitScreenLobbyWidget->SetIsJoining(false); + + // Skip splitscreen for PS4 +#if PLATFORM_PS4 || MAX_LOCAL_PLAYERS == 1 + BeginQuickMatchSearch(); +#else + UGameViewportClient* const GVC = GEngine->GameViewport; + + RemoveMenuFromGameViewport(); + GVC->AddViewportWidgetContent(SplitScreenLobbyWidgetContainer.ToSharedRef()); + + SplitScreenLobbyWidget->Clear(); + FSlateApplication::Get().SetKeyboardFocus(SplitScreenLobbyWidget); +#endif + } + else if (GameInstance.IsValid()) + { + + GameInstance->DisplayOnlinePrivilegeFailureDialogs(UserId, Privilege, PrivilegeResults); + } +} + +FReply FShooterMainMenu::OnConfirmGeneric() +{ + UShooterGameViewportClient* ShooterViewport = Cast(GameInstance->GetGameViewportClient()); + if (ShooterViewport) + { + ShooterViewport->HideDialog(); + } + + return FReply::Handled(); +} + +void FShooterMainMenu::BeginQuickMatchSearch() +{ + IOnlineSessionPtr Sessions = Online::GetSessionInterface(GetTickableGameObjectWorld()); + if(!Sessions.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("Quick match is not supported: couldn't find online session interface.")); + return; + } + + if (GetPlayerOwnerControllerId() == -1) + { + UE_LOG(LogOnline, Warning, TEXT("Quick match is not supported: Could not get controller id from player owner")); + return; + } + + QuickMatchSearchSettings = MakeShared(false, true); + QuickMatchSearchSettings->QuerySettings.Set(SEARCH_MATCHMAKING_QUEUE, FString("TeamDeathmatch"), EOnlineComparisonOp::Equals); + QuickMatchSearchSettings->QuerySettings.Set(SEARCH_XBOX_LIVE_HOPPER_NAME, FString("TeamDeathmatch"), EOnlineComparisonOp::Equals); + QuickMatchSearchSettings->QuerySettings.Set(SEARCH_XBOX_LIVE_SESSION_TEMPLATE_NAME, FString("MatchSession"), EOnlineComparisonOp::Equals); + QuickMatchSearchSettings->TimeoutInSeconds = 120.0f; + + FShooterOnlineSessionSettings SessionSettings(false, true, 8); + SessionSettings.Set(SETTING_GAMEMODE, FString("TDM"), EOnlineDataAdvertisementType::ViaOnlineService); + SessionSettings.Set(SETTING_MATCHING_HOPPER, FString("TeamDeathmatch"), EOnlineDataAdvertisementType::DontAdvertise); + SessionSettings.Set(SETTING_MATCHING_TIMEOUT, 120.0f, EOnlineDataAdvertisementType::ViaOnlineService); + SessionSettings.Set(SETTING_SESSION_TEMPLATE_NAME, FString("GameSession"), EOnlineDataAdvertisementType::DontAdvertise); + + TSharedRef QuickMatchSearchSettingsRef = QuickMatchSearchSettings.ToSharedRef(); + + DisplayQuickmatchSearchingUI(); + + // Perform matchmaking with all local players + TArray LocalPlayers; + for (auto It = GameInstance->GetLocalPlayerIterator(); It; ++It) + { + FUniqueNetIdRepl PlayerId = (*It)->GetPreferredUniqueNetId(); + if (PlayerId.IsValid()) + { + FSessionMatchmakingUser LocalPlayer = {(*PlayerId).AsShared()}; + LocalPlayers.Emplace(LocalPlayer); + } + } + + FOnStartMatchmakingComplete CompletionDelegate; + CompletionDelegate.BindSP(this, &FShooterMainMenu::OnMatchmakingComplete); + if (!Sessions->StartMatchmaking(LocalPlayers, NAME_GameSession, SessionSettings, QuickMatchSearchSettingsRef, CompletionDelegate)) + { + OnMatchmakingComplete(NAME_GameSession, FOnlineError(false), FSessionMatchmakingResults()); + } +} + + +void FShooterMainMenu::OnSplitScreenSelectedHostOnlineLoginRequired() +{ + IOnlineIdentityPtr Identity = Online::GetIdentityInterface(GetTickableGameObjectWorld()); + if (Identity.IsValid()) + { + int32 ControllerId = GetPlayerOwner()->GetControllerId(); + + if (bIsLanMatch) + { + Identity->Logout(ControllerId); + OnSplitScreenSelected(); + } + else + { + OnLoginCompleteDelegateHandle = Identity->AddOnLoginCompleteDelegate_Handle(ControllerId, FOnLoginCompleteDelegate::CreateRaw(this, &FShooterMainMenu::OnLoginCompleteHostOnline)); + Identity->Login(ControllerId, FOnlineAccountCredentials()); + } + } +} + +void FShooterMainMenu::OnSplitScreenSelected() +{ + if (!IsMapReady()) + { + return; + } + + RemoveMenuFromGameViewport(); + +#if PLATFORM_PS4 || MAX_LOCAL_PLAYERS == 1 || !SHOOTER_SUPPORTS_OFFLINE_SPLIT_SCREEEN + if (!SHOOTER_SUPPORTS_OFFLINE_SPLIT_SCREEEN || (GameInstance.IsValid() && GameInstance->GetOnlineMode() == EOnlineMode::Online)) + { + OnUIHostTeamDeathMatch(); + } + else +#endif + { + UGameViewportClient* const GVC = GEngine->GameViewport; + GVC->AddViewportWidgetContent(SplitScreenLobbyWidgetContainer.ToSharedRef()); + + SplitScreenLobbyWidget->Clear(); + FSlateApplication::Get().SetKeyboardFocus(SplitScreenLobbyWidget); + } +} + +void FShooterMainMenu::OnHostOnlineSelected() +{ +#if SHOOTER_CONSOLE_UI + if (!ValidatePlayerIsSignedIn(GetPlayerOwner())) + { + return; + } +#endif + + MatchType = EMatchType::Custom; + + EOnlineMode NewOnlineMode = bIsLanMatch ? EOnlineMode::LAN : EOnlineMode::Online; + if (GameInstance.IsValid()) + { + GameInstance->SetOnlineMode(NewOnlineMode); + } + SplitScreenLobbyWidget->SetIsJoining(false); + MenuWidget->EnterSubMenu(); +} + +void FShooterMainMenu::OnUserCanPlayHostOnline(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + CleanupOnlinePrivilegeTask(); + MenuWidget->LockControls(false); + if (PrivilegeResults == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + OnSplitScreenSelected(); + } + else if (GameInstance.IsValid()) + { + GameInstance->DisplayOnlinePrivilegeFailureDialogs(UserId, Privilege, PrivilegeResults); + } +} + +void FShooterMainMenu::OnLoginCompleteHostOnline(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error) +{ + Online::GetIdentityInterface(GetTickableGameObjectWorld())->ClearOnLoginCompleteDelegate_Handle(LocalUserNum, OnLoginCompleteDelegateHandle); + + OnSplitScreenSelectedHostOnline(); +} + +void FShooterMainMenu::OnSplitScreenSelectedHostOnline() +{ +#if SHOOTER_CONSOLE_UI + if (!ValidatePlayerForOnlinePlay(GetPlayerOwner())) + { + return; + } +#endif + + StartOnlinePrivilegeTask(IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateSP(this, &FShooterMainMenu::OnUserCanPlayHostOnline)); +} +void FShooterMainMenu::StartOnlinePrivilegeTask(const IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate& Delegate) +{ + if (GameInstance.IsValid()) + { + // Lock controls for the duration of the async task + MenuWidget->LockControls(true); + FUniqueNetIdRepl UserId; + if (PlayerOwner.IsValid()) + { + UserId = PlayerOwner->GetPreferredUniqueNetId(); + } + GameInstance->StartOnlinePrivilegeTask(Delegate, EUserPrivileges::CanPlayOnline, UserId.GetUniqueNetId()); + } +} + +void FShooterMainMenu::CleanupOnlinePrivilegeTask() +{ + if (GameInstance.IsValid()) + { + GameInstance->CleanupOnlinePrivilegeTask(); + } +} + +void FShooterMainMenu::OnHostOfflineSelected() +{ + MatchType = EMatchType::Custom; + +#if LOGIN_REQUIRED_FOR_ONLINE_PLAY + Online::GetIdentityInterface(GetTickableGameObjectWorld())->Logout(GetPlayerOwner()->GetControllerId()); +#endif + + if (GameInstance.IsValid()) + { + GameInstance->SetOnlineMode(EOnlineMode::Offline); + } + SplitScreenLobbyWidget->SetIsJoining( false ); + + MenuWidget->EnterSubMenu(); +} + +FReply FShooterMainMenu::OnSplitScreenBackedOut() +{ + SplitScreenLobbyWidget->Clear(); + SplitScreenBackedOut(); + return FReply::Handled(); +} + +FReply FShooterMainMenu::OnSplitScreenPlay() +{ + switch ( MatchType ) + { + case EMatchType::Custom: + { +#if SHOOTER_CONSOLE_UI + if ( SplitScreenLobbyWidget->GetIsJoining() ) + { +#if 1 + // Until we can make split-screen menu support sub-menus, we need to do it this way + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(SplitScreenLobbyWidgetContainer.ToSharedRef()); + } + AddMenuToGameViewport(); + + FSlateApplication::Get().SetKeyboardFocus(MenuWidget); + + // Grab the map filter if there is one + FString SelectedMapFilterName = TEXT("ANY"); + if (JoinMapOption.IsValid()) + { + int32 FilterChoice = JoinMapOption->SelectedMultiChoice; + if (FilterChoice != INDEX_NONE) + { + SelectedMapFilterName = JoinMapOption->MultiChoice[FilterChoice].ToString(); + } + } + + + MenuWidget->NextMenu = JoinServerItem->SubMenu; + ServerListWidget->BeginServerSearch(bIsLanMatch, bIsDedicatedServer, SelectedMapFilterName); + ServerListWidget->UpdateServerList(); + MenuWidget->EnterSubMenu(); +#else + SplitScreenLobbyWidget->NextMenu = JoinServerItem->SubMenu; + ServerListWidget->BeginServerSearch(bIsLanMatch, bIsDedicatedServer, SelectedMapFilterName); + ServerListWidget->UpdateServerList(); + SplitScreenLobbyWidget->EnterSubMenu(); +#endif + } + else +#endif + { + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(SplitScreenLobbyWidgetContainer.ToSharedRef()); + } + OnUIHostTeamDeathMatch(); + } + break; + } + + case EMatchType::Quick: + { + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(SplitScreenLobbyWidgetContainer.ToSharedRef()); + } + BeginQuickMatchSearch(); + break; + } + } + + return FReply::Handled(); +} + +void FShooterMainMenu::SplitScreenBackedOut() +{ + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(SplitScreenLobbyWidgetContainer.ToSharedRef()); + } + AddMenuToGameViewport(); + + FSlateApplication::Get().SetKeyboardFocus(MenuWidget); +} + +void FShooterMainMenu::HelperQuickMatchSearchingUICancel(bool bShouldRemoveSession) +{ + IOnlineSessionPtr Sessions = Online::GetSessionInterface(GetTickableGameObjectWorld()); + if (bShouldRemoveSession && Sessions.IsValid()) + { + if (PlayerOwner.IsValid() && PlayerOwner->GetPreferredUniqueNetId().IsValid()) + { + UGameViewportClient* const GVC = GEngine->GameViewport; + GVC->RemoveViewportWidgetContent(QuickMatchSearchingWidgetContainer.ToSharedRef()); + GVC->AddViewportWidgetContent(QuickMatchStoppingWidgetContainer.ToSharedRef()); + FSlateApplication::Get().SetKeyboardFocus(QuickMatchStoppingWidgetContainer); + + OnCancelMatchmakingCompleteDelegateHandle = Sessions->AddOnCancelMatchmakingCompleteDelegate_Handle(OnCancelMatchmakingCompleteDelegate); + Sessions->CancelMatchmaking(*PlayerOwner->GetPreferredUniqueNetId(), NAME_GameSession); + } + } + else + { + UGameViewportClient* const GVC = GEngine->GameViewport; + GVC->RemoveViewportWidgetContent(QuickMatchSearchingWidgetContainer.ToSharedRef()); + AddMenuToGameViewport(); + FSlateApplication::Get().SetKeyboardFocus(MenuWidget); + } +} + +FReply FShooterMainMenu::OnQuickMatchSearchingUICancel() +{ + HelperQuickMatchSearchingUICancel(true); + bUsedInputToCancelQuickmatchSearch = true; + bQuickmatchSearchRequestCanceled = true; + return FReply::Handled(); +} + +FReply FShooterMainMenu::OnQuickMatchFailureUICancel() +{ + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(QuickMatchFailureWidgetContainer.ToSharedRef()); + } + AddMenuToGameViewport(); + FSlateApplication::Get().SetKeyboardFocus(MenuWidget); + return FReply::Handled(); +} + +void FShooterMainMenu::DisplayQuickmatchFailureUI() +{ + UGameViewportClient* const GVC = GEngine->GameViewport; + RemoveMenuFromGameViewport(); + GVC->AddViewportWidgetContent(QuickMatchFailureWidgetContainer.ToSharedRef()); + FSlateApplication::Get().SetKeyboardFocus(QuickMatchFailureWidget); +} + +void FShooterMainMenu::DisplayQuickmatchSearchingUI() +{ + UGameViewportClient* const GVC = GEngine->GameViewport; + RemoveMenuFromGameViewport(); + GVC->AddViewportWidgetContent(QuickMatchSearchingWidgetContainer.ToSharedRef()); + FSlateApplication::Get().SetKeyboardFocus(QuickMatchSearchingWidget); + bAnimateQuickmatchSearchingUI = true; +} + +void FShooterMainMenu::OnMatchmakingComplete(FName SessionName, const FOnlineError& ErrorDetails, const FSessionMatchmakingResults& Results) +{ + const bool bWasSuccessful = ErrorDetails.WasSuccessful(); + IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(GetTickableGameObjectWorld()); + if (!SessionInterface.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("OnMatchmakingComplete: Couldn't find session interface.")); + return; + } + + if (bQuickmatchSearchRequestCanceled && bUsedInputToCancelQuickmatchSearch) + { + bQuickmatchSearchRequestCanceled = false; + // Clean up the session in case we get this event after canceling + if (bWasSuccessful) + { + if (PlayerOwner.IsValid() && PlayerOwner->GetPreferredUniqueNetId().IsValid()) + { + SessionInterface->DestroySession(NAME_GameSession); + } + } + return; + } + + if (bAnimateQuickmatchSearchingUI) + { + bAnimateQuickmatchSearchingUI = false; + HelperQuickMatchSearchingUICancel(false); + bUsedInputToCancelQuickmatchSearch = false; + } + else + { + return; + } + + if (!bWasSuccessful) + { + UE_LOG(LogOnline, Warning, TEXT("Matchmaking was unsuccessful.")); + DisplayQuickmatchFailureUI(); + return; + } + + UE_LOG(LogOnline, Log, TEXT("Matchmaking successful! Session name is %s."), *SessionName.ToString()); + + if (GetPlayerOwner() == NULL) + { + UE_LOG(LogOnline, Warning, TEXT("OnMatchmakingComplete: No owner.")); + return; + } + + FNamedOnlineSession* MatchmadeSession = SessionInterface->GetNamedSession(SessionName); + + if (!MatchmadeSession) + { + UE_LOG(LogOnline, Warning, TEXT("OnMatchmakingComplete: No session.")); + return; + } + + if(!MatchmadeSession->OwningUserId.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("OnMatchmakingComplete: No session owner/host.")); + return; + } + + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(QuickMatchSearchingWidgetContainer.ToSharedRef()); + } + bAnimateQuickmatchSearchingUI = false; + + UE_LOG(LogOnline, Log, TEXT("OnMatchmakingComplete: Session host is %d."), *MatchmadeSession->OwningUserId->ToString()); + + if (ensure(GameInstance.IsValid())) + { + MenuWidget->LockControls(true); + + IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetTickableGameObjectWorld()); + if (Subsystem != nullptr && Subsystem->IsLocalPlayer(*MatchmadeSession->OwningUserId)) + { + // This console is the host, start the map. + GameInstance->BeginHostingQuickMatch(); + } + else + { + // We are the client, join the host. + GameInstance->TravelToSession(SessionName); + } + } +} + +FShooterMainMenu::EMap FShooterMainMenu::GetSelectedMap() const +{ + if (GameInstance.IsValid() && GameInstance->GetOnlineMode() != EOnlineMode::Offline && HostOnlineMapOption.IsValid()) + { + return (EMap)HostOnlineMapOption->SelectedMultiChoice; + } + else if (HostOfflineMapOption.IsValid()) + { + return (EMap)HostOfflineMapOption->SelectedMultiChoice; + } + + return EMap::ESancturary; // Need to return something (we can hit this path in cooking) +} + +void FShooterMainMenu::CloseSubMenu() +{ + MenuWidget->MenuGoBack(true); +} + +void FShooterMainMenu::OnMenuGoBack(MenuPtr Menu) +{ + // if we are going back from options menu + if (ShooterOptions->OptionsItem->SubMenu == Menu) + { + ShooterOptions->RevertChanges(); + } + + // if we've backed all the way out we need to make sure online is false. + if (MenuWidget->GetMenuLevel() == 1) + { + GameInstance->SetOnlineMode(EOnlineMode::Offline); + } +} + +void FShooterMainMenu::BotCountOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + BotsCountOpt = MultiOptionIndex; + + if(GetPersistentUser()) + { + GetPersistentUser()->SetBotsCount(BotsCountOpt); + } +} + +void FShooterMainMenu::LanMatchChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + if (HostLANItem.IsValid()) + { + HostLANItem->SelectedMultiChoice = MultiOptionIndex; + } + + check(JoinLANItem.IsValid()); + JoinLANItem->SelectedMultiChoice = MultiOptionIndex; + bIsLanMatch = MultiOptionIndex > 0; + UShooterGameUserSettings* UserSettings = CastChecked(GEngine->GetGameUserSettings()); + UserSettings->SetLanMatch(bIsLanMatch); + + EOnlineMode NewOnlineMode = bIsLanMatch ? EOnlineMode::LAN : EOnlineMode::Online; + if (GameInstance.IsValid()) + { + GameInstance->SetOnlineMode(NewOnlineMode); + } +} + +void FShooterMainMenu::DedicatedServerChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + check(DedicatedItem.IsValid()); + DedicatedItem->SelectedMultiChoice = MultiOptionIndex; + bIsDedicatedServer = MultiOptionIndex > 0; + UShooterGameUserSettings* UserSettings = CastChecked(GEngine->GetGameUserSettings()); + UserSettings->SetDedicatedServer(bIsDedicatedServer); +} + +void FShooterMainMenu::RecordDemoChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + if (RecordDemoItem.IsValid()) + { + RecordDemoItem->SelectedMultiChoice = MultiOptionIndex; + } + + bIsRecordingDemo = MultiOptionIndex > 0; + + if(GetPersistentUser()) + { + GetPersistentUser()->SetIsRecordingDemos(bIsRecordingDemo); + GetPersistentUser()->SaveIfDirty(); + } +} + +void FShooterMainMenu::OnUIHostFreeForAll() +{ +#if WITH_EDITOR + if (GIsEditor == true) + { + return; + } +#endif + if (!IsMapReady()) + { + return; + } + +#if !SHOOTER_CONSOLE_UI + if (GameInstance.IsValid()) + { + GameInstance->SetOnlineMode(bIsLanMatch ? EOnlineMode::LAN : EOnlineMode::Online); + } +#endif + + MenuWidget->LockControls(true); + MenuWidget->HideMenu(); + + UWorld* World = GetTickableGameObjectWorld(); + const int32 ControllerId = GetPlayerOwnerControllerId(); + + if (World && ControllerId != -1) + { + const FShooterMenuSoundsStyle& MenuSounds = FShooterStyle::Get().GetWidgetStyle("DefaultShooterMenuSoundsStyle"); + MenuHelper::PlaySoundAndCall(World, MenuSounds.StartGameSound, ControllerId, this, &FShooterMainMenu::HostFreeForAll); + } +} + +void FShooterMainMenu::OnUIHostTeamDeathMatch() +{ +#if WITH_EDITOR + if (GIsEditor == true) + { + return; + } +#endif + if (!IsMapReady()) + { + return; + } + +#if !SHOOTER_CONSOLE_UI + if (GameInstance.IsValid()) + { + GameInstance->SetOnlineMode(bIsLanMatch ? EOnlineMode::LAN : EOnlineMode::Online); + } +#endif + + MenuWidget->LockControls(true); + MenuWidget->HideMenu(); + + if (GetTickableGameObjectWorld() && GetPlayerOwnerControllerId() != -1) + { + const FShooterMenuSoundsStyle& MenuSounds = FShooterStyle::Get().GetWidgetStyle("DefaultShooterMenuSoundsStyle"); + MenuHelper::PlaySoundAndCall(GetTickableGameObjectWorld(), MenuSounds.StartGameSound, GetPlayerOwnerControllerId(), this, &FShooterMainMenu::HostTeamDeathMatch); + } +} + +void FShooterMainMenu::HostGame(const FString& GameType) +{ + if (ensure(GameInstance.IsValid()) && GetPlayerOwner() != NULL) + { + FString const StartURL = FString::Printf(TEXT("/Game/Maps/%s?game=%s%s%s?%s=%d%s"), *GetMapName(), *GameType, GameInstance->GetOnlineMode() != EOnlineMode::Offline ? TEXT("?listen") : TEXT(""), GameInstance->GetOnlineMode() == EOnlineMode::LAN ? TEXT("?bIsLanMatch") : TEXT(""), *AShooterGameMode::GetBotsCountOptionName(), BotsCountOpt, bIsRecordingDemo ? TEXT("?DemoRec") : TEXT("") ); + + // Game instance will handle success, failure and dialogs + GameInstance->HostGame(GetPlayerOwner(), GameType, StartURL); + } +} + +void FShooterMainMenu::HostFreeForAll() +{ + HostGame(TEXT("FFA")); +} + +void FShooterMainMenu::HostTeamDeathMatch() +{ + HostGame(TEXT("TDM")); +} + +FReply FShooterMainMenu::OnConfirm() +{ + if (GEngine && GEngine->GameViewport) + { + UShooterGameViewportClient * ShooterViewport = Cast(GEngine->GameViewport); + + if (ShooterViewport) + { + // Hide the previous dialog + ShooterViewport->HideDialog(); + } + } + + return FReply::Handled(); +} + +bool FShooterMainMenu::ValidatePlayerForOnlinePlay(ULocalPlayer* LocalPlayer) +{ + if (!ensure(GameInstance.IsValid())) + { + return false; + } + + return GameInstance->ValidatePlayerForOnlinePlay(LocalPlayer); +} + +bool FShooterMainMenu::ValidatePlayerIsSignedIn(ULocalPlayer* LocalPlayer) +{ + if (!ensure(GameInstance.IsValid())) + { + return false; + } + + return GameInstance->ValidatePlayerIsSignedIn(LocalPlayer); +} + +void FShooterMainMenu::OnJoinServerLoginRequired() +{ + IOnlineIdentityPtr Identity = Online::GetIdentityInterface(GetTickableGameObjectWorld()); + if (Identity.IsValid()) + { + int32 ControllerId = GetPlayerOwner()->GetControllerId(); + + if (bIsLanMatch) + { + Identity->Logout(ControllerId); + OnUserCanPlayOnlineJoin(*GetPlayerOwner()->GetCachedUniqueNetId(), EUserPrivileges::CanPlayOnline, (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures); + } + else + { + OnLoginCompleteDelegateHandle = Identity->AddOnLoginCompleteDelegate_Handle(ControllerId, FOnLoginCompleteDelegate::CreateRaw(this, &FShooterMainMenu::OnLoginCompleteJoin)); + Identity->Login(ControllerId, FOnlineAccountCredentials()); + } + } +} + +void FShooterMainMenu::OnLoginCompleteJoin(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error) +{ + IOnlineIdentityPtr Identity = Online::GetIdentityInterface(GetTickableGameObjectWorld()); + if (Identity.IsValid()) + { + Identity->ClearOnLoginCompleteDelegate_Handle(LocalUserNum, OnLoginCompleteDelegateHandle); + } + + OnJoinServer(); +} + +void FShooterMainMenu::OnJoinSelected() +{ +#if SHOOTER_CONSOLE_UI + if (!ValidatePlayerIsSignedIn(GetPlayerOwner())) + { + return; + } +#endif + + MenuWidget->EnterSubMenu(); +} + +void FShooterMainMenu::OnJoinServer() +{ +#if SHOOTER_CONSOLE_UI + if ( !ValidatePlayerForOnlinePlay(GetPlayerOwner()) ) + { + return; + } +#endif + + StartOnlinePrivilegeTask(IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateSP(this, &FShooterMainMenu::OnUserCanPlayOnlineJoin)); +} + +void FShooterMainMenu::OnJoinClicked() +{ + UE_LOG(LogOnline, Display, TEXT("Fetching server to join from matchmaker at %s"), *MatchmakerEndpoint); + + TSharedRef Request = FHttpModule::Get().CreateRequest(); + Request->SetURL(MatchmakerEndpoint); + Request->SetVerb("GET"); + Request->SetHeader("Content-Type", TEXT("application/json")); + + // Add callback to join the server if the fetch was a success + Request->OnProcessRequestComplete().BindLambda([&](FHttpRequestPtr _, FHttpResponsePtr Response, bool bSuccessful) + { + if (!bSuccessful || !Response.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("Posting to matchmaker failed")); + return; + } + + const int32 ResponseCode = Response->GetResponseCode(); + if (ResponseCode < 200 || ResponseCode >= 300) + { + UE_LOG(LogOnline, Warning, TEXT("Posting to matchmaker failed with error code %d (message: %s)"), + ResponseCode, *Response->GetContentAsString()); + return; + } + + TSharedPtr JsonObject; + TSharedRef> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); + if (FJsonSerializer::Deserialize(Reader, JsonObject)) + { + const FString IP = JsonObject->GetStringField("IP"); + const int32 Port = JsonObject->GetIntegerField("Port"); + const FString Address = FString::Printf(TEXT("%s:%d"), *IP, Port); + + if (APlayerController* PlayerController = PlayerOwner->PlayerController) + { + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveAllViewportWidgets(); + } + else + { + UE_LOG(LogOnline, Warning, TEXT("Could not get game viewport, no widgets removed")); + } + + if (UShooterGameInstance* GInstance = Cast(PlayerController->GetGameInstance())) + { + GInstance->DirectConnectToSession(Address); + } + else + { + UE_LOG(LogOnline, Warning, TEXT("Could not get game instance, joining server failed")); + } + } + } + else + { + UE_LOG(LogOnline, Warning, TEXT("Could not deserialise JSON (raw: %s)"), + *Response->GetContentAsString()); + } + }); + + Request->ProcessRequest(); +} + +void FShooterMainMenu::OnUserCanPlayOnlineJoin(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + CleanupOnlinePrivilegeTask(); + MenuWidget->LockControls(false); + + if (PrivilegeResults == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + + //make sure to switch to custom match type so we don't instead use Quick type + MatchType = EMatchType::Custom; + + if (GameInstance.IsValid()) + { + GameInstance->SetOnlineMode(bIsLanMatch ? EOnlineMode::LAN : EOnlineMode::Online); + } + + MatchType = EMatchType::Custom; + // Grab the map filter if there is one + FString SelectedMapFilterName("Any"); + if( JoinMapOption.IsValid()) + { + int32 FilterChoice = JoinMapOption->SelectedMultiChoice; + if( FilterChoice != INDEX_NONE ) + { + SelectedMapFilterName = JoinMapOption->MultiChoice[FilterChoice].ToString(); + } + } + +#if SHOOTER_CONSOLE_UI + UGameViewportClient* const GVC = GEngine->GameViewport; +#if PLATFORM_PS4 || MAX_LOCAL_PLAYERS == 1 + // Show server menu (skip splitscreen) + AddMenuToGameViewport(); + FSlateApplication::Get().SetKeyboardFocus(MenuWidget); + + MenuWidget->NextMenu = JoinServerItem->SubMenu; + ServerListWidget->BeginServerSearch(bIsLanMatch, bIsDedicatedServer, SelectedMapFilterName); + ServerListWidget->UpdateServerList(); + MenuWidget->EnterSubMenu(); +#else + // Show splitscreen menu + RemoveMenuFromGameViewport(); + GVC->AddViewportWidgetContent(SplitScreenLobbyWidgetContainer.ToSharedRef()); + + SplitScreenLobbyWidget->Clear(); + FSlateApplication::Get().SetKeyboardFocus(SplitScreenLobbyWidget); + + SplitScreenLobbyWidget->SetIsJoining( true ); +#endif +#else + MenuWidget->NextMenu = JoinServerItem->SubMenu; + //FString SelectedMapFilterName = JoinMapOption->MultiChoice[JoinMapOption->SelectedMultiChoice].ToString(); + + ServerListWidget->BeginServerSearch(bIsLanMatch, bIsDedicatedServer, SelectedMapFilterName); + ServerListWidget->UpdateServerList(); + MenuWidget->EnterSubMenu(); +#endif + } + else if (GameInstance.IsValid()) + { + GameInstance->DisplayOnlinePrivilegeFailureDialogs(UserId, Privilege, PrivilegeResults); + } +} + +void FShooterMainMenu::OnShowLeaderboard() +{ + MenuWidget->NextMenu = LeaderboardItem->SubMenu; +#if LOGIN_REQUIRED_FOR_ONLINE_PLAY + LeaderboardWidget->ReadStatsLoginRequired(); +#else + LeaderboardWidget->ReadStats(); +#endif + MenuWidget->EnterSubMenu(); +} + +void FShooterMainMenu::OnShowDirectConnect() +{ + MenuWidget->NextMenu = DirectConnectItem->SubMenu; + MenuWidget->EnterSubMenu(); +} + +void FShooterMainMenu::OnShowOnlineStore() +{ + MenuWidget->NextMenu = OnlineStoreItem->SubMenu; +#if LOGIN_REQUIRED_FOR_ONLINE_PLAY + UE_LOG(LogOnline, Warning, TEXT("You need to be logged in before using the store")); +#endif + OnlineStoreWidget->BeginGettingOffers(); + MenuWidget->EnterSubMenu(); +} + +void FShooterMainMenu::OnShowDemoBrowser() +{ + MenuWidget->NextMenu = DemoBrowserItem->SubMenu; + DemoListWidget->BuildDemoList(); + MenuWidget->EnterSubMenu(); +} + +void FShooterMainMenu::OnUIQuit() +{ + bIsQuitting = true; + LockAndHideMenu(); + + const FShooterMenuSoundsStyle& MenuSounds = FShooterStyle::Get().GetWidgetStyle("DefaultShooterMenuSoundsStyle"); + + if (GetTickableGameObjectWorld() != NULL && GetPlayerOwnerControllerId() != -1) + { + FSlateApplication::Get().PlaySound(MenuSounds.ExitGameSound, GetPlayerOwnerControllerId()); + MenuHelper::PlaySoundAndCall(GetTickableGameObjectWorld(), MenuSounds.ExitGameSound, GetPlayerOwnerControllerId(), this, &FShooterMainMenu::Quit); + } +} + +void FShooterMainMenu::Quit() +{ + if (ensure(GameInstance.IsValid())) + { + UGameViewportClient* const Viewport = GameInstance->GetGameViewportClient(); + if (ensure(Viewport)) + { + Viewport->ConsoleCommand("quit"); + } + } +} + +void FShooterMainMenu::LockAndHideMenu() +{ + MenuWidget->LockControls(true); + MenuWidget->HideMenu(); +} + +void FShooterMainMenu::DisplayLoadingScreen() +{ + IShooterGameLoadingScreenModule* LoadingScreenModule = FModuleManager::LoadModulePtr("ShooterGameLoadingScreen"); + if( LoadingScreenModule != NULL ) + { + LoadingScreenModule->StartInGameLoadingScreen(); + } +} + +bool FShooterMainMenu::IsMapReady() const +{ + bool bReady = true; + IPlatformChunkInstall* ChunkInstaller = FPlatformMisc::GetPlatformChunkInstall(); + if (ChunkInstaller) + { + EMap SelectedMap = GetSelectedMap(); + // should use the AssetRegistry as soon as maps are added to the AssetRegistry + int32 MapChunk = ChunkMapping[(int)SelectedMap]; + EChunkLocation::Type ChunkLocation = ChunkInstaller->GetPakchunkLocation(MapChunk); + if (ChunkLocation == EChunkLocation::NotAvailable) + { + bReady = false; + } + } + return bReady; +} + +UShooterPersistentUser* FShooterMainMenu::GetPersistentUser() const +{ + UShooterLocalPlayer* const ShooterLocalPlayer = Cast(GetPlayerOwner()); + return ShooterLocalPlayer ? ShooterLocalPlayer->GetPersistentUser() : nullptr; +} + +UWorld* FShooterMainMenu::GetTickableGameObjectWorld() const +{ + ULocalPlayer* LocalPlayerOwner = GetPlayerOwner(); + return (LocalPlayerOwner ? LocalPlayerOwner->GetWorld() : nullptr); +} + +ULocalPlayer* FShooterMainMenu::GetPlayerOwner() const +{ + return PlayerOwner.Get(); +} + +int32 FShooterMainMenu::GetPlayerOwnerControllerId() const +{ + return ( PlayerOwner.IsValid() ) ? PlayerOwner->GetControllerId() : -1; +} + +FString FShooterMainMenu::GetMapName() const +{ + return MapNames[(int)GetSelectedMap()]; +} + +void FShooterMainMenu::OnCancelMatchmakingComplete(FName SessionName, bool bWasSuccessful) +{ + IOnlineSessionPtr Sessions = Online::GetSessionInterface(GetTickableGameObjectWorld()); + if (Sessions.IsValid()) + { + Sessions->ClearOnCancelMatchmakingCompleteDelegate_Handle(OnCancelMatchmakingCompleteDelegateHandle); + } + + bAnimateQuickmatchSearchingUI = false; + UGameViewportClient* const GVC = GEngine->GameViewport; + GVC->RemoveViewportWidgetContent(QuickMatchStoppingWidgetContainer.ToSharedRef()); + AddMenuToGameViewport(); + FSlateApplication::Get().SetKeyboardFocus(MenuWidget); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterMainMenu.h b/Source/ShooterGame/Private/UI/Menu/ShooterMainMenu.h new file mode 100644 index 0000000..bed9958 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterMainMenu.h @@ -0,0 +1,368 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "Widgets/ShooterMenuItem.h" +#include "Widgets/SShooterMenuWidget.h" +#include "Widgets/SShooterServerList.h" +#include "Widgets/SShooterDemoList.h" +#include "Widgets/SShooterDirectConnect.h" +#include "Widgets/SShooterLeaderboard.h" +#include "Widgets/SShooterOnlineStore.h" +#include "Widgets/SShooterSplitScreenLobbyWidget.h" +#include "ShooterOptions.h" + + +class FShooterMainMenu : public TSharedFromThis, public FTickableGameObject +{ +public: + explicit FShooterMainMenu(FString& MatchmakerEndpoint); + + virtual ~FShooterMainMenu(); + + /** build menu */ + void Construct(TWeakObjectPtr _GameInstance, TWeakObjectPtr _PlayerOwner); + + /** Add the menu to the gameviewport so it becomes visible */ + void AddMenuToGameViewport(); + + /** Remove from the gameviewport. */ + void RemoveMenuFromGameViewport(); + + /** TickableObject Functions */ + virtual void Tick(float DeltaTime) override; + virtual ETickableTickType GetTickableTickType() const override { return ETickableTickType::Always; } + virtual TStatId GetStatId() const override; + virtual bool IsTickableWhenPaused() const override { return true; } + virtual UWorld* GetTickableGameObjectWorld() const override; + + /** Returns the player that owns the main menu. */ + ULocalPlayer* GetPlayerOwner() const; + + /** Returns the controller id of player that owns the main menu. */ + int32 GetPlayerOwnerControllerId() const; + + /** Returns the string name of the currently selected map */ + FString GetMapName() const; + +protected: + + enum class EMap + { + ESancturary, + EHighRise, + EMax, + }; + + enum class EMatchType + { + Custom, + Quick + }; + + /** Owning game instance */ + TWeakObjectPtr GameInstance; + + /** Owning player */ + TWeakObjectPtr PlayerOwner; + + /** shooter options */ + TSharedPtr ShooterOptions; + + /** menu widget */ + TSharedPtr MenuWidget; + + /* used for removing the MenuWidget */ + TSharedPtr MenuWidgetContainer; + + /** SplitScreen Lobby Widget */ + TSharedPtr SplitScreenLobbyWidget; + + /* used for removing the SplitScreenLobby */ + TSharedPtr SplitScreenLobbyWidgetContainer; + + /** server list widget */ + TSharedPtr ServerListWidget; + + /** demo list widget */ + TSharedPtr DemoListWidget; + + /** leaderboard widget */ + TSharedPtr LeaderboardWidget; + + /** online store widget */ + TSharedPtr OnlineStoreWidget; + + /** The edit text widget. */ + TSharedPtr DirectConnectWidget; + + /** custom menu */ + TSharedPtr JoinServerItem; + + /** yet another custom menu */ + TSharedPtr LeaderboardItem; + + /** yet another custom menu */ + TSharedPtr OnlineStoreItem; + + /** custom menu item for direct connecting */ + TSharedPtr DirectConnectItem; + + /** Custom demo browser menu */ + TSharedPtr DemoBrowserItem; + + /** LAN Options */ + TSharedPtr HostLANItem; + TSharedPtr JoinLANItem; + + /** Dedicated Server Option */ + TSharedPtr DedicatedItem; + + /** Record demo option */ + TSharedPtr RecordDemoItem; + + /** Settings and storage for quickmatch searching */ + TSharedPtr QuickMatchSearchSettings; + + /** Map selection widget */ + TSharedPtr HostOfflineMapOption; + TSharedPtr HostOnlineMapOption; + TSharedPtr JoinMapOption; + + /** Host an onine session menu */ + TSharedPtr HostOnlineMenuItem; + + /** Track if we are showing a map download pct or not. */ + bool bShowingDownloadPct; + + /** Custom match or quick match */ + EMatchType MatchType; + + EMap GetSelectedMap() const; + + /** goes back in menu structure */ + void CloseSubMenu(); + + /** called when going back to previous menu */ + void OnMenuGoBack(MenuPtr Menu); + + /** called when menu hide animation is finished */ + void OnMenuHidden(); + + /** called when user chooses to start matchmaking. */ + void OnQuickMatchSelected(); + + /** called when user chooses to start matchmaking, but a login is required first. */ + void OnQuickMatchSelectedLoginRequired(); + + /** Called when user chooses split screen for the "host online" mode. Does some validation before moving on the split screen menu widget. */ + void OnSplitScreenSelectedHostOnlineLoginRequired(); + + /** Called when user chooses split screen for the "host online" mode.*/ + void OnSplitScreenSelectedHostOnline(); + + /** called when user chooses split screen. Goes to the split screen setup screen. Hides menu widget*/ + void OnSplitScreenSelected(); + + /** Called whne user selects "HOST ONLINE" */ + void OnHostOnlineSelected(); + + /** Called whne user selects "HOST OFFLINE" */ + void OnHostOfflineSelected(); + + /** called when users back out of split screen lobby screen. Shows main menu again. */ + void SplitScreenBackedOut(); + + FReply OnSplitScreenBackedOut(); + + FReply OnSplitScreenPlay(); + + void OnMatchmakingComplete(FName SessionName, const FOnlineError& ErrorDetails, const FSessionMatchmakingResults& Results); + + /** bot count option changed callback */ + void BotCountOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** lan match option changed callback */ + void LanMatchChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** dedicated server option changed callback */ + void DedicatedServerChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** record demo option changed callback */ + void RecordDemoChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** Plays StartGameSound sound and calls HostFreeForAll after sound is played */ + void OnUIHostFreeForAll(); + + /** Plays StartGameSound sound and calls HostTeamDeathMatch after sound is played */ + void OnUIHostTeamDeathMatch(); + + /** Hosts a game, using the passed in game type */ + void HostGame(const FString& GameType); + + /** Hosts free for all game */ + void HostFreeForAll(); + + /** Hosts team deathmatch game */ + void HostTeamDeathMatch(); + + /** General ok/cancel handler, that simply closes the dialog */ + FReply OnConfirm(); + + /** Returns true if owning player is online. Displays proper messaging if the user can't play */ + bool ValidatePlayerForOnlinePlay(ULocalPlayer* LocalPlayer); + + /** Returns true if owning player is signed in to an account. Displays proper messaging if the user can't play */ + bool ValidatePlayerIsSignedIn(ULocalPlayer* LocalPlayer); + + /** Called when the join menu option is chosen */ + void OnJoinSelected(); + + /** Join server, but login necessary first. */ + void OnJoinServerLoginRequired(); + + /** Join server */ + void OnJoinServer(); + + /** When the 'Join' button is clicked, fetch a sever from the matchmaker and connect to it */ + void OnJoinClicked(); + + /** Show leaderboard */ + void OnShowLeaderboard(); + + /** Show direct connect menu */ + void OnShowDirectConnect(); + + /** Show online store */ + void OnShowOnlineStore(); + + /** Show demo browser */ + void OnShowDemoBrowser(); + + /** Plays sound and calls Quit */ + void OnUIQuit(); + + /** Quits the game */ + void Quit(); + + /** Lock the controls and hide the main menu */ + void LockAndHideMenu(); + + /** Display the loading screen. */ + void DisplayLoadingScreen(); + + /** Begins searching for a quick match (matchmaking) */ + void BeginQuickMatchSearch(); + + /** Checks the ChunkInstaller to see if the selected map is ready for play */ + bool IsMapReady() const; + + /** Callback for when game is created */ + void OnGameCreated(bool bWasSuccessful); + + /** Displays the UI for when a quickmatch can not be found */ + void DisplayQuickmatchFailureUI(); + + /** Displays the UI for when a quickmatch is being searched for */ + void DisplayQuickmatchSearchingUI(); + + /** Get the persistence user associated with PCOwner*/ + UShooterPersistentUser* GetPersistentUser() const; + + /** Start the check for whether the owner of the menu has online privileges */ + void StartOnlinePrivilegeTask(const IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate& Delegate); + + /** Common cleanup code for any Privilege task delegate */ + void CleanupOnlinePrivilegeTask(); + + /** Delegate function executed after checking privileges for hosting an online game */ + void OnUserCanPlayHostOnline(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults); + + /** Delegate function executed after checking privileges for joining an online game */ + void OnUserCanPlayOnlineJoin(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults); + + /** Delegate function executed after checking privileges for starting quick match */ + void OnUserCanPlayOnlineQuickMatch(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults); + + // Generic confirmation handling (just hide the dialog) + FReply OnConfirmGeneric(); + + /** Delegate function executed when the quick match async cancel operation is complete */ + void OnCancelMatchmakingComplete(FName SessionName, bool bWasSuccessful); + + /** Delegate function executed when login completes before an online match is created */ + void OnLoginCompleteHostOnline(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error); + + /** Delegate function executed when login completes before an online match is joined */ + void OnLoginCompleteJoin(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error); + + /** Delegate function executed when login completes before quickmatch is started */ + void OnLoginCompleteQuickmatch(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error); + + /** Matchmaker endpoint to fetch server addresses from */ + FString MatchmakerEndpoint; + + /** Delegate for canceling matchmaking */ + FOnCancelMatchmakingCompleteDelegate OnCancelMatchmakingCompleteDelegate; + + /** number of bots in game */ + int32 BotsCountOpt; + + /** Length that the UI for searching for a quickmatch should animate */ + float QuickMAnimTimer; + + /** This is kind of hacky, but it's the simplest solution since we're out of time. + JoinSession was moved to an async event in the PS4 OSS and isn't called immediately + so we need to wait till it's triggered and then remove it */ + bool bRemoveSessionThatWeJustJoined; + + /** Custom animation var that is used to determine whether or not to inc or dec the alpha value of the quickmatch UI*/ + bool bIncQuickMAlpha; + + /** lan game? */ + bool bIsLanMatch; + + /** Recording demos? */ + bool bIsRecordingDemo; + + /** Are we currently animating the Searching for a QuickMatch UI? */ + bool bAnimateQuickmatchSearchingUI; + + /** Was the search request for quickmatch canceled while searching? */ + bool bQuickmatchSearchRequestCanceled; + + /** Was input used to cancel the search request for quickmatch? */ + bool bUsedInputToCancelQuickmatchSearch; + + /** Dedicated server? */ + bool bIsDedicatedServer; + + /** Quitting */ + bool bIsQuitting; + + /** used for displaying the quickmatch confirmation dialog when a quickmatch to join is not found */ + TSharedPtr QuickMatchFailureWidget; + + /** used for managing the QuickMatchFailureWidget */ + TSharedPtr QuickMatchFailureWidgetContainer; + + /** used for displaying UI for when we are actively searching for a quickmatch */ + TSharedPtr QuickMatchSearchingWidget; + + /* used for managing the QuickMatchSearchingWidget */ + TSharedPtr QuickMatchSearchingWidgetContainer; + + /* used for managing the QuickMatchStoppingWidget */ + TSharedPtr QuickMatchStoppingWidget; + + /* used for displaying a message while we wait for quick match to stop searching */ + TSharedPtr QuickMatchStoppingWidgetContainer; + + /** Handler for cancel confirmation confirmations on the quickmatch widgets */ + FReply OnQuickMatchFailureUICancel(); + void HelperQuickMatchSearchingUICancel(bool bShouldRemoveSession); //helper for removing QuickMatch Searching UI + FReply OnQuickMatchSearchingUICancel(); + + FDelegateHandle OnCancelMatchmakingCompleteDelegateHandle; + FDelegateHandle OnLoginCompleteDelegateHandle; +}; diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterMessageMenu.cpp b/Source/ShooterGame/Private/UI/Menu/ShooterMessageMenu.cpp new file mode 100644 index 0000000..7e35a60 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterMessageMenu.cpp @@ -0,0 +1,90 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterStyle.h" +#include "SShooterConfirmationDialog.h" +#include "ShooterMessageMenu.h" +#include "ShooterGameViewportClient.h" +#include "ShooterGameInstance.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +void FShooterMessageMenu::Construct(TWeakObjectPtr InGameInstance, TWeakObjectPtr InPlayerOwner, const FText& Message, const FText& OKButtonText, const FText& CancelButtonText, const FName& InPendingNextState) +{ + GameInstance = InGameInstance; + PlayerOwner = InPlayerOwner; + PendingNextState = InPendingNextState; + + if ( ensure( GameInstance.IsValid() ) ) + { + UShooterGameViewportClient* ShooterViewport = Cast( GameInstance->GetGameViewportClient() ); + + if ( ShooterViewport ) + { + // Hide the previous dialog + ShooterViewport->HideDialog(); + + // Show the new one + ShooterViewport->ShowDialog( + PlayerOwner, + EShooterDialogType::Generic, + Message, + OKButtonText, + CancelButtonText, + FOnClicked::CreateRaw(this, &FShooterMessageMenu::OnClickedOK), + FOnClicked::CreateRaw(this, &FShooterMessageMenu::OnClickedCancel) + ); + } + } +} + +void FShooterMessageMenu::RemoveFromGameViewport() +{ + if ( ensure( GameInstance.IsValid() ) ) + { + UShooterGameViewportClient * ShooterViewport = Cast( GameInstance->GetGameViewportClient() ); + + if ( ShooterViewport ) + { + // Hide the previous dialog + ShooterViewport->HideDialog(); + } + } +} + +void FShooterMessageMenu::HideDialogAndGotoNextState() +{ + RemoveFromGameViewport(); + + if ( ensure( GameInstance.IsValid() ) ) + { + GameInstance->GotoState( PendingNextState ); + } +}; + +FReply FShooterMessageMenu::OnClickedOK() +{ + OKButtonDelegate.ExecuteIfBound(); + HideDialogAndGotoNextState(); + return FReply::Handled(); +} + +FReply FShooterMessageMenu::OnClickedCancel() +{ + CancelButtonDelegate.ExecuteIfBound(); + HideDialogAndGotoNextState(); + return FReply::Handled(); +} + +void FShooterMessageMenu::SetOKClickedDelegate(FMessageMenuButtonClicked InButtonDelegate) +{ + OKButtonDelegate = InButtonDelegate; +} + +void FShooterMessageMenu::SetCancelClickedDelegate(FMessageMenuButtonClicked InButtonDelegate) +{ + CancelButtonDelegate = InButtonDelegate; +} + + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterMessageMenu.h b/Source/ShooterGame/Private/UI/Menu/ShooterMessageMenu.h new file mode 100644 index 0000000..3036900 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterMessageMenu.h @@ -0,0 +1,51 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once +#include "SlateBasics.h" +#include "SlateExtras.h" + +DECLARE_DELEGATE(FMessageMenuButtonClicked); + +class UShooterGameInstance; + +class FShooterMessageMenu : public TSharedFromThis +{ +public: + /** build menu */ + void Construct(TWeakObjectPtr InGameInstance, TWeakObjectPtr InPlayerOwner, const FText& Message, const FText& OKButtonText, const FText& CancelButtonText, const FName& InPendingNextState); + + /** Remove from the gameviewport. */ + void RemoveFromGameViewport(); + + /** + * The delegate function for external login UI closure when a user has signed in. + * + * @param UniqueId The unique Id of the user who just signed in. + * @param ControllerIndex The controller index of the player who just signed in. + */ + void HandleLoginUIClosed(TSharedPtr UniqueId, const int ControllerIndex); + + /** Remove dialog, and go to the next state */ + void HideDialogAndGotoNextState(); + + void SetOKClickedDelegate(FMessageMenuButtonClicked InButtonDelegate); + void SetCancelClickedDelegate(FMessageMenuButtonClicked InButtonDelegate); + + +private: + + /** Owning game instance */ + TWeakObjectPtr GameInstance; + + /** Local player that will have focus of the dialog box (can be NULL) */ + TWeakObjectPtr PlayerOwner; + + /** Cache the desired next state so we can advance to that after the confirmation dialog */ + FName PendingNextState; + + /** Handler for ok confirmation. */ + FReply OnClickedOK(); + FMessageMenuButtonClicked OKButtonDelegate; + + FReply OnClickedCancel(); + FMessageMenuButtonClicked CancelButtonDelegate; +}; diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterOptions.cpp b/Source/ShooterGame/Private/UI/Menu/ShooterOptions.cpp new file mode 100644 index 0000000..32e0145 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterOptions.cpp @@ -0,0 +1,423 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterOptions.h" +#include "ShooterTypes.h" +#include "ShooterStyle.h" +#include "ShooterOptionsWidgetStyle.h" +#include "ShooterGameUserSettings.h" +#include "Player/ShooterPersistentUser.h" +#include "Player/ShooterLocalPlayer.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +void FShooterOptions::Construct(ULocalPlayer* InPlayerOwner) +{ + OptionsStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterOptionsStyle"); + + PlayerOwner = InPlayerOwner; + MinSensitivity = 1; + + TArray ResolutionList; + TArray OnOffList; + TArray SensitivityList; + TArray GammaList; + TArray LowHighList; + + FDisplayMetrics DisplayMetrics; + FSlateApplication::Get().GetInitialDisplayMetrics(DisplayMetrics); + + bool bAddedNativeResolution = false; + const FIntPoint NativeResolution(DisplayMetrics.PrimaryDisplayWidth, DisplayMetrics.PrimaryDisplayHeight); + + for (int32 i = 0; i < DefaultShooterResCount; i++) + { + if (DefaultShooterResolutions[i].X <= DisplayMetrics.PrimaryDisplayWidth && DefaultShooterResolutions[i].Y <= DisplayMetrics.PrimaryDisplayHeight) + { + ResolutionList.Add(FText::Format(FText::FromString("{0}x{1}"), FText::FromString(FString::FromInt(DefaultShooterResolutions[i].X)), FText::FromString(FString::FromInt(DefaultShooterResolutions[i].Y)))); + Resolutions.Add(DefaultShooterResolutions[i]); + + bAddedNativeResolution = bAddedNativeResolution || (DefaultShooterResolutions[i] == NativeResolution); + } + } + + // Always make sure that the native resolution is available + if (!bAddedNativeResolution) + { + ResolutionList.Add(FText::Format(FText::FromString("{0}x{1}"), FText::FromString(FString::FromInt(NativeResolution.X)), FText::FromString(FString::FromInt(NativeResolution.Y)))); + Resolutions.Add(NativeResolution); + } + + OnOffList.Add(LOCTEXT("Off","OFF")); + OnOffList.Add(LOCTEXT("On","ON")); + + LowHighList.Add(LOCTEXT("Low","LOW")); + LowHighList.Add(LOCTEXT("High","HIGH")); + + //Mouse sensitivity 0-50 + for (int32 i = 0; i < 51; i++) + { + SensitivityList.Add(FText::AsNumber(i)); + } + + for (int32 i = -50; i < 51; i++) + { + GammaList.Add(FText::AsNumber(i)); + } + + /** Options menu root item */ + TSharedPtr OptionsRoot = FShooterMenuItem::CreateRoot(); + + /** Cheats menu root item */ + TSharedPtr CheatsRoot = FShooterMenuItem::CreateRoot(); + + CheatsItem = MenuHelper::AddMenuItem(CheatsRoot,LOCTEXT("Cheats", "CHEATS")); + MenuHelper::AddMenuOptionSP(CheatsItem, LOCTEXT("InfiniteAmmo", "INFINITE AMMO"), OnOffList, this, &FShooterOptions::InfiniteAmmoOptionChanged); + MenuHelper::AddMenuOptionSP(CheatsItem, LOCTEXT("InfiniteClip", "INFINITE CLIP"), OnOffList, this, &FShooterOptions::InfiniteClipOptionChanged); + MenuHelper::AddMenuOptionSP(CheatsItem, LOCTEXT("FreezeMatchTimer", "FREEZE MATCH TIMER"), OnOffList, this, &FShooterOptions::FreezeTimerOptionChanged); + MenuHelper::AddMenuOptionSP(CheatsItem, LOCTEXT("HealthRegen", "HP REGENERATION"), OnOffList, this, &FShooterOptions::HealthRegenOptionChanged); + + OptionsItem = MenuHelper::AddMenuItem(OptionsRoot,LOCTEXT("Options", "OPTIONS")); +#if PLATFORM_DESKTOP + VideoResolutionOption = MenuHelper::AddMenuOptionSP(OptionsItem,LOCTEXT("Resolution", "RESOLUTION"), ResolutionList, this, &FShooterOptions::VideoResolutionOptionChanged); + GraphicsQualityOption = MenuHelper::AddMenuOptionSP(OptionsItem,LOCTEXT("Quality", "QUALITY"),LowHighList, this, &FShooterOptions::GraphicsQualityOptionChanged); + FullScreenOption = MenuHelper::AddMenuOptionSP(OptionsItem,LOCTEXT("FullScreen", "FULL SCREEN"),OnOffList, this, &FShooterOptions::FullScreenOptionChanged); +#endif + GammaOption = MenuHelper::AddMenuOptionSP(OptionsItem,LOCTEXT("Gamma", "GAMMA CORRECTION"),GammaList, this, &FShooterOptions::GammaOptionChanged); + AimSensitivityOption = MenuHelper::AddMenuOptionSP(OptionsItem,LOCTEXT("AimSensitivity", "AIM SENSITIVITY"),SensitivityList, this, &FShooterOptions::AimSensitivityOptionChanged); + InvertYAxisOption = MenuHelper::AddMenuOptionSP(OptionsItem,LOCTEXT("InvertYAxis", "INVERT Y AXIS"),OnOffList, this, &FShooterOptions::InvertYAxisOptionChanged); + VibrationOption = MenuHelper::AddMenuOptionSP(OptionsItem, LOCTEXT("Vibration", "VIBRATION"), OnOffList, this, &FShooterOptions::ToggleVibration); + + MenuHelper::AddMenuItemSP(OptionsItem,LOCTEXT("ApplyChanges", "APPLY CHANGES"), this, &FShooterOptions::OnApplySettings); + + //Do not allow to set aim sensitivity to 0 + AimSensitivityOption->MinMultiChoiceIndex = MinSensitivity; + + //Default vibration to On. + VibrationOption->SelectedMultiChoice = 1; + + UserSettings = CastChecked(GEngine->GetGameUserSettings()); + bFullScreenOpt = UserSettings->GetFullscreenMode(); + GraphicsQualityOpt = UserSettings->GetGraphicsQuality(); + + if (UserSettings->IsForceSystemResolution()) + { + // store the current system resolution + ResolutionOpt = FIntPoint(GSystemResolution.ResX, GSystemResolution.ResY); + } + else + { + ResolutionOpt = UserSettings->GetScreenResolution(); + } + + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + bInvertYAxisOpt = PersistentUser->GetInvertedYAxis(); + SensitivityOpt = PersistentUser->GetAimSensitivity(); + GammaOpt = PersistentUser->GetGamma(); + bVibrationOpt = PersistentUser->GetVibration(); + } + else + { + bVibrationOpt = true; + bInvertYAxisOpt = false; + SensitivityOpt = 1.0f; + GammaOpt = 2.2f; + } + + if (ensure(PlayerOwner != nullptr)) + { + APlayerController* BaseController = Cast(UGameplayStatics::GetPlayerControllerFromID(PlayerOwner->GetWorld(), PlayerOwner->GetControllerId())); + ensure(BaseController); + if (BaseController) + { + AShooterPlayerController* ShooterPlayerController = Cast(BaseController); + if (ShooterPlayerController) + { + ShooterPlayerController->SetIsVibrationEnabled(bVibrationOpt); + } + else + { + // We are in the menus and therefore don't need to do anything as the controller is different + // and can't store the vibration setting. + } + } + } +} + +void FShooterOptions::OnApplySettings() +{ + FSlateApplication::Get().PlaySound(OptionsStyle->AcceptChangesSound, GetOwnerUserIndex()); + ApplySettings(); +} + +void FShooterOptions::ApplySettings() +{ + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + PersistentUser->SetAimSensitivity(SensitivityOpt); + PersistentUser->SetInvertedYAxis(bInvertYAxisOpt); + PersistentUser->SetGamma(GammaOpt); + PersistentUser->SetVibration(bVibrationOpt); + PersistentUser->TellInputAboutKeybindings(); + + PersistentUser->SaveIfDirty(); + } + + if (UserSettings->IsForceSystemResolution()) + { + // store the current system resolution + ResolutionOpt = FIntPoint(GSystemResolution.ResX, GSystemResolution.ResY); + } + UserSettings->SetScreenResolution(ResolutionOpt); + UserSettings->SetFullscreenMode(bFullScreenOpt); + UserSettings->SetGraphicsQuality(GraphicsQualityOpt); + UserSettings->ApplySettings(false); + + OnApplyChanges.ExecuteIfBound(); +} + +void FShooterOptions::TellInputAboutKeybindings() +{ + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + PersistentUser->TellInputAboutKeybindings(); + } +} + +void FShooterOptions::RevertChanges() +{ + FSlateApplication::Get().PlaySound(OptionsStyle->DiscardChangesSound, GetOwnerUserIndex()); + UpdateOptions(); + GEngine->DisplayGamma = 2.2f + 2.0f * (-0.5f + GammaOption->SelectedMultiChoice / 100.0f); +} + +int32 FShooterOptions::GetCurrentResolutionIndex(FIntPoint CurrentRes) +{ + int32 Result = 0; // return first valid resolution if match not found + for (int32 i = 0; i < Resolutions.Num(); i++) + { + if (Resolutions[i] == CurrentRes) + { + Result = i; + break; + } + } + return Result; +} + +int32 FShooterOptions::GetCurrentMouseYAxisInvertedIndex() +{ + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + return InvertYAxisOption->SelectedMultiChoice = PersistentUser->GetInvertedYAxis() ? 1 : 0; + } + else + { + return 0; + } +} + +int32 FShooterOptions::GetCurrentMouseSensitivityIndex() +{ + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + //mouse sensitivity is a floating point value ranged from 0.0f to 1.0f + int32 IntSensitivity = FMath::RoundToInt((PersistentUser->GetAimSensitivity() - 0.5f) * 10.0f); + //Clamp to valid index range + return FMath::Clamp(IntSensitivity, MinSensitivity, 100); + } + + return FMath::RoundToInt((1.0f - 0.5f) * 10.0f); +} + +int32 FShooterOptions::GetCurrentGammaIndex() +{ + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + //reverse gamma calculation + int32 GammaIndex = FMath::TruncToInt(((PersistentUser->GetGamma() - 2.2f) / 2.0f + 0.5f) * 100); + //Clamp to valid index range + return FMath::Clamp(GammaIndex, 0, 100); + } + + return FMath::TruncToInt(((2.2f - 2.2f) / 2.0f + 0.5f) * 100); +} + +int32 FShooterOptions::GetOwnerUserIndex() const +{ + return PlayerOwner ? PlayerOwner->GetControllerId() : 0; +} + +UShooterPersistentUser* FShooterOptions::GetPersistentUser() const +{ + UShooterLocalPlayer* const SLP = Cast(PlayerOwner); + if (SLP) + { + return SLP->GetPersistentUser(); + } + + return nullptr; +} + +void FShooterOptions::UpdateOptions() +{ +#if UE_BUILD_SHIPPING + CheatsItem->bVisible = false; +#else + //Toggle Cheat menu visibility depending if we are client or server + UWorld* const World = PlayerOwner->GetWorld(); + if (World && World->GetNetMode() == NM_Client) + { + CheatsItem->bVisible = false; + } + else + { + CheatsItem->bVisible = true; + } +#endif + + //grab the user settings + UShooterPersistentUser* const PersistentUser = GetPersistentUser(); + if (PersistentUser) + { + // Update bInvertYAxisOpt, SensitivityOpt and GammaOpt because the ShooterOptions can be created without the controller having a player + // by the in-game menu which will leave them with default values + bInvertYAxisOpt = PersistentUser->GetInvertedYAxis(); + SensitivityOpt = PersistentUser->GetAimSensitivity(); + GammaOpt = PersistentUser->GetGamma(); + bVibrationOpt = PersistentUser->GetVibration(); + } + + InvertYAxisOption->SelectedMultiChoice = GetCurrentMouseYAxisInvertedIndex(); + AimSensitivityOption->SelectedMultiChoice = GetCurrentMouseSensitivityIndex(); + GammaOption->SelectedMultiChoice = GetCurrentGammaIndex(); + VibrationOption->SelectedMultiChoice = bVibrationOpt ? 1 : 0; + + GammaOptionChanged(GammaOption, GammaOption->SelectedMultiChoice); +#if PLATFORM_DESKTOP + VideoResolutionOption->SelectedMultiChoice = GetCurrentResolutionIndex(UserSettings->GetScreenResolution()); + GraphicsQualityOption->SelectedMultiChoice = UserSettings->GetGraphicsQuality(); + FullScreenOption->SelectedMultiChoice = UserSettings->GetFullscreenMode() != EWindowMode::Windowed ? 1 : 0; +#endif +} + +void FShooterOptions::InfiniteAmmoOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + UWorld* const World = PlayerOwner->GetWorld(); + if (World) + { + for (FConstPlayerControllerIterator It = World->GetPlayerControllerIterator(); It; ++It) + { + AShooterPlayerController* ShooterPC = Cast(*It); + if (ShooterPC) + { + ShooterPC->SetInfiniteAmmo(MultiOptionIndex > 0 ? true : false); + } + } + } +} + +void FShooterOptions::InfiniteClipOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + UWorld* const World = PlayerOwner->GetWorld(); + if (World) + { + for (FConstPlayerControllerIterator It = World->GetPlayerControllerIterator(); It; ++It) + { + AShooterPlayerController* const ShooterPC = Cast(*It); + if (ShooterPC) + { + ShooterPC->SetInfiniteClip(MultiOptionIndex > 0 ? true : false); + } + } + } +} + +void FShooterOptions::FreezeTimerOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + UWorld* const World = PlayerOwner->GetWorld(); + AShooterGameState* const GameState = World ? World->GetGameState() : nullptr; + if (GameState) + { + GameState->bTimerPaused = MultiOptionIndex > 0 ? true : false; + } +} + + +void FShooterOptions::HealthRegenOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + UWorld* const World = PlayerOwner->GetWorld(); + if (World) + { + for (FConstPlayerControllerIterator It = World->GetPlayerControllerIterator(); It; ++It) + { + AShooterPlayerController* const ShooterPC = Cast(*It); + if (ShooterPC) + { + ShooterPC->SetHealthRegen(MultiOptionIndex > 0 ? true : false); + } + } + } +} + +void FShooterOptions::VideoResolutionOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + ResolutionOpt = Resolutions[MultiOptionIndex]; +} + +void FShooterOptions::GraphicsQualityOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + GraphicsQualityOpt = MultiOptionIndex; +} + +void FShooterOptions::FullScreenOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + static auto CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.FullScreenMode")); + EWindowMode::Type FullScreenMode = CVar->GetValueOnGameThread() == 1 ? EWindowMode::WindowedFullscreen : EWindowMode::Fullscreen; + bFullScreenOpt = MultiOptionIndex == 0 ? EWindowMode::Windowed : FullScreenMode; +} + +void FShooterOptions::AimSensitivityOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + SensitivityOpt = 0.5f + (MultiOptionIndex / 10.0f); +} + +void FShooterOptions::GammaOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + GammaOpt = 2.2f + 2.0f * (-0.5f + MultiOptionIndex / 100.0f); + GEngine->DisplayGamma = GammaOpt; +} + +void FShooterOptions::ToggleVibration(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + bVibrationOpt = MultiOptionIndex > 0 ? true : false; + APlayerController* BaseController = Cast(UGameplayStatics::GetPlayerController(PlayerOwner->GetWorld(), GetOwnerUserIndex())); + AShooterPlayerController* ShooterPlayerController = Cast(UGameplayStatics::GetPlayerController(PlayerOwner->GetWorld(), GetOwnerUserIndex())); + ensure(BaseController); + if(BaseController) + { + if (ShooterPlayerController) + { + ShooterPlayerController->SetIsVibrationEnabled(bVibrationOpt); + } + else + { + // We are in the menus and therefore don't need to do anything as the controller is different + // and can't store the vibration setting. + } + } +} + +void FShooterOptions::InvertYAxisOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex) +{ + bInvertYAxisOpt = MultiOptionIndex > 0 ? true : false; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterOptions.h b/Source/ShooterGame/Private/UI/Menu/ShooterOptions.h new file mode 100644 index 0000000..6f6ff0d --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterOptions.h @@ -0,0 +1,157 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "Widgets/ShooterMenuItem.h" +#include "Widgets/SShooterMenuWidget.h" + +/** supported resolutions */ +const FIntPoint DefaultShooterResolutions[] = { FIntPoint(1024,768), FIntPoint(1280,720), FIntPoint(1920,1080) }; + +/** supported resolutions count*/ +const int32 DefaultShooterResCount = UE_ARRAY_COUNT(DefaultShooterResolutions); + +/** delegate called when changes are applied */ +DECLARE_DELEGATE(FOnApplyChanges); + +class UShooterGameUserSettings; + +class FShooterOptions : public TSharedFromThis +{ +public: + /** sets owning player controller */ + void Construct(ULocalPlayer* InPlayerOwner); + + /** get current options values for display */ + void UpdateOptions(); + + /** UI callback for applying settings, plays sound */ + void OnApplySettings(); + + /** applies changes in game settings */ + void ApplySettings(); + + /** needed because we can recreate the subsystem that stores it */ + void TellInputAboutKeybindings(); + + /** reverts non-saved changes in game settings */ + void RevertChanges(); + + /** holds options menu item */ + TSharedPtr OptionsItem; + + /** holds cheats menu item */ + TSharedPtr CheatsItem; + + /** called when changes were applied - can be used to close submenu */ + FOnApplyChanges OnApplyChanges; + +protected: + /** User settings pointer */ + UShooterGameUserSettings* UserSettings; + + /** video resolution option changed handler */ + void VideoResolutionOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** graphics quality option changed handler */ + void GraphicsQualityOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** infinite ammo option changed handler */ + void InfiniteAmmoOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** infinite clip option changed handler */ + void InfiniteClipOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** freeze timer option changed handler */ + void FreezeTimerOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** health regen option changed handler */ + void HealthRegenOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** aim sensitivity option changed handler */ + void AimSensitivityOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** controller vibration toggle handler */ + void ToggleVibration(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** invert y axis option changed handler */ + void InvertYAxisOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** full screen option changed handler */ + void FullScreenOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** gamma correction option changed handler */ + void GammaOptionChanged(TSharedPtr MenuItem, int32 MultiOptionIndex); + + /** try to match current resolution with selected index */ + int32 GetCurrentResolutionIndex(FIntPoint CurrentRes); + + /** Get current mouse y-axis inverted option index*/ + int32 GetCurrentMouseYAxisInvertedIndex(); + + /** get current mouse sensitivity option index */ + int32 GetCurrentMouseSensitivityIndex(); + + /** get current gamma index */ + int32 GetCurrentGammaIndex(); + + /** get current user index out of PlayerOwner */ + int32 GetOwnerUserIndex() const; + + /** Get the persistence user associated with PlayerOwner*/ + UShooterPersistentUser* GetPersistentUser() const; + + /** Owning player controller */ + ULocalPlayer* PlayerOwner; + + /** holds vibration option menu item */ + TSharedPtr VibrationOption; + + /** holds invert y axis option menu item */ + TSharedPtr InvertYAxisOption; + + /** holds aim sensitivity option menu item */ + TSharedPtr AimSensitivityOption; + + /** holds mouse sensitivity option menu item */ + TSharedPtr VideoResolutionOption; + + /** holds graphics quality option menu item */ + TSharedPtr GraphicsQualityOption; + + /** holds gamma correction option menu item */ + TSharedPtr GammaOption; + + /** holds full screen option menu item */ + TSharedPtr FullScreenOption; + + /** graphics quality option */ + int32 GraphicsQualityOpt; + + /** minimum sensitivity index */ + int32 MinSensitivity; + + /** current sensitivity set in options */ + float SensitivityOpt; + + /** current gamma correction set in options */ + float GammaOpt; + + /** full screen setting set in options */ + EWindowMode::Type bFullScreenOpt; + + /** controller vibration setting set in options */ + uint8 bVibrationOpt : 1; + + /** invert mouse setting set in options */ + uint8 bInvertYAxisOpt : 1; + + /** resolution setting set in options */ + FIntPoint ResolutionOpt; + + /** available resolutions list */ + TArray Resolutions; + + /** style used for the shooter options */ + const struct FShooterOptionsStyle *OptionsStyle; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterRecentlyMet.cpp b/Source/ShooterGame/Private/UI/Menu/ShooterRecentlyMet.cpp new file mode 100644 index 0000000..6ef6f27 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterRecentlyMet.cpp @@ -0,0 +1,154 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterRecentlyMet.h" +#include "ShooterTypes.h" +#include "ShooterStyle.h" +#include "ShooterOptionsWidgetStyle.h" +#include "ShooterGameUserSettings.h" +#include "ShooterPersistentUser.h" +#include "Player/ShooterLocalPlayer.h" +#include "OnlineSubsystemUtils.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +void FShooterRecentlyMet::Construct(ULocalPlayer* _PlayerOwner, int32 LocalUserNum_) +{ + RecentlyMetStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterOptionsStyle"); + + PlayerOwner = _PlayerOwner; + LocalUserNum = LocalUserNum_; + CurrRecentlyMetIndex = 0; + MinRecentlyMetIndex = 0; + MaxRecentlyMetIndex = 0; //initialized after the recently met list is (read in/set) + + /** Recently Met menu items */ + RecentlyMetRoot = FShooterMenuItem::CreateRoot(); + RecentlyMetItem = MenuHelper::AddMenuItem(RecentlyMetRoot, LOCTEXT("Recently Met", "RECENTLY MET")); + + /** Init online items */ + if (PlayerOwner) + { + OnlineSub = Online::GetSubsystem(PlayerOwner->GetWorld()); + } + + UserSettings = CastChecked(GEngine->GetGameUserSettings()); +} + +void FShooterRecentlyMet::OnApplySettings() +{ + ApplySettings(); +} + +void FShooterRecentlyMet::ApplySettings() +{ + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + PersistentUser->TellInputAboutKeybindings(); + + PersistentUser->SaveIfDirty(); + } + + UserSettings->ApplySettings(false); + + OnApplyChanges.ExecuteIfBound(); +} + +void FShooterRecentlyMet::TellInputAboutKeybindings() +{ + UShooterPersistentUser* PersistentUser = GetPersistentUser(); + if(PersistentUser) + { + PersistentUser->TellInputAboutKeybindings(); + } +} + +UShooterPersistentUser* FShooterRecentlyMet::GetPersistentUser() const +{ + UShooterLocalPlayer* const SLP = Cast(PlayerOwner); + return SLP ? SLP->GetPersistentUser() : nullptr; +} + +void FShooterRecentlyMet::UpdateRecentlyMet(int32 NewOwnerIndex) +{ + LocalUserNum = NewOwnerIndex; + + if (OnlineSub) + { + IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if (Identity.IsValid()) + { + LocalUsername = Identity->GetPlayerNickname(LocalUserNum); + } + } + + MenuHelper::ClearSubMenu(RecentlyMetItem); + MaxRecentlyMetIndex = 0; + + AShooterGameState* const MyGameState = PlayerOwner->GetWorld()->GetGameState(); + if (MyGameState != nullptr) + { + MetPlayerArray = MyGameState->PlayerArray; + + for (int32 i = 0; i < MetPlayerArray.Num(); ++i) + { + const APlayerState* PlayerState = MetPlayerArray[i]; + FString Username = PlayerState->GetHumanReadableName(); + if (Username != LocalUsername && PlayerState->IsABot() == false) + { + TSharedPtr UserItem = MenuHelper::AddMenuItem(RecentlyMetItem, FText::FromString(Username)); + UserItem->OnControllerDownInputPressed.BindRaw(this, &FShooterRecentlyMet::IncrementRecentlyMetCounter); + UserItem->OnControllerUpInputPressed.BindRaw(this, &FShooterRecentlyMet::DecrementRecentlyMetCounter); + UserItem->OnControllerFacebuttonDownPressed.BindRaw(this, &FShooterRecentlyMet::ViewSelectedUsersProfile); + } + else + { + MetPlayerArray.RemoveAt(i); + --i; //we just deleted an item, so we need to go make sure i doesn't increment again, otherwise it would skip the player that was supposed to be looked at next + } + } + + MaxRecentlyMetIndex = MetPlayerArray.Num() - 1; + } + + MenuHelper::AddMenuItemSP(RecentlyMetItem, LOCTEXT("Close", "CLOSE"), this, &FShooterRecentlyMet::OnApplySettings); +} + +void FShooterRecentlyMet::IncrementRecentlyMetCounter() +{ + if (CurrRecentlyMetIndex + 1 <= MaxRecentlyMetIndex) + { + ++CurrRecentlyMetIndex; + } +} +void FShooterRecentlyMet::DecrementRecentlyMetCounter() +{ + if (CurrRecentlyMetIndex - 1 >= MinRecentlyMetIndex) + { + --CurrRecentlyMetIndex; + } +} +void FShooterRecentlyMet::ViewSelectedUsersProfile() +{ + if (OnlineSub) + { + IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if (Identity.IsValid() && MetPlayerArray.IsValidIndex(CurrRecentlyMetIndex)) + { + const APlayerState* PlayerState = MetPlayerArray[CurrRecentlyMetIndex]; + + TSharedPtr Requestor = Identity->GetUniquePlayerId(LocalUserNum); + TSharedPtr Requestee = PlayerState->GetUniqueId().GetUniqueNetId(); + + IOnlineExternalUIPtr ExternalUI = OnlineSub->GetExternalUIInterface(); + if (ExternalUI.IsValid() && Requestor.IsValid() && Requestee.IsValid()) + { + ExternalUI->ShowProfileUI(*Requestor, *Requestee, FOnProfileUIClosedDelegate()); + } + } + } +} + + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterRecentlyMet.h b/Source/ShooterGame/Private/UI/Menu/ShooterRecentlyMet.h new file mode 100644 index 0000000..dec8cbc --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterRecentlyMet.h @@ -0,0 +1,97 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "Widgets/ShooterMenuItem.h" +#include "Widgets/SShooterMenuWidget.h" + +class UShooterGameUserSettings; +class UShooterPersistentUser; + +/** delegate called when changes are applied */ +DECLARE_DELEGATE(FOnApplyChanges); + +/** delegate called when a menu item is confirmed */ +DECLARE_DELEGATE(FOnConfirmMenuItem); + +/** delegate called down on the gamepad is pressed*/ +DECLARE_DELEGATE(FOnControllerDownInputPressed); + +/** delegate called up on the gamepad is pressed*/ +DECLARE_DELEGATE(FOnControllerUpInputPressed); + +/** delegate called when facebutton_down is pressed */ +DECLARE_DELEGATE(FOnOnControllerFacebuttonDownPressed); + +class FShooterRecentlyMet : public TSharedFromThis +{ +public: + /** sets owning player controller */ + void Construct(ULocalPlayer* _PlayerOwner, int32 LocalUserNum); + + /** get current Friends values for display */ + void UpdateRecentlyMet(int32 NewOwnerIndex); + + /** UI callback for applying settings, plays sound */ + void OnApplySettings(); + + /** applies changes in game settings */ + void ApplySettings(); + + /** needed because we can recreate the subsystem that stores it */ + void TellInputAboutKeybindings(); + + /** increment the counter keeping track of which user we're looking at */ + void IncrementRecentlyMetCounter(); + + /** decrement the counter keeping track of which user we're looking at */ + void DecrementRecentlyMetCounter(); + + /** send friend request to selected user */ + void ViewSelectedUsersProfile(); + + /** holds Recently Met sub-menu */ + TSharedPtr RecentlyMetItem; + + /** holds Recently Met menu item */ + TSharedPtr RecentlyMetRoot; + + /** called when changes were applied - can be used to close submenu */ + FOnApplyChanges OnApplyChanges; + + /** delegate, which is executed by SShooterMenuWidget if user confirms this menu item */ + FOnConfirmMenuItem OnConfirmMenuItem; + + /** delegate, which is executed by SShooterMenuWidget if down input is pressed */ + FOnControllerDownInputPressed OnControllerDownInputPressed; + + /** delegate, which is executed by SShooterMenuWidget if up input is pressed */ + FOnControllerUpInputPressed OnControllerUpInputPressed; + + /** delegate, which is executed by SShooterMenuWidget if facebutton_down is pressed */ + FOnOnControllerFacebuttonDownPressed OnControllerFacebuttonDownPressed; + + int32 LocalUserNum; + int32 CurrRecentlyMetIndex; + int32 MinRecentlyMetIndex; + int32 MaxRecentlyMetIndex; + + FString LocalUsername; + + TArray MetPlayerArray; + + IOnlineSubsystem* OnlineSub; + +protected: + /** User settings pointer */ + UShooterGameUserSettings* UserSettings; + + /** Get the persistence user associated with PCOwner*/ + UShooterPersistentUser* GetPersistentUser() const; + + /** Owning player controller */ + ULocalPlayer* PlayerOwner; + + /** style used for the shooter Friends */ + const struct FShooterOptionsStyle *RecentlyMetStyle; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterWelcomeMenu.cpp b/Source/ShooterGame/Private/UI/Menu/ShooterWelcomeMenu.cpp new file mode 100644 index 0000000..5649624 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterWelcomeMenu.cpp @@ -0,0 +1,369 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterWelcomeMenu.h" +#include "ShooterStyle.h" +#include "SShooterConfirmationDialog.h" +#include "ShooterGameViewportClient.h" +#include "ShooterGameInstance.h" +#include "OnlineSubsystemUtils.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +class SShooterWelcomeMenuWidget : public SCompoundWidget +{ + /** The menu that owns this widget. */ + FShooterWelcomeMenu* MenuOwner; + + /** Animate the text so that the screen isn't static, for console cert requirements. */ + FCurveSequence TextAnimation; + + /** The actual curve that animates the text. */ + FCurveHandle TextColorCurve; + + TSharedPtr PressPlayText; + + /* On the first tick ensure we have set the keyboard focus */ + bool bFirstTick = true; + + SLATE_BEGIN_ARGS( SShooterWelcomeMenuWidget ) + {} + + SLATE_ARGUMENT(FShooterWelcomeMenu*, MenuOwner) + + SLATE_END_ARGS() + + virtual bool SupportsKeyboardFocus() const override + { + return true; + } + + UWorld* GetWorld() const + { + if (MenuOwner && MenuOwner->GetGameInstance().IsValid()) + { + return MenuOwner->GetGameInstance()->GetWorld(); + } + + return nullptr; + } + + void Construct( const FArguments& InArgs ) + { + MenuOwner = InArgs._MenuOwner; + + TextAnimation = FCurveSequence(); + const float AnimDuration = 1.5f; + TextColorCurve = TextAnimation.AddCurve(0, AnimDuration, ECurveEaseFunction::QuadInOut); + + ChildSlot + [ + SNew(SBorder) + .Padding(30.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SAssignNew( PressPlayText, SRichTextBlock ) +#if PLATFORM_PS4 + .Text( LOCTEXT("PressStartPS4", "PRESS CROSS BUTTON TO PLAY" ) ) +#elif PLATFORM_SWITCH + .Text(LOCTEXT("PressStartSwitch", "PRESS TO PLAY")) +#elif SHOOTER_XBOX_STRINGS + .Text( LOCTEXT("PressStartXboxOne", "PRESS A TO PLAY" ) ) +#else + .Text( LOCTEXT("PressStartPC", "PRESS ENTER TO PLAY" ) ) +#endif + .TextStyle( FShooterStyle::Get(), "ShooterGame.WelcomeScreen.WelcomeTextStyle" ) + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] + ]; + } + + virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) override + { + // During construction we may miss out on setting focus of the keyboard due to the GameViewport not having its Parent pointer + // setup. If this happens we will be unable to set the keyboard focus in AddToGameViewport() + if (bFirstTick) + { + bFirstTick = false; + FSlateApplication::Get().SetKeyboardFocus(this->AsShared()); + } + + if(!TextAnimation.IsPlaying()) + { + if(TextAnimation.IsAtEnd()) + { + TextAnimation.PlayReverse(this->AsShared()); + } + else + { + TextAnimation.Play(this->AsShared()); + } + } + + PressPlayText->SetRenderOpacity(FMath::Lerp(0.5f, 1.0f, TextColorCurve.GetLerp())); + } + + virtual FReply OnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override + { + return FReply::Handled(); + } + + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override + { + const FKey Key = InKeyEvent.GetKey(); + if (Key == EKeys::Enter) + { + TSharedPtr UserId; + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + if (IdentityInterface.IsValid()) + { + UserId = IdentityInterface->GetUniquePlayerId(InKeyEvent.GetUserIndex()); + } + } + MenuOwner->HandleLoginUIClosed(UserId, InKeyEvent.GetUserIndex()); + } + else if (!MenuOwner->GetControlsLocked() && Key == EKeys::Virtual_Accept) + { + bool bSkipToMainMenu = true; + + { + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + if (IdentityInterface.IsValid()) + { + TSharedPtr GenericApplication = FSlateApplication::Get().GetPlatformApplication(); + const bool bIsLicensed = GenericApplication->ApplicationLicenseValid(); + + const ELoginStatus::Type LoginStatus = IdentityInterface->GetLoginStatus(InKeyEvent.GetUserIndex()); + if (LoginStatus == ELoginStatus::NotLoggedIn || !bIsLicensed) + { + // Show the account picker. + const IOnlineExternalUIPtr ExternalUI = OnlineSub->GetExternalUIInterface(); + if (ExternalUI.IsValid()) + { + ExternalUI->ShowLoginUI(InKeyEvent.GetUserIndex(), false, true, FOnLoginUIClosedDelegate::CreateSP(MenuOwner, &FShooterWelcomeMenu::HandleLoginUIClosed)); + bSkipToMainMenu = false; + } + } + } + } + } + + if (bSkipToMainMenu) + { + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + if (IdentityInterface.IsValid()) + { + TSharedPtr UserId = IdentityInterface->GetUniquePlayerId(InKeyEvent.GetUserIndex()); + // If we couldn't show the external login UI for any reason, or if the user is + // already logged in, just advance to the main menu immediately. + MenuOwner->HandleLoginUIClosed(UserId, InKeyEvent.GetUserIndex()); + } + } + } + + return FReply::Handled(); + } + + return FReply::Unhandled(); + } + + virtual void OnFocusLost(const FFocusEvent& InFocusEvent) override + { + bFirstTick = true; + } + + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override + { + return FReply::Handled().ReleaseMouseCapture().SetUserFocus(SharedThis( this ), EFocusCause::SetDirectly, true); + } +}; + +void FShooterWelcomeMenu::Construct( TWeakObjectPtr< UShooterGameInstance > InGameInstance ) +{ + bControlsLocked = false; + GameInstance = InGameInstance; + PendingControllerIndex = -1; + + MenuWidget = SNew( SShooterWelcomeMenuWidget ) + .MenuOwner(this); +} + +void FShooterWelcomeMenu::AddToGameViewport() +{ + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->AddViewportWidgetContent(MenuWidget.ToSharedRef()); + FSlateApplication::Get().SetKeyboardFocus(MenuWidget); + } +} + +void FShooterWelcomeMenu::RemoveFromGameViewport() +{ + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(MenuWidget.ToSharedRef()); + } +} + +void FShooterWelcomeMenu::HandleLoginUIClosed(TSharedPtr UniqueId, const int ControllerIndex, const FOnlineError& Error) +{ + if ( !ensure( GameInstance.IsValid() ) ) + { + return; + } + + UShooterGameViewportClient* ShooterViewport = Cast( GameInstance->GetGameViewportClient() ); + + TSharedPtr GenericApplication = FSlateApplication::Get().GetPlatformApplication(); + const bool bIsLicensed = GenericApplication->ApplicationLicenseValid(); + + // If they don't currently have a license, let them know, but don't let them proceed + if (!bIsLicensed && ShooterViewport != NULL) + { + const FText StopReason = NSLOCTEXT( "ProfileMessages", "NeedLicense", "The signed in users do not have a license for this game. Please purchase ShooterGame from the Xbox Marketplace or sign in a user with a valid license." ); + const FText OKButton = NSLOCTEXT( "DialogButtons", "OKAY", "OK" ); + + ShooterViewport->ShowDialog( + nullptr, + EShooterDialogType::Generic, + StopReason, + OKButton, + FText::GetEmpty(), + FOnClicked::CreateRaw(this, &FShooterWelcomeMenu::OnConfirmGeneric), + FOnClicked::CreateRaw(this, &FShooterWelcomeMenu::OnConfirmGeneric) + ); + return; + } + + PendingControllerIndex = ControllerIndex; + + if (UniqueId.IsValid()) + { + // Next step, check privileges + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GameInstance->GetWorld()); + if (OnlineSub) + { + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + if (IdentityInterface.IsValid()) + { + IdentityInterface->GetUserPrivilege(*UniqueId, EUserPrivileges::CanPlay, IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateSP(this, &FShooterWelcomeMenu::OnUserCanPlay)); + } + } + } + else + { + // Show a warning that your progress won't be saved if you continue without logging in. + if (ShooterViewport != NULL) + { + ShooterViewport->ShowDialog( + nullptr, + EShooterDialogType::Generic, + NSLOCTEXT("ProfileMessages", "ProgressWillNotBeSaved", "If you continue without signing in, your progress will not be saved."), +#if SHOOTER_XBOX_STRINGS + NSLOCTEXT("DialogButtons", "AContinue", "A - Continue"), + NSLOCTEXT("DialogButtons", "BBack", "B - Back"), +#else + NSLOCTEXT("DialogButtons", "EnterContinue", "Enter - Continue"), + NSLOCTEXT("DialogButtons", "EscBack", "Esc - Back"), +#endif + FOnClicked::CreateRaw(this, &FShooterWelcomeMenu::OnContinueWithoutSavingConfirm), + FOnClicked::CreateRaw(this, &FShooterWelcomeMenu::OnConfirmGeneric) + ); + } + } +} + +void FShooterWelcomeMenu::SetControllerAndAdvanceToMainMenu(const int ControllerIndex) +{ + if ( !ensure( GameInstance.IsValid() ) ) + { + return; + } + + ULocalPlayer * NewPlayerOwner = GameInstance->GetFirstGamePlayer(); + + if ( NewPlayerOwner != nullptr && ControllerIndex != -1 ) + { + NewPlayerOwner->SetControllerId(ControllerIndex); + NewPlayerOwner->SetCachedUniqueNetId(NewPlayerOwner->GetUniqueNetIdFromCachedControllerId().GetUniqueNetId()); + + // tell gameinstance to transition to main menu + GameInstance->GotoState(ShooterGameInstanceState::MainMenu); + } +} + +FReply FShooterWelcomeMenu::OnContinueWithoutSavingConfirm() +{ + if ( !ensure( GameInstance.IsValid() ) ) + { + return FReply::Handled(); + } + + UShooterGameViewportClient * ShooterViewport = Cast( GameInstance->GetGameViewportClient() ); + + if (ShooterViewport != NULL) + { + ShooterViewport->HideDialog(); + } + + SetControllerAndAdvanceToMainMenu(PendingControllerIndex); + return FReply::Handled(); +} + +FReply FShooterWelcomeMenu::OnConfirmGeneric() +{ + if ( !ensure( GameInstance.IsValid() ) ) + { + return FReply::Handled(); + } + + UShooterGameViewportClient * ShooterViewport = Cast( GameInstance->GetGameViewportClient() ); + + if (ShooterViewport != NULL) + { + ShooterViewport->HideDialog(); + } + + return FReply::Handled(); +} + +void FShooterWelcomeMenu::OnUserCanPlay(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + if (PrivilegeResults == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + SetControllerAndAdvanceToMainMenu(PendingControllerIndex); + } + else + { + UShooterGameViewportClient * ShooterViewport = Cast( GameInstance->GetGameViewportClient() ); + + if ( ShooterViewport != NULL ) + { + const FText ReturnReason = NSLOCTEXT("PrivilegeFailures", "CannotPlayAgeRestriction", "You cannot play this game due to age restrictions."); + const FText OKButton = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + + ShooterViewport->ShowDialog( + nullptr, + EShooterDialogType::Generic, + ReturnReason, + OKButton, + FText::GetEmpty(), + FOnClicked::CreateRaw(this, &FShooterWelcomeMenu::OnConfirmGeneric), + FOnClicked::CreateRaw(this, &FShooterWelcomeMenu::OnConfirmGeneric) + ); + } + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Menu/ShooterWelcomeMenu.h b/Source/ShooterGame/Private/UI/Menu/ShooterWelcomeMenu.h new file mode 100644 index 0000000..63c9881 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/ShooterWelcomeMenu.h @@ -0,0 +1,65 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once +#include "SlateBasics.h" +#include "SlateExtras.h" + +class FShooterWelcomeMenu : public TSharedFromThis +{ +public: + /** build menu */ + void Construct( TWeakObjectPtr< class UShooterGameInstance > InGameInstance ); + + /** Add the menu to the gameviewport so it becomes visible */ + void AddToGameViewport(); + + /** Remove from the gameviewport. */ + void RemoveFromGameViewport(); + + /** + * The delegate function for external login UI closure when a user has signed in. + * + * @param UniqueId The unique Id of the user who just signed in. + * @param ControllerIndex The controller index of the player who just signed in. + */ + void HandleLoginUIClosed(TSharedPtr UniqueId, const int ControllerIndex, const FOnlineError& Error = FOnlineError()); + + /** + * Called when a user needs to advance from the welcome screen to the main menu. + * + * @param ControllerIndex The controller index of the player that's advancing. + */ + void SetControllerAndAdvanceToMainMenu(const int ControllerIndex); + + /** Lock/Unlock controls based*/ + void LockControls(bool bLock) + { + bControlsLocked = bLock; + } + + bool GetControlsLocked() const + { + return bControlsLocked; + } + TWeakObjectPtr< class UShooterGameInstance > GetGameInstance() { return GameInstance; } + +private: + /** Owning game instance */ + TWeakObjectPtr< class UShooterGameInstance > GameInstance; + + /** Cache the user id that tried to advance, so we can use it again after the confirmation dialog */ + int PendingControllerIndex; + + /** "Presss A/X to play" widget */ + TSharedPtr MenuWidget; + + /** Handler for continue-without-saving confirmation. */ + FReply OnContinueWithoutSavingConfirm(); + + /** Generic Handler for hiding the dialog. */ + FReply OnConfirmGeneric(); + + /** Handler for querying a user's privileges */ + void OnUserCanPlay(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults); + + bool bControlsLocked; +}; diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDemoList.cpp b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDemoList.cpp new file mode 100644 index 0000000..80fd24b --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDemoList.cpp @@ -0,0 +1,447 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "SShooterDemoList.h" +#include "SHeaderRow.h" +#include "ShooterStyle.h" +#include "CoreStyle.h" +#include "ShooterGameLoadingScreen.h" +#include "ShooterGameInstance.h" +#include "NetworkReplayStreaming.h" +#include "ShooterGameViewportClient.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +struct FDemoEntry +{ + FNetworkReplayStreamInfo StreamInfo; + FString Date; + FString Size; + int32 ResultsIndex; +}; + +void SShooterDemoList::Construct(const FArguments& InArgs) +{ + PlayerOwner = InArgs._PlayerOwner; + OwnerWidget = InArgs._OwnerWidget; + bUpdatingDemoList = false; + StatusText = FText::GetEmpty(); + + EnumerateStreamsVersion = FNetworkVersion::GetReplayVersion(); + + const int32 NameWidth = 280; + const int32 ViewersWidth = 64; + const int32 DateWidth = 210; + const int32 LengthWidth = 64; + + ChildSlot + .VAlign(VAlign_Fill) + .HAlign(HAlign_Fill) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .HAlign(HAlign_Left) + .Padding(FMargin(0.0f, 0.0f, 0.0f, 8.0f)) + [ + SNew(SCheckBox) + .IsChecked(this, &SShooterDemoList::IsShowAllReplaysChecked) + .OnCheckStateChanged(this, &SShooterDemoList::OnShowAllReplaysChecked) + .Style(FCoreStyle::Get(), "Checkbox") + [ + SNew(STextBlock) + .Text(LOCTEXT("ShowAllDemos", "Show demos from all versions")) + .TextStyle(FShooterStyle::Get(), "ShooterGame.DemoListCheckboxTextStyle") + ] + ] + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBox) + .WidthOverride(700) + .HeightOverride(300) + [ + SAssignNew(DemoListWidget, SListView>) + .ItemHeight(20) + .ListItemsSource(&DemoList) + .SelectionMode(ESelectionMode::Single) + .OnGenerateRow(this, &SShooterDemoList::MakeListViewWidget) + .OnSelectionChanged(this, &SShooterDemoList::EntrySelectionChanged) + .OnMouseButtonDoubleClick(this,&SShooterDemoList::OnListItemDoubleClicked) + .HeaderRow( + SNew(SHeaderRow) + + SHeaderRow::Column("DemoName").FixedWidth(NameWidth).DefaultLabel(NSLOCTEXT("DemoList", "DemoNameColumn", "Demo Name")) + + SHeaderRow::Column("Viewers").FixedWidth(ViewersWidth).DefaultLabel(NSLOCTEXT("Viewers", "ViewersColumn", "Viewers")) + + SHeaderRow::Column("Date").FixedWidth(DateWidth).DefaultLabel(NSLOCTEXT("DemoList", "DateColumn", "Date")) + + SHeaderRow::Column("Length").FixedWidth(LengthWidth).DefaultLabel(NSLOCTEXT("Length", "LengthColumn", "Length")) + + SHeaderRow::Column("Size").HAlignHeader(HAlign_Left).HAlignCell(HAlign_Right).DefaultLabel(NSLOCTEXT("DemoList", "SizeColumn", "Size"))) + ] + ] + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SOverlay) + +SOverlay::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(this, &SShooterDemoList::GetBottomText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuServerListTextStyle") + ] + ] + ]; + + ReplayStreamer = FNetworkReplayStreaming::Get().GetFactory().CreateReplayStreamer(); + + BuildDemoList(); +} + +void SShooterDemoList::OnEnumerateStreamsComplete(const FEnumerateStreamsResult& Result ) +{ + check(bUpdatingDemoList); // should not be called otherwise + + bool bFinished = true; + + for ( const auto& StreamInfo : Result.FoundStreams ) + { + float SizeInKilobytes = StreamInfo.SizeInBytes / 1024.0f; + + TSharedPtr NewDemoEntry = MakeShareable( new FDemoEntry() ); + + NewDemoEntry->StreamInfo = StreamInfo; + NewDemoEntry->Date = StreamInfo.Timestamp.ToString( TEXT( "%m/%d/%Y %h:%M %A" ) ); // UTC time + NewDemoEntry->Size = SizeInKilobytes >= 1024.0f ? FString::Printf( TEXT("%2.2f MB" ), SizeInKilobytes / 1024.0f ) : FString::Printf( TEXT("%i KB" ), (int)SizeInKilobytes ); + + DemoList.Add( NewDemoEntry ); + } + + // Sort demo names by date + struct FCompareDateTime + { + FORCEINLINE bool operator()( const TSharedPtr & A, const TSharedPtr & B ) const + { + return A->StreamInfo.Timestamp.GetTicks() > B->StreamInfo.Timestamp.GetTicks(); + } + }; + + Sort( DemoList.GetData(), DemoList.Num(), FCompareDateTime() ); + + //StatusText = ""; + StatusText = LOCTEXT("DemoSelectionInfo","Press ENTER to Play. Press DEL to delete."); + + if ( bFinished ) + { + OnBuildDemoListFinished(); + } +} + +FText SShooterDemoList::GetBottomText() const +{ + return StatusText; +} + +/** + * Ticks this widget. Override in derived classes, but always call the parent implementation. + * + * @param AllottedGeometry The space allotted for this widget + * @param InCurrentTime Current absolute real time + * @param InDeltaTime Real time passed since last tick + */ +void SShooterDemoList::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) +{ + SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); +} + +ECheckBoxState SShooterDemoList::IsShowAllReplaysChecked() const +{ + if (EnumerateStreamsVersion.NetworkVersion == 0 && EnumerateStreamsVersion.Changelist == 0) + { + return ECheckBoxState::Checked; + } + + return ECheckBoxState::Unchecked; +} + +void SShooterDemoList::OnShowAllReplaysChecked(ECheckBoxState NewCheckedState) +{ + EnumerateStreamsVersion = FNetworkVersion::GetReplayVersion(); + + // Always set CL to 0, we only want to ever check NetworkVersion (now that we have backwards compat) + EnumerateStreamsVersion.Changelist = 0; + + if (NewCheckedState == ECheckBoxState::Checked) + { + EnumerateStreamsVersion.NetworkVersion = 0; + } + + BuildDemoList(); +} + +/** Populates the demo list */ +void SShooterDemoList::BuildDemoList() +{ + bUpdatingDemoList = true; + DemoList.Empty(); + + if ( ReplayStreamer.IsValid() ) + { + ReplayStreamer->EnumerateStreams(EnumerateStreamsVersion, INDEX_NONE, FString(), TArray(), FEnumerateStreamsCallback::CreateSP(this, &SShooterDemoList::OnEnumerateStreamsComplete)); + } +} + +/** Called when demo list building is finished */ +void SShooterDemoList::OnBuildDemoListFinished() +{ + bUpdatingDemoList = false; + + int32 SelectedItemIndex = DemoList.IndexOfByKey(SelectedItem); + + DemoListWidget->RequestListRefresh(); + if (DemoList.Num() > 0) + { + DemoListWidget->UpdateSelectionSet(); + DemoListWidget->SetSelection(DemoList[SelectedItemIndex > -1 ? SelectedItemIndex : 0],ESelectInfo::OnNavigation); + } +} + +void SShooterDemoList::PlayDemo() +{ + if (bUpdatingDemoList) + { + // We're still building the list + return; + } + + if (SelectedItem.IsValid()) + { + UShooterGameInstance* const GI = Cast(PlayerOwner->GetGameInstance()); + + if ( GI != NULL ) + { + FString DemoName = SelectedItem->StreamInfo.Name; + + // Play the demo + GI->PlayDemo( PlayerOwner.Get(), DemoName ); + } + } +} + +void SShooterDemoList::DeleteDemo() +{ + if (bUpdatingDemoList) + { + // We're still building the list + return; + } + + if (SelectedItem.IsValid()) + { + UShooterGameInstance* const GI = Cast(PlayerOwner->GetGameInstance()); + + if ( GI != NULL ) + { + UShooterGameViewportClient* ShooterViewport = Cast( GI->GetGameViewportClient() ); + + if ( ShooterViewport ) + { + ShooterViewport->ShowDialog( + PlayerOwner, + EShooterDialogType::Generic, + FText::Format(LOCTEXT("DeleteDemoFmt", "Delete {0}?"), FText::FromString(SelectedItem->StreamInfo.FriendlyName)), + LOCTEXT("EnterYes", "ENTER - YES"), + LOCTEXT("EscapeNo", "ESC - NO"), + FOnClicked::CreateRaw(this, &SShooterDemoList::OnDemoDeleteConfirm), + FOnClicked::CreateRaw(this, &SShooterDemoList::OnDemoDeleteCancel) + ); + } + } + } +} + +FReply SShooterDemoList::OnDemoDeleteConfirm() +{ + if (SelectedItem.IsValid() && ReplayStreamer.IsValid()) + { + bUpdatingDemoList = true; + DemoList.Empty(); + + ReplayStreamer->DeleteFinishedStream(SelectedItem->StreamInfo.Name, FDeleteFinishedStreamCallback::CreateSP(this, &SShooterDemoList::OnDeleteFinishedStreamComplete)); + } + + UShooterGameInstance* const GI = Cast(PlayerOwner->GetGameInstance()); + + if ( GI != NULL ) + { + UShooterGameViewportClient * ShooterViewport = Cast( GI->GetGameViewportClient() ); + + if ( ShooterViewport ) + { + ShooterViewport->HideDialog(); + } + } + + return FReply::Handled(); +} + +FReply SShooterDemoList::OnDemoDeleteCancel() +{ + UShooterGameInstance* const GI = Cast(PlayerOwner->GetGameInstance()); + + if ( GI != NULL ) + { + UShooterGameViewportClient * ShooterViewport = Cast( GI->GetGameViewportClient() ); + + if ( ShooterViewport ) + { + ShooterViewport->HideDialog(); + } + } + + return FReply::Handled(); +} + +void SShooterDemoList::OnDeleteFinishedStreamComplete(const FDeleteFinishedStreamResult& Result) +{ + BuildDemoList(); +} + +void SShooterDemoList::OnFocusLost(const FFocusEvent& InFocusEvent) +{ + if (InFocusEvent.GetCause() != EFocusCause::SetDirectly) + { + FSlateApplication::Get().SetKeyboardFocus(SharedThis( this )); + } +} + +FReply SShooterDemoList::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) +{ + return FReply::Handled().SetUserFocus(SharedThis(this), EFocusCause::SetDirectly, true); +} + +void SShooterDemoList::EntrySelectionChanged(TSharedPtr InItem, ESelectInfo::Type SelectInfo) +{ + SelectedItem = InItem; +} + +void SShooterDemoList::OnListItemDoubleClicked(TSharedPtr InItem) +{ + SelectedItem = InItem; + PlayDemo(); + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); +} + +void SShooterDemoList::MoveSelection(int32 MoveBy) +{ + const int32 SelectedItemIndex = DemoList.IndexOfByKey(SelectedItem); + + if (SelectedItemIndex+MoveBy > -1 && SelectedItemIndex+MoveBy < DemoList.Num()) + { + DemoListWidget->SetSelection(DemoList[SelectedItemIndex+MoveBy]); + } +} + +FReply SShooterDemoList::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyboardEvent) +{ + if (bUpdatingDemoList) // lock input + { + return FReply::Handled(); + } + + FReply Result = FReply::Unhandled(); + const FKey Key = InKeyboardEvent.GetKey(); + if (Key == EKeys::Enter || Key == EKeys::Virtual_Accept) + { + PlayDemo(); + Result = FReply::Handled(); + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); + } + else if (Key == EKeys::Delete) + { + DeleteDemo(); + Result = FReply::Handled(); + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); + } + //hit space bar or left gamepad face button to search for demos again / refresh the list, only when not searching already + else if (Key == EKeys::SpaceBar || Key == EKeys::Gamepad_FaceButton_Left) + { + // Refresh demo list + BuildDemoList(); + } + else if (Key == EKeys::Up || Key == EKeys::Gamepad_DPad_Up || Key == EKeys::Gamepad_LeftStick_Up) + { + MoveSelection(-1); + Result = FReply::Handled(); + } + else if (Key == EKeys::Down || Key == EKeys::Gamepad_DPad_Down || Key == EKeys::Gamepad_LeftStick_Down) + { + MoveSelection(1); + Result = FReply::Handled(); + } + return Result; +} + +TSharedRef SShooterDemoList::MakeListViewWidget(TSharedPtr Item, const TSharedRef& OwnerTable) +{ + class SDemoEntryWidget : public SMultiColumnTableRow< TSharedPtr > + { + public: + SLATE_BEGIN_ARGS(SDemoEntryWidget){} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTable, TSharedPtr InItem) + { + Item = InItem; + SMultiColumnTableRow< TSharedPtr >::Construct(FSuperRowType::FArguments(), InOwnerTable); + } + + TSharedRef GenerateWidgetForColumn(const FName& ColumnName) + { + FText ItemText = FText::GetEmpty(); + + if (ColumnName == "DemoName") + { + FString NameString = Item->StreamInfo.FriendlyName.IsEmpty() ? Item->StreamInfo.Name : Item->StreamInfo.FriendlyName; + + const int MAX_DEMO_NAME_DISPLAY_LEN = 18; + if ( NameString.Len() > MAX_DEMO_NAME_DISPLAY_LEN ) + { + NameString = NameString.Left( MAX_DEMO_NAME_DISPLAY_LEN ) + TEXT( "..." ); + } + + if (Item->StreamInfo.bIsLive) + { + NameString += " (Live)"; + } + + ItemText = FText::FromString(NameString); + } + else if (ColumnName == "Viewers") + { + ItemText = FText::FromString( FString::Printf( TEXT( "%i" ), Item->StreamInfo.NumViewers ) ); + } + else if (ColumnName == "Date") + { + ItemText = FText::FromString(Item->Date); + } + else if (ColumnName == "Length") + { + const int32 Minutes = Item->StreamInfo.LengthInMS / ( 1000 * 60 ); + const int32 Seconds = ( Item->StreamInfo.LengthInMS / 1000 ) % 60; + + ItemText = FText::FromString( FString::Printf( TEXT( "%02i:%02i" ), Minutes, Seconds ) ); + } + else if (ColumnName == "Size") + { + ItemText = FText::FromString(Item->Size); + } + + return SNew(STextBlock) + .Text(ItemText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuServerListTextStyle"); + } + TSharedPtr Item; + }; + return SNew(SDemoEntryWidget, OwnerTable, Item); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDemoList.h b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDemoList.h new file mode 100644 index 0000000..3b2edb1 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDemoList.h @@ -0,0 +1,129 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ShooterGame.h" +#include "SShooterMenuWidget.h" +#include "NetworkReplayStreaming.h" +#include "Misc/NetworkVersion.h" + +struct FDemoEntry; + +//class declare +class SShooterDemoList : public SShooterMenuWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterDemoList) + {} + + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + SLATE_ARGUMENT(TSharedPtr, OwnerWidget) + + SLATE_END_ARGS() + + /** needed for every widget */ + void Construct(const FArguments& InArgs); + + /** if we want to receive focus */ + virtual bool SupportsKeyboardFocus() const override { return true; } + + /** focus received handler - keep the ActionBindingsList focused */ + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override; + + /** focus lost handler - keep the ActionBindingsList focused */ + virtual void OnFocusLost(const FFocusEvent& InFocusEvent) override; + + /** key down handler */ + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyboardEvent) override; + + /** SListView item double clicked */ + void OnListItemDoubleClicked(TSharedPtr InItem); + + /** creates single item widget, called for every list item */ + TSharedRef MakeListViewWidget(TSharedPtr Item, const TSharedRef& OwnerTable); + + /** selection changed handler */ + void EntrySelectionChanged(TSharedPtr InItem, ESelectInfo::Type SelectInfo); + + /** Updates the list until it's completely populated */ + void UpdateBuildDemoListStatus(); + + /** Populates the demo list */ + void BuildDemoList(); + + /** Called when demo list building finished */ + void OnBuildDemoListFinished(); + + /** Called when we get results from the replay streaming interface */ + void OnEnumerateStreamsComplete(const FEnumerateStreamsResult& Result); + + /** Play chosen demo */ + void PlayDemo(); + + /** Delete chosen demo */ + void DeleteDemo(); + + /** Delete chosen demo (really) */ + FReply OnDemoDeleteConfirm(); + + /** Cancel delete chosen demo */ + FReply OnDemoDeleteCancel(); + + /** Called by delegate when the replay streaming interface has finished deleting */ + void OnDeleteFinishedStreamComplete(const FDeleteFinishedStreamResult& Result); + + /** selects item at current + MoveBy index */ + void MoveSelection(int32 MoveBy); + + /** + * Ticks this widget. Override in derived classes, but always call the parent implementation. + * + * @param AllottedGeometry The space allotted for this widget + * @param InCurrentTime Current absolute real time + * @param InDeltaTime Real time passed since last tick + */ + void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ); + +private: + /** Callback for "show all replay versions" checkbox */ + ECheckBoxState IsShowAllReplaysChecked() const; + + /** Callback fired when "show all replay versions" checkbox is changed */ + void OnShowAllReplaysChecked(ECheckBoxState NewCheckedState); + + /** Version used for enumerating replays. This is manipulated depending on whether we want to show all versions or not. */ + FNetworkReplayVersion EnumerateStreamsVersion; + +protected: + + /** Whether we're building the demo list or not */ + bool bUpdatingDemoList; + + /** action bindings array */ + TArray< TSharedPtr > DemoList; + + /** action bindings list slate widget */ + TSharedPtr< SListView< TSharedPtr > > DemoListWidget; + + /** currently selected list item */ + TSharedPtr SelectedItem; + + /** get current status text */ + FText GetBottomText() const; + + /** current status text */ + FText StatusText; + + /** pointer to our owner PC */ + TWeakObjectPtr PlayerOwner; + + /** pointer to our parent widget */ + TSharedPtr OwnerWidget; + + /** Network replay streaming interface */ + TSharedPtr ReplayStreamer; +}; + + diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDirectConnect.cpp b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDirectConnect.cpp new file mode 100644 index 0000000..a41abed --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDirectConnect.cpp @@ -0,0 +1,107 @@ +#include "SShooterDirectConnect.h" + +#include "ShooterStyle.h" + +void SShooterDirectConnect::Construct(const FArguments& InArgs) +{ + PlayerOwner = InArgs._PlayerOwner; + OwnerWidget = InArgs._OwnerWidget; + + FSlateFontInfo TextEnterFont = FShooterStyle::Get().GetFontStyle("ShooterGame.ChatFont"); + TextEnterFont.Size = 14; + + FSlateFontInfo ButtonFont = FShooterStyle::Get().GetFontStyle("ShooterGame.MenuTextStyle"); + ButtonFont.Size = 18; + const FMargin ButtonPadding = FMargin(10.f); + + ChildSlot + .VAlign(VAlign_Fill) + .HAlign(HAlign_Fill) + [ + SNew(SVerticalBox) + + // Address text box + + SVerticalBox::Slot() + .AutoHeight() + [ + SAssignNew(AddressEditBox, SEditableTextBox) + .MinDesiredWidth(350.f) + .ClearKeyboardFocusOnCommit(false) + .HintText(NSLOCTEXT("DCWidget", "EnterAddress", "Enter server address with port...")) + .Font(TextEnterFont) + ] + + // Connect button + + SVerticalBox::Slot() + .Padding(ButtonPadding) + [ + SNew(SButton) + .OnClicked(this, &SShooterDirectConnect::OnConnectClicked) + [ + SNew(STextBlock) + .Text(NSLOCTEXT("DCWidget", "Connect", "CONNECT")) + .Font(ButtonFont) + .Justification(ETextJustify::Center) + ] + ] + ]; +} + +FReply SShooterDirectConnect::OnConnectClicked() +{ + if (AddressEditBox->GetText().IsEmpty()) + { + return FReply::Handled(); + } + + const FString Address = AddressEditBox->GetText().ToString(); + + const FRegexPattern IPPortRegex(TEXT( + "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):[0-9]+" + )); + FRegexMatcher Matcher(IPPortRegex, Address); + if (!Matcher.FindNext()) + { + // On-screen messaging for this would be better, but for now just handle gracefully + UE_LOG(LogOnline, Warning, TEXT("Did not match IP:Port regex (address: %s), will not attempt to connect"), + *Address); + return FReply::Handled(); + } + + UE_LOG(LogOnline, Display, TEXT("Direct connecting to %s"), *Address); + + if (APlayerController* PlayerController = PlayerOwner->PlayerController) + { + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(OwnerWidget.ToSharedRef()); + } + else + { + UE_LOG(LogOnline, Warning, TEXT("Could not get game viewport, no widgets removed")); + } + + if (UShooterGameInstance* GInstance = Cast(PlayerController->GetGameInstance())) + { + GInstance->DirectConnectToSession(Address); + } + else + { + UE_LOG(LogOnline, Warning, TEXT("Could not get game instance, direct connecting failed")); + } + } + + return FReply::Handled(); +} + +FReply SShooterDirectConnect::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) +{ + const FKey Key = InKeyEvent.GetKey(); + if ((Key == EKeys::Escape || Key == EKeys::Virtual_Back) && !InKeyEvent.IsRepeat()) + { + // Explicitly state unhandled so call propagates to SShooterMenuWidget, which will close this widget + return FReply::Unhandled(); + } + + return FReply::Handled(); +} diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDirectConnect.h b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDirectConnect.h new file mode 100644 index 0000000..0c98e9c --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterDirectConnect.h @@ -0,0 +1,36 @@ +#pragma once + +#include "ShooterGame.h" +#include "SlateBasics.h" +#include "SShooterMenuWidget.h" + +class SShooterDirectConnect : public SShooterMenuWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterDirectConnect) {} + + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + SLATE_ARGUMENT(TSharedPtr, OwnerWidget) + + SLATE_END_ARGS() + + /** Build the widget */ + void Construct(const FArguments& InArgs); + + /** If we want to receive focus, suppresses error */ + virtual bool SupportsKeyboardFocus() const override { return true; } + +protected: + FReply OnConnectClicked(); + + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override; + + /** Pointer to owning player */ + TWeakObjectPtr PlayerOwner; + + /** pointer to our parent widget */ + TSharedPtr OwnerWidget; + + /** The edit text widget. */ + TSharedPtr AddressEditBox; +}; diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterLeaderboard.cpp b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterLeaderboard.cpp new file mode 100644 index 0000000..e871b75 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterLeaderboard.cpp @@ -0,0 +1,317 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "SShooterLeaderboard.h" +#include "ShooterStyle.h" +#include "ShooterUIHelpers.h" +#include "OnlineSubsystemUtils.h" + +#if !defined(INTERACTIVE_LEADERBOARD) +#define INTERACTIVE_LEADERBOARD 0 +#endif + +FLeaderboardRow::FLeaderboardRow(const FOnlineStatsRow& Row) + : Rank(FString::FromInt(Row.Rank)) + , PlayerName(Row.NickName) + , PlayerId(Row.PlayerId) +{ + if (const FVariantData* ScoreData = Row.Columns.Find(LEADERBOARD_STAT_SCORE)) + { + int32 Val; + ScoreData->GetValue(Val); + Score = FString::FromInt(Val); + } +} + +void SShooterLeaderboard::Construct(const FArguments& InArgs) +{ + PlayerOwner = InArgs._PlayerOwner; + OwnerWidget = InArgs._OwnerWidget; + const int32 BoxWidth = 125; + bReadingStats = false; + + LeaderboardReadCompleteDelegate = FOnLeaderboardReadCompleteDelegate::CreateRaw(this, &SShooterLeaderboard::OnStatsRead); + + ChildSlot + .VAlign(VAlign_Fill) + .HAlign(HAlign_Fill) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBox) + .WidthOverride(600) + .HeightOverride(600) + [ + SAssignNew(RowListWidget, SListView< TSharedPtr >) + .ItemHeight(20) + .ListItemsSource(&StatRows) + .SelectionMode(ESelectionMode::Single) + .OnGenerateRow(this, &SShooterLeaderboard::MakeListViewWidget) + .OnSelectionChanged(this, &SShooterLeaderboard::EntrySelectionChanged) + .HeaderRow( + SNew(SHeaderRow) + + SHeaderRow::Column("Rank").FixedWidth(BoxWidth/3) .DefaultLabel(NSLOCTEXT("LeaderBoard", "PlayerRankColumn", "Rank")) + + SHeaderRow::Column("PlayerName").FixedWidth(BoxWidth*2) .DefaultLabel(NSLOCTEXT("LeaderBoard", "PlayerNameColumn", "Player Name")) + + SHeaderRow::Column("Score") .DefaultLabel(NSLOCTEXT("LeaderBoard", "ScoreColumn", "Score"))) + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Right) + [ + SNew(STextBlock) + .Text(ShooterUIHelpers::Get().GetProfileOpenText()) + .TextStyle(FShooterStyle::Get(), "ShooterGame.ScoreboardListTextStyle") + .Visibility(this, &SShooterLeaderboard::GetProfileUIVisibility) + ] + ]; +} + +UWorld* SShooterLeaderboard::GetWorld() const +{ + return PlayerOwner.IsValid() ? PlayerOwner->GetWorld() : nullptr; +} + +void SShooterLeaderboard::ReadStatsLoginRequired() +{ + if (!OnLoginCompleteDelegateHandle.IsValid()) + { + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if (Identity.IsValid()) + { + OnLoginCompleteDelegateHandle = Identity->AddOnLoginCompleteDelegate_Handle(0, FOnLoginCompleteDelegate::CreateRaw(this, &SShooterLeaderboard::OnLoginCompleteReadStats)); + Identity->Login(0, FOnlineAccountCredentials()); + } + } + } +} + +void SShooterLeaderboard::OnLoginCompleteReadStats(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error) +{ + Online::GetIdentityInterface(GetWorld())->ClearOnLoginCompleteDelegate_Handle(LocalUserNum, OnLoginCompleteDelegateHandle); + if (bWasSuccessful) + { + ReadStats(); + } +} + +/** Starts reading leaderboards for the game */ +void SShooterLeaderboard::ReadStats() +{ + StatRows.Reset(); + + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineLeaderboardsPtr Leaderboards = OnlineSub->GetLeaderboardsInterface(); + if (Leaderboards.IsValid()) + { + // We are about to read the stats. The delegate will set this to false once the read is complete. + LeaderboardReadCompleteDelegateHandle = Leaderboards->AddOnLeaderboardReadCompleteDelegate_Handle(LeaderboardReadCompleteDelegate); + bReadingStats = true; + + // There's no reason to request leaderboard requests while one is in progress, so only do it if there isn't one active. + if (!IsLeaderboardReadInProgress()) + { + ReadObject = MakeShareable(new FShooterAllTimeMatchResultsRead()); + FOnlineLeaderboardReadRef ReadObjectRef = ReadObject.ToSharedRef(); + + check(PlayerOwner.IsValid()); + FUniqueNetIdRepl OwnerNetId = PlayerOwner->GetPreferredUniqueNetId(); + TArray > Players; + Players.Add(OwnerNetId->AsShared()); + + bReadingStats = Leaderboards->ReadLeaderboards(Players, ReadObjectRef); + } + } + else + { + // TODO: message the user? + } + } +} + +/** Called on a particular leaderboard read */ +void SShooterLeaderboard::OnStatsRead(bool bWasSuccessful) +{ + // It's possible for another read request to happen while another one is ongoing (such as when the player leaves the menu and + // re-enters quickly). We only want to process a leaderboard read if our read object is done. + if (!IsLeaderboardReadInProgress()) + { + ClearOnLeaderboardReadCompleteDelegate(); + + if (bWasSuccessful) + { + for (int Idx = 0; Idx < ReadObject->Rows.Num(); ++Idx) + { + TSharedPtr NewLeaderboardRow = MakeShareable(new FLeaderboardRow(ReadObject->Rows[Idx])); + + StatRows.Add(NewLeaderboardRow); + } + + RowListWidget->RequestListRefresh(); + } + + bReadingStats = false; + } +} + +void SShooterLeaderboard::OnFocusLost( const FFocusEvent& InFocusEvent ) +{ + if (InFocusEvent.GetCause() != EFocusCause::SetDirectly) + { + FSlateApplication::Get().SetKeyboardFocus(SharedThis( this )); + } +} + +FReply SShooterLeaderboard::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) +{ + return FReply::Handled().SetUserFocus(RowListWidget.ToSharedRef(), EFocusCause::SetDirectly); +} + +void SShooterLeaderboard::EntrySelectionChanged(TSharedPtr InItem, ESelectInfo::Type SelectInfo) +{ + SelectedItem = InItem; +} + +bool SShooterLeaderboard::IsPlayerSelectedAndValid() const +{ +#if INTERACTIVE_LEADERBOARD + if (SelectedItem.IsValid()) + { + check(SelectedItem->PlayerId.IsValid()); + return true; + } +#endif + return false; +} + +EVisibility SShooterLeaderboard::GetProfileUIVisibility() const +{ + return IsPlayerSelectedAndValid() ? EVisibility::Visible : EVisibility::Hidden; +} + +bool SShooterLeaderboard::ProfileUIOpened() const +{ + if( IsPlayerSelectedAndValid() ) + { + check( PlayerOwner.IsValid() ); + FUniqueNetIdRepl OwnerNetId = PlayerOwner->GetPreferredUniqueNetId(); + check( OwnerNetId.IsValid() ); + + const TSharedPtr& PlayerId = SelectedItem->PlayerId; + check( PlayerId.IsValid() ); + return ShooterUIHelpers::Get().ProfileOpenedUI(GetWorld(), *OwnerNetId, *PlayerId.Get(), NULL); + } + return false; +} + +void SShooterLeaderboard::MoveSelection(int32 MoveBy) +{ + int32 SelectedItemIndex = StatRows.IndexOfByKey(SelectedItem); + + if (SelectedItemIndex+MoveBy > -1 && SelectedItemIndex+MoveBy < StatRows.Num()) + { + RowListWidget->SetSelection(StatRows[SelectedItemIndex+MoveBy]); + } +} + +FReply SShooterLeaderboard::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) +{ + FReply Result = FReply::Unhandled(); + const FKey Key = InKeyEvent.GetKey(); + if (Key == EKeys::Up || Key == EKeys::Gamepad_DPad_Up || Key == EKeys::Gamepad_LeftStick_Up) + { + MoveSelection(-1); + Result = FReply::Handled(); + } + else if (Key == EKeys::Down || Key == EKeys::Gamepad_DPad_Down || Key == EKeys::Gamepad_LeftStick_Down) + { + MoveSelection(1); + Result = FReply::Handled(); + } + else if (Key == EKeys::Escape || Key == EKeys::Virtual_Back || Key == EKeys::Gamepad_Special_Left) + { + if (bReadingStats) + { + ClearOnLeaderboardReadCompleteDelegate(); + bReadingStats = false; + } + } + else if (Key == EKeys::Enter || Key == EKeys::Virtual_Accept) + { + // Open the profile UI of the selected item + ProfileUIOpened(); + Result = FReply::Handled(); + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); + } + return Result; +} + +TSharedRef SShooterLeaderboard::MakeListViewWidget(TSharedPtr Item, const TSharedRef& OwnerTable) +{ + class SLeaderboardRowWidget : public SMultiColumnTableRow< TSharedPtr > + { + public: + SLATE_BEGIN_ARGS(SLeaderboardRowWidget){} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTable, TSharedPtr InItem) + { + Item = InItem; + SMultiColumnTableRow< TSharedPtr >::Construct(FSuperRowType::FArguments(), InOwnerTable); + } + + TSharedRef GenerateWidgetForColumn(const FName& ColumnName) + { + FText ItemText = FText::GetEmpty(); + if (ColumnName == "Rank") + { + ItemText = FText::FromString(Item->Rank); + } + else if (ColumnName == "PlayerName") + { + if (Item->PlayerName.Len() > MAX_PLAYER_NAME_LENGTH) + { + ItemText = FText::FromString(Item->PlayerName.Left(MAX_PLAYER_NAME_LENGTH) + "..."); + } + else + { + ItemText = FText::FromString(Item->PlayerName); + } + } + else if (ColumnName == "Score") + { + ItemText = FText::FromString(Item->Score); + } + return SNew(STextBlock) + .Text(ItemText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.ScoreboardListTextStyle"); + } + TSharedPtr Item; + }; + return SNew(SLeaderboardRowWidget, OwnerTable, Item); +} + +void SShooterLeaderboard::ClearOnLeaderboardReadCompleteDelegate() +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineLeaderboardsPtr Leaderboards = OnlineSub->GetLeaderboardsInterface(); + if (Leaderboards.IsValid()) + { + Leaderboards->ClearOnLeaderboardReadCompleteDelegate_Handle(LeaderboardReadCompleteDelegateHandle); + } + } +} + +bool SShooterLeaderboard::IsLeaderboardReadInProgress() +{ + return ReadObject.IsValid() && (ReadObject->ReadState == EOnlineAsyncTaskState::InProgress || ReadObject->ReadState == EOnlineAsyncTaskState::NotStarted); +} diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterLeaderboard.h b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterLeaderboard.h new file mode 100644 index 0000000..55014fd --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterLeaderboard.h @@ -0,0 +1,127 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ShooterGame.h" +#include "ShooterLeaderboards.h" + +/** leaderboard row display information */ +struct FLeaderboardRow +{ + /** player rank*/ + FString Rank; + + /** player name */ + FString PlayerName; + + /** player total score to display */ + FString Score; + + /** Unique Id for the player at this rank */ + const TSharedPtr PlayerId; + + /** Default Constructor */ + FLeaderboardRow(const FOnlineStatsRow& Row); +}; + +//class declare +class SShooterLeaderboard : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterLeaderboard) + {} + + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + SLATE_ARGUMENT(TSharedPtr, OwnerWidget) + + SLATE_END_ARGS() + + /** needed for every widget */ + void Construct(const FArguments& InArgs); + + /** if we want to receive focus */ + virtual bool SupportsKeyboardFocus() const override { return true; } + + /** focus received handler - keep the ActionBindingsList focused */ + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override; + + /** focus lost handler - keep the ActionBindingsList focused */ + virtual void OnFocusLost( const FFocusEvent& InFocusEvent ) override; + + /** key down handler */ + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override; + + /** creates single item widget, called for every list item */ + TSharedRef MakeListViewWidget(TSharedPtr Item, const TSharedRef& OwnerTable); + + /** selection changed handler */ + void EntrySelectionChanged(TSharedPtr InItem, ESelectInfo::Type SelectInfo); + + /** is there a valid selected item */ + bool IsPlayerSelectedAndValid() const; + + /** check to see if we can open the profile ui */ + EVisibility GetProfileUIVisibility() const; + + /** profile open ui handler */ + bool ProfileUIOpened() const; + + /** Starts reading leaderboards for the game */ + void ReadStats(); + + /** Called on a particular leaderboard read */ + void OnStatsRead(bool bWasSuccessful); + + /** Called to login on relevant platforms first before making a leaderboard read */ + void ReadStatsLoginRequired(); + + /** Delegate after login has been been completed */ + void OnLoginCompleteReadStats(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error); + + /** selects item at current + MoveBy index */ + void MoveSelection(int32 MoveBy); + + UWorld* GetWorld() const; + +protected: + + /** Removes the bound LeaderboardReadCompleteDelegate if possible*/ + void ClearOnLeaderboardReadCompleteDelegate(); + + /** Returns true if a leaderboard read request is in progress or scheduled */ + bool IsLeaderboardReadInProgress(); + + /** action bindings array */ + TArray< TSharedPtr > StatRows; + + /** Leaderboard read object */ + FOnlineLeaderboardReadPtr ReadObject; + + /** Indicates that a stats read operation has been initiated */ + bool bReadingStats; + + /** Delegate called when a leaderboard has been successfully read */ + FOnLeaderboardReadCompleteDelegate LeaderboardReadCompleteDelegate; + + /** action bindings list slate widget */ + TSharedPtr< SListView< TSharedPtr > > RowListWidget; + + /** currently selected list item */ + TSharedPtr SelectedItem; + + /** pointer to our owner PC */ + TWeakObjectPtr PlayerOwner; + + /** pointer to our parent widget */ + TSharedPtr OwnerWidget; + + /** Handle to the registered LeaderboardReadComplete delegate */ + FDelegateHandle LeaderboardReadCompleteDelegateHandle; + + /** Handle to the registered LoginComplete delegate */ + FDelegateHandle OnLoginCompleteDelegateHandle; +}; + + diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuItem.cpp b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuItem.cpp new file mode 100644 index 0000000..c43fd09 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuItem.cpp @@ -0,0 +1,236 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "SShooterMenuItem.h" +#include "ShooterStyle.h" +#include "ShooterMenuItemWidgetStyle.h" + +void SShooterMenuItem::Construct(const FArguments& InArgs) +{ + ItemStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterMenuItemStyle"); + + PlayerOwner = InArgs._PlayerOwner; + Text = InArgs._Text; + OptionText = InArgs._OptionText; + OnClicked = InArgs._OnClicked; + OnArrowPressed = InArgs._OnArrowPressed; + bIsMultichoice = InArgs._bIsMultichoice; + bIsActiveMenuItem = false; + LeftArrowVisible = EVisibility::Collapsed; + RightArrowVisible = EVisibility::Collapsed; + //if attribute is set, use its value, otherwise uses default + InactiveTextAlpha = InArgs._InactiveTextAlpha.Get(1.0f); + + const float ArrowMargin = 3.0f; + ItemMargin = 10.0f; + TextColor = FLinearColor(FColor(155,164,182)); + + ChildSlot + .VAlign(VAlign_Fill) + .HAlign(HAlign_Fill) + [ + SNew(SOverlay) + + SOverlay::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + [ + SNew(SBox) + .WidthOverride(374.0f) + .HeightOverride(23.0f) + [ + SNew(SImage) + .ColorAndOpacity(this,&SShooterMenuItem::GetButtonBgColor) + .Image(&ItemStyle->BackgroundBrush) + ] + ] + +SOverlay::Slot() + .HAlign(bIsMultichoice ? HAlign_Left : HAlign_Center) + .VAlign(VAlign_Center) + .Padding(FMargin(ItemMargin,0,0,0)) + [ + SAssignNew(TextWidget, STextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuTextStyle") + .ColorAndOpacity(this,&SShooterMenuItem::GetButtonTextColor) + .ShadowColorAndOpacity(this, &SShooterMenuItem::GetButtonTextShadowColor) + .Text(Text) + ] + +SOverlay::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("NoBorder")) + .Padding(FMargin(0,0,ArrowMargin,0)) + .Visibility(this,&SShooterMenuItem::GetLeftArrowVisibility) + .OnMouseButtonDown(this,&SShooterMenuItem::OnLeftArrowDown) + [ + SNew(SOverlay) + +SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(SImage) + .Image(&ItemStyle->LeftArrowImage) + ] + ] + ] + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(TAttribute(this, &SShooterMenuItem::GetOptionPadding)) + [ + SNew(STextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuTextStyle") + .Visibility(bIsMultichoice ? EVisibility:: Visible : EVisibility::Collapsed ) + .ColorAndOpacity(this,&SShooterMenuItem::GetButtonTextColor) + .ShadowColorAndOpacity(this, &SShooterMenuItem::GetButtonTextShadowColor) + .Text(OptionText) + ] + +SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("NoBorder")) + .Padding(FMargin(ArrowMargin,0,ItemMargin,0)) + .Visibility(this,&SShooterMenuItem::GetRightArrowVisibility) + .OnMouseButtonDown(this,&SShooterMenuItem::OnRightArrowDown) + [ + SNew(SOverlay) + +SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(SImage) + .Image(&ItemStyle->RightArrowImage) + ] + ] + ] + ] + + ]; +} + +FReply SShooterMenuItem::OnRightArrowDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + FReply Result = FReply::Unhandled(); + const int32 MoveRight = 1; + if (OnArrowPressed.IsBound() && bIsActiveMenuItem) + { + OnArrowPressed.Execute(MoveRight); + Result = FReply::Handled(); + } + return Result; +} + +FReply SShooterMenuItem::OnLeftArrowDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + FReply Result = FReply::Unhandled(); + const int32 MoveLeft = -1; + if (OnArrowPressed.IsBound() && bIsActiveMenuItem) + { + OnArrowPressed.Execute(MoveLeft); + Result = FReply::Handled(); + } + return Result; +} + +EVisibility SShooterMenuItem::GetLeftArrowVisibility() const +{ + return LeftArrowVisible; +} + +EVisibility SShooterMenuItem::GetRightArrowVisibility() const +{ + return RightArrowVisible; +} + +FMargin SShooterMenuItem::GetOptionPadding() const +{ + return RightArrowVisible == EVisibility::Visible ? FMargin(0) : FMargin(0,0,ItemMargin,0); +} + +FSlateColor SShooterMenuItem::GetButtonTextColor() const +{ + FLinearColor Result; + if (bIsActiveMenuItem) + { + Result = TextColor; + } + else + { + Result = FLinearColor(TextColor.R, TextColor.G, TextColor.B, InactiveTextAlpha); + } + return Result; +} + +FLinearColor SShooterMenuItem::GetButtonTextShadowColor() const +{ + FLinearColor Result; + if (bIsActiveMenuItem) + { + Result = FLinearColor(0,0,0,1); + } + else + { + Result = FLinearColor(0,0,0, InactiveTextAlpha); + } + return Result; +} + + +FSlateColor SShooterMenuItem::GetButtonBgColor() const +{ + const float MinAlpha = 0.1f; + const float MaxAlpha = 1.f; + const float AnimSpeedModifier = 1.5f; + + float AnimPercent = 0.f; + ULocalPlayer* const Player = PlayerOwner.Get(); + if (Player) + { + // @fixme, need a world get delta time? + UWorld* const World = Player->GetWorld(); + if (World) + { + const float GameTime = World->GetRealTimeSeconds(); + AnimPercent = FMath::Abs(FMath::Sin(GameTime*AnimSpeedModifier)); + } + } + + const float BgAlpha = bIsActiveMenuItem ? FMath::Lerp(MinAlpha, MaxAlpha, AnimPercent) : 0.f; + return FLinearColor(1.f, 1.f, 1.f, BgAlpha); +} + +FReply SShooterMenuItem::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + //execute our "OnClicked" delegate, if we have one + if(OnClicked.IsBound() == true) + { + return OnClicked.Execute(); + } + + return FReply::Handled(); +} + + +FReply SShooterMenuItem::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + return FReply::Handled(); +} + +void SShooterMenuItem::SetMenuItemActive(bool bIsMenuItemActive) +{ + this->bIsActiveMenuItem = bIsMenuItemActive; +} + +void SShooterMenuItem::UpdateItemText(const FText& UpdatedText) +{ + Text = UpdatedText; + if (TextWidget.IsValid()) + { + TextWidget->SetText(Text); + } +} diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuItem.h b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuItem.h new file mode 100644 index 0000000..80fc6e9 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuItem.h @@ -0,0 +1,128 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" + +//class declare +class SShooterMenuItem : public SCompoundWidget +{ +public: + DECLARE_DELEGATE_OneParam( FOnArrowPressed, int ); + + SLATE_BEGIN_ARGS(SShooterMenuItem) + {} + + /** weak pointer to the parent PC */ + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + + /** called when the button is clicked */ + SLATE_EVENT(FOnClicked, OnClicked) + + /** called when the left or right arrow is clicked */ + SLATE_EVENT(FOnArrowPressed, OnArrowPressed) + + /** menu item text attribute */ + SLATE_ATTRIBUTE(FText, Text) + + /** is it multi-choice item? */ + SLATE_ARGUMENT(bool, bIsMultichoice) + + /** menu item option text attribute */ + SLATE_ATTRIBUTE(FText, OptionText) + + /** menu item text transparency when item is not active, optional argument */ + SLATE_ARGUMENT(TOptional, InactiveTextAlpha) + + /** end of slate attributes definition */ + SLATE_END_ARGS() + + /** needed for every widget */ + void Construct(const FArguments& InArgs); + + /** says that we can support keyboard focus */ + virtual bool SupportsKeyboardFocus() const override { return true; } + + /** mouse button down callback */ + virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + /** mouse button up callback */ + virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + /** sets this menu item as active (selected) */ + void SetMenuItemActive(bool bIsMenuItemActive); + + /** modify the displayed item text */ + void UpdateItemText(const FText& UpdatedText); + + /** set in option item to enable left arrow*/ + EVisibility LeftArrowVisible; + + /** set in option item to enable right arrow*/ + EVisibility RightArrowVisible; + +protected: + /** the delegate to execute when the button is clicked */ + FOnClicked OnClicked; + + /** the delegate to execute when one of arrows was pressed */ + FOnArrowPressed OnArrowPressed; + +private: + /** menu item text attribute */ + TAttribute< FText > Text; + + /** menu item option text attribute */ + TAttribute< FText > OptionText; + + /** menu item text widget */ + TSharedPtr TextWidget; + + /** menu item text color */ + FLinearColor TextColor; + + /** item margin */ + float ItemMargin; + + /** getter for menu item background color */ + FSlateColor GetButtonBgColor() const; + + /** getter for menu item text color */ + FSlateColor GetButtonTextColor() const; + + /** getter for menu item text shadow color */ + FLinearColor GetButtonTextShadowColor() const; + + /** getter for left option arrow visibility */ + EVisibility GetLeftArrowVisibility() const; + + /** getter for right option arrow visibility */ + EVisibility GetRightArrowVisibility() const; + + /** getter option padding (depends on right arrow visibility) */ + FMargin GetOptionPadding() const; + + /** calls OnArrowPressed */ + FReply OnRightArrowDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent); + + /** calls OnArrowPressed */ + FReply OnLeftArrowDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent); + + /** inactive text alpha value*/ + float InactiveTextAlpha; + + /** active item flag */ + bool bIsActiveMenuItem; + + /** is this menu item represents multi-choice field */ + bool bIsMultichoice; + + /** pointer to our parent PC */ + TWeakObjectPtr PlayerOwner; + + /** style for the menu item */ + const struct FShooterMenuItemStyle *ItemStyle; +}; + + diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuWidget.cpp b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuWidget.cpp new file mode 100644 index 0000000..c788e34 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuWidget.cpp @@ -0,0 +1,957 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Engine/Console.h" +#include "SShooterMenuWidget.h" +#include "ShooterMenuItem.h" +#include "SShooterMenuItem.h" +#include "ShooterStyle.h" +#include "ShooterMenuWidgetStyle.h" +#include "ShooterUIHelpers.h" +#include "ShooterGameInstance.h" +#include "Player/ShooterLocalPlayer.h" +#include "ShooterGameUserSettings.h" +#include "Slate/SceneViewport.h" + +#define LOCTEXT_NAMESPACE "SShooterMenuWidget" + +#if !defined(PROFILE_SWAPPING) +#define PROFILE_SWAPPING 0 +#endif + +void SShooterMenuWidget::Construct(const FArguments& InArgs) +{ + MenuStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterMenuStyle"); + + bControlsLocked = false; + bConsoleVisible = false; + OutlineWidth = 20.0f; + SelectedIndex = 0; + PlayerOwner = InArgs._PlayerOwner; + bGameMenu = InArgs._IsGameMenu; + ControllerHideMenuKey = EKeys::Gamepad_Special_Right; + Visibility.Bind(this, &SShooterMenuWidget::GetSlateVisibility); + FLinearColor MenuTitleTextColor = FLinearColor(FColor(155,164,182)); + MenuHeaderHeight = 62.0f; + MenuHeaderWidth = 325.0f; + + // Calculate the size of the profile box based on the string it'll contain (+ padding) + const FText PlayerName = PlayerOwner.IsValid() ? FText::FromString(PlayerOwner->GetNickname()) : FText::GetEmpty(); + const FText ProfileSwap = ShooterUIHelpers::Get().GetProfileSwapText(); + const TSharedRef< FSlateFontMeasure > FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); + const FSlateFontInfo PlayerNameFontInfo = FShooterStyle::Get().GetWidgetStyle("ShooterGame.MenuProfileNameStyle").Font; + const FSlateFontInfo ProfileSwapFontInfo = FShooterStyle::Get().GetWidgetStyle("ShooterGame.MenuServerListTextStyle").Font; + MenuProfileWidth = FMath::Max( FontMeasure->Measure( PlayerName, PlayerNameFontInfo, 1.0f ).X, FontMeasure->Measure( ProfileSwap.ToString(), ProfileSwapFontInfo, 1.0f ).X ) + 32.0f; + + ChildSlot + [ + SNew(SOverlay) + + SOverlay::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Top) + [ + SNew(SOverlay) + + SOverlay::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Fill) + .Padding( GetProfileSwapOffset() ) + [ + SNew(SBox) + .WidthOverride(MenuProfileWidth) + [ + SNew(SImage) + .Visibility(this, &SShooterMenuWidget::GetProfileSwapVisibility) + .ColorAndOpacity(this, &SShooterMenuWidget::GetHeaderColor) + .Image(&MenuStyle->HeaderBackgroundBrush) + ] + ] + + SOverlay::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Fill) + .Padding( GetProfileSwapOffset() ) + [ + SNew(SVerticalBox) + .Visibility(this, &SShooterMenuWidget::GetProfileSwapVisibility) + + SVerticalBox::Slot() + .AutoHeight() + .Padding( 16.0f, 10.0f, 16.0f, 1.0f ) + .HAlign(HAlign_Right) + .VAlign(VAlign_Top) + [ + SNew(STextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuProfileNameStyle") + .ColorAndOpacity(MenuTitleTextColor) + .Text(PlayerName) + ] + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Right) + .VAlign(VAlign_Bottom) + .Padding( 16.0f, 1.0f, 16.0f, 10.0f ) + [ + SNew(STextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuServerListTextStyle") + .ColorAndOpacity(MenuTitleTextColor) + .Text(ProfileSwap) + ] + ] + ] + + SOverlay::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .HAlign(HAlign_Left) + .VAlign(VAlign_Top) + .Padding(TAttribute(this,&SShooterMenuWidget::GetMenuOffset)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SOverlay) + + SOverlay::Slot() + .HAlign(HAlign_Left) + .VAlign(VAlign_Fill) + [ + SNew(SBox) + .WidthOverride(MenuHeaderWidth) + .HeightOverride(MenuHeaderHeight) + [ + SNew(SImage) + .ColorAndOpacity(this, &SShooterMenuWidget::GetHeaderColor) + .Image(&MenuStyle->HeaderBackgroundBrush) + ] + ] + + SOverlay::Slot() + .HAlign(HAlign_Left) + .VAlign(VAlign_Fill) + [ + SNew(SBox) + .WidthOverride(MenuHeaderWidth) + .HeightOverride(MenuHeaderHeight) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + .ColorAndOpacity(MenuTitleTextColor) + .Text(this,&SShooterMenuWidget::GetMenuTitle) + ] + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("NoBorder")) + .ColorAndOpacity(this, &SShooterMenuWidget::GetBottomColor) + .VAlign(VAlign_Top) + .HAlign(HAlign_Left) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SVerticalBox) + .Clipping(EWidgetClipping::ClipToBounds) + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(TAttribute(this,&SShooterMenuWidget::GetLeftMenuOffset)) + [ + SNew(SBorder) + .BorderImage(&MenuStyle->LeftBackgroundBrush) + .BorderBackgroundColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) + .Padding(FMargin(OutlineWidth)) + .DesiredSizeScale(this, &SShooterMenuWidget::GetBottomScale) + .VAlign(VAlign_Top) + .HAlign(HAlign_Left) + [ + SAssignNew(LeftBox, SVerticalBox) + .Clipping(EWidgetClipping::ClipToBounds) + ] + ] + ] + + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SVerticalBox) + .Clipping(EWidgetClipping::ClipToBounds) + + + SVerticalBox::Slot() + .Padding(TAttribute(this,&SShooterMenuWidget::GetSubMenuOffset)) + .AutoHeight() + [ + SNew(SBorder) + .BorderImage(&MenuStyle->RightBackgroundBrush) + .BorderBackgroundColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) + .Padding(FMargin(OutlineWidth)) + .DesiredSizeScale(this, &SShooterMenuWidget::GetBottomScale) + .VAlign(VAlign_Top) + .HAlign(HAlign_Left) + [ + SAssignNew(RightBox, SVerticalBox) + .Clipping(EWidgetClipping::ClipToBounds) + ] + ] + ] + ] + ] + ] + ] + ]; +} + +EVisibility SShooterMenuWidget::GetSlateVisibility() const +{ + return bConsoleVisible ? EVisibility::HitTestInvisible : EVisibility::Visible; +} + +FText SShooterMenuWidget::GetMenuTitle() const +{ + return CurrentMenuTitle; +} + +FMargin SShooterMenuWidget::GetProfileSwapOffset() const +{ + return FMargin(0.0f, 50.0f, 50.0f, 0.0f); +} + +bool SShooterMenuWidget::IsProfileSwapActive() const +{ +#if PROFILE_SWAPPING + // Dont' show if ingame or not on the main menu screen + return !bGameMenu && MenuHistory.Num() == 0 ? true : false; +#else + return false; +#endif +} + +EVisibility SShooterMenuWidget::GetProfileSwapVisibility() const +{ + return IsProfileSwapActive() ? EVisibility::Visible : EVisibility::Collapsed; +} + +UWorld* SShooterMenuWidget::GetWorld() const +{ + return PlayerOwner.IsValid() ? PlayerOwner->GetWorld() : nullptr; +} + +bool SShooterMenuWidget::ProfileUISwap(const int ControllerIndex) const +{ + if (IsProfileSwapActive()) + { + const FOnLoginUIClosedDelegate Delegate = FOnLoginUIClosedDelegate::CreateSP( const_cast(this), &SShooterMenuWidget::HandleProfileUISwapClosed ); + if ( ShooterUIHelpers::Get().ProfileSwapUI(GetWorld(), ControllerIndex, false, &Delegate) ) + { + UShooterGameInstance* GameInstance = PlayerOwner.IsValid() ? Cast< UShooterGameInstance >( PlayerOwner->GetGameInstance() ) : nullptr; + + if ( GameInstance != nullptr ) + { + GameInstance->SetIgnorePairingChangeForControllerId( ControllerIndex ); + } + return true; + } + } + return false; +} + +void SShooterMenuWidget::HandleProfileUISwapClosed(TSharedPtr UniqueId, const int ControllerIndex, const FOnlineError& Error) +{ + UShooterGameInstance * GameInstance = PlayerOwner.IsValid() ? Cast< UShooterGameInstance >( PlayerOwner->GetGameInstance() ) : nullptr; + + if ( GameInstance != nullptr ) + { + GameInstance->SetIgnorePairingChangeForControllerId( -1 ); + } + + // If the id is null, the user backed out + if( UniqueId.IsValid() && PlayerOwner.IsValid() ) + { + // If the id is the same, the user picked the existing profile + // (use the cached unique net id, since we want to compare to the user that was selected at "press start" + FUniqueNetIdRepl OwnerId = PlayerOwner->GetCachedUniqueNetId(); + if( OwnerId.IsValid() && UniqueId.IsValid() && *OwnerId == *UniqueId) + { + return; + } + + // Go back to the welcome screen. + HideMenu(); + } + + UShooterLocalPlayer* LocalPlayer = Cast(PlayerOwner.Get()); + LocalPlayer->LoadPersistentUser(); +} + +void SShooterMenuWidget::LockControls(bool bEnable) +{ + bControlsLocked = bEnable; +} + +int32 SShooterMenuWidget::GetOwnerUserIndex() +{ + return PlayerOwner.IsValid() ? PlayerOwner->GetControllerId() : 0; +} + +int32 SShooterMenuWidget::GetMenuLevel() +{ + return MenuHistory.Num(); +} + +void SShooterMenuWidget::BuildAndShowMenu() +{ + //grab the user settings + UShooterGameUserSettings* UserSettings = CastChecked(GEngine->GetGameUserSettings()); + ScreenRes = UserSettings->GetScreenResolution(); + + //Build left menu panel + bLeftMenuChanging = false; + bGoingBack = false; + BuildLeftPanel(bGoingBack); + + //sets up whole main menu animations and launches them + SetupAnimations(); + + // Set up right side and launch animation if there is any submenu + if (CurrentMenu.Num() > 0 && CurrentMenu.IsValidIndex(SelectedIndex) && CurrentMenu[SelectedIndex]->bVisible) + { + NextMenu = CurrentMenu[SelectedIndex]->SubMenu; + if (NextMenu.Num() > 0) + { + BuildRightPanel(); + bSubMenuChanging = true; + } + } + + bMenuHiding = false; + FSlateApplication::Get().PlaySound(MenuStyle->MenuEnterSound, GetOwnerUserIndex()); +} + +void SShooterMenuWidget::HideMenu() +{ + if (!bMenuHiding) + { + if(MenuWidgetAnimation.IsAtEnd()) + { + MenuWidgetAnimation.PlayReverse(this->AsShared()); + } + else + { + MenuWidgetAnimation.Reverse(); + } + bMenuHiding = true; + } +} + + +void SShooterMenuWidget::SetupAnimations() +{ + //Setup a curve + const float StartDelay = 0.0f; + const float SecondDelay = bGameMenu ? 0.f : 0.3f; + const float AnimDuration = 0.5f; + const float MenuChangeDuration = 0.2f; + + //always animate the menu from the same side of the screen; it looks silly when it disappears to one place and reappears from another + AnimNumber = 1; + + MenuWidgetAnimation = FCurveSequence(); + SubMenuWidgetAnimation = FCurveSequence(); + SubMenuScrollOutCurve = SubMenuWidgetAnimation.AddCurve(0,MenuChangeDuration,ECurveEaseFunction::QuadInOut); + + MenuWidgetAnimation = FCurveSequence(); + LeftMenuWidgetAnimation = FCurveSequence(); + LeftMenuScrollOutCurve = LeftMenuWidgetAnimation.AddCurve(0,MenuChangeDuration,ECurveEaseFunction::QuadInOut); + LeftMenuWidgetAnimation.Play(this->AsShared()); + + //Define the fade in animation + TopColorCurve = MenuWidgetAnimation.AddCurve(StartDelay, AnimDuration, ECurveEaseFunction::QuadInOut); + + //now, we want these to animate some time later + + //rolling out + BottomScaleYCurve = MenuWidgetAnimation.AddCurve(StartDelay+SecondDelay, AnimDuration, ECurveEaseFunction::QuadInOut); + //fading in + BottomColorCurve = MenuWidgetAnimation.AddCurve(StartDelay+SecondDelay, AnimDuration, ECurveEaseFunction::QuadInOut); + //moving from left side off screen + ButtonsPosXCurve = MenuWidgetAnimation.AddCurve(StartDelay+SecondDelay, AnimDuration, ECurveEaseFunction::QuadInOut); +} + +void SShooterMenuWidget::BuildLeftPanel(bool bInGoingBack) +{ + if (CurrentMenu.Num() == 0) + { + //do not build anything if we do not have any active menu + return; + } + LeftBox->ClearChildren(); + int32 PreviousIndex = -1; + if (bLeftMenuChanging) + { + if (bInGoingBack && MenuHistory.Num() > 0) + { + FShooterMenuInfo MenuInfo = MenuHistory.Pop(); + CurrentMenu = MenuInfo.Menu; + CurrentMenuTitle = MenuInfo.MenuTitle; + PreviousIndex = MenuInfo.SelectedIndex; + if (CurrentMenu.Num() > 0 && CurrentMenu[PreviousIndex]->SubMenu.Num() > 0) + { + NextMenu = CurrentMenu[PreviousIndex]->SubMenu; + bSubMenuChanging = true; + } + } + else if (PendingLeftMenu.Num() > 0) + { + MenuHistory.Push(FShooterMenuInfo(CurrentMenu, SelectedIndex, CurrentMenuTitle)); + CurrentMenuTitle = CurrentMenu[SelectedIndex]->GetText(); + CurrentMenu = PendingLeftMenu; + } + } + SelectedIndex = PreviousIndex; + //Setup the buttons + for(int32 i = 0; i < CurrentMenu.Num(); ++i) + { + if (CurrentMenu[i]->bVisible) + { + TSharedPtr TmpWidget; + if (CurrentMenu[i]->MenuItemType == EShooterMenuItemType::Standard) + { + TmpWidget = SAssignNew(CurrentMenu[i]->Widget, SShooterMenuItem) + .PlayerOwner(PlayerOwner) + .OnClicked(this, &SShooterMenuWidget::ButtonClicked, i) + .Text(CurrentMenu[i]->GetText()) + .bIsMultichoice(false); + } + else if (CurrentMenu[i]->MenuItemType == EShooterMenuItemType::MultiChoice) + { + TmpWidget = SAssignNew(CurrentMenu[i]->Widget, SShooterMenuItem) + .PlayerOwner(PlayerOwner) + .OnClicked(this, &SShooterMenuWidget::ButtonClicked, i) + .Text(CurrentMenu[i]->GetText() ) + .bIsMultichoice(true) + .OnArrowPressed(this, &SShooterMenuWidget::ChangeOption) + .OptionText(this, &SShooterMenuWidget::GetOptionText, CurrentMenu[i]); + UpdateArrows(CurrentMenu[i]); + } + else if (CurrentMenu[i]->MenuItemType == EShooterMenuItemType::CustomWidget) + { + TmpWidget = CurrentMenu[i]->CustomWidget; + } + if (TmpWidget.IsValid()) + { + //set first selection to first valid widget + if (SelectedIndex == -1) + { + SelectedIndex = i; + } + LeftBox->AddSlot() .HAlign(HAlign_Left) .AutoHeight() + [ + TmpWidget.ToSharedRef() + ]; + } + } + } + + + TSharedPtr FirstMenuItem = CurrentMenu.IsValidIndex(SelectedIndex) ? CurrentMenu[SelectedIndex] : NULL; + if (FirstMenuItem.IsValid() && FirstMenuItem->MenuItemType != EShooterMenuItemType::CustomWidget) + { + FirstMenuItem->Widget->SetMenuItemActive(true); + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); + } +} + +FText SShooterMenuWidget::GetOptionText(TSharedPtr MenuItem) const +{ + FText Result = FText::GetEmpty(); + if (MenuItem->SelectedMultiChoice > -1 && MenuItem->SelectedMultiChoice < MenuItem->MultiChoice.Num()) + { + Result = MenuItem->MultiChoice[MenuItem->SelectedMultiChoice]; + } + return Result; +} + +void SShooterMenuWidget::BuildRightPanel() +{ + RightBox->ClearChildren(); + + if (NextMenu.Num() == 0) return; + + for(int32 i = 0; i < NextMenu.Num(); ++i) + { + if (NextMenu[i]->bVisible) + { + TSharedPtr TmpButton; + //Custom menu items are not supported in the right panel + if (NextMenu[i]->MenuItemType == EShooterMenuItemType::Standard) + { + TmpButton = SAssignNew(NextMenu[i]->Widget, SShooterMenuItem) + .PlayerOwner(PlayerOwner) + .Text(NextMenu[i]->GetText()) + .InactiveTextAlpha(0.3f) + .bIsMultichoice(false); + } + else if (NextMenu[i]->MenuItemType == EShooterMenuItemType::MultiChoice) + { + TmpButton = SAssignNew(NextMenu[i]->Widget, SShooterMenuItem) + .PlayerOwner(PlayerOwner) + .Text(NextMenu[i]->GetText() ) + .InactiveTextAlpha(0.3f) + .bIsMultichoice(true) + .OptionText(this, &SShooterMenuWidget::GetOptionText, NextMenu[i]); + } + if(TmpButton.IsValid()) + { + RightBox->AddSlot() + .HAlign(HAlign_Center) + .AutoHeight() + [ + TmpButton.ToSharedRef() + ]; + } + } + } +} + +void SShooterMenuWidget::UpdateArrows(TSharedPtr MenuItem) +{ + const int32 MinIndex = MenuItem->MinMultiChoiceIndex > -1 ? MenuItem->MinMultiChoiceIndex : 0; + const int32 MaxIndex = MenuItem->MaxMultiChoiceIndex > -1 ? MenuItem->MaxMultiChoiceIndex : MenuItem->MultiChoice.Num()-1; + const int32 CurIndex = MenuItem->SelectedMultiChoice; + if (CurIndex > MinIndex) + { + MenuItem->Widget->LeftArrowVisible = EVisibility::Visible; + } + else + { + MenuItem->Widget->LeftArrowVisible = EVisibility::Collapsed; + } + if (CurIndex < MaxIndex) + { + MenuItem->Widget->RightArrowVisible = EVisibility::Visible; + } + else + { + MenuItem->Widget->RightArrowVisible = EVisibility::Collapsed; + } +} + +void SShooterMenuWidget::EnterSubMenu() +{ + bLeftMenuChanging = true; + bGoingBack = false; + FSlateApplication::Get().PlaySound(MenuStyle->MenuEnterSound, GetOwnerUserIndex()); +} + +void SShooterMenuWidget::MenuGoBack(bool bSilent) +{ + if (MenuHistory.Num() > 0) + { + if (!bSilent) + { + FSlateApplication::Get().PlaySound(MenuStyle->MenuBackSound, GetOwnerUserIndex()); + } + bLeftMenuChanging = true; + bGoingBack = true; + OnGoBack.ExecuteIfBound(CurrentMenu); + } + else if (bGameMenu) // only when it's in-game menu variant + { + if (!bSilent) + { + FSlateApplication::Get().PlaySound(MenuStyle->MenuBackSound, GetOwnerUserIndex()); + } + OnToggleMenu.ExecuteIfBound(); + } + else + { +#if SHOOTER_CONSOLE_UI + // Go back to the welcome screen. + HideMenu(); +#endif + } +} + +void SShooterMenuWidget::ConfirmMenuItem() +{ + if (CurrentMenu[SelectedIndex]->OnConfirmMenuItem.IsBound()) + { + CurrentMenu[SelectedIndex]->OnConfirmMenuItem.Execute(); + } + else if (CurrentMenu[SelectedIndex]->SubMenu.Num() > 0) + { + EnterSubMenu(); + } +} + +void SShooterMenuWidget::ControllerFacebuttonLeftPressed() +{ + if (CurrentMenu[SelectedIndex]->OnControllerFacebuttonLeftPressed.IsBound()) + { + CurrentMenu[SelectedIndex]->OnControllerFacebuttonLeftPressed.Execute(); + } +} + +void SShooterMenuWidget::ControllerUpInputPressed() +{ + if (CurrentMenu[SelectedIndex]->OnControllerUpInputPressed.IsBound()) + { + CurrentMenu[SelectedIndex]->OnControllerUpInputPressed.Execute(); + } +} + +void SShooterMenuWidget::ControllerDownInputPressed() +{ + if (CurrentMenu[SelectedIndex]->OnControllerDownInputPressed.IsBound()) + { + CurrentMenu[SelectedIndex]->OnControllerDownInputPressed.Execute(); + } +} + +void SShooterMenuWidget::ControllerFacebuttonDownPressed() +{ + if (CurrentMenu[SelectedIndex]->OnControllerFacebuttonDownPressed.IsBound()) + { + CurrentMenu[SelectedIndex]->OnControllerFacebuttonDownPressed.Execute(); + } +} + +void SShooterMenuWidget::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) +{ + //Always tick the super + SCompoundWidget::Tick( AllottedGeometry, InCurrentTime, InDeltaTime ); + + //ugly code seeing if the console is open + UConsole* ViewportConsole = (GEngine !=NULL && GEngine->GameViewport != NULL) ? GEngine->GameViewport->ViewportConsole : NULL; + if (ViewportConsole != NULL && (ViewportConsole->ConsoleState == "Typing" || ViewportConsole->ConsoleState == "Open")) + { + if (!bConsoleVisible) + { + bConsoleVisible = true; + FSlateApplication::Get().SetAllUserFocusToGameViewport(); + } + } + else + { + if (bConsoleVisible) + { + bConsoleVisible = false; + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); + } + } + + if (GEngine && GEngine->GameViewport) + { + FVector2D Size = FVector2D::ZeroVector; + GEngine->GameViewport->GetViewportSize(Size); + if (!Size.IsZero()) + { + ScreenRes = (Size / AllottedGeometry.Scale).IntPoint(); + } + } + + if (MenuWidgetAnimation.IsAtStart() && !bMenuHiding) + { + //Start the menu widget animation, set keyboard focus + FadeIn(); + } + else if (MenuWidgetAnimation.IsAtStart() && bMenuHiding) + { + bMenuHiding = false; + //Send event, so menu can be removed + OnMenuHidden.ExecuteIfBound(); + } + + if (MenuWidgetAnimation.IsAtEnd()) + { + if (bLeftMenuChanging) + { + if (LeftMenuWidgetAnimation.IsAtEnd()) + { + PendingLeftMenu = NextMenu; + if (NextMenu.Num() > 0 + && NextMenu.Top()->SubMenu.Num() > 0) + { + NextMenu = NextMenu.Top()->SubMenu; + } + else + { + NextMenu.Reset(); + } + bSubMenuChanging = true; + + LeftMenuWidgetAnimation.PlayReverse(this->AsShared()); + } + if (!LeftMenuWidgetAnimation.IsPlaying()) + { + if (CurrentMenu.Num() > 0) + { + BuildLeftPanel(bGoingBack); + LeftMenuWidgetAnimation.Play(this->AsShared()); + } + //Focus the custom widget + if (CurrentMenu.Num() == 1 && CurrentMenu.Top()->MenuItemType == EShooterMenuItemType::CustomWidget) + { + FSlateApplication::Get().SetKeyboardFocus(CurrentMenu.Top()->CustomWidget); + } + bLeftMenuChanging = false; + RightBox->ClearChildren(); + } + } + if (bSubMenuChanging) + { + if (SubMenuWidgetAnimation.IsAtEnd()) + { + SubMenuWidgetAnimation.PlayReverse(this->AsShared()); + } + if (!SubMenuWidgetAnimation.IsPlaying()) + { + if (NextMenu.Num() > 0) + { + BuildRightPanel(); + SubMenuWidgetAnimation.Play(this->AsShared()); + } + bSubMenuChanging = false; + } + } + } +} + +FMargin SShooterMenuWidget::GetMenuOffset() const +{ + const float WidgetWidth = LeftBox->GetDesiredSize().X + RightBox->GetDesiredSize().X; + const float WidgetHeight = LeftBox->GetDesiredSize().Y + MenuHeaderHeight; + const float OffsetX = (ScreenRes.X - WidgetWidth - OutlineWidth*2)/2; + const float AnimProgress = ButtonsPosXCurve.GetLerp(); + FMargin Result; + + switch (AnimNumber) + { + case 0: + Result = FMargin(OffsetX + ScreenRes.X - AnimProgress*ScreenRes.X, (ScreenRes.Y - WidgetHeight)/2, 0, 0); + break; + case 1: + Result = FMargin(OffsetX - ScreenRes.X + AnimProgress*ScreenRes.X, (ScreenRes.Y - WidgetHeight)/2, 0, 0); + break; + case 2: + Result = FMargin(OffsetX, (ScreenRes.Y - WidgetHeight)/2 + ScreenRes.Y - AnimProgress*ScreenRes.Y, 0, 0); + break; + case 3: + Result = FMargin(OffsetX, (ScreenRes.Y - WidgetHeight)/2 + -ScreenRes.Y + AnimProgress*ScreenRes.Y, 0, 0); + break; + } + return Result; +} + +FMargin SShooterMenuWidget::GetLeftMenuOffset() const +{ + const float LeftBoxSizeX = LeftBox->GetDesiredSize().X + OutlineWidth * 2; + return FMargin(0, 0,-LeftBoxSizeX + LeftMenuScrollOutCurve.GetLerp() * LeftBoxSizeX,0); +} + +FMargin SShooterMenuWidget::GetSubMenuOffset() const +{ + const float RightBoxSizeX = RightBox->GetDesiredSize().X + OutlineWidth * 2; + return FMargin(0, 0,-RightBoxSizeX + SubMenuScrollOutCurve.GetLerp() * RightBoxSizeX,0); +} + + +FVector2D SShooterMenuWidget::GetBottomScale() const +{ + return FVector2D(BottomScaleYCurve.GetLerp(), BottomScaleYCurve.GetLerp()); +} + +FLinearColor SShooterMenuWidget::GetBottomColor() const +{ + return FMath::Lerp(FLinearColor(1,1,1,0), FLinearColor(1,1,1,1), BottomColorCurve.GetLerp()); +} + +FLinearColor SShooterMenuWidget::GetTopColor() const +{ + return FMath::Lerp(FLinearColor(1,1,1,0), FLinearColor(1,1,1,1), TopColorCurve.GetLerp()); +} + +FSlateColor SShooterMenuWidget::GetHeaderColor() const +{ + return CurrentMenuTitle.IsEmpty() ? FLinearColor::Transparent : FLinearColor::White; +} + +FReply SShooterMenuWidget::ButtonClicked(int32 ButtonIndex) +{ + if (bControlsLocked) + { + return FReply::Handled(); + } + + if (SelectedIndex != ButtonIndex) + { + TSharedPtr MenuItem = CurrentMenu[SelectedIndex]->Widget; + MenuItem->SetMenuItemActive(false); + SelectedIndex = ButtonIndex; + MenuItem = CurrentMenu[SelectedIndex]->Widget; + MenuItem->SetMenuItemActive(true); + NextMenu = CurrentMenu[SelectedIndex]->SubMenu; + bSubMenuChanging = true; + FSlateApplication::Get().PlaySound(MenuStyle->MenuItemChangeSound, GetOwnerUserIndex()); + } + else if (SelectedIndex == ButtonIndex) + { + ConfirmMenuItem(); + } + + return FReply::Handled().SetUserFocus(SharedThis(this), EFocusCause::SetDirectly); +} + +void SShooterMenuWidget::FadeIn() +{ + //Start the menu widget playing + MenuWidgetAnimation.Play(this->AsShared()); + + //Go into UI mode + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); +} + +FReply SShooterMenuWidget::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + //If we clicked anywhere, jump to the end + if(MenuWidgetAnimation.IsPlaying()) + { + MenuWidgetAnimation.JumpToEnd(); + } + + //Set the keyboard focus + return FReply::Handled() + .SetUserFocus(SharedThis(this), EFocusCause::SetDirectly); +} + +void SShooterMenuWidget::ChangeOption(int32 MoveBy) +{ + TSharedPtr MenuItem = CurrentMenu[SelectedIndex]; + + const int32 MinIndex = MenuItem->MinMultiChoiceIndex > -1 ? MenuItem->MinMultiChoiceIndex : 0; + const int32 MaxIndex = MenuItem->MaxMultiChoiceIndex > -1 ? MenuItem->MaxMultiChoiceIndex : MenuItem->MultiChoice.Num()-1; + const int32 CurIndex = MenuItem->SelectedMultiChoice; + + if (MenuItem->MenuItemType == EShooterMenuItemType::MultiChoice) + { + if ( CurIndex + MoveBy >= MinIndex && CurIndex + MoveBy <= MaxIndex) + { + MenuItem->SelectedMultiChoice += MoveBy; + MenuItem->OnOptionChanged.ExecuteIfBound(MenuItem, MenuItem->SelectedMultiChoice); + FSlateApplication::Get().PlaySound(MenuStyle->OptionChangeSound, GetOwnerUserIndex()); + } + UpdateArrows(MenuItem); + } +} + +int32 SShooterMenuWidget::GetNextValidIndex(int32 MoveBy) +{ + int32 Result = SelectedIndex; + if (MoveBy != 0 && SelectedIndex + MoveBy > -1 && SelectedIndex+MoveBy < CurrentMenu.Num()) + { + Result = SelectedIndex + MoveBy; + //look for non-hidden menu item + while (!CurrentMenu[Result]->Widget.IsValid()) + { + MoveBy > 0 ? Result++ : Result--; + //when moved outside of array, just return current selection + if (!CurrentMenu.IsValidIndex(Result)) + { + Result = SelectedIndex; + break; + } + } + } + return Result; +} + +FReply SShooterMenuWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) +{ + FReply Result = FReply::Unhandled(); + const int32 UserIndex = InKeyEvent.GetUserIndex(); + bool bEventUserCanInteract = GetOwnerUserIndex() == -1 || UserIndex == GetOwnerUserIndex(); + + if (!bControlsLocked && bEventUserCanInteract) + { + const FKey Key = InKeyEvent.GetKey(); + if (Key == EKeys::Up || Key == EKeys::Gamepad_DPad_Up || Key == EKeys::Gamepad_LeftStick_Up) + { + ControllerUpInputPressed(); + int32 NextValidIndex = GetNextValidIndex(-1); + if (NextValidIndex != SelectedIndex) + { + ButtonClicked(NextValidIndex); + } + Result = FReply::Handled(); + } + else if (Key == EKeys::Down || Key == EKeys::Gamepad_DPad_Down || Key == EKeys::Gamepad_LeftStick_Down) + { + ControllerDownInputPressed(); + int32 NextValidIndex = GetNextValidIndex(1); + if (NextValidIndex != SelectedIndex) + { + ButtonClicked(NextValidIndex); + } + Result = FReply::Handled(); + } + else if (Key == EKeys::Left || Key == EKeys::Gamepad_DPad_Left || Key == EKeys::Gamepad_LeftStick_Left) + { + ChangeOption(-1); + Result = FReply::Handled(); + } + else if (Key == EKeys::Right ||Key == EKeys::Gamepad_DPad_Right || Key == EKeys::Gamepad_LeftStick_Right) + { + ChangeOption(1); + Result = FReply::Handled(); + } + else if (Key == EKeys::Gamepad_FaceButton_Top || Key == EKeys::SpaceBar) + { + ProfileUISwap(UserIndex); + Result = FReply::Handled(); + } + else if (Key == EKeys::Enter) + { + ConfirmMenuItem(); + Result = FReply::Handled(); + } + else if (Key == EKeys::Virtual_Accept && !InKeyEvent.IsRepeat()) + { + ControllerFacebuttonDownPressed(); + ConfirmMenuItem(); + Result = FReply::Handled(); + } + else if ((Key == EKeys::Escape || Key == EKeys::Virtual_Back || Key == EKeys::Gamepad_Special_Left || Key == EKeys::Global_Back || Key == EKeys::Global_View) && !InKeyEvent.IsRepeat()) + { + MenuGoBack(); + Result = FReply::Handled(); + } + else if (Key == EKeys::Gamepad_FaceButton_Left) + { + ControllerFacebuttonLeftPressed(); + Result = FReply::Handled(); + } + else if ((Key == ControllerHideMenuKey || Key == EKeys::Global_Play || Key == EKeys::Global_Menu) && !InKeyEvent.IsRepeat()) + { + OnToggleMenu.ExecuteIfBound(); + Result = FReply::Handled(); + } + } + return Result; +} + +FReply SShooterMenuWidget::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) +{ + //Focus the custom widget + if (CurrentMenu.Num() == 1 && CurrentMenu.Top()->MenuItemType == EShooterMenuItemType::CustomWidget) + { + return FReply::Handled().SetUserFocus(CurrentMenu.Top()->CustomWidget.ToSharedRef(), EFocusCause::SetDirectly); + } + + return FReply::Handled().ReleaseMouseCapture().SetUserFocus(SharedThis(this), EFocusCause::SetDirectly, true); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuWidget.h b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuWidget.h new file mode 100644 index 0000000..3f38329 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterMenuWidget.h @@ -0,0 +1,379 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ShooterMenuItem.h" + +//class declare +class SShooterMenuWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterMenuWidget) + : _PlayerOwner() + , _IsGameMenu(false) + { + } + + /** weak pointer to the parent HUD base */ + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + + /** is this main menu or in game menu? */ + SLATE_ARGUMENT(bool, IsGameMenu) + + /** always goes here */ + SLATE_END_ARGS() + + /** delegate declaration */ + DECLARE_DELEGATE(FOnMenuHidden); + + /** external delegate to call when in-game menu should be hidden using controller buttons - + it's workaround as when joystick is captured, even when sending FReply::Unhandled, binding does not recieve input :( */ + DECLARE_DELEGATE(FOnToggleMenu); + + /** called when user is going back from submenu, useful for resetting changes if they were not confirmed */ + DECLARE_DELEGATE_OneParam(FOnMenuGoBack, MenuPtr); + + /** every widget needs a construction function */ + void Construct(const FArguments& InArgs); + + /** update function. Kind of a hack. Allows us to only start fading in once we are done loading. */ + virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) override; + + /** to have the mouse cursor show up at all times, we need the widget to handle all mouse events */ + virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + /** key down handler */ + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override; + + /** says that we can support keyboard focus */ + virtual bool SupportsKeyboardFocus() const override { return true; } + + /** The menu sets up the appropriate mouse settings upon focus */ + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override; + + /** setups animation lengths, start points and launches initial animations */ + void SetupAnimations(); + + /** builds left menu panel */ + void BuildLeftPanel(bool bGoingBack); + + /** builds inactive next menu panel (current selections submenu preview) */ + void BuildRightPanel(); + + /** starts animations to enter submenu, it will become active menu */ + void EnterSubMenu(); + + /** starts reverse animations to go one level up in menu hierarchy */ + void MenuGoBack(bool bSilent=false); + + /** confirms current menu item and performs an action */ + void ConfirmMenuItem(); + + /** views a friend's profile in the current user's in-game menu friend list */ + void ControllerFacebuttonLeftPressed(); + + /** decrement the index of the friend that the user is currently selecting while in the in-game menu friend list */ + void ControllerUpInputPressed(); + + /** increment the index of the friend that the user is currently selecting while in the in-game menu friend list */ + void ControllerDownInputPressed(); + + /** Sends a friend invite to a friend in the current user's in-game menu friend list */ + void ControllerFacebuttonDownPressed(); + + /** call to rebuild menu and start animating it */ + void BuildAndShowMenu(); + + /** call to hide menu */ + void HideMenu(); + + /** updates arrows visibility for multi-choice menu item */ + void UpdateArrows(TSharedPtr MenuItem); + + /** changes option in multi-choice menu item */ + void ChangeOption(int32 MoveBy); + + /** get next valid index, ignoring invisible items */ + int32 GetNextValidIndex(int32 MoveBy); + + /** disable/enable moving around menu */ + void LockControls(bool bEnable); + + /** Cache the UserIndex from the owning PlayerController */ + int32 GetOwnerUserIndex(); + + /** returns the number of sublevels on the menu stack */ + int32 GetMenuLevel(); + + UWorld* GetWorld() const; + + /** main menu for this instance of widget */ + MenuPtr MainMenu; + + /** currently active menu */ + MenuPtr CurrentMenu; + + /** next menu (for transition and displaying as the right menu) */ + MenuPtr NextMenu; + + /** stack of previous menus */ + TArray MenuHistory; + + /** delegate, which is executed when menu is finished hiding */ + FOnMenuHidden OnMenuHidden; + + /** bind if menu should be hidden from outside by controller button */ + FOnToggleMenu OnToggleMenu; + + /** executed when user wants to go back to previous menu */ + FOnMenuGoBack OnGoBack; + + /** current menu title if present */ + FText CurrentMenuTitle; + + /** default - start button, change to use different */ + FKey ControllerHideMenuKey; + + /** if console is currently opened */ + bool bConsoleVisible; + +private: + + /** sets hit test invisibility when console is up */ + EVisibility GetSlateVisibility() const; + + /** getters used for animating the menu */ + FVector2D GetBottomScale() const; + FLinearColor GetBottomColor() const; + FLinearColor GetTopColor() const; + FMargin GetMenuOffset() const; + FMargin GetLeftMenuOffset() const; + FMargin GetSubMenuOffset() const; + + /** gets header image color */ + FSlateColor GetHeaderColor() const; + + /** callback for when one of the N buttons is clicked */ + FReply ButtonClicked(int32 ButtonIndex); + + /** gets currently selected multi-choice option */ + FText GetOptionText(TSharedPtr MenuItem) const; + + /** gets current menu title string */ + FText GetMenuTitle() const; + + /** gets the offset of the swap profile UI from the edge of the screen */ + FMargin GetProfileSwapOffset() const; + + /** should the profile swap be active */ + bool IsProfileSwapActive() const; + + /** gets the visibility of the swap profile UI */ + EVisibility GetProfileSwapVisibility() const; + + /** called when we want to swap the logged in user */ + bool ProfileUISwap(const int ControllerIndex) const; + + /** delegate for if the profile is swapped */ + void HandleProfileUISwapClosed(TSharedPtr UniqueId, const int ControllerIndex, const FOnlineError& Error = FOnlineError()); + + /** this function starts the entire fade in process */ + void FadeIn(); + + /** our curve sequence and the related handles */ + FCurveSequence MenuWidgetAnimation; + + /** used for menu background scaling animation at the beginning */ + FCurveHandle BottomScaleYCurve; + + /** used for main menu logo fade in animation at the beginning */ + FCurveHandle TopColorCurve; + + /** used for menu background fade in animation at the beginning */ + FCurveHandle BottomColorCurve; + + /** used for menu buttons slide in animation at the beginning */ + FCurveHandle ButtonsPosXCurve; + + /** sub menu transition animation sequence */ + FCurveSequence SubMenuWidgetAnimation; + + /** sub menu transition animation curve */ + FCurveHandle SubMenuScrollOutCurve; + + /** current menu transition animation sequence */ + FCurveSequence LeftMenuWidgetAnimation; + + /** current menu transition animation curve */ + FCurveHandle LeftMenuScrollOutCurve; + + /** weak pointer to our parent PC */ + TWeakObjectPtr PlayerOwner; + + /** screen resolution */ + FIntPoint ScreenRes; + + /** space between menu item and border */ + float OutlineWidth; + + /** menu header height */ + float MenuHeaderHeight; + + /** menu header width */ + float MenuHeaderWidth; + + /** menu profile width */ + float MenuProfileWidth; + + /** animation type index */ + int32 AnimNumber; + + /** selected index of current menu */ + int32 SelectedIndex; + + + /** right panel animating flag */ + bool bSubMenuChanging; + + /** left panel animating flag */ + bool bLeftMenuChanging; + + /** going back to previous menu animation flag */ + bool bGoingBack; + + /** flag when playing hiding animation */ + bool bMenuHiding; + + /** if this is in game menu, do not show background or logo */ + bool bGameMenu; + + /** if moving around menu is currently locked */ + bool bControlsLocked; + + /** menu that will override current one after transition animation */ + MenuPtr PendingLeftMenu; + + /** left(current) menu layout box */ + TSharedPtr LeftBox; + + /** right(sub) menu layout box */ + TSharedPtr RightBox; + + /** style for the menu widget */ + const struct FShooterMenuStyle *MenuStyle; +}; + +namespace MenuHelper +{ + FORCEINLINE void EnsureValid(TSharedPtr& MenuItem) + { + if (!MenuItem.IsValid()) + { + MenuItem = FShooterMenuItem::CreateRoot(); + } + } + + //Helper functions for creating menu items + FORCEINLINE TSharedRef AddMenuItem(TSharedPtr& MenuItem, const FText& Text) + { + EnsureValid(MenuItem); + TSharedPtr Item = MakeShareable(new FShooterMenuItem(Text)); + MenuItem->SubMenu.Add(Item); + return Item.ToSharedRef(); + } + + /** add standard item to menu with UObject delegate */ + template< class UserClass > + FORCEINLINE TSharedRef AddMenuItem(TSharedPtr& MenuItem, const FText& Text, UserClass* inObj, typename FShooterMenuItem::FOnConfirmMenuItem::TUObjectMethodDelegate< UserClass >::FMethodPtr inMethod) + { + EnsureValid(MenuItem); + TSharedPtr Item = MakeShareable(new FShooterMenuItem(Text)); + Item->OnConfirmMenuItem.BindUObject(inObj,inMethod); + MenuItem->SubMenu.Add(Item); + return Item.ToSharedRef(); + } + + /** add standard item to menu with TSharedPtr delegate */ + template< class UserClass > + FORCEINLINE TSharedRef AddMenuItemSP(TSharedPtr& MenuItem, const FText& Text, UserClass* inObj, typename FShooterMenuItem::FOnConfirmMenuItem::TSPMethodDelegate< UserClass >::FMethodPtr inMethod) + { + EnsureValid(MenuItem); + TSharedPtr Item = MakeShareable(new FShooterMenuItem(Text)); + Item->OnConfirmMenuItem.BindSP(inObj,inMethod); + MenuItem->SubMenu.Add(Item); + return Item.ToSharedRef(); + } + + + FORCEINLINE TSharedRef AddMenuOption(TSharedPtr& MenuItem, const FText& Text, const TArray& OptionsList) + { + EnsureValid(MenuItem); + TSharedPtr Item = MakeShareable(new FShooterMenuItem(Text, OptionsList)); + MenuItem->SubMenu.Add(Item); + return MenuItem->SubMenu.Last().ToSharedRef(); + } + + /** add multi-choice item to menu with UObject delegate */ + template< class UserClass > + FORCEINLINE TSharedRef AddMenuOption(TSharedPtr& MenuItem, const FText& Text, const TArray& OptionsList, UserClass* inObj, typename FShooterMenuItem::FOnOptionChanged::TUObjectMethodDelegate< UserClass >::FMethodPtr inMethod) + { + EnsureValid(MenuItem); + TSharedPtr Item = MakeShareable(new FShooterMenuItem(Text, OptionsList)); + Item->OnOptionChanged.BindUObject(inObj, inMethod); + MenuItem->SubMenu.Add(Item); + return MenuItem->SubMenu.Last().ToSharedRef(); + } + + /** add multi-choice item to menu with TSharedPtr delegate */ + template< class UserClass > + FORCEINLINE TSharedRef AddMenuOptionSP(TSharedPtr& MenuItem, const FText& Text, const TArray& OptionsList, UserClass* inObj, typename FShooterMenuItem::FOnOptionChanged::TSPMethodDelegate< UserClass >::FMethodPtr inMethod) + { + EnsureValid(MenuItem); + TSharedPtr Item = MakeShareable(new FShooterMenuItem(Text, OptionsList)); + Item->OnOptionChanged.BindSP(inObj, inMethod); + MenuItem->SubMenu.Add(Item); + return MenuItem->SubMenu.Last().ToSharedRef(); + } + + + FORCEINLINE TSharedRef AddExistingMenuItem(TSharedPtr& MenuItem, TSharedRef SubMenuItem) + { + EnsureValid(MenuItem); + MenuItem->SubMenu.Add(SubMenuItem); + return MenuItem->SubMenu.Last().ToSharedRef(); + } + + + FORCEINLINE TSharedRef AddCustomMenuItem(TSharedPtr& MenuItem, TSharedPtr CustomWidget) + { + EnsureValid(MenuItem); + MenuItem->SubMenu.Add(MakeShareable(new FShooterMenuItem(CustomWidget))); + return MenuItem->SubMenu.Last().ToSharedRef(); + } + + FORCEINLINE void ClearSubMenu(TSharedPtr& MenuItem) + { + EnsureValid(MenuItem); + MenuItem->SubMenu.Empty(); + } + + template< class UserClass > + FORCEINLINE void PlaySoundAndCall(UWorld* World, const FSlateSound& Sound, int32 UserIndex, UserClass* inObj, typename FShooterMenuItem::FOnConfirmMenuItem::TSPMethodDelegate< UserClass >::FMethodPtr inMethod) + { + FSlateApplication::Get().PlaySound(Sound, UserIndex); + if (World) + { + const float SoundDuration = FMath::Max(FSlateApplication::Get().GetSoundDuration(Sound), 0.1f); + FTimerHandle DummyHandle; + World->GetTimerManager().SetTimer(DummyHandle, FTimerDelegate::CreateSP(inObj, inMethod), SoundDuration, false); + } + else + { + FTimerDelegate D = FTimerDelegate::CreateSP(inObj, inMethod); + D.ExecuteIfBound(); + } + } + +} diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterOnlineStore.cpp b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterOnlineStore.cpp new file mode 100644 index 0000000..f8f928f --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterOnlineStore.cpp @@ -0,0 +1,444 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "SShooterOnlineStore.h" +#include "SHeaderRow.h" +#include "ShooterStyle.h" +#include "ShooterGameLoadingScreen.h" +#include "ShooterGameInstance.h" +#include "Online/ShooterGameSession.h" +#include "Interfaces/OnlineStoreInterfaceV2.h" +#include "Interfaces/OnlinePurchaseInterface.h" +#include "OnlineSubsystemUtils.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +void SShooterOnlineStore::Construct(const FArguments& InArgs) +{ + PlayerOwner = InArgs._PlayerOwner; + OwnerWidget = InArgs._OwnerWidget; + State = EStoreState::Browsing; + StatusText = FText::GetEmpty(); + BoxWidth = 125; + + ChildSlot + .VAlign(VAlign_Fill) + .HAlign(HAlign_Fill) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBox) + .WidthOverride(600) + .HeightOverride(250) + [ + SAssignNew(OfferListWidget, SListView>) + .ItemHeight(20) + .ListItemsSource(&OfferList) + .SelectionMode(ESelectionMode::Single) + .OnGenerateRow(this, &SShooterOnlineStore::MakeListViewWidget) + .OnSelectionChanged(this, &SShooterOnlineStore::EntrySelectionChanged) + .OnMouseButtonDoubleClick(this,&SShooterOnlineStore::OnListItemDoubleClicked) + .HeaderRow( + SNew(SHeaderRow) + + SHeaderRow::Column("Title").FillWidth(3).DefaultLabel(NSLOCTEXT("OfferList", "OfferTitleColumn", "Offer Title")) + + SHeaderRow::Column("Description").FillWidth(6).DefaultLabel(NSLOCTEXT("OfferList", "OfferDescColumn", "Description")) + + SHeaderRow::Column("Price").FillWidth(2).HAlignCell(HAlign_Right).DefaultLabel(NSLOCTEXT("OfferList", "OfferPriceColumn", "Price")) + + SHeaderRow::Column("Purchased").FillWidth(2).HAlignCell(HAlign_Right).DefaultLabel(NSLOCTEXT("OfferList", "OfferPurchaseColumn", "Purchased?")) + ) + ] + ] + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SOverlay) + +SOverlay::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(SRichTextBlock) + .Text(this, &SShooterOnlineStore::GetBottomText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuServerListTextStyle") + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] + ] + + ]; +} + + +FText SShooterOnlineStore::GetBottomText() const +{ + return StatusText; +} + +/** + * Ticks this widget. Override in derived classes, but always call the parent implementation. + * + * @param InCurrentTime Current absolute real time + * @param InDeltaTime Real time passed since last tick + */ +void SShooterOnlineStore::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) +{ + SCompoundWidget::Tick( AllottedGeometry, InCurrentTime, InDeltaTime ); +} + +/** Returns logged in user */ +TSharedPtr SShooterOnlineStore::GetLoggedInUser() +{ + TSharedPtr UserIdPtr; + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(PlayerOwner->GetWorld()); + if (OnlineSub) + { + IOnlineIdentityPtr IdentityInt = OnlineSub->GetIdentityInterface(); + if (IdentityInt.IsValid()) + { + UserIdPtr = GetFirstSignedInUser(IdentityInt); + } + } + return UserIdPtr; +} + + +/** Starts searching for servers */ +void SShooterOnlineStore::BeginGettingOffers() +{ + if (State != EStoreState::Browsing) + { + UE_LOG(LogOnline, Warning, TEXT("We cannot start getting the offers, the store state is not Browsing (state = %d)"), static_cast(State)); + return; + } + + OfferList.Reset(); + + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(PlayerOwner->GetWorld()); + if (OnlineSub) + { + IOnlineStoreV2Ptr StoreV2Int = OnlineSub->GetStoreV2Interface(); + IOnlinePurchasePtr PurchaseInt = OnlineSub->GetPurchaseInterface(); + if (StoreV2Int.IsValid()) + { + TSharedPtr LoggedInUser = GetLoggedInUser(); + if (!LoggedInUser.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("There's no logged in user")); + return; + } + + TSharedRef UserId = LoggedInUser.ToSharedRef(); + + const FOnQueryOnlineStoreOffersComplete QueryStoreOffersDelegate = FOnQueryOnlineStoreOffersComplete::CreateLambda( + [this, StoreV2Int, PurchaseInt, UserId](bool bWasSuccessful, const TArray& OfferIds, const FString& ErrorString) + { + UE_LOG(LogOnline, Verbose, TEXT("Query store offers completed with status bWasSuccessful=[%d] Error=[%s] OfferIds=[%d]"), bWasSuccessful, ErrorString.IsEmpty() ? TEXT("None") : *ErrorString, OfferIds.Num()); + + if (bWasSuccessful) + { + TArray StoreOffers; + StoreV2Int->GetOffers(StoreOffers); + + UE_LOG(LogOnline, Verbose, TEXT("Found %d offers in cache"), StoreOffers.Num()); + + for (const FOnlineStoreOfferRef& OfferRef : StoreOffers) + { + UE_LOG(LogOnline, Verbose, TEXT(" Offer=[%s] CurrencyCode=[%s] PriceInt=[%d] DisplayPrice=[%s]"), *OfferRef->OfferId, *OfferRef->CurrencyCode, OfferRef->NumericPrice, *OfferRef->GetDisplayPrice().ToString()); + + TSharedPtr NewOffer = MakeShareable(new FStoreEntry());; + NewOffer->OnlineId = OfferRef->OfferId; + NewOffer->Title = OfferRef->Title.IsEmptyOrWhitespace() ? NSLOCTEXT("ShooterOnlineStore", "DefaultOfferTitle", "EmptyTitle") : OfferRef->Title; + NewOffer->Description = OfferRef->Description.IsEmptyOrWhitespace() ? NSLOCTEXT("ShooterOnlineStore", "DefaultOfferDescription", "EmptyDescription") : OfferRef->Description; + NewOffer->Price = OfferRef->GetDisplayPrice().IsEmptyOrWhitespace() ? NSLOCTEXT("ShooterOnlineStore", "DefaultOfferDescription", "EmptyPrice") : OfferRef->GetDisplayPrice(); + + OfferList.Add(NewOffer); + } + } + + OfferListWidget->RequestListRefresh(); + if (OfferList.Num() > 0) + { + OfferListWidget->UpdateSelectionSet(); + OfferListWidget->SetSelection(0, ESelectInfo::OnNavigation); + } + + if (PurchaseInt.IsValid()) + { + const FOnQueryReceiptsComplete QueryReceiptsDelegate = FOnQueryReceiptsComplete::CreateLambda( + [this, PurchaseInt, UserId](const FOnlineError& Result) + { + if (Result.bSucceeded) + { + TArray PurchasedReceipts; + PurchaseInt->GetReceipts(*UserId, PurchasedReceipts); + + TArray OffersPurchased; + for (const FPurchaseReceipt& Receipt : PurchasedReceipts) + { + for (const FPurchaseReceipt::FReceiptOfferEntry& ReceiptOffer : Receipt.ReceiptOffers) + { + OffersPurchased.Add(ReceiptOffer.OfferId); + } + } + + MarkAsPurchased(OffersPurchased); + } + + SetStoreState(EStoreState::Browsing); + }); + + PurchaseInt->QueryReceipts(*UserId, true, QueryReceiptsDelegate); + } + else + { + SetStoreState(EStoreState::Browsing); + } + }); + + SetStoreState(EStoreState::GettingOffers); + StoreV2Int->QueryOffersByFilter(*UserId, FOnlineStoreFilter(), QueryStoreOffersDelegate); + } + else + { + // just for test + for (int32 Idx = 0; Idx < 16; ++Idx) + { + TSharedPtr NewOffer = MakeShareable(new FStoreEntry());; + NewOffer->OnlineId = FUniqueOfferId(FString::Printf(TEXT("FakeOfferId%d"), Idx)); + NewOffer->Title = FText::FromString(FString::Printf(TEXT("Offer #%d"), Idx)); + NewOffer->Description = FText::FromString(FString::Printf(TEXT("Somewhat long description for the offer #%d"), Idx)); + NewOffer->Price = FText::FromString(FString::Printf(TEXT("$%d"), Idx * 2)); + + OfferList.Add(NewOffer); + } + + SetStoreState(EStoreState::Browsing); + + OfferListWidget->RequestListRefresh(); + if (OfferList.Num() > 0) + { + OfferListWidget->UpdateSelectionSet(); + OfferListWidget->SetSelection(0, ESelectInfo::OnNavigation); + } + } + } +} + +void SShooterOnlineStore::SetStoreState(EStoreState NewState) +{ + UE_LOG(LogOnline, Verbose, TEXT("Transitioning the store from state %d to state %d"), static_cast(State), static_cast(NewState)); + State = NewState; + + switch (State) + { + case EStoreState::PurchasingAnOffer: + StatusText = FText(NSLOCTEXT("ShooterOnlineStore", "Status", "Purchasing...")); + break; + + case EStoreState::GettingOffers: + StatusText = FText(NSLOCTEXT("ShooterOnlineStore", "Status", "Checking what's available...")); + break; + + case EStoreState::Browsing: + if (OfferList.Num() == 0) + { + StatusText = FText(NSLOCTEXT("ShooterOnlineStore", "Status", "No offers found - press Space to refresh")); + break; + } + // intended fall-through + default: + StatusText = FText::GetEmpty(); + break; + } +} + +void SShooterOnlineStore::PurchaseOffer() +{ + if (State != EStoreState::Browsing) + { + UE_LOG(LogOnline, Warning, TEXT("We cannot purchase an offer, the store state is not Browsing (state = %d)"), static_cast(State)); + return; + } + + if (SelectedItem.IsValid()) + { + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(PlayerOwner->GetWorld()); + if (OnlineSub) + { + IOnlinePurchasePtr PurchaseInt = OnlineSub->GetPurchaseInterface(); + if (PurchaseInt.IsValid()) + { + TSharedPtr LoggedInUser = GetLoggedInUser(); + if (!LoggedInUser.IsValid()) + { + UE_LOG(LogOnline, Warning, TEXT("There's no logged in user")); + return; + } + + TSharedRef UserId = LoggedInUser.ToSharedRef(); + + FPurchaseCheckoutRequest CheckoutParams; + CheckoutParams.AddPurchaseOffer(FString(), SelectedItem->OnlineId, 1, false); + + + UE_LOG(LogOnline, Verbose, TEXT("Attempting to checkout OfferId %s"), *SelectedItem->OnlineId); + + SetStoreState(EStoreState::PurchasingAnOffer); + PurchaseInt->Checkout(*UserId, CheckoutParams, FOnPurchaseCheckoutComplete::CreateLambda( + [this](const FOnlineError& Result, const TSharedRef& Receipt) + { + UE_LOG(LogOnline, Verbose, TEXT("Checkout completed with status %s"), *Result.ToLogString()); + + TArray OffersPurchased; + for (const FPurchaseReceipt::FReceiptOfferEntry& ReceiptOffer : Receipt->ReceiptOffers) + { + OffersPurchased.Add(ReceiptOffer.OfferId); + } + MarkAsPurchased(OffersPurchased); + + SetStoreState(EStoreState::Browsing); + })); + } + else + { + UE_LOG(LogOnline, Warning, TEXT("IOnlinePurchase interface not available")); + } + } + } +} + +void SShooterOnlineStore::MarkAsPurchased(const TArray & Offers) +{ + for (TSharedPtr StoreOffer : OfferList) + { + for (const FUniqueOfferId& PurchasedOffer : Offers) + { + if (StoreOffer->OnlineId == PurchasedOffer) + { + StoreOffer->bPurchased = true; + } + } + } + + // otherwise the items won't be picked up + OfferListWidget->RebuildList(); +} + + +void SShooterOnlineStore::OnFocusLost( const FFocusEvent& InFocusEvent ) +{ + if (InFocusEvent.GetCause() != EFocusCause::SetDirectly) + { + FSlateApplication::Get().SetKeyboardFocus(SharedThis( this )); + } +} + +FReply SShooterOnlineStore::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) +{ + return FReply::Handled().SetUserFocus(OfferListWidget.ToSharedRef(), EFocusCause::SetDirectly).SetUserFocus(SharedThis(this), EFocusCause::SetDirectly, true); +} + +void SShooterOnlineStore::EntrySelectionChanged(TSharedPtr InItem, ESelectInfo::Type SelectInfo) +{ + SelectedItem = InItem; +} + +void SShooterOnlineStore::OnListItemDoubleClicked(TSharedPtr InItem) +{ + SelectedItem = InItem; + PurchaseOffer(); + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); +} + +void SShooterOnlineStore::MoveSelection(int32 MoveBy) +{ + int32 SelectedItemIndex = OfferList.IndexOfByKey(SelectedItem); + + if (SelectedItemIndex+MoveBy > -1 && SelectedItemIndex+MoveBy < OfferList.Num()) + { + TSharedPtr NewSelectedItem = OfferList[SelectedItemIndex + MoveBy]; + OfferListWidget->SetSelection(NewSelectedItem); + + if (!OfferListWidget->IsItemVisible(NewSelectedItem)) + { + OfferListWidget->RequestScrollIntoView(NewSelectedItem); + } + } +} + +FReply SShooterOnlineStore::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) +{ + if (State != EStoreState::Browsing) // lock input + { + return FReply::Handled(); + } + + FReply Result = FReply::Unhandled(); + const FKey Key = InKeyEvent.GetKey(); + + if (Key == EKeys::Up || Key == EKeys::Gamepad_DPad_Up || Key == EKeys::Gamepad_LeftStick_Up) + { + MoveSelection(-1); + Result = FReply::Handled(); + } + else if (Key == EKeys::Down || Key == EKeys::Gamepad_DPad_Down || Key == EKeys::Gamepad_LeftStick_Down) + { + MoveSelection(1); + Result = FReply::Handled(); + } + else if (Key == EKeys::Enter || Key == EKeys::Virtual_Accept) + { + PurchaseOffer(); + Result = FReply::Handled(); + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); + } + else if (Key == EKeys::SpaceBar || Key == EKeys::Gamepad_FaceButton_Left) + { + BeginGettingOffers(); + } + return Result; +} + +TSharedRef SShooterOnlineStore::MakeListViewWidget(TSharedPtr Item, const TSharedRef& OwnerTable) +{ + class SStoreEntryWidget : public SMultiColumnTableRow< TSharedPtr > + { + public: + SLATE_BEGIN_ARGS(SStoreEntryWidget){} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTable, TSharedPtr InItem) + { + Item = InItem; + SMultiColumnTableRow< TSharedPtr >::Construct(FSuperRowType::FArguments(), InOwnerTable); + } + + TSharedRef GenerateWidgetForColumn(const FName& ColumnName) + { + FText ItemText = FText::GetEmpty(); + if (ColumnName == "Title") + { + ItemText = Item->Title; + } + else if (ColumnName == "Description") + { + ItemText = Item->Description; + } + else if (ColumnName == "Price") + { + ItemText = Item->Price; + } + else if (ColumnName == "Purchased") + { + ItemText = Item->bPurchased ? FText(NSLOCTEXT("OfferList", "PurchasedYes", "Yes")) : FText(NSLOCTEXT("OfferList", "PurchasedNo", "No")); + } + return SNew(STextBlock) + .Text(ItemText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuStoreListTextStyle"); + } + TSharedPtr Item; + }; + return SNew(SStoreEntryWidget, OwnerTable, Item); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterOnlineStore.h b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterOnlineStore.h new file mode 100644 index 0000000..bc5d58a --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterOnlineStore.h @@ -0,0 +1,127 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ShooterGame.h" +#include "SShooterMenuWidget.h" + +class AShooterGameSession; + +struct FStoreEntry +{ + FUniqueOfferId OnlineId; + FText Title; + FText Description; + FText Price; + bool bPurchased; +}; + +//class declare +class SShooterOnlineStore : public SShooterMenuWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterOnlineStore) + {} + + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + SLATE_ARGUMENT(TSharedPtr, OwnerWidget) + + SLATE_END_ARGS() + + /** needed for every widget */ + void Construct(const FArguments& InArgs); + + /** if we want to receive focus */ + virtual bool SupportsKeyboardFocus() const override { return true; } + + /** focus received handler - keep the ActionBindingsList focused */ + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override; + + /** focus lost handler - keep the ActionBindingsList focused */ + virtual void OnFocusLost( const FFocusEvent& InFocusEvent ) override; + + /** key down handler */ + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override; + + /** SListView item double clicked */ + void OnListItemDoubleClicked(TSharedPtr InItem); + + /** creates single item widget, called for every list item */ + TSharedRef MakeListViewWidget(TSharedPtr Item, const TSharedRef& OwnerTable); + + /** selection changed handler */ + void EntrySelectionChanged(TSharedPtr InItem, ESelectInfo::Type SelectInfo); + + /** Starts getting the offers etc */ + void BeginGettingOffers(); + + /** Called when server search is finished */ + void OnGettingOffersFinished(); + + /** fill/update server list, should be called before showing this control */ + void UpdateServerList(); + + /** purchases the chose offer */ + void PurchaseOffer(); + + /** selects item at current + MoveBy index */ + void MoveSelection(int32 MoveBy); + + /** + * Ticks this widget. Override in derived classes, but always call the parent implementation. + * + * @param AllottedGeometry The space allotted for this widget + * @param InCurrentTime Current absolute real time + * @param InDeltaTime Real time passed since last tick + */ + void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ); + +protected: + + enum class EStoreState + { + Browsing, + GettingOffers, + PurchasingAnOffer, + }; + + /** Store state */ + EStoreState State; + + /** Transitions to new store state */ + void SetStoreState(EStoreState NewState); + + /** Marks offers as purchased */ + void MarkAsPurchased(const TArray & Offers); + + /** Returns logged in user */ + TSharedPtr GetLoggedInUser(); + + /** action bindings array */ + TArray< TSharedPtr > OfferList; + + /** action bindings list slate widget */ + TSharedPtr< SListView< TSharedPtr > > OfferListWidget; + + /** currently selected list item */ + TSharedPtr SelectedItem; + + /** get current status text */ + FText GetBottomText() const; + + /** current status text */ + FText StatusText; + + /** size of standard column in pixels */ + int32 BoxWidth; + + /** pointer to our owner PC */ + TWeakObjectPtr PlayerOwner; + + /** pointer to our parent widget */ + TSharedPtr OwnerWidget; +}; + + diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterServerList.cpp b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterServerList.cpp new file mode 100644 index 0000000..dae70d5 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterServerList.cpp @@ -0,0 +1,405 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "SShooterServerList.h" +#include "SHeaderRow.h" +#include "ShooterStyle.h" +#include "ShooterGameLoadingScreen.h" +#include "ShooterGameInstance.h" +#include "Online/ShooterGameSession.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +void SShooterServerList::Construct(const FArguments& InArgs) +{ + PlayerOwner = InArgs._PlayerOwner; + OwnerWidget = InArgs._OwnerWidget; + MapFilterName = "Any"; + bSearchingForServers = false; + bLANMatchSearch = false; + StatusText = FText::GetEmpty(); + BoxWidth = 125; + LastSearchTime = 0.0f; + +#if PLATFORM_SWITCH + MinTimeBetweenSearches = 6.0; +#else + MinTimeBetweenSearches = 0.0; +#endif + + ChildSlot + .VAlign(VAlign_Fill) + .HAlign(HAlign_Fill) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBox) + .WidthOverride(600) + .HeightOverride(300) + [ + SAssignNew(ServerListWidget, SListView>) + .ItemHeight(20) + .ListItemsSource(&ServerList) + .SelectionMode(ESelectionMode::Single) + .OnGenerateRow(this, &SShooterServerList::MakeListViewWidget) + .OnSelectionChanged(this, &SShooterServerList::EntrySelectionChanged) + .OnMouseButtonDoubleClick(this,&SShooterServerList::OnListItemDoubleClicked) + .HeaderRow( + SNew(SHeaderRow) + + SHeaderRow::Column("ServerName").FixedWidth(BoxWidth*2) .DefaultLabel(NSLOCTEXT("ServerList", "ServerNameColumn", "Server Name")) + + SHeaderRow::Column("GameType") .DefaultLabel(NSLOCTEXT("ServerList", "GameTypeColumn", "Game Type")) + + SHeaderRow::Column("Map").DefaultLabel(NSLOCTEXT("ServerList", "MapNameColumn", "Map")) + + SHeaderRow::Column("Players") .DefaultLabel(NSLOCTEXT("ServerList", "PlayersColumn", "Players")) + + SHeaderRow::Column("Ping") .DefaultLabel(NSLOCTEXT("ServerList", "NetworkPingColumn", "Ping"))) + ] + ] + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SOverlay) + +SOverlay::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(SRichTextBlock) + .Text(this, &SShooterServerList::GetBottomText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuServerListTextStyle") + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] + ] + + ]; +} + +/** + * Get the current game session + */ +AShooterGameSession* SShooterServerList::GetGameSession() const +{ + UShooterGameInstance* const GI = Cast(PlayerOwner->GetGameInstance()); + return GI ? GI->GetGameSession() : nullptr; +} + +/** Updates current search status */ +void SShooterServerList::UpdateSearchStatus() +{ + check(bSearchingForServers); // should not be called otherwise + + bool bFinishSearch = true; + AShooterGameSession* ShooterSession = GetGameSession(); + if (ShooterSession) + { + int32 CurrentSearchIdx, NumSearchResults; + EOnlineAsyncTaskState::Type SearchState = ShooterSession->GetSearchResultStatus(CurrentSearchIdx, NumSearchResults); + + UE_LOG(LogOnlineGame, Log, TEXT("ShooterSession->GetSearchResultStatus: %s"), EOnlineAsyncTaskState::ToString(SearchState) ); + + switch(SearchState) + { + case EOnlineAsyncTaskState::InProgress: + StatusText = LOCTEXT("Searching","SEARCHING..."); + bFinishSearch = false; + break; + + case EOnlineAsyncTaskState::Done: + // copy the results + { + ServerList.Empty(); + const TArray & SearchResults = ShooterSession->GetSearchResults(); + check(SearchResults.Num() == NumSearchResults); + if (NumSearchResults == 0) + { +#if PLATFORM_PS4 + StatusText = LOCTEXT("NoServersFound","NO SERVERS FOUND, PRESS SQUARE TO TRY AGAIN"); +#elif SHOOTER_XBOX_STRINGS + StatusText = LOCTEXT("NoServersFound","NO SERVERS FOUND, PRESS X TO TRY AGAIN"); +#elif PLATFORM_SWITCH + StatusText = LOCTEXT("NoServersFound", "NO SERVERS FOUND, PRESS TO TRY AGAIN"); +#else + StatusText = LOCTEXT("NoServersFound","NO SERVERS FOUND, PRESS SPACE TO TRY AGAIN"); +#endif + } + else + { +#if PLATFORM_PS4 + StatusText = LOCTEXT("ServersRefresh","PRESS SQUARE TO REFRESH SERVER LIST"); +#elif SHOOTER_XBOX_STRINGS + StatusText = LOCTEXT("ServersRefresh","PRESS X TO REFRESH SERVER LIST"); +#elif PLATFORM_SWITCH + StatusText = LOCTEXT("ServersRefresh", "PRESS TO REFRESH SERVER LIST"); +#else + StatusText = LOCTEXT("ServersRefresh","PRESS SPACE TO REFRESH SERVER LIST"); +#endif + } + + for (int32 IdxResult = 0; IdxResult < NumSearchResults; ++IdxResult) + { + TSharedPtr NewServerEntry = MakeShareable(new FServerEntry()); + + const FOnlineSessionSearchResult& Result = SearchResults[IdxResult]; + + NewServerEntry->ServerName = Result.Session.OwningUserName; + NewServerEntry->Ping = FString::FromInt(Result.PingInMs); + NewServerEntry->CurrentPlayers = FString::FromInt(Result.Session.SessionSettings.NumPublicConnections + + Result.Session.SessionSettings.NumPrivateConnections + - Result.Session.NumOpenPublicConnections + - Result.Session.NumOpenPrivateConnections); + NewServerEntry->MaxPlayers = FString::FromInt(Result.Session.SessionSettings.NumPublicConnections + + Result.Session.SessionSettings.NumPrivateConnections); + NewServerEntry->SearchResultsIndex = IdxResult; + + Result.Session.SessionSettings.Get(SETTING_GAMEMODE, NewServerEntry->GameType); + Result.Session.SessionSettings.Get(SETTING_MAPNAME, NewServerEntry->MapName); + + ServerList.Add(NewServerEntry); + } + } + break; + + case EOnlineAsyncTaskState::Failed: + // intended fall-through + case EOnlineAsyncTaskState::NotStarted: + StatusText = FText::GetEmpty(); + // intended fall-through + default: + break; + } + } + + if (bFinishSearch) + { + OnServerSearchFinished(); + } +} + + +FText SShooterServerList::GetBottomText() const +{ + return StatusText; +} + +/** + * Ticks this widget. Override in derived classes, but always call the parent implementation. + * + * @param InCurrentTime Current absolute real time + * @param InDeltaTime Real time passed since last tick + */ +void SShooterServerList::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) +{ + SCompoundWidget::Tick( AllottedGeometry, InCurrentTime, InDeltaTime ); + + if ( bSearchingForServers ) + { + UpdateSearchStatus(); + } +} + +/** Starts searching for servers */ +void SShooterServerList::BeginServerSearch(bool bLANMatch, bool bIsDedicatedServer, const FString& InMapFilterName) +{ + double CurrentTime = FApp::GetCurrentTime(); + if (!bLANMatch && CurrentTime - LastSearchTime < MinTimeBetweenSearches) + { + OnServerSearchFinished(); + } + else + { + bLANMatchSearch = bLANMatch; + bDedicatedServer = bIsDedicatedServer; + MapFilterName = InMapFilterName; + bSearchingForServers = true; + ServerList.Empty(); + LastSearchTime = CurrentTime; + + UShooterGameInstance* const GI = Cast(PlayerOwner->GetGameInstance()); + if (GI) + { + GI->FindSessions(PlayerOwner.Get(), bIsDedicatedServer, bLANMatchSearch); + } + } +} + +/** Called when server search is finished */ +void SShooterServerList::OnServerSearchFinished() +{ + bSearchingForServers = false; + + UpdateServerList(); +} + +void SShooterServerList::UpdateServerList() +{ + /** Only filter maps if a specific map is specified */ + if (MapFilterName != "Any") + { + for (int32 i = 0; i < ServerList.Num(); ++i) + { + /** Only filter maps if a specific map is specified */ + if (ServerList[i]->MapName != MapFilterName) + { + ServerList.RemoveAt(i); + i--; + } + } + } + + int32 SelectedItemIndex = ServerList.IndexOfByKey(SelectedItem); + + ServerListWidget->RequestListRefresh(); + if (ServerList.Num() > 0) + { + ServerListWidget->UpdateSelectionSet(); + ServerListWidget->SetSelection(ServerList[SelectedItemIndex > -1 ? SelectedItemIndex : 0],ESelectInfo::OnNavigation); + } + +} + +void SShooterServerList::ConnectToServer() +{ + if (bSearchingForServers) + { + // unsafe + return; + } +#if WITH_EDITOR + if (GIsEditor == true) + { + return; + } +#endif + if (SelectedItem.IsValid()) + { + int ServerToJoin = SelectedItem->SearchResultsIndex; + + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveAllViewportWidgets(); + } + + UShooterGameInstance* const GI = Cast(PlayerOwner->GetGameInstance()); + if (GI) + { + GI->JoinSession(PlayerOwner.Get(), ServerToJoin); + } + } +} + +void SShooterServerList::OnFocusLost( const FFocusEvent& InFocusEvent ) +{ + if (InFocusEvent.GetCause() != EFocusCause::SetDirectly) + { + FSlateApplication::Get().SetKeyboardFocus(SharedThis( this )); + } +} + +FReply SShooterServerList::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) +{ + return FReply::Handled().SetUserFocus(ServerListWidget.ToSharedRef(), EFocusCause::SetDirectly).SetUserFocus(SharedThis(this), EFocusCause::SetDirectly, true); +} + +void SShooterServerList::EntrySelectionChanged(TSharedPtr InItem, ESelectInfo::Type SelectInfo) +{ + SelectedItem = InItem; +} + +void SShooterServerList::OnListItemDoubleClicked(TSharedPtr InItem) +{ + SelectedItem = InItem; + ConnectToServer(); + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); +} + +void SShooterServerList::MoveSelection(int32 MoveBy) +{ + int32 SelectedItemIndex = ServerList.IndexOfByKey(SelectedItem); + + if (SelectedItemIndex+MoveBy > -1 && SelectedItemIndex+MoveBy < ServerList.Num()) + { + ServerListWidget->SetSelection(ServerList[SelectedItemIndex+MoveBy]); + } +} + +FReply SShooterServerList::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) +{ + if (bSearchingForServers) // lock input + { + return FReply::Handled(); + } + + FReply Result = FReply::Unhandled(); + const FKey Key = InKeyEvent.GetKey(); + + if (Key == EKeys::Up || Key == EKeys::Gamepad_DPad_Up || Key == EKeys::Gamepad_LeftStick_Up) + { + MoveSelection(-1); + Result = FReply::Handled(); + } + else if (Key == EKeys::Down || Key == EKeys::Gamepad_DPad_Down || Key == EKeys::Gamepad_LeftStick_Down) + { + MoveSelection(1); + Result = FReply::Handled(); + } + else if (Key == EKeys::Enter || Key == EKeys::Virtual_Accept) + { + ConnectToServer(); + Result = FReply::Handled(); + FSlateApplication::Get().SetKeyboardFocus(SharedThis(this)); + } + //hit space bar to search for servers again / refresh the list, only when not searching already + else if (Key == EKeys::SpaceBar || Key == EKeys::Gamepad_FaceButton_Left) + { + BeginServerSearch(bLANMatchSearch, bDedicatedServer, MapFilterName); + } + return Result; +} + +TSharedRef SShooterServerList::MakeListViewWidget(TSharedPtr Item, const TSharedRef& OwnerTable) +{ + class SServerEntryWidget : public SMultiColumnTableRow< TSharedPtr > + { + public: + SLATE_BEGIN_ARGS(SServerEntryWidget){} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTable, TSharedPtr InItem) + { + Item = InItem; + SMultiColumnTableRow< TSharedPtr >::Construct(FSuperRowType::FArguments(), InOwnerTable); + } + + TSharedRef GenerateWidgetForColumn(const FName& ColumnName) + { + FText ItemText = FText::GetEmpty(); + if (ColumnName == "ServerName") + { + ItemText = FText::FromString(Item->ServerName + "'s Server"); + } + else if (ColumnName == "GameType") + { + ItemText = FText::FromString(Item->GameType); + } + else if (ColumnName == "Map") + { + ItemText = FText::FromString(Item->MapName); + } + else if (ColumnName == "Players") + { + ItemText = FText::Format( FText::FromString("{0}/{1}"), FText::FromString(Item->CurrentPlayers), FText::FromString(Item->MaxPlayers) ); + } + else if (ColumnName == "Ping") + { + ItemText = FText::FromString(Item->Ping); + } + return SNew(STextBlock) + .Text(ItemText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuServerListTextStyle"); + } + TSharedPtr Item; + }; + return SNew(SServerEntryWidget, OwnerTable, Item); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterServerList.h b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterServerList.h new file mode 100644 index 0000000..30a54d1 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/SShooterServerList.h @@ -0,0 +1,138 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ShooterGame.h" +#include "SShooterMenuWidget.h" + +class AShooterGameSession; + +struct FServerEntry +{ + FString ServerName; + FString CurrentPlayers; + FString MaxPlayers; + FString GameType; + FString MapName; + FString Ping; + int32 SearchResultsIndex; +}; + +//class declare +class SShooterServerList : public SShooterMenuWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterServerList) + {} + + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + SLATE_ARGUMENT(TSharedPtr, OwnerWidget) + + SLATE_END_ARGS() + + /** needed for every widget */ + void Construct(const FArguments& InArgs); + + /** if we want to receive focus */ + virtual bool SupportsKeyboardFocus() const override { return true; } + + /** focus received handler - keep the ActionBindingsList focused */ + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override; + + /** focus lost handler - keep the ActionBindingsList focused */ + virtual void OnFocusLost( const FFocusEvent& InFocusEvent ) override; + + /** key down handler */ + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override; + + /** SListView item double clicked */ + void OnListItemDoubleClicked(TSharedPtr InItem); + + /** creates single item widget, called for every list item */ + TSharedRef MakeListViewWidget(TSharedPtr Item, const TSharedRef& OwnerTable); + + /** selection changed handler */ + void EntrySelectionChanged(TSharedPtr InItem, ESelectInfo::Type SelectInfo); + + /** + * Get the current game session + * + * @return The current game session + */ + AShooterGameSession* GetGameSession() const; + + /** Updates current search status */ + void UpdateSearchStatus(); + + /** Starts searching for servers */ + void BeginServerSearch(bool bLANMatch, bool bIsDedicatedServer, const FString& InMapFilterName); + + /** Called when server search is finished */ + void OnServerSearchFinished(); + + /** fill/update server list, should be called before showing this control */ + void UpdateServerList(); + + /** connect to chosen server */ + void ConnectToServer(); + + /** selects item at current + MoveBy index */ + void MoveSelection(int32 MoveBy); + + /** + * Ticks this widget. Override in derived classes, but always call the parent implementation. + * + * @param AllottedGeometry The space allotted for this widget + * @param InCurrentTime Current absolute real time + * @param InDeltaTime Real time passed since last tick + */ + void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ); + +protected: + + /** Whether last searched for LAN (so spacebar works) */ + bool bLANMatchSearch; + + /** Whether last searched for Dedicated Server (so spacebar works) */ + bool bDedicatedServer; + + /** Whether we're searching for servers */ + bool bSearchingForServers; + + /** Time the last search began */ + double LastSearchTime; + + /** Minimum time between searches (platform dependent) */ + double MinTimeBetweenSearches; + + /** action bindings array */ + TArray< TSharedPtr > ServerList; + + /** action bindings list slate widget */ + TSharedPtr< SListView< TSharedPtr > > ServerListWidget; + + /** currently selected list item */ + TSharedPtr SelectedItem; + + /** get current status text */ + FText GetBottomText() const; + + /** current status text */ + FText StatusText; + + /** Map filter name to use during server searches */ + FString MapFilterName; + + /** size of standard column in pixels */ + int32 BoxWidth; + + /** pointer to our owner PC */ + TWeakObjectPtr PlayerOwner; + + /** pointer to our parent widget */ + TSharedPtr OwnerWidget; +}; + + diff --git a/Source/ShooterGame/Private/UI/Menu/Widgets/ShooterMenuItem.h b/Source/ShooterGame/Private/UI/Menu/Widgets/ShooterMenuItem.h new file mode 100644 index 0000000..48f4cc1 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Menu/Widgets/ShooterMenuItem.h @@ -0,0 +1,165 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "SShooterMenuItem.h" + +namespace EShooterMenuItemType +{ + enum Type + { + Root, + Standard, + MultiChoice, + CustomWidget, + }; +}; + +/** TArray< TSharedPtr > */ +typedef TArray< TSharedPtr > MenuPtr; + +class FShooterMenuInfo +{ +public: + /** menu items array */ + MenuPtr Menu; + + /** last selection in this menu */ + int32 SelectedIndex; + + /** menu title */ + FText MenuTitle; + + /** constructor making filling required information easy */ + FShooterMenuInfo(MenuPtr _Menu, int32 _SelectedIndex, FText _MenuTitle) + { + Menu = _Menu; + SelectedIndex = _SelectedIndex; + MenuTitle = MoveTemp(_MenuTitle); + } +}; + +class FShooterMenuItem : public TSharedFromThis +{ +public: + /** confirm menu item delegate */ + DECLARE_DELEGATE(FOnConfirmMenuItem); + + /** view profile delegate */ + DECLARE_DELEGATE(FOnControllerFacebuttonLeftPressed); + + /** Increment friend index counter delegate */ + DECLARE_DELEGATE(FOnControllerDownInputPressed); + + /** Decrement friend index counter delegate */ + DECLARE_DELEGATE(FOnControllerUpInputPressed); + + /** Send friend invite delegate */ + DECLARE_DELEGATE(FOnOnControllerFacebuttonDownPressed); + + /** multi-choice option changed, parameters are menu item itself and new multi-choice index */ + DECLARE_DELEGATE_TwoParams(FOnOptionChanged, TSharedPtr, int32); + + /** delegate, which is executed by SShooterMenuWidget if user confirms this menu item */ + FOnConfirmMenuItem OnConfirmMenuItem; + + /** multi-choice option changed, parameters are menu item itself and new multi-choice index */ + FOnOptionChanged OnOptionChanged; + + /** delegate, which is executed by SShooterMenuWidget if user presses FacebuttonLeft */ + FOnControllerFacebuttonLeftPressed OnControllerFacebuttonLeftPressed; + + /** delegate, which is executed by SShooterMenuWidget if user presses ControllerDownInput */ + FOnControllerDownInputPressed OnControllerDownInputPressed; + + /** delegate, which is executed by SShooterMenuWidget if user presses ControllerUpInput */ + FOnControllerUpInputPressed OnControllerUpInputPressed; + + /** delegate, which is executed by SShooterMenuWidget if user presses FacebuttonDown */ + FOnOnControllerFacebuttonDownPressed OnControllerFacebuttonDownPressed; + + /** menu item type */ + EShooterMenuItemType::Type MenuItemType; + + /** if this menu item will be created when menu is opened */ + bool bVisible; + + /** sub menu if present */ + TArray< TSharedPtr > SubMenu; + + /** shared pointer to actual slate widget representing the menu item */ + TSharedPtr Widget; + + /** shared pointer to actual slate widget representing the custom menu item, ie whole options screen */ + TSharedPtr CustomWidget; + + /** texts for multiple choice menu item (like INF AMMO ON/OFF or difficulty/resolution etc) */ + TArray MultiChoice; + + /** set to other value than -1 to limit the options range */ + int32 MinMultiChoiceIndex; + + /** set to other value than -1 to limit the options range */ + int32 MaxMultiChoiceIndex; + + /** selected multi-choice index for this menu item */ + int32 SelectedMultiChoice; + + /** constructor accepting menu item text */ + FShooterMenuItem(FText _text) + { + bVisible = true; + Text = MoveTemp(_text); + MenuItemType = EShooterMenuItemType::Standard; + } + + /** custom widgets cannot contain sub menus, all functionality must be handled by custom widget itself */ + FShooterMenuItem(TSharedPtr _Widget) + { + bVisible = true; + MenuItemType = EShooterMenuItemType::CustomWidget; + CustomWidget = _Widget; + } + + /** constructor for multi-choice item */ + FShooterMenuItem(FText _text, TArray _choices, int32 DefaultIndex=0) + { + bVisible = true; + Text = MoveTemp(_text); + MenuItemType = EShooterMenuItemType::MultiChoice; + MultiChoice = MoveTemp(_choices); + MinMultiChoiceIndex = MaxMultiChoiceIndex = -1; + SelectedMultiChoice = DefaultIndex; + } + + const FText& GetText() const + { + return Text; + } + + void SetText(FText UpdatedText) + { + Text = MoveTemp(UpdatedText); + if (Widget.IsValid()) + { + Widget->UpdateItemText(Text); + } + } + + /** create special root item */ + static TSharedRef CreateRoot() + { + return MakeShareable(new FShooterMenuItem()); + } + +private: + + /** menu item text */ + FText Text; + + FShooterMenuItem() + { + bVisible = false; + MenuItemType = EShooterMenuItemType::Root; + } +}; \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/ShooterHUD.cpp b/Source/ShooterGame/Private/UI/ShooterHUD.cpp new file mode 100644 index 0000000..c9dc756 --- /dev/null +++ b/Source/ShooterGame/Private/UI/ShooterHUD.cpp @@ -0,0 +1,1181 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "UI/ShooterHUD.h" +#include "SShooterScoreboardWidget.h" +#include "SChatWidget.h" +#include "Engine/ViewportSplitScreen.h" +#include "Weapons/ShooterWeapon.h" +#include "Weapons/ShooterDamageType.h" +#include "Weapons/ShooterWeapon_Instant.h" +#include "Online/ShooterPlayerState.h" +#include "Misc/NetworkVersion.h" +#include "OnlineSubsystemUtils.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.HUD.Menu" + +const float AShooterHUD::MinHudScale = 0.5f; + +AShooterHUD::AShooterHUD(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + NoAmmoFadeOutTime = 1.0f; + HitNotifyDisplayTime = 0.75f; + KillFadeOutTime = 2.0f; + LastEnemyHitDisplayTime = 0.2f; + NoAmmoNotifyTime = -NoAmmoFadeOutTime; + LastKillTime = - KillFadeOutTime; + LastEnemyHitTime = -LastEnemyHitDisplayTime; + + OnPlayerTalkingStateChangedDelegate = FOnPlayerTalkingStateChangedDelegate::CreateUObject(this, &AShooterHUD::OnPlayerTalkingStateChanged); + + static ConstructorHelpers::FObjectFinder HitTextureOb(TEXT("/Game/UI/HUD/HitIndicator")); + static ConstructorHelpers::FObjectFinder HUDMainTextureOb(TEXT("/Game/UI/HUD/HUDMain")); + static ConstructorHelpers::FObjectFinder HUDAssets02TextureOb(TEXT("/Game/UI/HUD/HUDAssets02")); + static ConstructorHelpers::FObjectFinder LowHealthOverlayTextureOb(TEXT("/Game/UI/HUD/LowHealthOverlay")); + + // Fonts are not included in dedicated server builds. + #if !UE_SERVER + { + static ConstructorHelpers::FObjectFinder BigFontOb(TEXT("/Game/UI/HUD/Roboto51")); + static ConstructorHelpers::FObjectFinder NormalFontOb(TEXT("/Game/UI/HUD/Roboto18")); + BigFont = BigFontOb.Object; + NormalFont = NormalFontOb.Object; + } + #endif //!UE_SERVER + + HitNotifyTexture = HitTextureOb.Object; + HUDMainTexture = HUDMainTextureOb.Object; + HUDAssets02Texture = HUDAssets02TextureOb.Object; + LowHealthOverlayTexture = LowHealthOverlayTextureOb.Object; + + HitNotifyIcon[EShooterHudPosition::Left] = UCanvas::MakeIcon(HitNotifyTexture, 158, 831, 585, 392); + HitNotifyIcon[EShooterHudPosition::FrontLeft] = UCanvas::MakeIcon(HitNotifyTexture, 369, 434, 460, 378); + HitNotifyIcon[EShooterHudPosition::Front] = UCanvas::MakeIcon(HitNotifyTexture, 848, 284, 361, 395); + HitNotifyIcon[EShooterHudPosition::FrontRight] = UCanvas::MakeIcon(HitNotifyTexture, 1212, 397, 427, 394); + HitNotifyIcon[EShooterHudPosition::Right] = UCanvas::MakeIcon(HitNotifyTexture, 1350, 844, 547, 321); + HitNotifyIcon[EShooterHudPosition::BackRight] = UCanvas::MakeIcon(HitNotifyTexture, 1232, 1241, 458, 341); + HitNotifyIcon[EShooterHudPosition::Back] = UCanvas::MakeIcon(HitNotifyTexture, 862, 1384, 353, 408); + HitNotifyIcon[EShooterHudPosition::BackLeft] = UCanvas::MakeIcon(HitNotifyTexture, 454, 1251, 371, 410); + + KillsBg = UCanvas::MakeIcon(HUDMainTexture, 15, 16, 235, 62); + TimePlaceBg = UCanvas::MakeIcon(HUDMainTexture, 262, 16, 255, 62); + PrimaryWeapBg = UCanvas::MakeIcon(HUDMainTexture, 543, 17, 441, 81); + SecondaryWeapBg = UCanvas::MakeIcon(HUDMainTexture, 676, 111, 293, 50); + + DeathMessagesBg = UCanvas::MakeIcon(HUDMainTexture, 502, 177, 342, 187); + HealthBar = UCanvas::MakeIcon(HUDAssets02Texture, 67, 212, 372, 50); + HealthBarBg = UCanvas::MakeIcon(HUDAssets02Texture, 67, 162, 372, 50); + + HealthIcon = UCanvas::MakeIcon(HUDAssets02Texture, 78, 262, 28, 28); + KillsIcon = UCanvas::MakeIcon(HUDMainTexture, 318, 93, 24, 24); + TimerIcon = UCanvas::MakeIcon(HUDMainTexture, 381, 93, 24, 24); + KilledIcon = UCanvas::MakeIcon(HUDMainTexture, 425, 92, 38, 36); + PlaceIcon = UCanvas::MakeIcon(HUDMainTexture, 250, 468, 21, 28); + + Crosshair[EShooterCrosshairDirection::Left] = UCanvas::MakeIcon(HUDMainTexture, 43, 402, 25, 9); // left + Crosshair[EShooterCrosshairDirection::Right] = UCanvas::MakeIcon(HUDMainTexture, 88, 402, 25, 9); // right + Crosshair[EShooterCrosshairDirection::Top] = UCanvas::MakeIcon(HUDMainTexture, 74, 371, 9, 25); // top + Crosshair[EShooterCrosshairDirection::Bottom] = UCanvas::MakeIcon(HUDMainTexture, 74, 415, 9, 25); // bottom + Crosshair[EShooterCrosshairDirection::Center] = UCanvas::MakeIcon(HUDMainTexture, 75, 403, 7, 7); // center + + Offsets[EShooterHudPosition::Left] = FVector2D(173,0); + Offsets[EShooterHudPosition::FrontLeft] = FVector2D(120,125); + Offsets[EShooterHudPosition::Front] = FVector2D(0,173); + Offsets[EShooterHudPosition::FrontRight] = FVector2D(-120,125); + Offsets[EShooterHudPosition::Right] = FVector2D(-173,0); + Offsets[EShooterHudPosition::BackRight] = FVector2D(-120,-125); + Offsets[EShooterHudPosition::Back] = FVector2D(0,-173); + Offsets[EShooterHudPosition::BackLeft] = FVector2D(120,-125); + + + HitNotifyCrosshair = UCanvas::MakeIcon(HUDMainTexture, 54, 453, 50, 50); + + Offset = 20.0f; + HUDLight = FColor(175,202,213,255); + HUDDark = FColor(110,124,131,255); + ShadowedFont.bEnableShadow = true; +} + +void AShooterHUD::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + ConditionalCloseScoreboard(true); + + AShooterPlayerController* ShooterPC = Cast(PlayerOwner); + if (ShooterPC != NULL ) + { + // Reset the ignore input flags, so we can control the camera during warmup + ShooterPC->SetCinematicMode(false,false,false,true,true); + } + + Super::EndPlay(EndPlayReason); +} + +void AShooterHUD::SetMatchState(EShooterMatchState::Type NewState) +{ + MatchState = NewState; +} + +EShooterMatchState::Type AShooterHUD::GetMatchState() const +{ + return MatchState; +} + +FString AShooterHUD::GetTimeString(float TimeSeconds) +{ + // only minutes and seconds are relevant + const int32 TotalSeconds = FMath::Max(0, FMath::TruncToInt(TimeSeconds) % 3600); + const int32 NumMinutes = TotalSeconds / 60; + const int32 NumSeconds = TotalSeconds % 60; + + const FString TimeDesc = FString::Printf(TEXT("%02d:%02d"), NumMinutes, NumSeconds); + return TimeDesc; +} + +void AShooterHUD::DrawWeaponHUD() +{ + AShooterCharacter* MyPawn = CastChecked(GetOwningPawn()); + AShooterWeapon* MyWeapon = MyPawn->GetWeapon(); + if (MyWeapon) + { + FCanvasTextItem TextItem( FVector2D::ZeroVector, FText::GetEmpty(), BigFont, HUDDark ); + TextItem.EnableShadow( FLinearColor::Black ); + + //PRIMARY WEAPON + { + const float PriWeapOffsetY = 65; + const float PriWeaponBoxWidth = 150; + + Canvas->SetDrawColor(FColor::White); + const float PriWeapBgPosY = Canvas->ClipY - Canvas->OrgY - (PriWeapOffsetY + PrimaryWeapBg.VL + Offset) * ScaleUI; + + //Weapon draw position + const float PriWeapPosX = Canvas->ClipX - Canvas->OrgX - ((PriWeaponBoxWidth + MyWeapon->PrimaryIcon.UL) / 2.0f + 2 * Offset) * ScaleUI; + const float PriWeapPosY = Canvas->ClipY - Canvas->OrgY - (PriWeapOffsetY + (PrimaryWeapBg.VL + MyWeapon->PrimaryIcon.VL) / 2 + Offset) * ScaleUI; + + //Clip draw position + const float ClipWidth = MyWeapon->PrimaryClipIcon.UL + MyWeapon->PrimaryClipIconOffset * (MyWeapon->AmmoIconsCount-1); + const float BoxWidth = 65.0f; + const float PriClipPosX = PriWeapPosX - (BoxWidth + ClipWidth) * ScaleUI; + const float PriClipPosY = Canvas->ClipY - Canvas->OrgY - (PriWeapOffsetY + (PrimaryWeapBg.VL + MyWeapon->PrimaryClipIcon.VL) / 2 + Offset) * ScaleUI; + + const float LeftCornerWidth = 60; + + FCanvasTileItem TileItem(FVector2D( PriClipPosX - Offset * ScaleUI, PriWeapBgPosY ), PrimaryWeapBg.Texture->Resource, + FVector2D( LeftCornerWidth * ScaleUI, PrimaryWeapBg.VL * ScaleUI ), FLinearColor::White); + MakeUV(PrimaryWeapBg, TileItem.UV0, TileItem.UV1, PrimaryWeapBg.U, PrimaryWeapBg.V, LeftCornerWidth, PrimaryWeapBg.VL); + TileItem.BlendMode = SE_BLEND_Translucent; + Canvas->DrawItem( TileItem ); + + const float RestWidth = Canvas->ClipX - PriClipPosX - LeftCornerWidth * ScaleUI; + TileItem.Position = FVector2D(PriClipPosX - (Offset - LeftCornerWidth) * ScaleUI, PriWeapBgPosY); + TileItem.Size = FVector2D(RestWidth, PrimaryWeapBg.VL * ScaleUI); + MakeUV(PrimaryWeapBg, TileItem.UV0, TileItem.UV1, PrimaryWeapBg.U + PrimaryWeapBg.UL - RestWidth / ScaleUI, PrimaryWeapBg.V, RestWidth / ScaleUI, PrimaryWeapBg.VL); + Canvas->DrawItem( TileItem ); + + //Drawing primary weapon icon, ammo in the clip and total spare ammo numbers + Canvas->DrawIcon(MyWeapon->PrimaryIcon, PriWeapPosX, PriWeapPosY, ScaleUI); + + const float TextOffset = 12; + float SizeX, SizeY; + float TopTextHeight; + FString Text = FString::FromInt(MyWeapon->GetCurrentAmmoInClip()); + + Canvas->StrLen(BigFont, Text, SizeX, SizeY); + + const float TopTextScale = 0.73f; // of 51pt font + const float TopTextPosX = Canvas->ClipX - Canvas->OrgX - (PriWeaponBoxWidth + Offset * 2 + (BoxWidth + SizeX * TopTextScale) / 2.0f) * ScaleUI; + const float TopTextPosY = Canvas->ClipY - Canvas->OrgY - (PriWeapOffsetY + PrimaryWeapBg.VL + Offset - TextOffset / 2.0f) * ScaleUI; + TextItem.Text = FText::FromString( Text ); + TextItem.Scale = FVector2D( TopTextScale * ScaleUI, TopTextScale * ScaleUI ); + TextItem.FontRenderInfo = ShadowedFont; + Canvas->DrawItem( TextItem, TopTextPosX, TopTextPosY ); + TopTextHeight = SizeY * TopTextScale; + Text = FString::FromInt(MyWeapon->GetCurrentAmmo() - MyWeapon->GetCurrentAmmoInClip()); + Canvas->StrLen(BigFont, Text, SizeX, SizeY); + + const float BottomTextScale = 0.49f; // of 51pt font + const float BottomTextPosX = Canvas->ClipX - Canvas->OrgX - (PriWeaponBoxWidth + Offset * 2 + (BoxWidth + SizeX * BottomTextScale) / 2.0f) * ScaleUI; + const float BottomTextPosY = TopTextPosY + (TopTextHeight - 0.8f * TextOffset) * ScaleUI; + TextItem.Text = FText::FromString( Text ); + TextItem.Scale = FVector2D( BottomTextScale*ScaleUI, BottomTextScale * ScaleUI ); + TextItem.FontRenderInfo = ShadowedFont; + Canvas->DrawItem( TextItem, BottomTextPosX, BottomTextPosY ); + + // Drawing clip icons + Canvas->SetDrawColor(FColor::White); + + const float AmmoPerIcon = MyWeapon->GetAmmoPerClip() / MyWeapon->AmmoIconsCount; + for (int32 i = 0; i < MyWeapon->AmmoIconsCount; i++) + { + if ((i+1) * AmmoPerIcon > MyWeapon->GetCurrentAmmoInClip()) + { + const float UsedPerIcon = (i+1) * AmmoPerIcon - MyWeapon->GetCurrentAmmoInClip(); + float PercentLeftInIcon = 0; + if (UsedPerIcon < AmmoPerIcon) + { + PercentLeftInIcon = (AmmoPerIcon - UsedPerIcon) / AmmoPerIcon; + } + const int32 Color = 128 + 128 * PercentLeftInIcon; + Canvas->SetDrawColor(Color, Color, Color, Color); + } + + const float ClipOffset = MyWeapon->PrimaryClipIconOffset * ScaleUI * i; + Canvas->DrawIcon(MyWeapon->PrimaryClipIcon, PriClipPosX + ClipOffset, PriClipPosY, ScaleUI); + } + Canvas->SetDrawColor(HUDDark); + } + // + + //SECONDARY WEAPON + AShooterWeapon* SecondaryWeapon = NULL; + for (int32 i=0; i < MyPawn->GetInventoryCount(); i++) + { + if (MyPawn->GetInventoryWeapon(i) != MyWeapon) + { + SecondaryWeapon = MyPawn->GetInventoryWeapon(i); + break; + } + } + if (SecondaryWeapon) + { + Canvas->SetDrawColor(FColor::White); + //offsets + const float SecWeapOffsetY = 0; + const float SecWeaponBoxWidth = 120; + + //background positioning + const float SecWeapBgPosX = Canvas->ClipX - Canvas->OrgX - (SecondaryWeapBg.UL + Offset) * ScaleUI; + const float SecWeapBgPosY = Canvas->ClipY - Canvas->OrgY - (SecondaryWeapBg.VL + Offset) * ScaleUI; + + //weapon draw position + const float SecWeapPosX = Canvas->ClipX - Canvas->OrgX - ((SecWeaponBoxWidth + SecondaryWeapon->SecondaryIcon.UL) / 2.0f + 2 * Offset) * ScaleUI; + const float SecWeapPosY = Canvas->ClipY - Canvas->OrgY - (SecWeapOffsetY + (SecondaryWeapBg.VL + SecondaryWeapon->SecondaryIcon.VL) / 2.0f + Offset) * ScaleUI; + + //secondary clip draw position + const float SecClipWidth = SecondaryWeapon->SecondaryClipIcon.UL + SecondaryWeapon->SecondaryClipIconOffset * (SecondaryWeapon->AmmoIconsCount-1); + const float SecClipBoxWidth = 45.0f; + const float SecClipPosX = Canvas->ClipX - Canvas->OrgX - (SecWeaponBoxWidth + SecClipBoxWidth + SecClipWidth + 2 * Offset) * ScaleUI; + const float SecClipPosY = Canvas->ClipY - Canvas->OrgY - (SecWeapOffsetY + (SecondaryWeapBg.VL + SecondaryWeapon->SecondaryClipIcon.VL) / 2.0f + Offset) * ScaleUI; + + //draw background in two parts to match number of clip icons + const float LeftCornerWidth = 38; + FCanvasTileItem TileItem(FVector2D( SecClipPosX - Offset * ScaleUI, SecWeapBgPosY ), SecondaryWeapBg.Texture->Resource, + FVector2D( LeftCornerWidth * ScaleUI, SecondaryWeapBg.VL * ScaleUI ), FLinearColor::White); + MakeUV(SecondaryWeapBg, TileItem.UV0, TileItem.UV1, SecondaryWeapBg.U, SecondaryWeapBg.V, LeftCornerWidth, SecondaryWeapBg.VL); + TileItem.BlendMode = SE_BLEND_Translucent; + Canvas->DrawItem(TileItem); + + const float RestWidth = Canvas->ClipX - SecClipPosX - LeftCornerWidth * ScaleUI; + TileItem.Position = FVector2D(SecClipPosX - (Offset - LeftCornerWidth) * ScaleUI, SecWeapBgPosY); + TileItem.Size = FVector2D(RestWidth, SecondaryWeapBg.VL * ScaleUI); + MakeUV(SecondaryWeapBg, TileItem.UV0, TileItem.UV1, SecondaryWeapBg.U + SecondaryWeapBg.UL - RestWidth / ScaleUI, SecondaryWeapBg.V, RestWidth / ScaleUI, SecondaryWeapBg.VL); + Canvas->DrawItem(TileItem); + + /** Drawing secondary clip **/ + const float AmmoPerIcon = SecondaryWeapon->GetAmmoPerClip() / SecondaryWeapon->AmmoIconsCount; + for (int32 i = 0; i < SecondaryWeapon->AmmoIconsCount; i++) + { + if ((i+1) * AmmoPerIcon > SecondaryWeapon->GetCurrentAmmoInClip()) + { + const float UsedPerIcon = (i+1) * AmmoPerIcon - SecondaryWeapon->GetCurrentAmmoInClip(); + float PercentLeftInIcon = 0; + if (UsedPerIcon < AmmoPerIcon) + { + PercentLeftInIcon = (AmmoPerIcon - UsedPerIcon) / AmmoPerIcon; + } + const int32 Color = 128 + 128 * PercentLeftInIcon; + Canvas->SetDrawColor(Color, Color, Color, Color); + } + + const float ClipOffset = SecondaryWeapon->SecondaryClipIconOffset * ScaleUI * i; + Canvas->DrawIcon(SecondaryWeapon->SecondaryClipIcon, SecClipPosX + ClipOffset, SecClipPosY, ScaleUI); + } + + //Drawing secondary weapon icon, ammo in the clip and total ammo numbers + Canvas->SetDrawColor(FColor::White); + Canvas->DrawIcon(SecondaryWeapon->SecondaryIcon, SecWeapPosX, SecWeapPosY, ScaleUI); + + const float TextOffset = 10; + float SizeX, SizeY; + float TopTextHeight; + FString Text = FString::FromInt(SecondaryWeapon->GetCurrentAmmo()); + + Canvas->StrLen(BigFont,Text, SizeX, SizeY); + const float TopTextScale = 0.53f; // of 51pt font + TopTextHeight = SizeY * TopTextScale; + + const float TopTextPosX = Canvas->ClipX - Canvas->OrgX - (SecWeaponBoxWidth + Offset * 2 + (SecClipBoxWidth + SizeX * TopTextScale) / 2.0f) * ScaleUI; + const float TopTextPosY = SecWeapBgPosY + (SecondaryWeapBg.VL - TopTextHeight) / 2.0f * ScaleUI; + + TextItem.Text = FText::FromString( Text ); + TextItem.Scale = FVector2D( TopTextScale * ScaleUI, TopTextScale * ScaleUI ); + Canvas->DrawItem( TextItem, TopTextPosX, TopTextPosY ); + } + // END OF SECONDARY WEAPON + } +} + +void AShooterHUD::DrawHealth() +{ + AShooterCharacter* MyPawn = Cast(GetOwningPawn()); + Canvas->SetDrawColor(FColor::White); + const float HealthPosX = (Canvas->ClipX - HealthBarBg.UL * ScaleUI) / 2; + const float HealthPosY = Canvas->ClipY - (Offset + HealthBarBg.VL) * ScaleUI; + Canvas->DrawIcon(HealthBarBg, HealthPosX, HealthPosY, ScaleUI); + const float HealthAmount = FMath::Min(1.0f,MyPawn->Health / MyPawn->GetMaxHealth()); + + FCanvasTileItem TileItem(FVector2D(HealthPosX,HealthPosY), HealthBar.Texture->Resource, + FVector2D(HealthBar.UL * HealthAmount * ScaleUI, HealthBar.VL * ScaleUI), FLinearColor::White); + MakeUV(HealthBar, TileItem.UV0, TileItem.UV1, HealthBar.U, HealthBar.V, HealthBar.UL * HealthAmount, HealthBar.VL); + TileItem.BlendMode = SE_BLEND_Translucent; + Canvas->DrawItem(TileItem); + + Canvas->DrawIcon(HealthIcon,HealthPosX + Offset * ScaleUI, HealthPosY + (HealthBar.VL - HealthIcon.VL) / 2.0f * ScaleUI, ScaleUI); +} + +void AShooterHUD::DrawMatchTimerAndPosition() +{ + AShooterGameState* const MyGameState = GetWorld()->GetGameState(); + Canvas->SetDrawColor(FColor::White); + const float TimerPosX = Canvas->ClipX - Canvas->OrgX - (TimePlaceBg.UL + Offset) * ScaleUI; + const float TimerPosY = Canvas->OrgY + Offset * ScaleUI; + if (MyGameState && MatchState == EShooterMatchState::Playing) + { + Canvas->DrawIcon(TimePlaceBg, TimerPosX, TimerPosY, ScaleUI); + Canvas->DrawIcon(TimerIcon, TimerPosX + Offset * ScaleUI, TimerPosY + ((TimePlaceBg.VL - TimerIcon.VL ) / 2) * ScaleUI, ScaleUI); + } + // match timer + if (MyGameState && MyGameState->RemainingTime > 0) + { + FCanvasTextItem TextItem( FVector2D::ZeroVector, FText::GetEmpty(), BigFont, HUDDark ); + TextItem.EnableShadow( FLinearColor::Black ); + float SizeX, SizeY; + float TextScale = 0.57f; + FString Text; + TextItem.FontRenderInfo = ShadowedFont; + TextItem.Scale = FVector2D( TextScale*ScaleUI, TextScale*ScaleUI ); + if (MyGameState->GetMatchState() == MatchState::WaitingToStart) + { + TextItem.Scale = FVector2D( ScaleUI, ScaleUI ); + Text = LOCTEXT("WarmupString","MATCH STARTS IN: ").ToString() + FString::FromInt(MyGameState->RemainingTime); + TextItem.SetColor( HUDLight ); + TextItem.Text = FText::FromString( Text ); + AddMatchInfoString(TextItem); + } + else if (MyGameState->GetMatchState() == MatchState::InProgress) + { + Text = GetTimeString(MyGameState->RemainingTime); + Canvas->StrLen(BigFont, Text, SizeX, SizeY); + + TextItem.SetColor( HUDDark ); + TextItem.Text = FText::FromString( Text ); + TextItem.Position = FVector2D( TimerPosX + Offset * 1.5f * ScaleUI + TimerIcon.UL * ScaleUI, + TimerPosY + (TimePlaceBg.VL * ScaleUI - SizeY * TextScale * ScaleUI) / 2 ); + Canvas->DrawItem(TextItem); + } + + float BoxWidth = 45.0f * ScaleUI; + Text = FString(); + AShooterPlayerController* MyPC = Cast(PlayerOwner); + if (MyPC && MyGameState && MatchState == EShooterMatchState::Playing) + { + AShooterPlayerState* MyPlayerState = Cast(MyPC->PlayerState); + if (MyPlayerState) + { + if (MyGameState->NumTeams > 1) // team based game + { + int32 MyTeam = MyPlayerState->GetTeamNum(); + int32 MyPos = FMath::Max(1, MyGameState->TeamScores.Num()); + for (int32 i=0; i < MyGameState->TeamScores.Num(); i++) + { + if (MyGameState->TeamScores.Num() > MyTeam && + MyGameState->TeamScores[MyTeam] >= MyGameState->TeamScores[i] && MyTeam != i) + { + MyPos--; + } + } + int32 NumTeams = 0; + for (int32 i=0; i < MyGameState->NumTeams; i++) + { + RankedPlayerMap PlayerStateMap; + MyGameState->GetRankedMap(i,PlayerStateMap); + if(PlayerStateMap.Num() > 0) + { + NumTeams++; + } + } + Text = FString::Printf(TEXT("%d/%d"), MyPos, NumTeams); + } + else // free for all + { + RankedPlayerMap PlayerStateMap; + MyGameState->GetRankedMap(0,PlayerStateMap); + const int32* MyRank = PlayerStateMap.FindKey(MyPlayerState); + int32 MyPos = MyRank ? *MyRank + 1 : 0; + Text = FString::Printf(TEXT("%d/%d"), MyPos, PlayerStateMap.Num()); + } + Canvas->StrLen(BigFont, Text, SizeX, SizeY); + Canvas->DrawIcon(PlaceIcon, + Canvas->ClipX - Canvas->OrgX - BoxWidth - (SizeX * TextScale + PlaceIcon.UL + Offset/4) * ScaleUI, + TimerPosY + (TimePlaceBg.VL - PlaceIcon.VL) / 2.0f * ScaleUI, ScaleUI); + + TextItem.Text = FText::FromString( Text); + TextItem.Scale = FVector2D(TextScale*ScaleUI, TextScale*ScaleUI); + TextItem.FontRenderInfo = ShadowedFont; + Canvas->DrawItem( TextItem, Canvas->ClipX - Canvas->OrgX - (BoxWidth + SizeX * TextScale * ScaleUI), + TimerPosY + (TimePlaceBg.VL * ScaleUI - SizeY * TextScale * ScaleUI) / 2 ); + } + } + } +} + +void AShooterHUD::DrawKills() +{ + AShooterPlayerController* MyPC = Cast(PlayerOwner); + AShooterPlayerState* MyPlayerState = Cast(MyPC->PlayerState); + + if (!MyPlayerState) + return; + + Canvas->SetDrawColor(FColor::White); + float KillsPosX = Canvas->OrgX + Offset * ScaleUI; + float KillsPosY = Canvas->OrgY + Offset * ScaleUI; + Canvas->DrawIcon(KillsBg, KillsPosX, KillsPosY, ScaleUI); + + Canvas->DrawIcon(KillsIcon, KillsPosX + Offset * ScaleUI, KillsPosY + ((KillsBg.VL - KillsIcon.VL ) / 2) * ScaleUI, ScaleUI); + float TextScale = 0.57f; + FCanvasTextItem TextItem( FVector2D::ZeroVector, FText::GetEmpty(), BigFont, HUDDark ); + TextItem.EnableShadow( FLinearColor::Black ); + + float SizeX, SizeY; + FString Text = LOCTEXT("Kills", "KILLS:").ToString(); + Canvas->StrLen(BigFont, Text, SizeX, SizeY); + + TextItem.Text = FText::FromString( Text ); + TextItem.Scale = FVector2D( TextScale * ScaleUI, TextScale * ScaleUI ); + TextItem.FontRenderInfo = ShadowedFont; + TextItem.SetColor(HUDDark); + Canvas->DrawItem( TextItem, KillsPosX + Offset * ScaleUI + KillsIcon.UL * 1.5f * ScaleUI, + KillsPosY + (KillsBg.VL * ScaleUI - SizeY * TextScale * ScaleUI) / 2 ); + + if (MyPlayerState) + { + Text = FString::FromInt(MyPlayerState->GetKills()); + } + else + { + Text = FString("0"); + } + TextScale = 0.88f; + float BoxWidth = 135.0f * ScaleUI; + Canvas->StrLen(BigFont, Text, SizeX, SizeY); + TextItem.Text = FText::FromString( Text ); + TextItem.Scale = FVector2D( TextScale * ScaleUI, TextScale * ScaleUI ); + Canvas->DrawItem( TextItem, KillsPosX + KillsBg.UL * ScaleUI - (BoxWidth + SizeX * TextScale * ScaleUI) /2, + KillsPosY + (KillsBg.VL* ScaleUI - SizeY * TextScale * ScaleUI) / 2 ); + +} + +void AShooterHUD::NotifyOutOfAmmo() +{ + NoAmmoNotifyTime = GetWorld()->GetTimeSeconds(); +} + +void AShooterHUD::NotifyEnemyHit() +{ + LastEnemyHitTime = GetWorld()->GetTimeSeconds(); +} + +void AShooterHUD::DrawHUD() +{ + Super::DrawHUD(); + if (Canvas == nullptr) + { + return; + } + ScaleUI = Canvas->ClipY / 1080.0f; + + // make any adjustments for splitscreen + int32 SSPlayerIndex = 0; + if ( PlayerOwner && PlayerOwner->IsSplitscreenPlayer(&SSPlayerIndex) ) + { + ULocalPlayer* const LP = Cast(PlayerOwner->Player); + if (LP) + { + const ESplitScreenType::Type SSType = LP->ViewportClient->GetCurrentSplitscreenConfiguration(); + + if ( (SSType == ESplitScreenType::TwoPlayer_Horizontal) || + (SSType == ESplitScreenType::ThreePlayer_FavorBottom && SSPlayerIndex == 2) || + (SSType == ESplitScreenType::ThreePlayer_FavorTop && SSPlayerIndex == 0)) + { + // full-width splitscreen viewports can handle same size HUD elements as full-screen viewports + ScaleUI *= 2.f; + } + } + } + + + // Empty the info item array + InfoItems.Empty(); + float TextScale = 1.0f; + // enforce min + ScaleUI = FMath::Max(ScaleUI, MinHudScale); + + AShooterCharacter* MyPawn = Cast(GetOwningPawn()); + if (MyPawn && MyPawn->IsAlive() && MyPawn->Health < MyPawn->GetMaxHealth() * MyPawn->GetLowHealthPercentage()) + { + const float AnimSpeedModifier = 1.0f + 5.0f * (1.0f - MyPawn->Health/(MyPawn->GetMaxHealth() * MyPawn->GetLowHealthPercentage())); + int32 EffectValue = 32 + 72 * (1.0f - MyPawn->Health/(MyPawn->GetMaxHealth() * MyPawn->GetLowHealthPercentage())); + PulseValue += GetWorld()->GetDeltaSeconds() * AnimSpeedModifier; + float EffectAlpha = FMath::Abs(FMath::Sin(PulseValue)); + + float AlphaValue = ( 1.0f / 255.0f ) * ( EffectAlpha * EffectValue ); + + // Full screen low health overlay + Canvas->PopSafeZoneTransform(); + FCanvasTileItem TileItem( FVector2D( 0, 0 ), LowHealthOverlayTexture->Resource, FVector2D( Canvas->ClipX, Canvas->ClipY ), FLinearColor( 1.0f, 0.0f, 0.0f, AlphaValue ) ); + TileItem.BlendMode = SE_BLEND_Translucent; + Canvas->DrawItem( TileItem ); + Canvas->ApplySafeZoneTransform(); + } + + // net mode + if (GetNetMode() != NM_Standalone) + { + FString NetModeDesc = (GetNetMode() == NM_Client) ? TEXT("Client") : TEXT("Server"); + IOnlineSubsystem * OnlineSubsystem = Online::GetSubsystem(GetWorld()); + if(OnlineSubsystem) + { + IOnlineSessionPtr SessionSubsystem = OnlineSubsystem->GetSessionInterface(); + if(SessionSubsystem.IsValid()) + { + FNamedOnlineSession * Session = SessionSubsystem->GetNamedSession(NAME_GameSession); + if(Session && Session->SessionInfo.IsValid()) + { + NetModeDesc += TEXT("\nSession: "); + NetModeDesc += Session->GetSessionIdStr(); + } + } + + } + + NetModeDesc += FString::Printf( TEXT( "\nVersion: %i, %s, %s" ), FNetworkVersion::GetNetworkCompatibleChangelist(), UTF8_TO_TCHAR(__DATE__), UTF8_TO_TCHAR(__TIME__) ); + + DrawDebugInfoString(NetModeDesc, Canvas->OrgX + Offset*ScaleUI, Canvas->OrgY + 5*Offset*ScaleUI, true, true, HUDLight); + } + + DrawMatchTimerAndPosition(); + + float MessageOffset = (Canvas->ClipY / 4.0)* ScaleUI; + if (MatchState == EShooterMatchState::Playing) + { + AShooterPlayerController* MyPC = Cast(PlayerOwner); + if (MyPC) + { + DrawKills(); + } + if (MyPawn && MyPawn->IsAlive()) + { + DrawHealth(); + DrawWeaponHUD(); + } + else + { + // respawn + FString Text = LOCTEXT("WaitingForRespawn", "WAITING FOR RESPAWN").ToString(); + FCanvasTextItem TextItem( FVector2D::ZeroVector, FText::GetEmpty(), BigFont, HUDDark ); + TextItem.EnableShadow( FLinearColor::Black ); + TextItem.Text = FText::FromString( Text ); + TextItem.Scale = FVector2D( TextScale * ScaleUI, TextScale * ScaleUI ); + TextItem.FontRenderInfo = ShadowedFont; + TextItem.SetColor(HUDLight); + AddMatchInfoString(TextItem); + } + + DrawDeathMessages(); + DrawCrosshair(); + DrawHitIndicator(); + + // Draw any recent killed player - cache the used Y coord for later when we draw the large onscreen messages. + MessageOffset = DrawRecentlyKilledPlayer(); + + // No ammo message if required + const float CurrentTime = GetWorld()->GetTimeSeconds(); + if (CurrentTime - NoAmmoNotifyTime >= 0 && CurrentTime - NoAmmoNotifyTime <= NoAmmoFadeOutTime) + { + FString Text = FString(); + + const float Alpha = FMath::Min(1.0f, 1 - (CurrentTime - NoAmmoNotifyTime) / NoAmmoFadeOutTime); + Text = LOCTEXT("NoAmmo", "NO AMMO").ToString(); + + FCanvasTextItem TextItem( FVector2D::ZeroVector, FText::GetEmpty(), BigFont, HUDDark ); + TextItem.EnableShadow( FLinearColor::Black ); + TextItem.Text = FText::FromString( Text ); + TextItem.Scale = FVector2D( TextScale * ScaleUI, TextScale * ScaleUI ); + TextItem.FontRenderInfo = ShadowedFont; + TextItem.SetColor(FLinearColor(0.75f, 0.125f, 0.125f, Alpha )); + AddMatchInfoString(TextItem); + } + } + + // Render the info messages such as wating to respawn - these will be drawn below any 'killed player' message. + ShowInfoItems(MessageOffset, 1.0f); + +} + +void AShooterHUD::DrawDebugInfoString(const FString& Text, float PosX, float PosY, bool bAlignLeft, bool bAlignTop, const FColor& TextColor) +{ +#if !UE_BUILD_SHIPPING + float SizeX, SizeY; + Canvas->StrLen(NormalFont, Text, SizeX, SizeY); + + const float UsePosX = bAlignLeft ? PosX : PosX - SizeX; + const float UsePosY = bAlignTop ? PosY : PosY - SizeY; + + const float BoxPadding = 5.0f; + + FColor DrawColor(HUDDark.R, HUDDark.G, HUDDark.B, HUDDark.A * 0.2f); + const float X = UsePosX - BoxPadding * ScaleUI; + const float Y = UsePosY - BoxPadding * ScaleUI; + // hack in the *2.f scaling for Y since UCanvas::StrLen doesn't take into account newlines + const float SCALE_Y = 3.0f; + + FCanvasTileItem TileItem( FVector2D( X, Y ), FVector2D( (SizeX + BoxPadding * SCALE_Y) * ScaleUI, (SizeY * SCALE_Y + BoxPadding * SCALE_Y) * ScaleUI ), DrawColor ); + TileItem.BlendMode = SE_BLEND_Translucent; + Canvas->DrawItem( TileItem ); + + FCanvasTextItem TextItem( FVector2D( UsePosX, UsePosY), FText::FromString( Text ), NormalFont, TextColor ); + TextItem.EnableShadow( FLinearColor::Black ); + TextItem.FontRenderInfo = ShadowedFont; + TextItem.Scale = FVector2D( ScaleUI, ScaleUI ); + Canvas->DrawItem( TextItem ); +#endif +} + +void AShooterHUD::DrawCrosshair() +{ + AShooterPlayerController* PCOwner = Cast(PlayerOwner); + if (PCOwner) + { + AShooterCharacter* Pawn = Cast(PCOwner->GetPawn()); + if (Pawn && Pawn->GetWeapon() && !Pawn->IsRunning() + && (Pawn->IsTargeting() || (!Pawn->IsTargeting() && !Pawn->GetWeapon()->bHideCrosshairWhileNotAiming))) + { + const float SpreadMulti = 300; + AShooterWeapon_Instant* InstantWeapon = Cast(Pawn->GetWeapon()); + AShooterWeapon* MyWeapon = Pawn->GetWeapon(); + const float CurrentTime = GetWorld()->GetTimeSeconds(); + + float AnimOffset = 0; + if (MyWeapon) + { + const float EquipStartedTime = MyWeapon->GetEquipStartedTime(); + const float EquipDuration = MyWeapon->GetEquipDuration(); + AnimOffset = 300 * (1.0f - FMath::Min(1.0f,(CurrentTime - EquipStartedTime)/EquipDuration)); + } + float CrossSpread = 2 + AnimOffset; + if (InstantWeapon != NULL) + { + CrossSpread += SpreadMulti*FMath::Tan(FMath::DegreesToRadians(InstantWeapon->GetCurrentSpread())); + } + float CenterX = Canvas->ClipX / 2; + float CenterY = Canvas->ClipY / 2; + Canvas->SetDrawColor(255,255,255,192); + + FCanvasIcon* CurrentCrosshair[5]; + for (int32 i=0; i< 5; i++) + { + if (MyWeapon && MyWeapon->UseCustomAimingCrosshair && Pawn->IsTargeting()) + { + CurrentCrosshair[i] = &MyWeapon->AimingCrosshair[i]; + } + else if (MyWeapon && MyWeapon->UseCustomCrosshair) + { + CurrentCrosshair[i] = &MyWeapon->Crosshair[i]; + } + else + { + CurrentCrosshair[i] = &Crosshair[i]; + } + } + + if (Pawn->IsTargeting() && MyWeapon && MyWeapon->UseLaserDot) + { + Canvas->SetDrawColor(255,0,0,192); + Canvas->DrawIcon(*CurrentCrosshair[EShooterCrosshairDirection::Center], + CenterX - (*CurrentCrosshair[EShooterCrosshairDirection::Center]).UL*ScaleUI / 2.0f, + CenterY - (*CurrentCrosshair[EShooterCrosshairDirection::Center]).VL*ScaleUI / 2.0f, ScaleUI); + } + else + { + Canvas->DrawIcon(*CurrentCrosshair[EShooterCrosshairDirection::Center], + CenterX - (*CurrentCrosshair[EShooterCrosshairDirection::Center]).UL*ScaleUI / 2.0f, + CenterY - (*CurrentCrosshair[EShooterCrosshairDirection::Center]).VL*ScaleUI / 2.0f, ScaleUI); + + Canvas->DrawIcon(*CurrentCrosshair[EShooterCrosshairDirection::Left], + CenterX - 1 - (*CurrentCrosshair[EShooterCrosshairDirection::Left]).UL * ScaleUI - CrossSpread * ScaleUI, + CenterY - (*CurrentCrosshair[EShooterCrosshairDirection::Left]).VL*ScaleUI / 2.0f, ScaleUI); + Canvas->DrawIcon(*CurrentCrosshair[EShooterCrosshairDirection::Right], + CenterX + CrossSpread * ScaleUI, + CenterY - (*CurrentCrosshair[EShooterCrosshairDirection::Right]).VL * ScaleUI / 2.0f, ScaleUI); + + Canvas->DrawIcon(*CurrentCrosshair[EShooterCrosshairDirection::Top], + CenterX - (*CurrentCrosshair[EShooterCrosshairDirection::Top]).UL * ScaleUI / 2.0f, + CenterY - 1 - (*CurrentCrosshair[EShooterCrosshairDirection::Top]).VL * ScaleUI - CrossSpread * ScaleUI, ScaleUI); + Canvas->DrawIcon(*CurrentCrosshair[EShooterCrosshairDirection::Bottom], + CenterX - (*CurrentCrosshair[EShooterCrosshairDirection::Bottom]).UL * ScaleUI / 2.0f, + CenterY + CrossSpread * ScaleUI, ScaleUI); + } + + if (CurrentTime - LastEnemyHitTime >= 0 && CurrentTime - LastEnemyHitTime <= LastEnemyHitDisplayTime) + { + const float Alpha = FMath::Min(1.0f, 1 - (CurrentTime - LastEnemyHitTime) / LastEnemyHitDisplayTime); + Canvas->SetDrawColor(255,255,255,255*Alpha); + + Canvas->DrawIcon(HitNotifyCrosshair, + CenterX - HitNotifyCrosshair.UL*ScaleUI / 2.0f, + CenterY - HitNotifyCrosshair.VL*ScaleUI / 2.0f, ScaleUI); + } + } + } +} + +void AShooterHUD::DrawDeathMessages() +{ + if (PlayerOwner == NULL) + { + return; + } + + float OffsetX = 20; + float OffsetY = 20; + + Canvas->SetDrawColor(FColor::White); + float DeathMsgsPosX = Canvas->OrgX + Offset * ScaleUI; + float DeathMsgsPosY = Canvas->ClipY - (OffsetY + DeathMessagesBg.VL) * ScaleUI; + FVector Scale(ScaleUI, ScaleUI, 0.f); + // hardcoded value to make sure the box is big enough to hold 16 W's for both players' names + Scale.X *= 1.85; + Canvas->DrawScaledIcon(DeathMessagesBg, DeathMsgsPosX, DeathMsgsPosY, Scale); + + const FColor BlueTeamColor = FColor(70, 70, 152, 255); + const FColor RedTeamColor = FColor(152, 70, 70, 255); + const FColor OwnerColor = HUDLight; + + const FString KilledText = LOCTEXT("killed"," killed ").ToString(); + FVector2D KilledTextSize(0.0f, 0.0f); + + const float GameTime = GetWorld()->GetTimeSeconds(); + const float LinePadding = 6.0f; + const float BoxPadding = 2.0f; + const float MaxLineX = 300.0f; + const float InitialX = Offset * 2.0f * ScaleUI; + const float InitialY = DeathMsgsPosY + (DeathMessagesBg.VL - Offset * 2.5f) * ScaleUI ; + + // draw messages + float CurrentY = InitialY; + + Canvas->StrLen(NormalFont, KilledText, KilledTextSize.X, KilledTextSize.Y); + + FCanvasTextItem TextItem( FVector2D::ZeroVector, FText::GetEmpty(), NormalFont, HUDDark ); + TextItem.EnableShadow( FLinearColor::Black ); + for (int32 i = DeathMessages.Num() - 1; i >= 0; i--) + { + const FDeathMessage& Message = DeathMessages[i]; + float CurrentX = InitialX; + FVector2D KillerSize(0.0f, 0.0f); + FVector2D VictimNameSize(0.0f, 0.0f); + float TextScale = 1.00f; + Canvas->StrLen(NormalFont, Message.KillerDesc, KillerSize.X, KillerSize.Y); + TextItem.Scale = FVector2D( TextScale * ScaleUI, TextScale * ScaleUI ); + TextItem.FontRenderInfo = ShadowedFont; + TextItem.SetColor(Message.bKillerIsOwner == true ? HUDLight : ( Message.KillerTeamNum == 0 ? RedTeamColor : BlueTeamColor)); + + TextItem.Text = FText::FromString( Message.KillerDesc ); + Canvas->DrawItem(TextItem, CurrentX, CurrentY); + CurrentX += KillerSize.X * TextScale * ScaleUI; + + if (Message.DamageType.IsValid()) + { + Canvas->SetDrawColor(FColor::White); + float ItemSizeY = KilledTextSize.Y * TextScale * ScaleUI; + Canvas->DrawIcon(Message.DamageType->KillIcon, + CurrentX + (OffsetX / 4.0f) * ScaleUI, + CurrentY + (ItemSizeY - Message.DamageType->KillIcon.VL * ScaleUI) / 2.0f, ScaleUI); + CurrentX += (Message.DamageType->KillIcon.UL + OffsetX / 2.0f) * ScaleUI; + } + else + { + TextItem.Text = FText::FromString( KilledText ); + TextItem.Scale = FVector2D( TextScale * ScaleUI, TextScale * ScaleUI ); + TextItem.FontRenderInfo = ShadowedFont; + TextItem.SetColor(HUDDark); + Canvas->DrawItem( TextItem, CurrentX, CurrentY ); + + CurrentX += KilledTextSize.X * TextScale * ScaleUI; + } + + TextItem.SetColor(Message.bVictimIsOwner == true ? HUDLight : (Message.VictimTeamNum == 0 ? RedTeamColor : BlueTeamColor)); + + Canvas->StrLen(NormalFont, Message.VictimDesc, VictimNameSize.X, VictimNameSize.Y); + TextItem.Text = FText::FromString( Message.VictimDesc ); + Canvas->DrawItem( TextItem, CurrentX, CurrentY ); + CurrentY -= (KilledTextSize.Y + LinePadding) * TextScale * ScaleUI; + } +} + +void AShooterHUD::ShowDeathMessage(class AShooterPlayerState* KillerPlayerState, class AShooterPlayerState* VictimPlayerState, const UDamageType* KillerDamageType) +{ + const int32 MaxDeathMessages = 5; + const float MessageDuration = 10.0f; + + if (GetWorld()->GetGameState()) + { + const AShooterGameMode* DefGame = GetWorld()->GetGameState()->GetDefaultGameMode(); + AShooterPlayerState* MyPlayerState = PlayerOwner ? Cast(PlayerOwner->PlayerState) : NULL; + + if (DefGame && KillerPlayerState && VictimPlayerState && MyPlayerState) + { + if (DeathMessages.Num() >= MaxDeathMessages) + { + DeathMessages.RemoveAt(0, 1, false); + } + + FDeathMessage NewMessage; + NewMessage.KillerDesc = KillerPlayerState->GetShortPlayerName(); + NewMessage.VictimDesc = VictimPlayerState->GetShortPlayerName(); + NewMessage.KillerTeamNum = KillerPlayerState->GetTeamNum(); + NewMessage.VictimTeamNum = VictimPlayerState->GetTeamNum(); + NewMessage.bKillerIsOwner = MyPlayerState == KillerPlayerState; + NewMessage.bVictimIsOwner = MyPlayerState == VictimPlayerState; + + NewMessage.DamageType = MakeWeakObjectPtr(const_cast(Cast(KillerDamageType))); + NewMessage.HideTime = GetWorld()->GetTimeSeconds() + MessageDuration; + + DeathMessages.Add(NewMessage); + if (KillerPlayerState == MyPlayerState && VictimPlayerState != MyPlayerState) + { + LastKillTime = GetWorld()->GetTimeSeconds(); + CenteredKillMessage = FText::FromString(NewMessage.VictimDesc); + } + } + } +} + +void AShooterHUD::NotifyWeaponHit(float DamageTaken, struct FDamageEvent const& DamageEvent, class APawn* PawnInstigator) +{ + const float CurrentTime = GetWorld()->GetTimeSeconds(); + AShooterCharacter* MyPawn = (PlayerOwner) ? Cast(PlayerOwner->GetPawn()) : NULL; + if (MyPawn) + { + if (CurrentTime - LastHitTime > HitNotifyDisplayTime) + { + for (uint8 i = 0; i < 8; i++) + { + HitNotifyData[i].HitPercentage = 0; + } + } + + FVector ImpulseDir; + FHitResult Hit; + DamageEvent.GetBestHitInfo(this, PawnInstigator, Hit, ImpulseDir); + + //check hit vector against pre-defined direction vectors - left, front, right, back + const FVector HitVector = FRotationMatrix(PlayerOwner->GetControlRotation()).InverseTransformVector(-ImpulseDir); + + FVector Dirs2[8] = { + FVector(0,-1,0) /*left*/, + FVector(1,-1,0) /*front left*/, + FVector(1,0,0) /*front*/, + FVector(1,1,0) /*front right*/, + FVector(0,1,0) /*right*/, + FVector(-1,1,0) /*back right*/, + FVector(-1,0,0), /*back*/ + FVector(-1,-1,0) /*back left*/ + }; + int32 DirIndex = -1; + float HighestModifier = 0; + + for (uint8 i = 0; i < 8; i++) + { + //Normalize our direction vectors + Dirs2[i].Normalize(); + const float DirModifier = FMath::Max(0.0f, FVector::DotProduct(Dirs2[i], HitVector)); + if (DirModifier > HighestModifier) + { + DirIndex = i; + HighestModifier = DirModifier; + } + } + if (DirIndex > -1) + { + const float DamageTakenPercentage = (DamageTaken / MyPawn->Health); + HitNotifyData[DirIndex].HitPercentage += DamageTakenPercentage * 2; + HitNotifyData[DirIndex].HitPercentage = FMath::Clamp(HitNotifyData[DirIndex].HitPercentage, 0.0f, 1.0f); + HitNotifyData[DirIndex].HitTime = CurrentTime; + } + + } + + LastHitTime = CurrentTime; +} + + +void AShooterHUD::DrawHitIndicator() +{ + const float CurrentTime = GetWorld()->GetTimeSeconds(); + if (CurrentTime - LastHitTime <= HitNotifyDisplayTime) + { + const float StartX = Canvas->ClipX / 2.0f; + const float StartY = Canvas->ClipY / 2.0f; + + for (uint8 i = 0; i < 8; i++) + { + const float TimeModifier = FMath::Max(0.0f, 1 - (CurrentTime - HitNotifyData[i].HitTime) / HitNotifyDisplayTime); + const float Alpha = TimeModifier * HitNotifyData[i].HitPercentage; + Canvas->SetDrawColor(255, 255, 255, FMath::Clamp(FMath::TruncToInt(Alpha * 255 * 1.5f), 0, 255)); + Canvas->DrawIcon(HitNotifyIcon[i], + StartX + (HitNotifyIcon[i].U - HitNotifyTexture->GetSizeX() / 2 + Offsets[i].X) * ScaleUI, + StartY + (HitNotifyIcon[i].V - HitNotifyTexture->GetSizeY() / 2 + Offsets[i].Y) * ScaleUI, + ScaleUI); + } + } +} + +void AShooterHUD::OnPlayerTalkingStateChanged(TSharedRef TalkingPlayerId, bool bIsTalking) +{ + if (bIsScoreBoardVisible) + { + ScoreboardWidget->StoreTalkingPlayerData(TalkingPlayerId.Get(), bIsTalking); + } +} + +void AShooterHUD::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + + bIsScoreBoardVisible = false; + + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld()); + if (OnlineSub) + { + IOnlineVoicePtr Voice = OnlineSub->GetVoiceInterface(); + if (Voice.IsValid()) + { + Voice->AddOnPlayerTalkingStateChangedDelegate_Handle(OnPlayerTalkingStateChangedDelegate); + } + } +} + +void AShooterHUD::ConditionalCloseScoreboard(bool bFocus) +{ + if (bIsScoreBoardVisible) + { + ShowScoreboard(false, bFocus); + } +} + +void AShooterHUD::ToggleScoreboard() +{ + ShowScoreboard(!bIsScoreBoardVisible); +} + +bool AShooterHUD::ShowScoreboard(bool bEnable, bool bFocus) +{ + if( bIsScoreBoardVisible == bEnable) + { + // if the scoreboard is already enabled, disable it in favour of the new request + if( bEnable ) + { + ToggleScoreboard(); + } + else + { + return false; + } + } + + if (bEnable) + { + AShooterPlayerController* ShooterPC = Cast(PlayerOwner); + if (ShooterPC == NULL || ShooterPC->IsGameMenuVisible() ) + { + return false; + } + } + + bIsScoreBoardVisible = bEnable; + if (bIsScoreBoardVisible) + { + SAssignNew(ScoreboardWidgetOverlay,SOverlay) + +SOverlay::Slot() + .HAlign(EHorizontalAlignment::HAlign_Center) + .VAlign(EVerticalAlignment::VAlign_Center) + .Padding(FMargin(50)) + [ + SAssignNew(ScoreboardWidget, SShooterScoreboardWidget) + .PCOwner(MakeWeakObjectPtr(PlayerOwner)) + .MatchState(GetMatchState()) + ]; + + GEngine->GameViewport->AddViewportWidgetContent( + SAssignNew(ScoreboardWidgetContainer,SWeakWidget) + .PossiblyNullContent(ScoreboardWidgetOverlay)); + + if( bFocus ) + { + // Give input focus to the scoreboard + FSlateApplication::Get().SetKeyboardFocus(ScoreboardWidget); + } + } + else + { + if (ScoreboardWidgetContainer.IsValid()) + { + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(ScoreboardWidgetContainer.ToSharedRef()); + } + } + + if( bFocus ) + { + // Make sure viewport has focus + FSlateApplication::Get().SetAllUserFocusToGameViewport(); + } + } + return true; +} + +void AShooterHUD::ToggleChat() +{ + AShooterPlayerController* ShooterPC = Cast(PlayerOwner); + // If the game menu is visible dont show the chat menu + if( (ShooterPC == NULL ) || ShooterPC->IsGameMenuVisible() || GetMatchState() == EShooterMatchState::Warmup ) + { + return; + } + + if( TryCreateChatWidget() == false ) + { + EVisibility RequiredVisibility = ChatWidget->GetEntryVisibility() == EVisibility::Visible ? EVisibility::Hidden :EVisibility::Visible; + SetChatVisibilty( RequiredVisibility ); + } +} + +void AShooterHUD::SetChatVisibilty( const EVisibility RequiredVisibility ) +{ + TryCreateChatWidget(); + + if (ChatWidget->GetEntryVisibility() == RequiredVisibility) + { + // State is already that which we require + return; + } + + ChatWidget->SetEntryVisibility(RequiredVisibility); +} + +void AShooterHUD::AddChatLine(const FText& ChatString, bool bWantFocus) +{ + TryCreateChatWidget(); + if( ChatWidget.IsValid() == true ) + { + ChatWidget->AddChatLine(ChatString, bWantFocus); + } +} + +void AShooterHUD::MakeUV(FCanvasIcon& Icon, FVector2D& UV0, FVector2D& UV1, uint16 U, uint16 V, uint16 UL, uint16 VL) +{ + if (Icon.Texture) + { + const float Width = Icon.Texture->GetSurfaceWidth(); + const float Height = Icon.Texture->GetSurfaceHeight(); + UV0 = FVector2D(U / Width, V / Height); + UV1 = UV0 + FVector2D(UL / Width, VL / Height); + } +} + +bool AShooterHUD::TryCreateChatWidget() +{ + bool bCreated = false; + AShooterPlayerController* ShooterPC = Cast(PlayerOwner); + if(ShooterPC == NULL ) + { + UE_LOG(LogShooter, Warning, TEXT("Unable to create chat widget - Invalid player controller") ); + } + else + { + // Only create the widget if its invalid + if( ChatWidget.IsValid() == false ) + { + bCreated = true; + FLocalPlayerContext WorldContext(ShooterPC); + GEngine->GameViewport->AddViewportWidgetContent( + SAssignNew(ChatWidget,SChatWidget, WorldContext)/*.bKeepVisible(true)*/ + ); + } + } + return bCreated; +} + +bool AShooterHUD::IsMatchOver() const +{ + return GetMatchState() == EShooterMatchState::Lost || GetMatchState() == EShooterMatchState::Won; +} + +void AShooterHUD::AddMatchInfoString(const FCanvasTextItem InInfoItem ) +{ + InfoItems.Add(InInfoItem); +} + +float AShooterHUD::ShowInfoItems(float YOffset, float TextScale) +{ + float Y = YOffset; + float CanvasCentre = Canvas->ClipX / 2.0f; + + for (int32 iItem = 0; iItem < InfoItems.Num() ; iItem++) + { + float X = 0.0f; + float SizeX, SizeY; + Canvas->StrLen(InfoItems[iItem].Font, InfoItems[iItem].Text.ToString(), SizeX, SizeY); + X = CanvasCentre - ( SizeX * InfoItems[iItem].Scale.X)/2.0f; + Canvas->DrawItem(InfoItems[iItem], X, Y); + Y += SizeY * InfoItems[iItem].Scale.Y; + } + return Y; +} + +float AShooterHUD::DrawRecentlyKilledPlayer() +{ + const float DrawPos = (Canvas->ClipY / 4.0)* ScaleUI; + float LastYPos = DrawPos; + if (MatchState == EShooterMatchState::Playing) + { + const float CurrentTime = GetWorld()->GetTimeSeconds(); + if (CurrentTime - LastKillTime >= 0 && CurrentTime - LastKillTime <= KillFadeOutTime) + { + FCanvasTextItem TextItem(FVector2D::ZeroVector, FText::GetEmpty(), NormalFont, HUDDark); + TextItem.EnableShadow(FLinearColor::Black); + float SizeX, SizeY; + float TextScale = 0.71f; + FString Text = CenteredKillMessage.ToString(); + + const float Alpha = FMath::Min(1.0f, 1 - (CurrentTime - LastKillTime) / KillFadeOutTime); + TextItem.Font = BigFont; + Canvas->StrLen(BigFont, Text, SizeX, SizeY); + Canvas->SetDrawColor(255, 255, 255, 255 * Alpha); + Canvas->DrawIcon(KilledIcon, Canvas->OrgX + Canvas->ClipX / 2 - (KilledIcon.UL * ScaleUI + SizeX * TextScale * ScaleUI) / 2.0f, + DrawPos - (Offset * 4 - SizeY / 2 * TextScale + KilledIcon.VL / 2) * ScaleUI, ScaleUI); + TextItem.SetColor(FColor(HUDLight.R, HUDLight.G, HUDLight.B, HUDLight.A*Alpha)); + TextItem.Text = FText::FromString(Text); + TextItem.Scale = FVector2D(TextScale*ScaleUI, TextScale*ScaleUI); + LastYPos = (DrawPos - (Offset * 4 * ScaleUI)) + SizeY; + Canvas->DrawItem(TextItem, Canvas->OrgX + Canvas->ClipX / 2 - (KilledIcon.UL * ScaleUI + SizeX * TextScale * ScaleUI) / 2.0f + KilledIcon.UL * ScaleUI, + DrawPos - ( Offset * 4 * ScaleUI)); + } + } + return LastYPos; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/ShooterHUDPCTrackerBase.cpp b/Source/ShooterGame/Private/UI/ShooterHUDPCTrackerBase.cpp new file mode 100644 index 0000000..1846eaf --- /dev/null +++ b/Source/ShooterGame/Private/UI/ShooterHUDPCTrackerBase.cpp @@ -0,0 +1,58 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterHUDPCTrackerBase.h" + + +/** Initialize with a world context. */ +void ShooterHUDPCTrackerBase::Init( const FLocalPlayerContext& InContext ) +{ + Context = InContext; +} + +TWeakObjectPtr ShooterHUDPCTrackerBase::GetPlayerController() const +{ + if ( ensureMsgf( Context.IsValid(), TEXT("Game context must be initialized!") ) ) + { + APlayerController* PC = Context.GetPlayerController(); + AShooterPlayerController* ShooterPC = Cast(PC); + return MakeWeakObjectPtr(ShooterPC); + } + else + { + return NULL; + } +} + + +UWorld* ShooterHUDPCTrackerBase::GetWorld() const +{ + if ( ensureMsgf( Context.IsValid(), TEXT("Game context must be initialized!") ) ) + { + return Context.GetWorld(); + } + else + { + return NULL; + } +} + +AShooterGameState* ShooterHUDPCTrackerBase::GetGameState() const +{ + if ( ensureMsgf( Context.IsValid(), TEXT("Game context must be initialized!") ) ) + { + return Context.GetWorld()->GetGameState(); + } + else + { + return NULL; + } +} + +const FLocalPlayerContext& ShooterHUDPCTrackerBase::GetContext() const +{ + return Context; +} + + + diff --git a/Source/ShooterGame/Private/UI/ShooterHUDPCTrackerBase.h b/Source/ShooterGame/Private/UI/ShooterHUDPCTrackerBase.h new file mode 100644 index 0000000..981919e --- /dev/null +++ b/Source/ShooterGame/Private/UI/ShooterHUDPCTrackerBase.h @@ -0,0 +1,36 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + + +/** + * Helps HUD widgets know their context within the game world. + * e.g. Is this a widget for player 1 or player 2? + * e.g. In case of multiple PIE sessions, which world do I belong to? + */ +class ShooterHUDPCTrackerBase +{ +public: + + virtual ~ShooterHUDPCTrackerBase(){} + + /** Initialize with a world context. */ + void Init( const FLocalPlayerContext& InContext ); + + /** Returns a pointer to the player controller */ + TWeakObjectPtr GetPlayerController() const; + + /** Returns a pointer to the World. (Via Player Controller) */ + UWorld* GetWorld() const; + + /** Get the current game GameState */ + class AShooterGameState* GetGameState() const; + + /** @return the game world context */ + const FLocalPlayerContext& GetContext() const; + +private: + /** Which player and world this piece of UI belongs to. This is necessary to support */ + FLocalPlayerContext Context; + +}; \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/ShooterUIHelpers.cpp b/Source/ShooterGame/Private/UI/ShooterUIHelpers.cpp new file mode 100644 index 0000000..9539186 --- /dev/null +++ b/Source/ShooterGame/Private/UI/ShooterUIHelpers.cpp @@ -0,0 +1,77 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterUIHelpers.h" +#include "OnlineSubsystemUtils.h" + +FText ShooterUIHelpers::GetProfileOpenText() const +{ + // @todo: replace button with icon + // @todo: replace 'GamerCard' with distribution specific terminology (Steam, Origin, UPlay, etc) +#if SHOOTER_XBOX_STRINGS + return NSLOCTEXT("Network", "XB1OpenProfile", "Press A for GamerCard"); +#elif PLATFORM_PS4 + return NSLOCTEXT("Network", "PS4OpenProfile", "Press cross button for GamerCard"); +#else + return NSLOCTEXT("Network", "PCOpenProfile", "Press Enter for GamerCard"); +#endif +} + +bool ShooterUIHelpers::ProfileOpenedUI(UWorld* World, const FUniqueNetId& Requestor, const FUniqueNetId& Requestee, const FOnProfileUIClosedDelegate* Delegate) const +{ + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(World); + if (OnlineSub) + { + // Show the profile UI. + const IOnlineExternalUIPtr ExternalUI = OnlineSub->GetExternalUIInterface(); + if (ExternalUI.IsValid()) + { + // Create a dummy delegate, if one wasn't specified + struct Local + { + static void DummyOnProfileOpenedUIClosedDelegate() + { + // do nothing + } + }; + return ExternalUI->ShowProfileUI(Requestor, Requestee, Delegate ? *Delegate : FOnProfileUIClosedDelegate::CreateStatic(&Local::DummyOnProfileOpenedUIClosedDelegate) ); + } + } + return false; +} + +FText ShooterUIHelpers::GetProfileSwapText() const +{ + // @todo: replace button with icon +#if SHOOTER_XBOX_STRINGS + return NSLOCTEXT("Network", "XB1SwapProfile", "Y Switch User"); +/*#elif PLATFORM_PS4 + return NSLOCTEXT("Network", "PS4SwapProfile", "Triangle button Switch User"); +*/ +#else + return NSLOCTEXT("Network", "PCSwapProfile", "Space - Switch User"); +#endif +} + +bool ShooterUIHelpers::ProfileSwapUI(UWorld* World, const int ControllerIndex, bool bShowOnlineOnly, const FOnLoginUIClosedDelegate* Delegate) const +{ + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(World); + if (OnlineSub) + { + // Show the profile UI. + const IOnlineExternalUIPtr ExternalUI = OnlineSub->GetExternalUIInterface(); + if (ExternalUI.IsValid()) + { + // Create a dummy delegate, if one wasn't specified + struct Local + { + static void DummyOnProfileSwapUIClosedDelegate(TSharedPtr UniqueId, const int InControllerIndex, const FOnlineError& Error) + { + // do nothing + } + }; + return ExternalUI->ShowLoginUI(ControllerIndex, bShowOnlineOnly, false, Delegate ? *Delegate : FOnLoginUIClosedDelegate::CreateStatic(&Local::DummyOnProfileSwapUIClosedDelegate) ); + } + } + return false; +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/ShooterUIHelpers.h b/Source/ShooterGame/Private/UI/ShooterUIHelpers.h new file mode 100644 index 0000000..e3ea490 --- /dev/null +++ b/Source/ShooterGame/Private/UI/ShooterUIHelpers.h @@ -0,0 +1,31 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ShooterGame.h" + +/** Singleton that contains commonly used UI actions */ +class ShooterUIHelpers +{ +public: + /** gets the singleton. */ + static ShooterUIHelpers& Get() + { + static ShooterUIHelpers Singleton; + return Singleton; + } + + /** fetches the string for displaying the profile */ + FText GetProfileOpenText() const; + + /** profile open ui handler */ + bool ProfileOpenedUI(UWorld* World, const FUniqueNetId& Requestor, const FUniqueNetId& Requestee, const FOnProfileUIClosedDelegate* Delegate) const; + + /** fetches the string for swapping the profile */ + FText GetProfileSwapText() const; + + /** profile swap ui handler */ + bool ProfileSwapUI(UWorld* World, const int ControllerIndex, bool bShowOnlineOnly, const FOnLoginUIClosedDelegate* Delegate) const; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/Style/ShooterChatWidgetStyle.cpp b/Source/ShooterGame/Private/UI/Style/ShooterChatWidgetStyle.cpp new file mode 100644 index 0000000..c180b46 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterChatWidgetStyle.cpp @@ -0,0 +1,34 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterChatWidgetStyle.h" + +FShooterChatStyle::FShooterChatStyle() +{ +} + +FShooterChatStyle::~FShooterChatStyle() +{ +} + +const FName FShooterChatStyle::TypeName(TEXT("FShooterChatStyle")); + +const FShooterChatStyle& FShooterChatStyle::GetDefault() +{ + static FShooterChatStyle Default; + return Default; +} + +void FShooterChatStyle::GetResources(TArray& OutBrushes) const +{ + TextEntryStyle.GetResources(OutBrushes); + + OutBrushes.Add(&BackingBrush); +} + + +UShooterChatWidgetStyle::UShooterChatWidgetStyle( const FObjectInitializer& ObjectInitializer ) + : Super(ObjectInitializer) +{ + +} diff --git a/Source/ShooterGame/Private/UI/Style/ShooterChatWidgetStyle.h b/Source/ShooterGame/Private/UI/Style/ShooterChatWidgetStyle.h new file mode 100644 index 0000000..4da55b0 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterChatWidgetStyle.h @@ -0,0 +1,85 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateWidgetStyleContainerBase.h" +#include "ShooterChatWidgetStyle.generated.h" + +/** + * Represents the appearance of an SChatWidget + */ +USTRUCT() +struct FShooterChatStyle : public FSlateWidgetStyle +{ + GENERATED_USTRUCT_BODY() + + FShooterChatStyle(); + virtual ~FShooterChatStyle(); + + // FSlateWidgetStyle + virtual void GetResources(TArray& OutBrushes) const override; + static const FName TypeName; + virtual const FName GetTypeName() const override { return TypeName; }; + static const FShooterChatStyle& GetDefault(); + + /** + * The style used for entering chat text + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FEditableTextBoxStyle TextEntryStyle; + FShooterChatStyle& SetChatEntryStyle(const FEditableTextBoxStyle& InTextEntryStyle) { TextEntryStyle = InTextEntryStyle; return *this; } + + /** + * The brush used for the chat backing + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateBrush BackingBrush; + FShooterChatStyle& SetBackingBrush(const FSlateBrush& InBackingBrush) { BackingBrush = InBackingBrush; return *this; } + + /** + * The color used for the chat box border + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateColor BoxBorderColor; + FShooterChatStyle& SetBoxBorderColor(const FSlateColor& InBoxBorderColor) { BoxBorderColor = InBoxBorderColor; return *this; } + + /** + * The color used for the chat box text + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateColor TextColor; + FShooterChatStyle& SetTextColor(const FSlateColor& InTextColor) { TextColor = InTextColor; return *this; } + + /** + * The sound that should play when receiving a chat message + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound RxMessgeSound; + FShooterChatStyle& SetRxMessgeSound(const FSlateSound& InRxMessgeSound) { RxMessgeSound = InRxMessgeSound; return *this; } + + /** + * The sound that should play when sending a chat message + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound TxMessgeSound; + FShooterChatStyle& SetTxMessgeSound(const FSlateSound& InTxMessgeSound) { TxMessgeSound = InTxMessgeSound; return *this; } +}; + + +/** + */ +UCLASS(hidecategories=Object, MinimalAPI) +class UShooterChatWidgetStyle : public USlateWidgetStyleContainerBase +{ + GENERATED_UCLASS_BODY() + +public: + /** The actual data describing the chat appearance. */ + UPROPERTY(Category=Appearance, EditAnywhere, meta=(ShowOnlyInnerProperties)) + FShooterChatStyle ChatStyle; + + virtual const struct FSlateWidgetStyle* const GetStyle() const override + { + return static_cast< const struct FSlateWidgetStyle* >( &ChatStyle ); + } +}; diff --git a/Source/ShooterGame/Private/UI/Style/ShooterMenuItemWidgetStyle.cpp b/Source/ShooterGame/Private/UI/Style/ShooterMenuItemWidgetStyle.cpp new file mode 100644 index 0000000..778ee82 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterMenuItemWidgetStyle.cpp @@ -0,0 +1,34 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterMenuItemWidgetStyle.h" + +FShooterMenuItemStyle::FShooterMenuItemStyle() +{ +} + +FShooterMenuItemStyle::~FShooterMenuItemStyle() +{ +} + +const FName FShooterMenuItemStyle::TypeName(TEXT("FShooterMenuItemStyle")); + +const FShooterMenuItemStyle& FShooterMenuItemStyle::GetDefault() +{ + static FShooterMenuItemStyle Default; + return Default; +} + +void FShooterMenuItemStyle::GetResources(TArray& OutBrushes) const +{ + OutBrushes.Add(&BackgroundBrush); + OutBrushes.Add(&LeftArrowImage); + OutBrushes.Add(&RightArrowImage); +} + + +UShooterMenuItemWidgetStyle::UShooterMenuItemWidgetStyle( const FObjectInitializer& ObjectInitializer ) + : Super(ObjectInitializer) +{ + +} diff --git a/Source/ShooterGame/Private/UI/Style/ShooterMenuItemWidgetStyle.h b/Source/ShooterGame/Private/UI/Style/ShooterMenuItemWidgetStyle.h new file mode 100644 index 0000000..8638a1c --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterMenuItemWidgetStyle.h @@ -0,0 +1,64 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateWidgetStyleContainerBase.h" +#include "ShooterMenuItemWidgetStyle.generated.h" + +/** + * Represents the appearance of an FShooterMenuItem + */ +USTRUCT() +struct FShooterMenuItemStyle : public FSlateWidgetStyle +{ + GENERATED_USTRUCT_BODY() + + FShooterMenuItemStyle(); + virtual ~FShooterMenuItemStyle(); + + // FSlateWidgetStyle + virtual void GetResources(TArray& OutBrushes) const override; + static const FName TypeName; + virtual const FName GetTypeName() const override { return TypeName; }; + static const FShooterMenuItemStyle& GetDefault(); + + /** + * The brush used for the item background + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateBrush BackgroundBrush; + FShooterMenuItemStyle& SetBackgroundBrush(const FSlateBrush& InBackgroundBrush) { BackgroundBrush = InBackgroundBrush; return *this; } + + /** + * The image used for the left arrow + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateBrush LeftArrowImage; + FShooterMenuItemStyle& SetLeftArrowImage(const FSlateBrush& InLeftArrowImage) { LeftArrowImage = InLeftArrowImage; return *this; } + + /** + * The image used for the right arrow + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateBrush RightArrowImage; + FShooterMenuItemStyle& SetRightArrowImage(const FSlateBrush& InRightArrowImage) { RightArrowImage = InRightArrowImage; return *this; } +}; + + +/** + */ +UCLASS(hidecategories=Object, MinimalAPI) +class UShooterMenuItemWidgetStyle : public USlateWidgetStyleContainerBase +{ + GENERATED_UCLASS_BODY() + +public: + /** The actual data describing the menu's appearance. */ + UPROPERTY(Category=Appearance, EditAnywhere, meta=(ShowOnlyInnerProperties)) + FShooterMenuItemStyle MenuItemStyle; + + virtual const struct FSlateWidgetStyle* const GetStyle() const override + { + return static_cast< const struct FSlateWidgetStyle* >( &MenuItemStyle ); + } +}; diff --git a/Source/ShooterGame/Private/UI/Style/ShooterMenuSoundsWidgetStyle.cpp b/Source/ShooterGame/Private/UI/Style/ShooterMenuSoundsWidgetStyle.cpp new file mode 100644 index 0000000..e03847a --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterMenuSoundsWidgetStyle.cpp @@ -0,0 +1,31 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterMenuSoundsWidgetStyle.h" + +FShooterMenuSoundsStyle::FShooterMenuSoundsStyle() +{ +} + +FShooterMenuSoundsStyle::~FShooterMenuSoundsStyle() +{ +} + +const FName FShooterMenuSoundsStyle::TypeName(TEXT("FShooterMenuSoundsStyle")); + +const FShooterMenuSoundsStyle& FShooterMenuSoundsStyle::GetDefault() +{ + static FShooterMenuSoundsStyle Default; + return Default; +} + +void FShooterMenuSoundsStyle::GetResources(TArray& OutBrushes) const +{ +} + + +UShooterMenuSoundsWidgetStyle::UShooterMenuSoundsWidgetStyle( const FObjectInitializer& ObjectInitializer ) + : Super(ObjectInitializer) +{ + +} diff --git a/Source/ShooterGame/Private/UI/Style/ShooterMenuSoundsWidgetStyle.h b/Source/ShooterGame/Private/UI/Style/ShooterMenuSoundsWidgetStyle.h new file mode 100644 index 0000000..ffdf0c6 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterMenuSoundsWidgetStyle.h @@ -0,0 +1,57 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateWidgetStyleContainerBase.h" +#include "ShooterMenuSoundsWidgetStyle.generated.h" + +/** + * Represents the common menu sounds used in the shooter game + */ +USTRUCT() +struct FShooterMenuSoundsStyle : public FSlateWidgetStyle +{ + GENERATED_USTRUCT_BODY() + + FShooterMenuSoundsStyle(); + virtual ~FShooterMenuSoundsStyle(); + + // FSlateWidgetStyle + virtual void GetResources(TArray& OutBrushes) const override; + static const FName TypeName; + virtual const FName GetTypeName() const override { return TypeName; }; + static const FShooterMenuSoundsStyle& GetDefault(); + + /** + * The sound that should play when starting the game + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound StartGameSound; + FShooterMenuSoundsStyle& SetStartGameSound(const FSlateSound& InStartGameSound) { StartGameSound = InStartGameSound; return *this; } + + /** + * The sound that should play when exiting the game + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound ExitGameSound; + FShooterMenuSoundsStyle& SetExitGameSound(const FSlateSound& InExitGameSound) { ExitGameSound = InExitGameSound; return *this; } +}; + + +/** + */ +UCLASS(hidecategories=Object, MinimalAPI) +class UShooterMenuSoundsWidgetStyle : public USlateWidgetStyleContainerBase +{ + GENERATED_UCLASS_BODY() + +public: + /** The actual data describing the sounds */ + UPROPERTY(Category=Appearance, EditAnywhere, meta=(ShowOnlyInnerProperties)) + FShooterMenuSoundsStyle SoundsStyle; + + virtual const struct FSlateWidgetStyle* const GetStyle() const override + { + return static_cast< const struct FSlateWidgetStyle* >( &SoundsStyle ); + } +}; diff --git a/Source/ShooterGame/Private/UI/Style/ShooterMenuWidgetStyle.cpp b/Source/ShooterGame/Private/UI/Style/ShooterMenuWidgetStyle.cpp new file mode 100644 index 0000000..a6c5818 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterMenuWidgetStyle.cpp @@ -0,0 +1,34 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterMenuWidgetStyle.h" + +FShooterMenuStyle::FShooterMenuStyle() +{ +} + +FShooterMenuStyle::~FShooterMenuStyle() +{ +} + +const FName FShooterMenuStyle::TypeName(TEXT("FShooterMenuStyle")); + +const FShooterMenuStyle& FShooterMenuStyle::GetDefault() +{ + static FShooterMenuStyle Default; + return Default; +} + +void FShooterMenuStyle::GetResources(TArray& OutBrushes) const +{ + OutBrushes.Add(&HeaderBackgroundBrush); + OutBrushes.Add(&LeftBackgroundBrush); + OutBrushes.Add(&RightBackgroundBrush); +} + + +UShooterMenuWidgetStyle::UShooterMenuWidgetStyle( const FObjectInitializer& ObjectInitializer ) + : Super(ObjectInitializer) +{ + +} diff --git a/Source/ShooterGame/Private/UI/Style/ShooterMenuWidgetStyle.h b/Source/ShooterGame/Private/UI/Style/ShooterMenuWidgetStyle.h new file mode 100644 index 0000000..4f95412 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterMenuWidgetStyle.h @@ -0,0 +1,92 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateWidgetStyleContainerBase.h" +#include "ShooterMenuWidgetStyle.generated.h" + +/** + * Represents the appearance of an SShooterMenuWidget + */ +USTRUCT() +struct FShooterMenuStyle : public FSlateWidgetStyle +{ + GENERATED_USTRUCT_BODY() + + FShooterMenuStyle(); + virtual ~FShooterMenuStyle(); + + // FSlateWidgetStyle + virtual void GetResources(TArray& OutBrushes) const override; + static const FName TypeName; + virtual const FName GetTypeName() const override { return TypeName; }; + static const FShooterMenuStyle& GetDefault(); + + /** + * The brush used for the header background + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateBrush HeaderBackgroundBrush; + FShooterMenuStyle& SetHeaderBackgroundBrush(const FSlateBrush& InHeaderBackgroundBrush) { HeaderBackgroundBrush = InHeaderBackgroundBrush; return *this; } + + /** + * The brush used for the left side of the menu + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateBrush LeftBackgroundBrush; + FShooterMenuStyle& SetLeftBackgroundBrush(const FSlateBrush& InLeftBackgroundBrush) { LeftBackgroundBrush = InLeftBackgroundBrush; return *this; } + + /** + * The brush used for the right side of the menu + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateBrush RightBackgroundBrush; + FShooterMenuStyle& SetRightBackgroundBrush(const FSlateBrush& InRightBackgroundBrush) { RightBackgroundBrush = InRightBackgroundBrush; return *this; } + + /** + * The sound that should play when entering a sub-menu + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound MenuEnterSound; + FShooterMenuStyle& SetMenuEnterSound(const FSlateSound& InMenuEnterSound) { MenuEnterSound = InMenuEnterSound; return *this; } + + /** + * The sound that should play when leaving a sub-menu + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound MenuBackSound; + FShooterMenuStyle& SetMenuBackSound(const FSlateSound& InMenuBackSound) { MenuBackSound = InMenuBackSound; return *this; } + + /** + * The sound that should play when changing an option + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound OptionChangeSound; + FShooterMenuStyle& SetOptionChangeSound(const FSlateSound& InOptionChangeSound) { OptionChangeSound = InOptionChangeSound; return *this; } + + /** + * The sound that should play when changing the selected menu item + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound MenuItemChangeSound; + FShooterMenuStyle& SetMenuItemChangeSound(const FSlateSound& InMenuItemChangeSound) { MenuItemChangeSound = InMenuItemChangeSound; return *this; } +}; + + +/** + */ +UCLASS(hidecategories=Object, MinimalAPI) +class UShooterMenuWidgetStyle : public USlateWidgetStyleContainerBase +{ + GENERATED_UCLASS_BODY() + +public: + /** The actual data describing the menu's appearance. */ + UPROPERTY(Category=Appearance, EditAnywhere, meta=(ShowOnlyInnerProperties)) + FShooterMenuStyle MenuStyle; + + virtual const struct FSlateWidgetStyle* const GetStyle() const override + { + return static_cast< const struct FSlateWidgetStyle* >( &MenuStyle ); + } +}; diff --git a/Source/ShooterGame/Private/UI/Style/ShooterOptionsWidgetStyle.cpp b/Source/ShooterGame/Private/UI/Style/ShooterOptionsWidgetStyle.cpp new file mode 100644 index 0000000..e441d18 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterOptionsWidgetStyle.cpp @@ -0,0 +1,31 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterOptionsWidgetStyle.h" + +FShooterOptionsStyle::FShooterOptionsStyle() +{ +} + +FShooterOptionsStyle::~FShooterOptionsStyle() +{ +} + +const FName FShooterOptionsStyle::TypeName(TEXT("FShooterOptionsStyle")); + +const FShooterOptionsStyle& FShooterOptionsStyle::GetDefault() +{ + static FShooterOptionsStyle Default; + return Default; +} + +void FShooterOptionsStyle::GetResources(TArray& OutBrushes) const +{ +} + + +UShooterOptionsWidgetStyle::UShooterOptionsWidgetStyle( const FObjectInitializer& ObjectInitializer ) + : Super(ObjectInitializer) +{ + +} diff --git a/Source/ShooterGame/Private/UI/Style/ShooterOptionsWidgetStyle.h b/Source/ShooterGame/Private/UI/Style/ShooterOptionsWidgetStyle.h new file mode 100644 index 0000000..23850ae --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterOptionsWidgetStyle.h @@ -0,0 +1,57 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateWidgetStyleContainerBase.h" +#include "ShooterOptionsWidgetStyle.generated.h" + +/** + * Represents the appearance of an FShooterOptions + */ +USTRUCT() +struct FShooterOptionsStyle : public FSlateWidgetStyle +{ + GENERATED_USTRUCT_BODY() + + FShooterOptionsStyle(); + virtual ~FShooterOptionsStyle(); + + // FSlateWidgetStyle + virtual void GetResources(TArray& OutBrushes) const override; + static const FName TypeName; + virtual const FName GetTypeName() const override { return TypeName; }; + static const FShooterOptionsStyle& GetDefault(); + + /** + * The sound the options should play when changes are accepted + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound AcceptChangesSound; + FShooterOptionsStyle& SetAcceptChangesSound(const FSlateSound& InAcceptChangesSound) { AcceptChangesSound = InAcceptChangesSound; return *this; } + + /** + * The sound the options should play when changes are discarded + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound DiscardChangesSound; + FShooterOptionsStyle& SetDiscardChangesSound(const FSlateSound& InDiscardChangesSound) { DiscardChangesSound = InDiscardChangesSound; return *this; } +}; + + +/** + */ +UCLASS(hidecategories=Object, MinimalAPI) +class UShooterOptionsWidgetStyle : public USlateWidgetStyleContainerBase +{ + GENERATED_UCLASS_BODY() + +public: + /** The actual data describing the menu's appearance. */ + UPROPERTY(Category=Appearance, EditAnywhere, meta=(ShowOnlyInnerProperties)) + FShooterOptionsStyle OptionsStyle; + + virtual const struct FSlateWidgetStyle* const GetStyle() const override + { + return static_cast< const struct FSlateWidgetStyle* >( &OptionsStyle ); + } +}; diff --git a/Source/ShooterGame/Private/UI/Style/ShooterScoreboardWidgetStyle.cpp b/Source/ShooterGame/Private/UI/Style/ShooterScoreboardWidgetStyle.cpp new file mode 100644 index 0000000..aaefea0 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterScoreboardWidgetStyle.cpp @@ -0,0 +1,32 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterScoreboardWidgetStyle.h" + +FShooterScoreboardStyle::FShooterScoreboardStyle() +{ +} + +FShooterScoreboardStyle::~FShooterScoreboardStyle() +{ +} + +const FName FShooterScoreboardStyle::TypeName(TEXT("FShooterScoreboardStyle")); + +const FShooterScoreboardStyle& FShooterScoreboardStyle::GetDefault() +{ + static FShooterScoreboardStyle Default; + return Default; +} + +void FShooterScoreboardStyle::GetResources(TArray& OutBrushes) const +{ + OutBrushes.Add(&ItemBorderBrush); +} + + +UShooterScoreboardWidgetStyle::UShooterScoreboardWidgetStyle( const FObjectInitializer& ObjectInitializer ) + : Super(ObjectInitializer) +{ + +} diff --git a/Source/ShooterGame/Private/UI/Style/ShooterScoreboardWidgetStyle.h b/Source/ShooterGame/Private/UI/Style/ShooterScoreboardWidgetStyle.h new file mode 100644 index 0000000..70f213a --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterScoreboardWidgetStyle.h @@ -0,0 +1,78 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateWidgetStyleContainerBase.h" +#include "ShooterScoreboardWidgetStyle.generated.h" + +/** + * Represents the appearance of an SShooterScoreboardWidget + */ +USTRUCT() +struct FShooterScoreboardStyle : public FSlateWidgetStyle +{ + GENERATED_USTRUCT_BODY() + + FShooterScoreboardStyle(); + virtual ~FShooterScoreboardStyle(); + + // FSlateWidgetStyle + virtual void GetResources(TArray& OutBrushes) const override; + static const FName TypeName; + virtual const FName GetTypeName() const override { return TypeName; }; + static const FShooterScoreboardStyle& GetDefault(); + + /** + * The brush used for the item border + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateBrush ItemBorderBrush; + FShooterScoreboardStyle& SetItemBorderBrush(const FSlateBrush& InItemBorderBrush) { ItemBorderBrush = InItemBorderBrush; return *this; } + + /** + * The color used for the kill stat + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateColor KillStatColor; + FShooterScoreboardStyle& SetKillStatColor(const FSlateColor& InKillStatColor) { KillStatColor = InKillStatColor; return *this; } + + /** + * The color used for the death stat + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateColor DeathStatColor; + FShooterScoreboardStyle& SetDeathStatColor(const FSlateColor& InDeathStatColor) { DeathStatColor = InDeathStatColor; return *this; } + + /** + * The color used for the score stat + */ + UPROPERTY(EditAnywhere, Category=Appearance) + FSlateColor ScoreStatColor; + FShooterScoreboardStyle& SetScoreStatColor(const FSlateColor& InScoreStatColor) { ScoreStatColor = InScoreStatColor; return *this; } + + /** + * The sound that should play when the highlighted player changes in the scoreboard + */ + UPROPERTY(EditAnywhere, Category=Sound) + FSlateSound PlayerChangeSound; + FShooterScoreboardStyle& SetPlayerChangeSound(const FSlateSound& InPlayerChangeSound) { PlayerChangeSound = InPlayerChangeSound; return *this; } +}; + + +/** + */ +UCLASS(hidecategories=Object, MinimalAPI) +class UShooterScoreboardWidgetStyle : public USlateWidgetStyleContainerBase +{ + GENERATED_UCLASS_BODY() + +public: + /** The actual data describing the scoreboard's appearance. */ + UPROPERTY(Category=Appearance, EditAnywhere, meta=(ShowOnlyInnerProperties)) + FShooterScoreboardStyle ScoreboardStyle; + + virtual const struct FSlateWidgetStyle* const GetStyle() const override + { + return static_cast< const struct FSlateWidgetStyle* >( &ScoreboardStyle ); + } +}; diff --git a/Source/ShooterGame/Private/UI/Style/ShooterStyle.cpp b/Source/ShooterGame/Private/UI/Style/ShooterStyle.cpp new file mode 100644 index 0000000..b00252f --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterStyle.cpp @@ -0,0 +1,156 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterStyle.h" +#include "SlateGameResources.h" + +TSharedPtr< FSlateStyleSet > FShooterStyle::ShooterStyleInstance = NULL; + +void FShooterStyle::Initialize() +{ + if ( !ShooterStyleInstance.IsValid() ) + { + ShooterStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle( *ShooterStyleInstance ); + } +} + +void FShooterStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle( *ShooterStyleInstance ); + ensure( ShooterStyleInstance.IsUnique() ); + ShooterStyleInstance.Reset(); +} + +FName FShooterStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("ShooterStyle")); + return StyleSetName; +} + +#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( FPaths::ProjectContentDir() / "Slate"/ RelativePath + TEXT(".png"), __VA_ARGS__ ) +#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( FPaths::ProjectContentDir() / "Slate"/ RelativePath + TEXT(".png"), __VA_ARGS__ ) +#define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( FPaths::ProjectContentDir() / "Slate"/ RelativePath + TEXT(".png"), __VA_ARGS__ ) +#define TTF_FONT( RelativePath, ... ) FSlateFontInfo( FPaths::ProjectContentDir() / "Slate"/ RelativePath + TEXT(".ttf"), __VA_ARGS__ ) +#define OTF_FONT( RelativePath, ... ) FSlateFontInfo( FPaths::ProjectContentDir() / "Slate"/ RelativePath + TEXT(".otf"), __VA_ARGS__ ) + +PRAGMA_DISABLE_OPTIMIZATION +TSharedRef< FSlateStyleSet > FShooterStyle::Create() +{ + TSharedRef StyleRef = FSlateGameResources::New(FShooterStyle::GetStyleSetName(), "/Game/UI/Styles", "/Game/UI/Styles"); + FSlateStyleSet& Style = StyleRef.Get(); + + // Load the speaker icon to be used for displaying when a user is talking + Style.Set("ShooterGame.Speaker", new IMAGE_BRUSH("Images/SoundCue_SpeakerIcon", FVector2D(32, 32))); + + // The border image used to draw the replay timeline bar + Style.Set("ShooterGame.ReplayTimelineBorder", new BOX_BRUSH("Images/ReplayTimeline", FMargin(3.0f / 8.0f))); + + // The border image used to draw the replay timeline bar + Style.Set("ShooterGame.ReplayTimelineIndicator", new IMAGE_BRUSH("Images/ReplayTimelineIndicator", FVector2D(4.0f, 26.0f))); + + // The image used to draw the replay pause button + Style.Set("ShooterGame.ReplayPauseIcon", new IMAGE_BRUSH("Images/ReplayPause", FVector2D(32.0f, 32.0f))); + + // Fonts still need to be specified in code for now + Style.Set("ShooterGame.MenuServerListTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Black", 14)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FIntPoint(-1,1)) + ); + + Style.Set("ShooterGame.MenuStoreListTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Black", 14)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FIntPoint(-1, 1)) + ); + + Style.Set("ShooterGame.ScoreboardListTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Black", 14)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FIntPoint(-1,1)) + ); + + Style.Set("ShooterGame.MenuProfileNameStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Black", 18)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FIntPoint(-1,1)) + ); + + Style.Set("ShooterGame.MenuTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Black", 20)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FIntPoint(-1,1)) + ); + + Style.Set("ShooterGame.MenuHeaderTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Black", 26)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FIntPoint(-1,1)) + ); + + Style.Set("ShooterGame.WelcomeScreen.WelcomeTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Medium", 32)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FIntPoint(-1,1)) + ); + + Style.Set("ShooterGame.DefaultScoreboard.Row.HeaderTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Black", 24)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FVector2D(0,1)) + ); + + Style.Set("ShooterGame.DefaultScoreboard.Row.StatTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Regular", 18)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FVector2D(0,1)) + ); + + Style.Set("ShooterGame.SplitScreenLobby.StartMatchTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Regular", 16)) + .SetColorAndOpacity(FLinearColor::Green) + .SetShadowOffset(FVector2D(0,1)) + ); + + Style.Set("ShooterGame.DemoListCheckboxTextStyle", FTextBlockStyle() + .SetFont(TTF_FONT("Fonts/Roboto-Black", 12)) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FIntPoint(-1,1)) + ); + + Style.Set("ShooterGame.Switch.Left", FInlineTextImageStyle() + .SetImage(IMAGE_BRUSH("Images/SwitchButtonLeft", FVector2D(32, 32))) + ); + + Style.Set("ShooterGame.Switch.Right", FInlineTextImageStyle() + .SetImage(IMAGE_BRUSH("Images/SwitchButtonRight", FVector2D(32, 32))) + ); + + Style.Set("ShooterGame.Switch.Up", FInlineTextImageStyle() + .SetImage(IMAGE_BRUSH("Images/SwitchButtonUp", FVector2D(32, 32))) + ); + + Style.Set("ShooterGame.Switch.Down", FInlineTextImageStyle() + .SetImage(IMAGE_BRUSH("Images/SwitchButtonDown", FVector2D(32, 32))) + ); + + return StyleRef; +} +PRAGMA_ENABLE_OPTIMIZATION + +#undef IMAGE_BRUSH +#undef BOX_BRUSH +#undef BORDER_BRUSH +#undef TTF_FONT +#undef OTF_FONT + +void FShooterStyle::ReloadTextures() +{ + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); +} + +const ISlateStyle& FShooterStyle::Get() +{ + return *ShooterStyleInstance; +} diff --git a/Source/ShooterGame/Private/UI/Style/ShooterStyle.h b/Source/ShooterGame/Private/UI/Style/ShooterStyle.h new file mode 100644 index 0000000..90af8c5 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Style/ShooterStyle.h @@ -0,0 +1,31 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" + +class FShooterStyle +{ +public: + + static void Initialize(); + + static void Shutdown(); + + /** reloads textures used by slate renderer */ + static void ReloadTextures(); + + /** @return The Slate style set for the Shooter game */ + static const ISlateStyle& Get(); + + static FName GetStyleSetName(); + +private: + + static TSharedRef< class FSlateStyleSet > Create(); + +private: + + static TSharedPtr< class FSlateStyleSet > ShooterStyleInstance; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Private/UI/Widgets/SChatWidget.cpp b/Source/ShooterGame/Private/UI/Widgets/SChatWidget.cpp new file mode 100644 index 0000000..7b548a3 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SChatWidget.cpp @@ -0,0 +1,245 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "SChatWidget.h" +#include "ShooterStyle.h" +#include "ShooterChatWidgetStyle.h" + +#define CHAT_BOX_WIDTH 576.0f +#define CHAT_BOX_HEIGHT 192.0f +#define CHAT_BOX_PADDING 20.0f + +void SChatWidget::Construct(const FArguments& InArgs, const FLocalPlayerContext& InContext) +{ + ShooterHUDPCTrackerBase::Init(InContext); + + ChatStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterChatStyle"); + + bAlwaysVisible = InArgs._bAlwaysVisible; + bDismissAfterSay = InArgs._bDismissAfterSay; + + ChatFadeTime = 10.0; + LastChatLineTime = -1.0; + bVisibiltyNeedsFocus = true; + + //some constant values + const int32 PaddingValue = 2; + + // Copy the font we'll be using for chat, and limit the font fallback to localized only, for performance reasons + ChatFont = FShooterStyle::Get().GetFontStyle("ShooterGame.ChatFont"); + ChatFont.FontFallback = EFontFallback::FF_NoFallback; + + // Initialize Menu + ChildSlot + .HAlign(HAlign_Left) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + // The main background + .AutoHeight() + [ + SNew(SBorder) + .BorderImage(&ChatStyle->BackingBrush) + .Padding(FMargin(CHAT_BOX_PADDING, 16.0f, CHAT_BOX_PADDING, 24.0f)) + .BorderBackgroundColor(this, &SChatWidget::GetBorderColor) + [ + SNew(SBox) + .HeightOverride(CHAT_BOX_HEIGHT) + .WidthOverride(CHAT_BOX_WIDTH) + [ + SAssignNew(ChatHistoryListView, SListView< TSharedPtr >) + .SelectionMode(ESelectionMode::None) + .ListItemsSource(&ChatHistory) + .OnGenerateRow(this, &SChatWidget::GenerateChatRow) + ] + ] + ] + // Chat input + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBox) + .WidthOverride(CHAT_BOX_WIDTH) + .Padding(FMargin(11.0f, 0.0f)) + [ + SAssignNew(ChatEditBox, SEditableTextBox) + .OnTextCommitted(this, &SChatWidget::OnChatTextCommitted) + .MinDesiredWidth(CHAT_BOX_WIDTH) + .ClearKeyboardFocusOnCommit(false) + .HintText(NSLOCTEXT("ChatWidget", "SaySomething", "Say Something...")) + .Font(ChatFont) + .Style(&ChatStyle->TextEntryStyle) + ] + ] + ]; + // Setup visibilty + LastVisibility = bAlwaysVisible ? EVisibility::Visible : EVisibility::Hidden; + SetEntryVisibility( LastVisibility ); +} + + +FSlateColor SChatWidget::GetBorderColor() const +{ + return GetStyleColor(ChatStyle->BoxBorderColor.GetSpecifiedColor()); +} + +FSlateColor SChatWidget::GetChatLineColor() const +{ + return GetStyleColor(ChatStyle->TextColor.GetSpecifiedColor()); +} + +FSlateColor SChatWidget::GetStyleColor( const FLinearColor& InColor ) const +{ + const double EndTime = LastChatLineTime + ChatFadeTime; + const double CurrentTime = FSlateApplication::Get().GetCurrentTime(); + + // Get the requested color.on + FLinearColor ReturnColor = InColor; + + // Set the alpha to zero if we are not visible. (We could also fade out here if the time has ALMOST expired). + if (bAlwaysVisible == false) + { + if ((ChatEditBox->GetVisibility() == EVisibility::Hidden) || (CurrentTime > EndTime)) + { + ReturnColor.A = 0.0f; + } + } + + return FSlateColor( ReturnColor ); +} + +void SChatWidget::AddChatLine(const FText& ChatString, bool SetFocus) +{ + ChatHistory.Add(MakeShareable(new FChatLine(ChatString))); + + if(ChatHistoryListView.IsValid()) + { + FChatLine* LastLine = ChatHistory[ChatHistory.Num() - 1].Get(); + UE_LOG(LogOnline, Warning, TEXT("request scroll last=%s"), *LastLine->ChatString.ToString()); + ChatHistoryListView->RequestScrollIntoView(ChatHistory[ChatHistory.Num() - 1]); + } + + FSlateApplication::Get().PlaySound(ChatStyle->RxMessgeSound); + SetEntryVisibility( EVisibility::Visible ); + bVisibiltyNeedsFocus = SetFocus; +} + +EVisibility SChatWidget::GetEntryVisibility() const +{ + return ChatEditBox->GetVisibility(); +} + +void SChatWidget::SetEntryVisibility( TAttribute InVisibility ) +{ + // If we are making it visible reset the 'start' time + if( InVisibility.Get() == EVisibility::Visible ) + { + LastChatLineTime = FSlateApplication::Get().GetCurrentTime(); + } + + ChatEditBox->SetVisibility(InVisibility); + ChatHistoryListView->SetVisibility(InVisibility); + SetVisibility(InVisibility); +} + +void SChatWidget::OnChatTextCommitted(const FText& InText, ETextCommit::Type InCommitInfo) +{ + if (InCommitInfo == ETextCommit::OnEnter) + { + if (GetPlayerController().IsValid() && !InText.IsEmpty()) + { + // broadcast chat to other players + GetPlayerController()->Say(InText.ToString()); + + if(ChatEditBox.IsValid()) + { + // Add the string so we see it too (we will ignore our own strings in the receive function) + AddChatLine( InText, true ); + + // Clear the text + ChatEditBox->SetText(FText()); + + // Audible indication we sent a message + FSlateApplication::Get().PlaySound(ChatStyle->TxMessgeSound); + } + } + } + + // If we want to dismiss chat after say, and we are not always visible, hide it now. + if ((bAlwaysVisible == false) && ( bDismissAfterSay == true ) ) + { + SetEntryVisibility(EVisibility::Hidden); + } +} + +void SChatWidget::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) +{ + // Always tick the super. + SCompoundWidget::Tick( AllottedGeometry, InCurrentTime, InDeltaTime ); + + // If we have not got the keep visible flag set, and the fade time has expired hide the widget + const double CurrentTime = FSlateApplication::Get().GetCurrentTime(); + if( ( bAlwaysVisible == false ) && ( CurrentTime > ( LastChatLineTime + ChatFadeTime ) ) ) + { + if (LastVisibility != EVisibility::Hidden) + { + SetEntryVisibility(EVisibility::Hidden); + } + LastVisibility = GetEntryVisibility(); + } + + // Update the visibility. + if (GetEntryVisibility() != LastVisibility) + { + LastVisibility = GetEntryVisibility(); + if (LastVisibility == EVisibility::Visible) + { + // Enter UI mode + FSlateApplication::Get().SetKeyboardFocus( SharedThis(this) ); + + if (ChatEditBox.IsValid()) + { + FWidgetPath WidgetToFocusPath; + + bool bFoundPath = FSlateApplication::Get().FindPathToWidget(ChatEditBox.ToSharedRef(), WidgetToFocusPath); + if (bFoundPath && WidgetToFocusPath.IsValid() && bVisibiltyNeedsFocus == true ) + { + FSlateApplication::Get().SetKeyboardFocus(WidgetToFocusPath, EFocusCause::SetDirectly); + } + } + } + else + { + // Exit UI mode + FSlateApplication::Get().SetAllUserFocusToGameViewport(); + } + } + FSlateApplication::Get().GetPlatformApplication().Get()->Cursor->SetType(EMouseCursor::None); +} + + +FReply SChatWidget::OnFocusReceived( const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent ) +{ + return FReply::Handled().ReleaseMouseCapture().LockMouseToWidget( SharedThis( this ) ); +} + +TSharedRef SChatWidget::GenerateChatRow(TSharedPtr ChatLine, const TSharedRef& OwnerTable) +{ + return + SNew(STableRow< TSharedPtr< FChatLine> >, OwnerTable ) + [ + SNew(STextBlock) + .Text(ChatLine->ChatString) + .Font(ChatFont) + .ColorAndOpacity(this, &SChatWidget::GetChatLineColor) + .WrapTextAt(CHAT_BOX_WIDTH - CHAT_BOX_PADDING) + ]; +} + + +TSharedRef SChatWidget::AsWidget() +{ + return SharedThis(this); +} + diff --git a/Source/ShooterGame/Private/UI/Widgets/SChatWidget.h b/Source/ShooterGame/Private/UI/Widgets/SChatWidget.h new file mode 100644 index 0000000..20165f2 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SChatWidget.h @@ -0,0 +1,132 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ShooterHUDPCTrackerBase.h" + + +/** + * A chat widget. Contains a box with history and a text entry box. + */ +class SChatWidget : public SCompoundWidget, public ShooterHUDPCTrackerBase +{ +public: + SLATE_BEGIN_ARGS(SChatWidget) + : _bAlwaysVisible(false) + , _bDismissAfterSay(true) + {} + + /** Should the chat widget be kept visible at all times */ + SLATE_ARGUMENT(bool, bAlwaysVisible) + + /** Should the chat widget be dismissed after 'say'. */ + SLATE_ARGUMENT(bool, bDismissAfterSay) + + SLATE_END_ARGS() + + /** Needed for every widget */ + void Construct(const FArguments& InArgs, const FLocalPlayerContext& InContext); + + /** Gets the visibility of the entry widget. */ + EVisibility GetEntryVisibility() const; + + /** + * Sets the visibility of the chat widgets. + * + * @param InVisibility Required visibility. + */ + void SetEntryVisibility( TAttribute InVisibility ); + + /** + * Add a new chat line. + * + * @param ChatString String to add. + * @param SetFocus Should the window be given focus + */ + void AddChatLine(const FText &ChatString, bool SetFocus); + + TSharedRef AsWidget(); + +protected: + + // Struct to hold chat lines. + struct FChatLine + { + // Source string of this chat message. + FText ChatString; + + FChatLine(const FText& InChatString) + : ChatString(InChatString) + { + } + }; + + /** Update function. Allows us to focus keyboard. */ + void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ); + + /** The UI sets up the appropriate mouse settings upon focus. */ + FReply OnFocusReceived( const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent ); + + /** + * Delegate called when the text is commited. + * + * @param InText The committed text. + * @Param InCommitInfo The type of commit (eg. Pressed enter, changed focus) + */ + void OnChatTextCommitted(const FText& InText, ETextCommit::Type InCommitInfo); + + /** Return the border color. */ + FSlateColor GetBorderColor() const; + + /** Return the font color. */ + FSlateColor GetChatLineColor() const; + + /** + * Return the adjusted color based on whether the chatbox is visible + * + * @param InColor The color value to use + * + * @returns The color - with the alpha set to zero if the chatbox is not visible. + */ + FSlateColor GetStyleColor( const FLinearColor& InColor ) const; + + TSharedRef GenerateChatRow(TSharedPtr ChatLine, const TSharedRef& OwnerTable); + + /** Visibility of the entry widget previous frame. */ + EVisibility LastVisibility; + + /* If try we should set the focus when we change the visibility */ + uint32 bVisibiltyNeedsFocus:1; + + /** Time to show the chat lines as visible after receiving a chat message. */ + double ChatFadeTime; + + /** When we received our last chat message. */ + double LastChatLineTime; + + /** The edit text widget. */ + TSharedPtr< SEditableTextBox > ChatEditBox; + + /** The chat history list view. */ + TSharedPtr< SListView< TSharedPtr< FChatLine> > > ChatHistoryListView; + + /** The array of chat history. */ + TArray< TSharedPtr< FChatLine> > ChatHistory; + + /** Should this chatbox be kept visible. */ + uint32 bAlwaysVisible : 1; + + /** Should this chatbox be dismmised on sending a string. */ + uint32 bDismissAfterSay : 1; + + /** Style to use for this chat widget */ + const struct FShooterChatStyle *ChatStyle; + + /** Copy of the font used for chat, with the font fallback value modified */ + FSlateFontInfo ChatFont; +}; + + + diff --git a/Source/ShooterGame/Private/UI/Widgets/SShooterConfirmationDialog.cpp b/Source/ShooterGame/Private/UI/Widgets/SShooterConfirmationDialog.cpp new file mode 100644 index 0000000..f26f901 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SShooterConfirmationDialog.cpp @@ -0,0 +1,218 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterStyle.h" +#include "SShooterConfirmationDialog.h" +#include "ShooterMenuItemWidgetStyle.h" +#include "ShooterGameInstance.h" +#include "OnlineSubsystemUtils.h" + +#if !defined(SHOOTER_CONTROLLER_DISCONNECT_STRICT) + #define SHOOTER_CONTROLLER_DISCONNECT_STRICT 0 +#endif + +void SShooterConfirmationDialog::Construct( const FArguments& InArgs ) +{ + PlayerOwner = InArgs._PlayerOwner; + DialogType = InArgs._DialogType; + + OnConfirm = InArgs._OnConfirmClicked; + OnCancel = InArgs._OnCancelClicked; + + const FShooterMenuItemStyle* ItemStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterMenuItemStyle"); + const FButtonStyle* ButtonStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterButtonStyle"); + FLinearColor MenuTitleTextColor = FLinearColor(FColor(155,164,182)); + ChildSlot + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew( SVerticalBox ) + +SVerticalBox::Slot() + .AutoHeight() + .Padding(20.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(SBorder) + .Padding(50.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .BorderImage(&ItemStyle->BackgroundBrush) + .BorderBackgroundColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) + [ + SNew( STextBlock ) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + .ColorAndOpacity(MenuTitleTextColor) + .Text(InArgs._MessageText) + .WrapTextAt(500.0f) + ] + ] + +SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Padding(20.0f) + [ + SNew( SHorizontalBox) + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(20.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew( SButton ) + .ContentPadding(100) + .OnClicked(this, &SShooterConfirmationDialog::OnConfirmHandler) + .Text(InArgs._ConfirmText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + .ButtonStyle(ButtonStyle) + .IsFocusable(false) + ] + + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(20.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew( SButton ) + .ContentPadding(100) + .OnClicked(InArgs._OnCancelClicked) + .Text(InArgs._CancelText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + .ButtonStyle(ButtonStyle) + .Visibility(InArgs._CancelText.IsEmpty() == false ? EVisibility::Visible : EVisibility::Collapsed) + .IsFocusable(false) + ] + ] + ]; +} + +bool SShooterConfirmationDialog::SupportsKeyboardFocus() const +{ + return true; +} + +FReply SShooterConfirmationDialog::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) +{ + return FReply::Handled().ReleaseMouseCapture().SetUserFocus(SharedThis(this), EFocusCause::SetDirectly, true); +} + +FReply SShooterConfirmationDialog::OnConfirmHandler() +{ + return ExecuteConfirm(FSlateApplication::Get().GetUserIndexForKeyboard()); +} + +FReply SShooterConfirmationDialog::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) +{ + const FKey Key = KeyEvent.GetKey(); + const int32 UserIndex = KeyEvent.GetUserIndex(); + + // Filter input based on the type of this dialog + switch (DialogType) + { + case EShooterDialogType::Generic: + { + // Ignore input from users that don't own this dialog + if (PlayerOwner != nullptr && PlayerOwner->GetControllerId() != UserIndex) + { + return FReply::Unhandled(); + } + break; + } + + case EShooterDialogType::ControllerDisconnected: + { + // Only ignore input from controllers that are bound to local users + if(PlayerOwner != nullptr && PlayerOwner->GetGameInstance() != nullptr) + { + if (PlayerOwner->GetGameInstance()->FindLocalPlayerFromControllerId(UserIndex)) + { + return FReply::Unhandled(); + } + } + break; + } + } + + // For testing on PC + if ((Key == EKeys::Enter || Key == EKeys::Virtual_Accept) && !KeyEvent.IsRepeat()) + { + return ExecuteConfirm(UserIndex); + } + else if (Key == EKeys::Escape || Key == EKeys::Virtual_Back) + { + if(OnCancel.IsBound()) + { + return OnCancel.Execute(); + } + } + + return FReply::Unhandled(); +} + +FReply SShooterConfirmationDialog::ExecuteConfirm(const int32 UserIndex) +{ + if (OnConfirm.IsBound()) + { + //these two cases should be combined when we move to using PlatformUserIDs rather than ControllerIDs. +#if PLATFORM_PS4 || SHOOTER_CONTROLLER_DISCONNECT_STRICT + bool bExecute = false; + // For controller reconnection, bind the confirming controller to the owner of this dialog + if (DialogType == EShooterDialogType::ControllerDisconnected && PlayerOwner != nullptr) + { + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(PlayerOwner->GetWorld()); + if (OnlineSub) + { + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + if (IdentityInterface.IsValid()) + { + TSharedPtr IncomingUserId = IdentityInterface->GetUniquePlayerId(UserIndex); + FUniqueNetIdRepl DisconnectedId = PlayerOwner->GetCachedUniqueNetId(); + + if (DisconnectedId.IsValid() && IncomingUserId.IsValid() && *IncomingUserId == *DisconnectedId) + { + PlayerOwner->SetControllerId(UserIndex); + bExecute = true; + } +#if SHOOTER_CONTROLLER_PAIRING_PROMPT_FOR_NEW_USER_WHEN_RECONNECTING + else if (PlayerOwner->GetCachedUniqueNetId().IsValid()) //only show account picker if there is a user signed in + { + const IOnlineExternalUIPtr ExternalUIInterface = OnlineSub->GetExternalUIInterface(); + if (ExternalUIInterface.IsValid()) + { + ExternalUIInterface->ShowLoginUI(UserIndex, false, true, nullptr); + } + } + else + { + // no signed in user - just bind to this controller + PlayerOwner->SetControllerId(UserIndex); + bExecute = true; + } +#endif + } + } + } + else + { + bExecute = true; + } + + if (bExecute) + { + return OnConfirm.Execute(); + } +#else + // For controller reconnection, bind the confirming controller to the owner of this dialog + if (DialogType == EShooterDialogType::ControllerDisconnected && PlayerOwner != nullptr) + { + PlayerOwner->SetControllerId(UserIndex); + } + + return OnConfirm.Execute(); +#endif + } + + return FReply::Unhandled(); +} diff --git a/Source/ShooterGame/Private/UI/Widgets/SShooterConfirmationDialog.h b/Source/ShooterGame/Private/UI/Widgets/SShooterConfirmationDialog.h new file mode 100644 index 0000000..d94c1c4 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SShooterConfirmationDialog.h @@ -0,0 +1,50 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ShooterGame.h" +#include "ShooterTypes.h" + +class SShooterConfirmationDialog : public SCompoundWidget +{ +public: + /** The player that owns the dialog. */ + TWeakObjectPtr PlayerOwner; + + /** The delegate for confirming */ + FOnClicked OnConfirm; + + /** The delegate for cancelling */ + FOnClicked OnCancel; + + /* The type of dialog this is */ + EShooterDialogType::Type DialogType; + + SLATE_BEGIN_ARGS( SShooterConfirmationDialog ) + {} + + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + SLATE_ARGUMENT(EShooterDialogType::Type, DialogType) + + SLATE_ARGUMENT(FText, MessageText) + SLATE_ARGUMENT(FText, ConfirmText) + SLATE_ARGUMENT(FText, CancelText) + + SLATE_ARGUMENT(FOnClicked, OnConfirmClicked) + SLATE_ARGUMENT(FOnClicked, OnCancelClicked) + + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + virtual bool SupportsKeyboardFocus() const override; + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override; + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) override; + +private: + + FReply OnConfirmHandler(); + FReply ExecuteConfirm(const int32 UserIndex); + +}; diff --git a/Source/ShooterGame/Private/UI/Widgets/SShooterDemoHUD.cpp b/Source/ShooterGame/Private/UI/Widgets/SShooterDemoHUD.cpp new file mode 100644 index 0000000..89e8188 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SShooterDemoHUD.cpp @@ -0,0 +1,306 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "SShooterDemoHUD.h" +#include "Engine/DemoNetDriver.h" +#include "ShooterStyle.h" +#include "CoreStyle.h" + +/** Widget to represent the main replay timeline bar */ +class SShooterReplayTimeline : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterReplayTimeline) + : _DemoDriver(nullptr) + , _BackgroundBrush( FCoreStyle::Get().GetDefaultBrush() ) + , _IndicatorBrush( FCoreStyle::Get().GetDefaultBrush() ) + {} + SLATE_ARGUMENT(TWeakObjectPtr, DemoDriver) + SLATE_ATTRIBUTE( FMargin, BackgroundPadding ) + SLATE_ATTRIBUTE( const FSlateBrush*, BackgroundBrush ) + SLATE_ATTRIBUTE( const FSlateBrush*, IndicatorBrush ) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + +private: + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; + virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override; + + /** Jumps the replay to the time on the bar that was clicked */ + FReply OnTimelineClicked(const FGeometry& Geometry, const FPointerEvent& Event); + + /** The demo net driver underlying the current replay */ + TWeakObjectPtr DemoDriver; + + /** The FName of the image resource to show */ + TAttribute< const FSlateBrush* > BackgroundBrush; + + /** The FName of the image resource to show */ + TAttribute< const FSlateBrush* > IndicatorBrush; +}; + +void SShooterReplayTimeline::Construct(const FArguments& InArgs) +{ + DemoDriver = InArgs._DemoDriver; + BackgroundBrush = InArgs._BackgroundBrush; + IndicatorBrush = InArgs._IndicatorBrush; + + ChildSlot + .Padding(InArgs._BackgroundPadding) + [ + SNew(SBorder) + .BorderImage(BackgroundBrush) + .OnMouseButtonDown(this, &SShooterReplayTimeline::OnTimelineClicked) + ]; +} + +FVector2D SShooterReplayTimeline::ComputeDesiredSize(float LayoutScaleMultiplier) const +{ + // Just use the size of the indicator image here + const FSlateBrush* ImageBrush = IndicatorBrush.Get(); + if (ImageBrush != nullptr) + { + return ImageBrush->ImageSize; + } + return FVector2D::ZeroVector; +} + +int32 SShooterReplayTimeline::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const +{ + const int32 ParentLayerId = SCompoundWidget::OnPaint(Args, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); + + // Manually draw the position indicator + const FSlateBrush* ImageBrush = IndicatorBrush.Get(); + if ((ImageBrush != nullptr) && (ImageBrush->DrawAs != ESlateBrushDrawType::NoDrawType) && DemoDriver.IsValid()) + { + const bool bIsEnabled = ShouldBeEnabled(bParentEnabled); + const ESlateDrawEffect DrawEffects = bIsEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect; + + const FLinearColor FinalColorAndOpacity( InWidgetStyle.GetColorAndOpacityTint() * ColorAndOpacity.Get() * ImageBrush->GetTint( InWidgetStyle ) ); + + // Adjust clipping rect to replay time + const float ReplayPercent = DemoDriver->GetDemoCurrentTime() / DemoDriver->GetDemoTotalTime(); + + const FVector2D Center( + AllottedGeometry.GetLocalSize().X * ReplayPercent, + AllottedGeometry.GetLocalSize().Y * 0.5f + ); + + const FVector2D Offset = Center - ImageBrush->ImageSize * 0.5f; + + const int32 IndicatorLayerId = ParentLayerId + 1; + + FSlateDrawElement::MakeBox( + OutDrawElements, + IndicatorLayerId, + AllottedGeometry.ToPaintGeometry(Offset, ImageBrush->ImageSize), + ImageBrush, + DrawEffects, + FinalColorAndOpacity + ); + + return IndicatorLayerId; + } + + return ParentLayerId; +} + +FReply SShooterReplayTimeline::OnTimelineClicked(const FGeometry& Geometry, const FPointerEvent& Event) +{ + if (DemoDriver.IsValid()) + { + const FVector2D LocalPos = Geometry.AbsoluteToLocal(Event.GetScreenSpacePosition()); + + const float TimelinePercentage = LocalPos.X / Geometry.GetLocalSize().X; + + DemoDriver->GotoTimeInSeconds( TimelinePercentage * DemoDriver->GetDemoTotalTime() ); + + return FReply::Handled(); + } + + return FReply::Unhandled(); +} + +void SShooterDemoHUD::Construct(const FArguments& InArgs) +{ + PlayerOwner = InArgs._PlayerOwner; + check(PlayerOwner.IsValid()); + + ChildSlot + [ + SNew(SVerticalBox) + + +SVerticalBox::Slot() + .FillHeight(8.5f) + .VAlign(VAlign_Bottom) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .FillWidth(1.5f) + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Margin(3.0f) + .Text(this, &SShooterDemoHUD::GetCurrentReplayTime) + ] + + +SHorizontalBox::Slot() + .FillWidth(7.0f) + [ + + SNew(SOverlay) + +SOverlay::Slot() + [ + SNew(SShooterReplayTimeline) + .DemoDriver(PlayerOwner->GetWorld()->GetDemoNetDriver()) + .BackgroundBrush(FShooterStyle::Get().GetBrush("ShooterGame.ReplayTimelineBorder")) + .BackgroundPadding(FMargin(0.0f, 3.0)) + .IndicatorBrush(FShooterStyle::Get().GetBrush("ShooterGame.ReplayTimelineIndicator")) + ] + + +SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Visibility(EVisibility::HitTestInvisible) + .Margin(3.0f) + .Text(this, &SShooterDemoHUD::GetPlaybackSpeed) + ] + ] + + +SHorizontalBox::Slot() + .FillWidth(1.5f) + .HAlign(HAlign_Left) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Margin(3.0f) + .Text(this, &SShooterDemoHUD::GetTotalReplayTime) + ] + ] + + +SVerticalBox::Slot() + .FillHeight(1.5f) + .HAlign(HAlign_Center) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(FMargin(6.0)) + .AutoHeight() + [ + SNew(SCheckBox) + .IsFocusable(false) + .Style(FCoreStyle::Get(), "ToggleButtonCheckbox") + .IsChecked(this, &SShooterDemoHUD::IsPauseChecked) + .OnCheckStateChanged(this, &SShooterDemoHUD::OnPauseCheckStateChanged) + [ + SNew(SImage) + .Image(FShooterStyle::Get().GetBrush("ShooterGame.ReplayPauseIcon")) + ] + ] + + +SVerticalBox::Slot() + ] + ]; +} + +FText SShooterDemoHUD::GetCurrentReplayTime() const +{ + if (!PlayerOwner.IsValid()) + { + return FText::GetEmpty(); + } + + UDemoNetDriver* DemoDriver = PlayerOwner->GetWorld()->GetDemoNetDriver(); + + if (DemoDriver == nullptr) + { + return FText::GetEmpty(); + } + + return FText::AsTimespan(FTimespan::FromSeconds(DemoDriver->GetDemoCurrentTime())); +} + +FText SShooterDemoHUD::GetTotalReplayTime() const +{ + if (!PlayerOwner.IsValid()) + { + return FText::GetEmpty(); + } + + UDemoNetDriver* DemoDriver = PlayerOwner->GetWorld()->GetDemoNetDriver(); + + if (DemoDriver == nullptr) + { + return FText::GetEmpty(); + } + + return FText::AsTimespan(FTimespan::FromSeconds(DemoDriver->GetDemoTotalTime())); +} + +FText SShooterDemoHUD::GetPlaybackSpeed() const +{ + if (!PlayerOwner.IsValid() || PlayerOwner->GetWorldSettings() == nullptr) + { + return FText::GetEmpty(); + } + + if (PlayerOwner->GetWorldSettings()->GetPauserPlayerState() == nullptr) + { + FNumberFormattingOptions FormatOptions = FNumberFormattingOptions() + .SetMinimumFractionalDigits(2) + .SetMaximumFractionalDigits(2); + + return FText::Format(NSLOCTEXT("ShooterGame.HUD.Menu", "PlaybackSpeed", "{0} X"), FText::AsNumber(PlayerOwner->GetWorldSettings()->DemoPlayTimeDilation, &FormatOptions)); + } + + return NSLOCTEXT("ShooterGame.HUD.Menu", "Paused", "PAUSED"); +} + +ECheckBoxState SShooterDemoHUD::IsPauseChecked() const +{ + if (PlayerOwner.IsValid() && PlayerOwner->GetWorldSettings() != nullptr && PlayerOwner->GetWorldSettings()->GetPauserPlayerState() != nullptr) + { + return ECheckBoxState::Checked; + } + + return ECheckBoxState::Unchecked; +} + +void SShooterDemoHUD::OnPauseCheckStateChanged(ECheckBoxState CheckState) const +{ + if (!PlayerOwner.IsValid()) + { + return; + } + + AWorldSettings* WorldSettings = PlayerOwner->GetWorldSettings(); + + if (WorldSettings == nullptr) + { + return; + } + + switch(CheckState) + { + case ECheckBoxState::Checked: + { + WorldSettings->SetPauserPlayerState(PlayerOwner->PlayerState); + break; + } + + case ECheckBoxState::Unchecked: + { + WorldSettings->SetPauserPlayerState(nullptr); + break; + } + + case ECheckBoxState::Undetermined: + { + break; + } + } +} diff --git a/Source/ShooterGame/Private/UI/Widgets/SShooterDemoHUD.h b/Source/ShooterGame/Private/UI/Widgets/SShooterDemoHUD.h new file mode 100644 index 0000000..33a8905 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SShooterDemoHUD.h @@ -0,0 +1,38 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" + +class APlayerController; + +/** Shows the replay timeline bar, current time and total time of the replay, current playback speed, and a pause toggle button. */ +class SShooterDemoHUD : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterDemoHUD) + : _PlayerOwner(nullptr) + {} + + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + virtual bool SupportsKeyboardFocus() const override { return true; } + +private: + + TWeakObjectPtr PlayerOwner; + + FText GetCurrentReplayTime() const; + FText GetTotalReplayTime() const; + FText GetPlaybackSpeed() const; + + ECheckBoxState IsPauseChecked() const; + void OnPauseCheckStateChanged(ECheckBoxState CheckState) const; +}; + + diff --git a/Source/ShooterGame/Private/UI/Widgets/SShooterScoreboardWidget.cpp b/Source/ShooterGame/Private/UI/Widgets/SShooterScoreboardWidget.cpp new file mode 100644 index 0000000..74e88e7 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SShooterScoreboardWidget.cpp @@ -0,0 +1,875 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "SShooterScoreboardWidget.h" +#include "ShooterStyle.h" +#include "ShooterScoreboardWidgetStyle.h" +#include "ShooterUIHelpers.h" +#include "Online/ShooterPlayerState.h" + +#define LOCTEXT_NAMESPACE "ShooterScoreboard" + +// @todo: prevent interaction on PC for now (see OnFocusReceived for reasons) +#if !defined(INTERACTIVE_SCOREBOARD) + #define INTERACTIVE_SCOREBOARD 0 +#endif + +#define NORM_PADDING (FMargin(5)) + +void SShooterScoreboardWidget::Construct(const FArguments& InArgs) +{ + ScoreboardStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterScoreboardStyle"); + + PCOwner = InArgs._PCOwner; + ScoreboardTint = FLinearColor(0.0f,0.0f,0.0f,0.4f); + ScoreBoxWidth = 140.0f; + ScoreCountUpTime = 2.0f; + + ScoreboardStartTime = FPlatformTime::Seconds(); + MatchState = InArgs._MatchState.Get(); + + UpdatePlayerStateMaps(); + + Columns.Add(FColumnData(LOCTEXT("KillsColumn", "Kills"), + ScoreboardStyle->KillStatColor, + FOnGetPlayerStateAttribute::CreateSP(this, &SShooterScoreboardWidget::GetAttributeValue_Kills))); + + Columns.Add(FColumnData(LOCTEXT("DeathsColumn", "Deaths"), + ScoreboardStyle->DeathStatColor, + FOnGetPlayerStateAttribute::CreateSP(this, &SShooterScoreboardWidget::GetAttributeValue_Deaths))); + + Columns.Add(FColumnData(LOCTEXT("ScoreColumn", "Score"), + ScoreboardStyle->ScoreStatColor, + FOnGetPlayerStateAttribute::CreateSP(this, &SShooterScoreboardWidget::GetAttributeValue_Score))); + + TSharedPtr HeaderCols; + + const TSharedRef ScoreboardGrid = SNew(SVerticalBox) + // HEADER ROW + +SVerticalBox::Slot() .AutoHeight() + [ + //Padding in the header row (invisible) for speaker icon + SAssignNew(HeaderCols, SHorizontalBox) + + SHorizontalBox::Slot().Padding(NORM_PADDING+FMargin(2,0,0,0)).AutoWidth() + [ + SNew(SImage) + .Image(FShooterStyle::Get().GetBrush("ShooterGame.Speaker")) + .Visibility(EVisibility::Hidden) + ] + + //Player Name autosized column + +SHorizontalBox::Slot() .Padding(NORM_PADDING) + [ + SNew(SBorder) + .Padding(NORM_PADDING) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .BorderImage(&ScoreboardStyle->ItemBorderBrush) + .BorderBackgroundColor(ScoreboardTint) + [ + SNew(STextBlock) + .Text(LOCTEXT("PlayerNameColumn", "Player Name")) + .TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultScoreboard.Row.HeaderTextStyle") + ] + ] + ]; + + for (uint8 ColIdx = 0; ColIdx < Columns.Num(); ColIdx++ ) + { + //Header constant sized columns + HeaderCols->AddSlot() .VAlign(VAlign_Center) .HAlign(HAlign_Center) .AutoWidth() .Padding(NORM_PADDING) + [ + SNew(SBorder) + .Padding(NORM_PADDING) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .BorderImage(&ScoreboardStyle->ItemBorderBrush) + .BorderBackgroundColor(ScoreboardTint) + [ + SNew(SBox) + .WidthOverride(ScoreBoxWidth) + .HAlign(HAlign_Center) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(Columns[ColIdx].Name) + .TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultScoreboard.Row.HeaderTextStyle") + .ColorAndOpacity(Columns[ColIdx].Color) + ] + ] + ] + ]; + } + + ScoreboardGrid->AddSlot() .AutoHeight() + [ + SAssignNew(ScoreboardData, SVerticalBox) + ]; + UpdateScoreboardGrid(); + + SBorder::Construct( + SBorder::FArguments() + .BorderImage(&ScoreboardStyle->ItemBorderBrush) + .BorderBackgroundColor(ScoreboardTint) + [ + ScoreboardGrid + ] + ); +} + +void SShooterScoreboardWidget::StoreTalkingPlayerData(const FUniqueNetId& PlayerId, bool bIsTalking) +{ + static TMap LastTimeSpoken; + int32 FoundIndex = -1; + const double IconTimeout = 0.1; + double CurrentTime = FPlatformTime::Seconds(); + + for (int32 i = 0; i < PlayersTalkingThisFrame.Num(); ++i) + { + if (PlayersTalkingThisFrame[i].Key.Get() == PlayerId) + { + FoundIndex = i; + } + } + + if (FoundIndex < 0) + { + FoundIndex = PlayersTalkingThisFrame.Emplace(PlayerId.AsShared(), false); + } + + if (bIsTalking) + { + double* Value = LastTimeSpoken.Find(PlayerId.ToString()); + if (Value) + { + *Value = CurrentTime; + } + else + { + LastTimeSpoken.Add(PlayerId.ToString(), CurrentTime); + } + } + + if (LastTimeSpoken.FindRef(PlayerId.ToString()) > CurrentTime - IconTimeout) + { + PlayersTalkingThisFrame[FoundIndex].Value = true; + } + else + { + PlayersTalkingThisFrame[FoundIndex].Value = false; + } +} + +FText SShooterScoreboardWidget::GetMatchEndText() const +{ + if (PCOwner.IsValid() && (PCOwner->GetWorld() != NULL )) + { + AShooterGameState* const GameState = PCOwner->GetWorld()->GetGameState(); + if (GameState) + { + if (GameState->RemainingTime > 0) + { + return FText::Format(LOCTEXT("MatchEndTimeString", "Match ending in: {0}"), FText::AsNumber(GameState->RemainingTime)); + } + else + { + return LOCTEXT("MatchEndingString", "Match ending..."); + } + } + } + + return FText::GetEmpty(); +} + +FText SShooterScoreboardWidget::GetMatchOutcomeText() const +{ + FText OutcomeText = FText::GetEmpty(); + + if (MatchState == EShooterMatchState::Won) + { + OutcomeText = LOCTEXT("Winner", "YOU ARE THE WINNER!"); + } + else if (MatchState == EShooterMatchState::Lost) + { + OutcomeText = LOCTEXT("Loser", "Match has finished"); + } + + return OutcomeText; +} + +void SShooterScoreboardWidget::UpdateScoreboardGrid() +{ + ScoreboardData->ClearChildren(); + for (uint8 TeamNum = 0; TeamNum < PlayerStateMaps.Num(); TeamNum++) + { + //Player rows from each team + ScoreboardData->AddSlot() .AutoHeight() + [ + MakePlayerRows(TeamNum) + ]; + //If we have more than one team, we are playing team based game mode, add totals + if (PlayerStateMaps.Num() > 1 && PlayerStateMaps[TeamNum].Num() > 0) + { + // Horizontal Ruler + ScoreboardData->AddSlot() .AutoHeight() .Padding(NORM_PADDING) + [ + SNew(SBorder) + .Padding(1) + .BorderImage(&ScoreboardStyle->ItemBorderBrush) + ]; + ScoreboardData->AddSlot() .AutoHeight() + [ + MakeTotalsRow(TeamNum) + ]; + } + } + + if (MatchState > EShooterMatchState::Playing) + { + ScoreboardData->AddSlot() .AutoHeight() .Padding(NORM_PADDING) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() .HAlign(HAlign_Fill) + [ + SNew(SBox) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(this, &SShooterScoreboardWidget::GetMatchOutcomeText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + ] + ] + ]; + + ScoreboardData->AddSlot() .AutoHeight() .Padding(NORM_PADDING) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() .HAlign(HAlign_Fill) + [ + SNew(SBox) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(this, &SShooterScoreboardWidget::GetMatchEndText) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + ] + ] + ]; + + ScoreboardData->AddSlot() .AutoHeight() .Padding(NORM_PADDING) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() .HAlign(HAlign_Right) + [ + SNew(SBox) + .HAlign(HAlign_Right) + [ + SNew(STextBlock) + .Text(ShooterUIHelpers::Get().GetProfileOpenText()) + .TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultScoreboard.Row.StatTextStyle") + .Visibility(this, &SShooterScoreboardWidget::GetProfileUIVisibility) + ] + ] + ]; + } +} + +void SShooterScoreboardWidget::UpdatePlayerStateMaps() +{ + if (PCOwner.IsValid()) + { + AShooterGameState* const GameState = PCOwner->GetWorld()->GetGameState(); + if (GameState) + { + bool bRequiresWidgetUpdate = false; + const int32 NumTeams = FMath::Max(GameState->NumTeams, 1); + LastTeamPlayerCount.Reset(); + LastTeamPlayerCount.AddZeroed(PlayerStateMaps.Num()); + for (int32 i = 0; i < PlayerStateMaps.Num(); i++) + { + LastTeamPlayerCount[i] = PlayerStateMaps[i].Num(); + } + + PlayerStateMaps.Reset(); + PlayerStateMaps.AddZeroed(NumTeams); + + for (int32 i = 0; i < NumTeams; i++) + { + GameState->GetRankedMap(i, PlayerStateMaps[i]); + + if (LastTeamPlayerCount.Num() > 0 && PlayerStateMaps[i].Num() != LastTeamPlayerCount[i]) + { + bRequiresWidgetUpdate = true; + } + } + if (bRequiresWidgetUpdate) + { + UpdateScoreboardGrid(); + } + } + } + + UpdateSelectedPlayer(); +} + +void SShooterScoreboardWidget::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) +{ + UpdatePlayerStateMaps(); +} + +bool SShooterScoreboardWidget::SupportsKeyboardFocus() const +{ +#if INTERACTIVE_SCOREBOARD + if (MatchState > EShooterMatchState::Playing) + { + return true; + } +#endif + return false; +} + +FReply SShooterScoreboardWidget::OnFocusReceived( const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent ) +{ + // @todo: may not want to affect all controllers if split screen + + // @todo: not-pc: need to support mouse focus too (alt+tabbing, windowed, etc) + // @todo: not-pc: after each round, the mouse is released but as soon as you click it's recaptured and input stops working + return FReply::Handled().ReleaseMouseCapture().SetUserFocus(SharedThis(this), EFocusCause::SetDirectly, true); +} + +FReply SShooterScoreboardWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) +{ + FReply Result = FReply::Unhandled(); + const FKey Key = InKeyEvent.GetKey(); + if (MatchState > EShooterMatchState::Playing) + { + if (Key == EKeys::Up || Key == EKeys::Gamepad_DPad_Up || Key == EKeys::Gamepad_LeftStick_Up) + { + OnSelectedPlayerPrev(); + Result = FReply::Handled(); + } + else if (Key == EKeys::Down || Key == EKeys::Gamepad_DPad_Down || Key == EKeys::Gamepad_LeftStick_Down) + { + OnSelectedPlayerNext(); + Result = FReply::Handled(); + } + else if (Key == EKeys::Enter || Key == EKeys::Virtual_Accept) + { + ProfileUIOpened(); + Result = FReply::Handled(); + } + else if ((Key == EKeys::Escape || Key == EKeys::Gamepad_Special_Right || Key == EKeys::Global_Play || Key == EKeys::Global_Menu) && !InKeyEvent.IsRepeat()) + { + // Allow the user to pause from the scoreboard still + if (AShooterPlayerController* const PC = Cast(PCOwner.Get())) + { + PC->OnToggleInGameMenu(); + } + Result = FReply::Handled(); + } + } + return Result; +} + +void SShooterScoreboardWidget::PlaySound(const FSlateSound& SoundToPlay) const +{ + if( PCOwner.IsValid() ) + { + if( ULocalPlayer* LocalPlayer = Cast(PCOwner->Player) ) + { + FSlateApplication::Get().PlaySound(SoundToPlay, LocalPlayer->GetControllerId()); + } + } +} + +FReply SShooterScoreboardWidget::OnMouseOverPlayer(const FGeometry& Geometry, const FPointerEvent& Event, const FTeamPlayer TeamPlayer) +{ +#if INTERACTIVE_SCOREBOARD + if( !(SelectedPlayer == TeamPlayer) ) + { + SelectedPlayer = TeamPlayer; + PlaySound(ScoreboardStyle->PlayerChangeSound); + } +#endif + return FReply::Handled(); +} + +void SShooterScoreboardWidget::OnSelectedPlayerPrev() +{ + // Make sure we have a valid index to start with + if( !SelectedPlayer.IsValid() && !SetSelectedPlayerUs()) + { + return; + } + + // We don't want to decrease the player Id, we want to find the current one in the list and then select the one before it + int32 PrevPlayerId = -1; + for (RankedPlayerMap::TConstIterator PlayerIt(PlayerStateMaps[SelectedPlayer.TeamNum]); PlayerIt; ++PlayerIt) // This would be easier with a .ReverseIterator call + { + const int32 PlayerId = PlayerIt.Key(); + if( PlayerId == SelectedPlayer.PlayerId ) + { + break; + } + PrevPlayerId = PlayerId; + } + + if( PrevPlayerId == -1 ) + { + // If the id is still invalid, that means the current selection was first in their team, try the previous team... + SelectedPlayer.TeamNum--; + if (!PlayerStateMaps.IsValidIndex(SelectedPlayer.TeamNum)) + { + // If there isn't a previous team, move to the last team + SelectedPlayer.TeamNum = PlayerStateMaps.Num() - 1; + check(PlayerStateMaps.IsValidIndex(SelectedPlayer.TeamNum)); + } + + // We want the last player in the team + for (RankedPlayerMap::TConstIterator PlayerIt(PlayerStateMaps[SelectedPlayer.TeamNum]); PlayerIt; ++PlayerIt) // This would be easier with a .Last call + { + const int32 PlayerId = PlayerIt.Key(); + PrevPlayerId = PlayerId; + } + } + + check( SelectedPlayer.PlayerId != -1 ); + SelectedPlayer.PlayerId = PrevPlayerId; + PlaySound(ScoreboardStyle->PlayerChangeSound); +} + +void SShooterScoreboardWidget::OnSelectedPlayerNext() +{ + // Make sure we have a valid index to start with + if( !SelectedPlayer.IsValid() && !SetSelectedPlayerUs()) + { + return; + } + + // We don't want to increase the player Id, we want to find the current one in the list and then select the one after + bool bNext = false; + for (RankedPlayerMap::TConstIterator PlayerIt(PlayerStateMaps[SelectedPlayer.TeamNum]); PlayerIt; ++PlayerIt) + { + const int32 PlayerId = PlayerIt.Key(); + if( PlayerId == SelectedPlayer.PlayerId ) + { + bNext = true; + } + else if( bNext == true ) + { + SelectedPlayer.PlayerId = PlayerId; + PlaySound(ScoreboardStyle->PlayerChangeSound); + + bNext = false; + break; + } + } + + if( bNext == true ) + { + // If next is still true, our current selection was last in their team, try the next team... + SelectedPlayer.TeamNum++; + if (!PlayerStateMaps.IsValidIndex(SelectedPlayer.TeamNum)) + { + // If there isn't a next team, move to the first team + SelectedPlayer.TeamNum = 0; + check(PlayerStateMaps.IsValidIndex(SelectedPlayer.TeamNum)); + } + + SelectedPlayer.PlayerId = 0; + PlaySound(ScoreboardStyle->PlayerChangeSound); + } +} + +void SShooterScoreboardWidget::ResetSelectedPlayer() +{ + SelectedPlayer = FTeamPlayer(); +} + +void SShooterScoreboardWidget::UpdateSelectedPlayer() +{ + // Make sure the selected player is still valid... + if( SelectedPlayer.IsValid() ) + { + const AShooterPlayerState* PlayerState = GetSortedPlayerState(SelectedPlayer); + if( !PlayerState ) + { + // Player is no longer valid, reset (note: reset implies 'us' in IsSelectedPlayer and IsPlayerSelectedAndValid) + ResetSelectedPlayer(); + } + } +} + +bool SShooterScoreboardWidget::SetSelectedPlayerUs() +{ + ResetSelectedPlayer(); + + // Set the owner player to be the default focused one + if( APlayerController* const PC = PCOwner.Get() ) + { + for (uint8 TeamNum = 0; TeamNum < PlayerStateMaps.Num(); TeamNum++) + { + for (RankedPlayerMap::TConstIterator PlayerIt(PlayerStateMaps[TeamNum]); PlayerIt; ++PlayerIt) + { + const TWeakObjectPtr PlayerState = PlayerIt.Value(); + if( PlayerState.IsValid() && PC->PlayerState && PC->PlayerState == PlayerState.Get() ) + { + SelectedPlayer = FTeamPlayer(TeamNum, PlayerIt.Key()); + return true; + } + } + } + } + return false; +} + +bool SShooterScoreboardWidget::IsSelectedPlayer(const FTeamPlayer& TeamPlayer) const +{ + if( !SelectedPlayer.IsValid() ) + { + // If not explicitly set, test to see if the owner player was passed. + if( IsOwnerPlayer(TeamPlayer) ) + { + return true; + } + } + else if( SelectedPlayer == TeamPlayer ) + { + return true; + } + return false; +} + +bool SShooterScoreboardWidget::IsPlayerSelectedAndValid() const +{ +#if INTERACTIVE_SCOREBOARD + if( !SelectedPlayer.IsValid() ) + { + // Nothing is selected, default to the player + if( PCOwner.IsValid() && PCOwner->PlayerState ) + { + const TSharedPtr OwnerNetId = PCOwner->PlayerState->GetUniqueId().GetUniqueNetId(); + return OwnerNetId.IsValid(); + } + } + else if( const AShooterPlayerState* PlayerState = GetSortedPlayerState(SelectedPlayer) ) + { + const TSharedPtr& PlayerId = PlayerState->GetUniqueId().GetUniqueNetId(); + return PlayerId.IsValid(); + } +#endif + return false; +} + +EVisibility SShooterScoreboardWidget::GetProfileUIVisibility() const +{ + return IsPlayerSelectedAndValid() ? EVisibility::Visible : EVisibility::Hidden; +} + +bool SShooterScoreboardWidget::ProfileUIOpened() const +{ + if( IsPlayerSelectedAndValid() ) + { + check( PCOwner.IsValid() && PCOwner->PlayerState ); + const TSharedPtr& OwnerNetId = PCOwner->PlayerState->GetUniqueId().GetUniqueNetId(); + check( OwnerNetId.IsValid() ); + + const TSharedPtr& PlayerId = ( !SelectedPlayer.IsValid() ? OwnerNetId : GetSortedPlayerState(SelectedPlayer)->GetUniqueId().GetUniqueNetId() ); + check( PlayerId.IsValid() ); + return ShooterUIHelpers::Get().ProfileOpenedUI(PCOwner->GetWorld(), *OwnerNetId.Get(), *PlayerId.Get(), NULL); + } + return false; +} + +EVisibility SShooterScoreboardWidget::PlayerPresenceToItemVisibility(const FTeamPlayer TeamPlayer) const +{ + return GetSortedPlayerState(TeamPlayer) ? EVisibility::Visible : EVisibility::Collapsed; +} + +EVisibility SShooterScoreboardWidget::SpeakerIconVisibility(const FTeamPlayer TeamPlayer) const +{ + AShooterPlayerState* PlayerState = GetSortedPlayerState(TeamPlayer); + if (PlayerState) + { + const FUniqueNetIdRepl& PlayerUniqueId = PlayerState->GetUniqueId(); + for (int32 i = 0; i < PlayersTalkingThisFrame.Num(); ++i) + { + if (PlayerUniqueId == PlayersTalkingThisFrame[i].Key && PlayersTalkingThisFrame[i].Value) + { + return EVisibility::Visible; + } + } + } + return EVisibility::Hidden; +} + +FSlateColor SShooterScoreboardWidget::GetScoreboardBorderColor(const FTeamPlayer TeamPlayer) const +{ + const bool bIsSelected = IsSelectedPlayer(TeamPlayer); + const int32 RedTeam = 0; + const float BaseValue = bIsSelected == true ? 0.15f : 0.0f; + const float AlphaValue = bIsSelected == true ? 1.0f : 0.3f; + float RedValue = TeamPlayer.TeamNum == RedTeam ? 0.25f : 0.0f; + float BlueValue = TeamPlayer.TeamNum != RedTeam ? 0.25f : 0.0f; + return FLinearColor(BaseValue + RedValue, BaseValue, BaseValue + BlueValue, AlphaValue); +} + +FText SShooterScoreboardWidget::GetPlayerName(const FTeamPlayer TeamPlayer) const +{ + const AShooterPlayerState* PlayerState = GetSortedPlayerState(TeamPlayer); + if (PlayerState) + { + return FText::FromString(PlayerState->GetShortPlayerName()); + } + + return FText::GetEmpty(); +} + +bool SShooterScoreboardWidget::ShouldPlayerBeDisplayed(const FTeamPlayer TeamPlayer) const +{ + const AShooterPlayerState* PlayerState = GetSortedPlayerState(TeamPlayer); + + return PlayerState != nullptr && !PlayerState->IsOnlyASpectator(); +} + +FSlateColor SShooterScoreboardWidget::GetPlayerColor(const FTeamPlayer TeamPlayer) const +{ + // If this is the owner players row, tint the text color to show ourselves more clearly + if( IsOwnerPlayer(TeamPlayer) ) + { + return FSlateColor(FLinearColor::Yellow); + } + + const FTextBlockStyle& TextStyle = FShooterStyle::Get().GetWidgetStyle("ShooterGame.DefaultScoreboard.Row.StatTextStyle"); + return TextStyle.ColorAndOpacity; +} + +FSlateColor SShooterScoreboardWidget::GetColumnColor(const FTeamPlayer TeamPlayer, uint8 ColIdx) const +{ + // If this is the owner players row, tint the text color to show ourselves more clearly + if( IsOwnerPlayer(TeamPlayer) ) + { + return FSlateColor(FLinearColor::Yellow); + } + + check(Columns.IsValidIndex(ColIdx)); + return Columns[ColIdx].Color; +} + +bool SShooterScoreboardWidget::IsOwnerPlayer(const FTeamPlayer& TeamPlayer) const +{ + return ( PCOwner.IsValid() && PCOwner->PlayerState && PCOwner->PlayerState == GetSortedPlayerState(TeamPlayer) ); +} + +FText SShooterScoreboardWidget::GetStat(FOnGetPlayerStateAttribute Getter, const FTeamPlayer TeamPlayer) const +{ + int32 StatTotal = 0; + if (TeamPlayer.PlayerId != SpecialPlayerIndex::All) + { + AShooterPlayerState* PlayerState = GetSortedPlayerState(TeamPlayer); + if (PlayerState) + { + StatTotal = Getter.Execute(PlayerState); + } + } + else + { + for (RankedPlayerMap::TConstIterator PlayerIt(PlayerStateMaps[TeamPlayer.TeamNum]); PlayerIt; ++PlayerIt) + { + AShooterPlayerState* PlayerState = PlayerIt.Value().Get(); + if (PlayerState) + { + StatTotal += Getter.Execute(PlayerState); + } + } + } + + return FText::AsNumber(LerpForCountup(StatTotal)); +} + +int32 SShooterScoreboardWidget::LerpForCountup(int32 ScoreValue) const +{ + if (MatchState > EShooterMatchState::Playing) + { + const float LerpAmount = FMath::Min(1.0f, (FPlatformTime::Seconds() - ScoreboardStartTime) / ScoreCountUpTime); + return FMath::Lerp(0, ScoreValue, LerpAmount); + } + else + { + return ScoreValue; + } +} + +TSharedRef SShooterScoreboardWidget::MakeTotalsRow(uint8 TeamNum) const +{ + TSharedPtr TotalsRow; + + SAssignNew(TotalsRow, SHorizontalBox) + +SHorizontalBox::Slot() .Padding(NORM_PADDING) + [ + SNew(SBorder) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .Padding(NORM_PADDING) + .BorderImage(&ScoreboardStyle->ItemBorderBrush) + .BorderBackgroundColor(ScoreboardTint) + [ + SNew(STextBlock) + .Text(LOCTEXT("ScoreTotal", "Team Score")) + .TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultScoreboard.Row.HeaderTextStyle") + ] + ]; + + //Leave two columns empty + for (uint8 i = 0; i < 2; i++) + { + TotalsRow->AddSlot() .Padding(NORM_PADDING) .AutoWidth() .HAlign(HAlign_Center) .VAlign(VAlign_Center) + [ + SNew(SBorder) + .Padding(NORM_PADDING) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .BorderImage(FStyleDefaults::GetNoBrush()) + .BorderBackgroundColor(ScoreboardTint) + [ + SNew(SBox) + .WidthOverride(ScoreBoxWidth) + .HAlign(HAlign_Center) + ] + ]; + } + //Total team score / captures in CTF mode + TotalsRow->AddSlot() .Padding(NORM_PADDING) .AutoWidth() .HAlign(HAlign_Center) .VAlign(VAlign_Center) + [ + SNew(SBorder) + .Padding(NORM_PADDING) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .BorderImage(&ScoreboardStyle->ItemBorderBrush) + .BorderBackgroundColor(ScoreboardTint) + [ + SNew(SBox) + .WidthOverride(ScoreBoxWidth) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(this, &SShooterScoreboardWidget::GetStat, Columns.Last().AttributeGetter, FTeamPlayer(TeamNum, SpecialPlayerIndex::All)) + .TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultScoreboard.Row.HeaderTextStyle") + ] + ] + ]; + + return TotalsRow.ToSharedRef(); +} + +TSharedRef SShooterScoreboardWidget::MakePlayerRows(uint8 TeamNum) const +{ + TSharedRef PlayerRows = SNew(SVerticalBox); + + for (int32 PlayerIndex=0; PlayerIndex < PlayerStateMaps[TeamNum].Num(); PlayerIndex++ ) + { + FTeamPlayer TeamPlayer(TeamNum, PlayerIndex); + + if (ShouldPlayerBeDisplayed(TeamPlayer)) + { + PlayerRows->AddSlot().AutoHeight() + [ + MakePlayerRow(TeamPlayer) + ]; + } + } + + return PlayerRows; +} + +TSharedRef SShooterScoreboardWidget::MakePlayerRow(const FTeamPlayer& TeamPlayer) const +{ + // Make the padding here slightly smaller than NORM_PADDING, to fit in more players + const FMargin Pad = FMargin(5,1); + + TSharedPtr PlayerRow; + //Speaker Icon display + SAssignNew(PlayerRow, SHorizontalBox) + + SHorizontalBox::Slot().Padding(Pad+FMargin(2,0,0,0)).AutoWidth() + [ + SNew(SImage) + .Image(FShooterStyle::Get().GetBrush("ShooterGame.Speaker")) + .Visibility(this, &SShooterScoreboardWidget::SpeakerIconVisibility, TeamPlayer) + ]; + + //first autosized row with player name + PlayerRow->AddSlot() .Padding(Pad) + [ + SNew(SBorder) + .Padding(Pad) + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .Visibility(this, &SShooterScoreboardWidget::PlayerPresenceToItemVisibility, TeamPlayer) + .OnMouseMove(const_cast(this), &SShooterScoreboardWidget::OnMouseOverPlayer, TeamPlayer) + .BorderBackgroundColor(const_cast(this), &SShooterScoreboardWidget::GetScoreboardBorderColor, TeamPlayer) + .BorderImage(&ScoreboardStyle->ItemBorderBrush) + [ + SNew(STextBlock) + .Text(this, &SShooterScoreboardWidget::GetPlayerName, TeamPlayer) + .TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultScoreboard.Row.StatTextStyle") + .ColorAndOpacity(this, &SShooterScoreboardWidget::GetPlayerColor, TeamPlayer) + ] + ]; + //attributes rows (kills, deaths, score/captures) + for (uint8 ColIdx = 0; ColIdx < Columns.Num(); ColIdx++) + { + PlayerRow->AddSlot() + .Padding(Pad) .AutoWidth() .HAlign(HAlign_Center) .VAlign(VAlign_Center) + [ + SNew(SBorder) + .Padding(Pad) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Visibility(this, &SShooterScoreboardWidget::PlayerPresenceToItemVisibility, TeamPlayer) + .OnMouseMove(const_cast(this), &SShooterScoreboardWidget::OnMouseOverPlayer, TeamPlayer) + .BorderBackgroundColor(this, &SShooterScoreboardWidget::GetScoreboardBorderColor, TeamPlayer) + .BorderImage(&ScoreboardStyle->ItemBorderBrush) + [ + SNew(SBox) + .WidthOverride(ScoreBoxWidth) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(this, &SShooterScoreboardWidget::GetStat, Columns[ColIdx].AttributeGetter, TeamPlayer) + .TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultScoreboard.Row.StatTextStyle") + .ColorAndOpacity(this, &SShooterScoreboardWidget::GetColumnColor, TeamPlayer, ColIdx) + ] + ] + ]; + } + return PlayerRow.ToSharedRef(); +} + +AShooterPlayerState* SShooterScoreboardWidget::GetSortedPlayerState(const FTeamPlayer& TeamPlayer) const +{ + if (PlayerStateMaps.IsValidIndex(TeamPlayer.TeamNum) && PlayerStateMaps[TeamPlayer.TeamNum].Contains(TeamPlayer.PlayerId)) + { + return PlayerStateMaps[TeamPlayer.TeamNum].FindRef(TeamPlayer.PlayerId).Get(); + } + + return NULL; +} + +int32 SShooterScoreboardWidget::GetAttributeValue_Kills(AShooterPlayerState* PlayerState) const +{ + return PlayerState->GetKills(); +} + +int32 SShooterScoreboardWidget::GetAttributeValue_Deaths(AShooterPlayerState* PlayerState) const +{ + return PlayerState->GetDeaths(); +} + +int32 SShooterScoreboardWidget::GetAttributeValue_Score(AShooterPlayerState* PlayerState) const +{ + return FMath::TruncToInt(PlayerState->GetScore()); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Widgets/SShooterScoreboardWidget.h b/Source/ShooterGame/Private/UI/Widgets/SShooterScoreboardWidget.h new file mode 100644 index 0000000..6303cf3 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SShooterScoreboardWidget.h @@ -0,0 +1,245 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" + +DECLARE_DELEGATE_RetVal_OneParam(int32, FOnGetPlayerStateAttribute, AShooterPlayerState*); + +namespace SpecialPlayerIndex +{ + const int32 All = -1; +} + +struct FColumnData +{ + /** Column name */ + FText Name; + + /** Column color */ + FSlateColor Color; + + /** Stat value getter delegate */ + FOnGetPlayerStateAttribute AttributeGetter; + + /** defaults */ + FColumnData() + { + Color = FLinearColor::White; + } + + FColumnData(FText InName, FSlateColor InColor, FOnGetPlayerStateAttribute InAtrGetter) + : Name(InName) + , Color(InColor) + , AttributeGetter(InAtrGetter) + { + } +}; + +struct FTeamPlayer +{ + /** The team the player belongs to */ + uint8 TeamNum; + + /** The number within that team */ + int32 PlayerId; + + /** defaults */ + FTeamPlayer() + : TeamNum(0) + , PlayerId(SpecialPlayerIndex::All) + { + } + + FTeamPlayer(uint8 InTeamNum, int32 InPlayerId) + : TeamNum(InTeamNum) + , PlayerId(InPlayerId) + { + } + + /** comparator */ + bool operator==( const FTeamPlayer& Other ) const + { + return (TeamNum == Other.TeamNum && PlayerId == Other.PlayerId); + } + + /** check to see if we have valid player data */ + bool IsValid() const + { + return !(*this == FTeamPlayer()); + } +}; + + +//class declare +class SShooterScoreboardWidget : public SBorder +{ + + SLATE_BEGIN_ARGS(SShooterScoreboardWidget) + {} + + SLATE_ARGUMENT(TWeakObjectPtr, PCOwner) + + SLATE_ATTRIBUTE(EShooterMatchState::Type, MatchState) + + SLATE_END_ARGS() + + /** needed for every widget */ + void Construct(const FArguments& InArgs); + + /** update PlayerState maps with every tick when scoreboard is shown */ + virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) override; + + /** if we want to receive focus */ + virtual bool SupportsKeyboardFocus() const override; + + /** when the widget recieves keyboard focus */ + virtual FReply OnFocusReceived( const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent ) override; + + /** handle keyboard input */ + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override; + + /** Called when the scoreboard is displayed, Stores the name and whether or not a player is currently talking */ + void StoreTalkingPlayerData(const FUniqueNetId& PlayerId, bool bIsTalking); + +protected: + + /** updates widgets when players leave or join */ + void UpdateScoreboardGrid(); + + /** makes total row widget */ + TSharedRef MakeTotalsRow(uint8 TeamNum) const; + + /** makes player rows */ + TSharedRef MakePlayerRows(uint8 TeamNum) const; + + /** makes player row */ + TSharedRef MakePlayerRow(const FTeamPlayer& TeamPlayer) const; + + /** updates PlayerState maps to display accurate scores */ + void UpdatePlayerStateMaps(); + + /** gets ranked map for specific team */ + void GetRankedMap(int32 TeamIndex, RankedPlayerMap& OutRankedMap) const; + + /** gets PlayerState for specific team and player */ + AShooterPlayerState* GetSortedPlayerState(const FTeamPlayer& TeamPlayer) const; + + /** get player visibility */ + EVisibility PlayerPresenceToItemVisibility(const FTeamPlayer TeamPlayer) const; + + /** get speaker icon visibility */ + EVisibility SpeakerIconVisibility(const FTeamPlayer TeamPlayer) const; + + /** get scoreboard border color */ + FSlateColor GetScoreboardBorderColor(const FTeamPlayer TeamPlayer) const; + + /** get player name */ + FText GetPlayerName(const FTeamPlayer TeamPlayer) const; + + /** get whether or not the player should be displayed on the scoreboard */ + bool ShouldPlayerBeDisplayed(const FTeamPlayer TeamPlayer) const; + + /** get player color */ + FSlateColor GetPlayerColor(const FTeamPlayer TeamPlayer) const; + + /** get the column color */ + FSlateColor GetColumnColor(const FTeamPlayer TeamPlayer, uint8 ColIdx) const; + + /** checks to see if the specified player is the owner */ + bool IsOwnerPlayer(const FTeamPlayer& TeamPlayer) const; + + /** get specific stat for team number and optionally player */ + FText GetStat(FOnGetPlayerStateAttribute Getter, const FTeamPlayer TeamPlayer) const; + + /** linear interpolated score for match outcome animation */ + int32 LerpForCountup(int32 ScoreValue) const; + + /** get match outcome text */ + FText GetMatchOutcomeText() const; + + /** Get text for match-restart notification. */ + FText GetMatchEndText() const; + + /** get attribute value for kills */ + int32 GetAttributeValue_Kills(class AShooterPlayerState* PlayerState) const; + + /** get attribute value for deaths */ + int32 GetAttributeValue_Deaths(class AShooterPlayerState* PlayerState) const; + + /** get attribute value for score */ + int32 GetAttributeValue_Score(class AShooterPlayerState* PlayerState) const; + + /** triggers a sound effect to play */ + void PlaySound(const FSlateSound& SoundToPlay) const; + + /** handle the mouse moving over scoreboard entry */ + FReply OnMouseOverPlayer(const FGeometry& Geometry, const FPointerEvent& Event, const FTeamPlayer TeamPlayer); + + /** called when the previous player wants to be selected */ + void OnSelectedPlayerPrev(); + + /** called when the next player wants to be selected */ + void OnSelectedPlayerNext(); + + /** resets the selected player to be that of the local user */ + void ResetSelectedPlayer(); + + /** makes sure the selected player is valid */ + void UpdateSelectedPlayer(); + + /** sets the currently selected player to be ourselves */ + bool SetSelectedPlayerUs(); + + /** checks to see if the specified player is the selected one */ + bool IsSelectedPlayer(const FTeamPlayer& TeamPlayer) const; + + /** is there a valid selected item */ + bool IsPlayerSelectedAndValid() const; + + /** check to see if we can open the profile ui */ + EVisibility GetProfileUIVisibility() const; + + /** profile open ui handler */ + bool ProfileUIOpened() const; + + /** scoreboard tint color */ + FLinearColor ScoreboardTint; + + /** width of scoreboard item */ + int32 ScoreBoxWidth; + + /** scoreboard count up time */ + float ScoreCountUpTime; + + /** when the scoreboard was brought up. */ + double ScoreboardStartTime; + + /** the player currently selected in the scoreboard */ + FTeamPlayer SelectedPlayer; + + /** the Ranked PlayerState map...cleared every frame */ + TArray PlayerStateMaps; + + /** player count in each team in the last tick */ + TArray LastTeamPlayerCount; + + /** holds talking player data */ + TArray, bool>> PlayersTalkingThisFrame; + + /** holds player info rows */ + TSharedPtr ScoreboardData; + + /** stat columns data */ + TArray Columns; + + /** get state of current match */ + EShooterMatchState::Type MatchState; + + /** pointer to our parent HUD */ + TWeakObjectPtr PCOwner; + + /** style for the scoreboard */ + const struct FShooterScoreboardStyle *ScoreboardStyle; +}; diff --git a/Source/ShooterGame/Private/UI/Widgets/SShooterSplitScreenLobbyWidget.cpp b/Source/ShooterGame/Private/UI/Widgets/SShooterSplitScreenLobbyWidget.cpp new file mode 100644 index 0000000..c0e0547 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SShooterSplitScreenLobbyWidget.cpp @@ -0,0 +1,673 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "ShooterStyle.h" +#include "SShooterSplitScreenLobbyWidget.h" +#include "ShooterMenuItemWidgetStyle.h" +#include "ShooterMenuWidgetStyle.h" +#include "SShooterConfirmationDialog.h" +#include "ShooterGameViewportClient.h" +#include "OnlineSubsystemUtils.h" + +#define LOCTEXT_NAMESPACE "ShooterGame.SplitScreenLobby" + +int32 GShooterSplitScreenMax = 2; +static FAutoConsoleVariableRef CVarShooterSplitScreenMax( + TEXT("r.ShooterSplitScreenMax"), + GShooterSplitScreenMax, + TEXT("Maximum number of split screen players.\n") + TEXT("This will set the number of slots available in the split screen lobby.\n") + TEXT("Default is 2."), + ECVF_Default + ); + +void SShooterSplitScreenLobby::Construct( const FArguments& InArgs ) +{ +#if PLATFORM_PS4 + PressToPlayText = LOCTEXT("PressToPlay", "Press cross button to Play"); + PressToFindText = LOCTEXT("PressToFind", "Press cross button to Find Match"); + PressToStartMatchText = LOCTEXT("PressToStart", "Press cross button To Start Match"); +#else + PressToPlayText = LOCTEXT("PressToPlay", "Press A to Play"); + PressToFindText = LOCTEXT("PressToFind", "Press A to Find Match"); + PressToStartMatchText = LOCTEXT("PressToStart", "Press A To Start Match"); +#endif + +#if PLATFORM_SWITCH + PressToPlayText = LOCTEXT("PressToPlay", " Select User"); + PressToFindText = LOCTEXT("PressToFind", "Press to Find Match"); + PressToStartMatchText = LOCTEXT("PressToStart", " Start Match / Connect Controllers"); + PlayAsGuestText = LOCTEXT("PlayAsGuest", " Play As Guest"); +#endif + + PlayerOwner = InArgs._PlayerOwner; + + bIsJoining = false; + + const FShooterMenuStyle* ItemStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterMenuStyle"); + const FSlateBrush* SlotBrush = &ItemStyle->LeftBackgroundBrush; + + const FButtonStyle* ButtonStyle = &FShooterStyle::Get().GetWidgetStyle("DefaultShooterButtonStyle"); + FLinearColor MenuTitleTextColor = FLinearColor(FColor(155,164,182)); + FLinearColor PressXToStartColor = FLinearColor::Green; + + MasterUserBack = InArgs._OnCancelClicked; + MasterUserPlay = InArgs._OnPlayClicked; + + const float PaddingBetweenSlots = 10.0f; + const float SlotWidth = 565.0f; + const float SlotHeight = 300.0f; + + const FText Msg = LOCTEXT("You must be signed in to play online", "You must be signed in to play online"); + const FText OKButtonString = NSLOCTEXT("DialogButtons", "OKAY", "OK"); + + ChildSlot + [ + //main container. Just start us out centered on the whole screen. + SNew(SOverlay) + +SOverlay::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + //at maximum we need a 2x2 grid. So we need two vertical slots, which will each contain 2 horizontal slots. + SNew( SVerticalBox ) + +SVerticalBox::Slot() + .AutoHeight() + .Padding(PaddingBetweenSlots) //put some space in between the slots + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew( SHorizontalBox) + +SHorizontalBox::Slot() + .Padding(PaddingBetweenSlots) //put some space in between the slots + [ + SAssignNew(UserSlots[0], SBox) + .HeightOverride(SlotHeight) + .WidthOverride(SlotWidth) + [ + SNew(SBorder) + .Padding(0.0f) + .BorderImage(SlotBrush) + .BorderBackgroundColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(0.0f) + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Center) + [ + //first slot needs to have some text to say 'press X to start match'. Only master user can start the match. + SAssignNew(UserTextWidgets[0], SRichTextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") +// .ColorAndOpacity(MenuTitleTextColor) + .Text(PressToPlayText) + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] + +SVerticalBox::Slot() + .Padding(5.0f) + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Center) + [ + SNew(SRichTextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.SplitScreenLobby.StartMatchTextStyle") + .Text(this, &SShooterSplitScreenLobby::GetPlayFindText) + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] + ] + ] + ] + +SHorizontalBox::Slot() + .Padding(PaddingBetweenSlots) + [ + SAssignNew(UserSlots[1], SBox) + .HeightOverride(SlotHeight) + .WidthOverride(SlotWidth) + [ +#if PLATFORM_SWITCH + SNew(SBorder) + .Padding(0.0f) + .BorderImage(SlotBrush) + .BorderBackgroundColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(0.0f) + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Center) + [ + //first slot needs to have some text to say 'press X to start match'. Only master user can start the match. + SAssignNew(UserTextWidgets[1], SRichTextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + //.ColorAndOpacity(MenuTitleTextColor) + .Text(PressToPlayText) + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] + +SVerticalBox::Slot() + .Padding(5.0f) + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Center) + [ + SNew(SRichTextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.SplitScreenLobby.StartMatchTextStyle") + .Text(this, &SShooterSplitScreenLobby::GetPlayAsGuestText) + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] + ] +#else + SNew(SBorder) + .Padding(0.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .BorderImage(SlotBrush) + .BorderBackgroundColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) + [ + SAssignNew(UserTextWidgets[1], SRichTextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + //.ColorAndOpacity(MenuTitleTextColor) + .Text(PressToPlayText) + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] +#endif + ] + ] + ] + +SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Padding(PaddingBetweenSlots) + [ + SNew( SHorizontalBox) + +SHorizontalBox::Slot() + .Padding(PaddingBetweenSlots) + [ + SAssignNew(UserSlots[2], SBox) + .HeightOverride(SlotHeight) + .WidthOverride(SlotWidth) + [ + SNew(SBorder) + .Padding(0.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .BorderImage(SlotBrush) + .BorderBackgroundColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) + [ + SAssignNew(UserTextWidgets[2], SRichTextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + //.ColorAndOpacity(MenuTitleTextColor) + .Text(PressToPlayText) + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] + ] + ] + + +SHorizontalBox::Slot() + .Padding(PaddingBetweenSlots) + [ + SAssignNew(UserSlots[3], SBox) + .HeightOverride(SlotHeight) + .WidthOverride(SlotWidth) + [ + SNew(SBorder) + .Padding(0.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .BorderImage(SlotBrush) + .BorderBackgroundColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) + [ + SAssignNew(UserTextWidgets[3], SRichTextBlock) + .TextStyle(FShooterStyle::Get(), "ShooterGame.MenuHeaderTextStyle") + //.ColorAndOpacity(MenuTitleTextColor) + .Text(PressToPlayText) + .DecoratorStyleSet(&FShooterStyle::Get()) + + SRichTextBlock::ImageDecorator() + ] + ] + ] + ] + ] + ]; + + Clear(); +} + +void SShooterSplitScreenLobby::Clear() +{ + if (GetGameInstance() == nullptr) + { + return; + } + + // Remove existing splitscreen players + GetGameInstance()->RemoveSplitScreenPlayers(); + + // Sync the list with the actively tracked local users + UpdateSlots(); +} + +void SShooterSplitScreenLobby::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) +{ + // Parent tick + SCompoundWidget::Tick( AllottedGeometry, InCurrentTime, InDeltaTime); + + // Make sure the slots match the local user list + UpdateSlots(); +} + +void SShooterSplitScreenLobby::UpdateSlots() +{ + if (GetGameInstance() == nullptr) + { + return; + } + + // Show active players + for ( int i = 0; i < GetNumSupportedSlots(); ++i ) + { + ULocalPlayer * LocalPlayer = ( i < GetGameInstance()->GetNumLocalPlayers() ) ? GetGameInstance()->GetLocalPlayerByIndex( i ) : NULL; + + if ( LocalPlayer != NULL ) + { + UserTextWidgets[i]->SetText( FText::FromString(LocalPlayer->GetNickname()) ); + } + else + { + UserTextWidgets[i]->SetText( PressToPlayText ); + } + + UserSlots[i]->SetVisibility( EVisibility::Visible ); + } + + // Hide non supported slots + for ( int i = GetNumSupportedSlots(); i < MAX_POSSIBLE_SLOTS; ++i ) + { + UserSlots[i]->SetVisibility( EVisibility::Collapsed ); + } +} + +void SShooterSplitScreenLobby::ConditionallyReadyPlayer( const int ControllerId, const bool bCanShowUI ) +{ + UShooterGameInstance* const GameInstance = GetGameInstance(); + if (GameInstance == nullptr) + { + return; + } + + if (GameInstance->GetNumLocalPlayers() >= GetNumSupportedSlots() ) + { + return; // Can't fit any more players at this point + } + + // If we already have this controller id registered, ignore this request + if (GameInstance->FindLocalPlayerFromControllerId( ControllerId ) != NULL ) + { + return; + } + + TSharedPtr< const FUniqueNetId > UniqueNetId = GameInstance->GetUniqueNetIdFromControllerId( ControllerId ); + + const bool bIsPlayerOnline = ( UniqueNetId.IsValid() && IsUniqueIdOnline( *UniqueNetId ) ); + const bool bFoundExistingPlayer = GameInstance->FindLocalPlayerFromUniqueNetId( UniqueNetId ) != nullptr; + const EOnlineMode OnlineMode = GameInstance->GetOnlineMode(); + // If this is an online game, reject and show sign-in if: + // 1. We have already registered this FUniqueNetId + // 2. This player is not currently signed in and online + // on Swtich, always show the login UI even if a Lan match + if ( +#if PLATFORM_SWITCH + bCanShowUI && +#else + OnlineMode == EOnlineMode::Online && +#endif + ( bFoundExistingPlayer || !bIsPlayerOnline ) ) + { + const IOnlineExternalUIPtr ExternalUI = Online::GetExternalUIInterface(GameInstance->GetWorld()); + + if ( bCanShowUI && ExternalUI.IsValid() ) + { + ExternalUI->ShowLoginUI( ControllerId, OnlineMode == EOnlineMode::Online, false, FOnLoginUIClosedDelegate::CreateSP( this, &SShooterSplitScreenLobby::HandleLoginUIClosedAndReady ) ); + } + + return; + } + + if (bIsPlayerOnline) + { + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GameInstance->GetWorld()); + if (OnlineSub) + { + const IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if(Identity.IsValid()) + { + PendingControllerId = ControllerId; + Identity->GetUserPrivilege( + *UniqueNetId, + OnlineMode != EOnlineMode::Offline ? EUserPrivileges::CanPlayOnline : EUserPrivileges::CanPlay, + IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateSP(this, &SShooterSplitScreenLobby::OnUserCanPlay)); + } + } + } + else + { + ReadyPlayer(ControllerId); + } +} + +void SShooterSplitScreenLobby::ReadyPlayer( const int ControllerId ) +{ + FString Error; + ULocalPlayer * LocalPlayer = GetGameInstance()->CreateLocalPlayer(ControllerId, Error, false); + + if (LocalPlayer != NULL) + { + // We have UserId, but we want to make sure we're using the same shared pointer from the game instance so all the reference counting works out + TSharedPtr< const FUniqueNetId > UniqueNetId = GetGameInstance()->GetUniqueNetIdFromControllerId(ControllerId); + LocalPlayer->SetCachedUniqueNetId(UniqueNetId); + + // The new player is given the game viewport as the focus widget by default, so ensure it is focused on the lobby + int32 LocalPlayerIndex = GetGameInstance()->GetLocalPlayers().IndexOfByKey(LocalPlayer); + if (ensure(LocalPlayerIndex != INDEX_NONE)) + { + FSlateApplication::Get().SetUserFocus(LocalPlayerIndex, SharedThis(this), EFocusCause::SetDirectly); + } + } +} + +void SShooterSplitScreenLobby::UnreadyPlayer( const int ControllerId ) +{ + if (GetGameInstance() == nullptr) + { + return; + } + + ULocalPlayer * LocalPlayer = GEngine->GetLocalPlayerFromControllerId( GetGameInstance()->GetGameViewportClient(), ControllerId ); + + if ( LocalPlayer == NULL ) + { + UE_LOG( LogOnline, Warning, TEXT( "SShooterSplitScreenLobby::UnreadyPlayer: ControllerId not found: %i" ), ControllerId ); + return; + } + + GetGameInstance()->RemoveExistingLocalPlayer( LocalPlayer ); +} + +FReply SShooterSplitScreenLobby::OnOkOrCancel() +{ + UShooterGameViewportClient* ShooterViewport = Cast(GetGameInstance()->GetGameViewportClient()); + + if (ShooterViewport != NULL) + { + ShooterViewport->HideDialog(); + } + + return FReply::Handled(); +} + +bool SShooterSplitScreenLobby::ConfirmSponsorsSatisfied() const +{ + if (GetGameInstance() == nullptr) + { + return false; + } + + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetGameInstance()->GetWorld()); + + if(OnlineSub) + { + const IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface(); + if(IdentityInterface.IsValid()) + { + const int32 NumActiveSlots = FMath::Min( GetNumSupportedSlots(), GetGameInstance()->GetNumLocalPlayers() ); + + for ( int i = 0; i < NumActiveSlots; ++i ) + { + ULocalPlayer * LocalPlayer = GetGameInstance()->GetLocalPlayerByIndex( i ); + + TSharedPtr Sponsor = IdentityInterface->GetSponsorUniquePlayerId( LocalPlayer->GetControllerId() ); + + // If this user has a sponsor (is a guest), make sure our sponsor is playing with us + if(Sponsor.IsValid()) + { + return ( GetGameInstance()->FindLocalPlayerFromUniqueNetId( Sponsor ) != NULL ); + } + } + } + } + + return true; +} + +void SShooterSplitScreenLobby::OnUserCanPlay(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) +{ + if (PrivilegeResults != (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures && GetGameInstance()) + { + // Xbox shows its own system dialog currently +#if PLATFORM_PS4 + const IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetGameInstance()->GetWorld()); + if (OnlineSub) + { + const IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface(); + if (Identity.IsValid()) + { + FString Nickname = Identity->GetPlayerNickname(UserId); + + // Show warning that the user cannot play due to age restrictions + UShooterGameViewportClient * ShooterViewport = Cast(GetGameInstance()->GetGameViewportClient()); + + if (ShooterViewport != NULL) + { + ShooterViewport->ShowDialog( + PlayerOwner.Get(), + EShooterDialogType::Generic, + FText::Format(NSLOCTEXT("ProfileMessages", "AgeRestrictionFmt", "{0} cannot play due to age restrictions!"), FText::FromString(Nickname)), + NSLOCTEXT("DialogButtons", "OKAY", "OK"), + FText::GetEmpty(), + FOnClicked::CreateRaw(this, &SShooterSplitScreenLobby::OnOkOrCancel), + FOnClicked::CreateRaw(this, &SShooterSplitScreenLobby::OnOkOrCancel) + ); + } + } + } +#endif + } + else + { + ReadyPlayer(PendingControllerId); + } +} + +int32 SShooterSplitScreenLobby::GetNumSupportedSlots() const +{ + return FMath::Clamp( GShooterSplitScreenMax, 1, MAX_POSSIBLE_SLOTS ); +} + +bool SShooterSplitScreenLobby::IsUniqueIdOnline( const FUniqueNetId& UniqueId ) const +{ + if (GetGameInstance() == nullptr) + { + return false; + } + + IOnlineIdentityPtr Identity = Online::GetIdentityInterface(GetGameInstance()->GetWorld()); + + if ( !Identity.IsValid() ) + { + return false; + } + + return Identity->GetLoginStatus( UniqueId ) == ELoginStatus::LoggedIn; +} + +FReply SShooterSplitScreenLobby::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) +{ + const UShooterGameInstance* GameInstance = GetGameInstance(); + if (GameInstance == nullptr) + { + return FReply::Unhandled(); + } + + const FKey Key = InKeyEvent.GetKey(); + const int32 UserIndex = InKeyEvent.GetUserIndex(); + + ULocalPlayer * ExistingPlayer = GEngine->GetLocalPlayerFromControllerId(GetGameInstance()->GetGameViewportClient(), UserIndex); + + if ((Key == EKeys::Enter || Key == EKeys::Virtual_Accept) && !InKeyEvent.IsRepeat()) + { + // See if we are already tracking this user + if ( ExistingPlayer != NULL ) + { + // See if this is the initial user + if ( ExistingPlayer == GameInstance->GetFirstGamePlayer() ) + { + // If this is the initial user, start the game + if (GameInstance->GetOnlineMode() != EOnlineMode::Online || ConfirmSponsorsSatisfied() ) + { + return MasterUserPlay.Execute(); + } + else + { + // Show warning that the guest needs the sponsor + UShooterGameViewportClient * ShooterViewport = Cast(GameInstance->GetGameViewportClient()); + + if ( ShooterViewport != NULL ) + { + ShooterViewport->ShowDialog( + PlayerOwner.Get(), + EShooterDialogType::Generic, + NSLOCTEXT("ProfileMessages", "GuestAccountNeedsSponsor", "A guest account cannot play without its sponsor!"), + NSLOCTEXT("DialogButtons", "OKAY", "OK"), + FText::GetEmpty(), + FOnClicked::CreateRaw(this, &SShooterSplitScreenLobby::OnOkOrCancel), + FOnClicked::CreateRaw(this, &SShooterSplitScreenLobby::OnOkOrCancel) + ); + } + + return FReply::Handled(); + } + } + + return FReply::Handled(); + } + + ConditionallyReadyPlayer(UserIndex, true); + + return FReply::Handled(); + } +#if PLATFORM_SWITCH + else if ((Key == EKeys::Gamepad_FaceButton_Top) && !InKeyEvent.IsRepeat()) + { + GEngine->DeferredCommands.Add(TEXT("Npad.ConnectUI")); + } + else if (GameInstance->GetOnlineMode() != EOnlineMode::Online && (Key == EKeys::Gamepad_FaceButton_Left) && !InKeyEvent.IsRepeat()) + { + ConditionallyReadyPlayer(UserIndex, false); + } +#endif + else if (Key == EKeys::Escape || Key == EKeys::Virtual_Back || Key == EKeys::Global_Back) + { + if ( ExistingPlayer != NULL && ExistingPlayer == GetGameInstance()->GetFirstGamePlayer() ) + { + return MasterUserBack.Execute(); + } + else + { + UnreadyPlayer(UserIndex); + } + } + + return FReply::Unhandled(); +} + +FReply SShooterSplitScreenLobby::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) +{ + // focus all possible users + FSlateApplication::Get().SetAllUserFocus(SharedThis(this), EFocusCause::SetDirectly); + return FReply::Handled().ReleaseMouseCapture(); + +} + +void SShooterSplitScreenLobby::OnFocusLost( const FFocusEvent& InFocusEvent ) +{ +} + +void SShooterSplitScreenLobby::HandleLoginUIClosedAndReady( TSharedPtr UniqueId, const int UserIndex, const FOnlineError& Error ) +{ + const UShooterGameInstance* GameInstance = GetGameInstance(); + if (GameInstance == nullptr) + { + return; + } + + EOnlineMode OnlineMode = GameInstance->GetOnlineMode(); + + // If a player signed in, UniqueId will be valid, and we can place him in the open slot. + if ( UniqueId.IsValid() ) + { + if ( OnlineMode != EOnlineMode::Online || IsUniqueIdOnline( *UniqueId ) ) + { + ConditionallyReadyPlayer( UserIndex, false ); + } + else if (!IsUniqueIdOnline(*UniqueId)) + { + if (OnlineMode == EOnlineMode::Online) + { + const UWorld* World = GetGameInstance()->GetWorld(); + + OnLoginCompleteDelegateHandle = Online::GetIdentityInterface(World)->AddOnLoginCompleteDelegate_Handle(UserIndex, FOnLoginCompleteDelegate::CreateRaw(this, &SShooterSplitScreenLobby::OnLoginComplete)); + Online::GetIdentityInterface(World)->Login(UserIndex, FOnlineAccountCredentials()); + } + else + { + ConditionallyReadyPlayer(UserIndex, false); + } + } + } +} + +void SShooterSplitScreenLobby::OnLoginComplete(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error) +{ + Online::GetIdentityInterface(GetGameInstance()->GetWorld())->ClearOnLoginCompleteDelegate_Handle(LocalUserNum, OnLoginCompleteDelegateHandle); + + if (bWasSuccessful && IsUniqueIdOnline(*GetGameInstance()->GetUniqueNetIdFromControllerId(LocalUserNum))) + { + ConditionallyReadyPlayer(LocalUserNum, false); + } +} + +UShooterGameInstance * SShooterSplitScreenLobby::GetGameInstance() const +{ + if ( !PlayerOwner.IsValid() ) + { + return NULL; + } + + check( PlayerOwner->GetGameInstance() == nullptr || CastChecked< UShooterGameInstance >( PlayerOwner->GetGameInstance() ) != nullptr ); + + return Cast< UShooterGameInstance >( PlayerOwner->GetGameInstance() ); +} + +FText SShooterSplitScreenLobby::GetPlayFindText() const +{ + return bIsJoining ? PressToFindText : PressToStartMatchText; +} + +#if PLATFORM_SWITCH +FText SShooterSplitScreenLobby::GetPlayAsGuestText() const +{ + const UShooterGameInstance* GameInstance = GetGameInstance(); + if (GameInstance == nullptr) + { + return FText(); + } + + return GameInstance->GetOnlineMode() != EOnlineMode::Offline ? FText() : PlayAsGuestText; +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ShooterGame/Private/UI/Widgets/SShooterSplitScreenLobbyWidget.h b/Source/ShooterGame/Private/UI/Widgets/SShooterSplitScreenLobbyWidget.h new file mode 100644 index 0000000..d8fe5b3 --- /dev/null +++ b/Source/ShooterGame/Private/UI/Widgets/SShooterSplitScreenLobbyWidget.h @@ -0,0 +1,91 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ShooterGameInstance.h" + +class SShooterSplitScreenLobby : public SCompoundWidget +{ +public: + + SLATE_BEGIN_ARGS( SShooterSplitScreenLobby ) + {} + + SLATE_ARGUMENT(TWeakObjectPtr, PlayerOwner) + + SLATE_ARGUMENT(FOnClicked, OnPlayClicked) + SLATE_ARGUMENT(FOnClicked, OnCancelClicked) + + SLATE_END_ARGS() + + /** says that we can support keyboard focus */ + virtual bool SupportsKeyboardFocus() const override { return true; } + + void Construct(const FArguments& InArgs); + + void Clear(); + + int32 GetNumSupportedSlots() const; + + bool GetIsJoining() const { return bIsJoining; } + + void SetIsJoining( const bool _bIsJoining ) { bIsJoining =_bIsJoining; } + +private: + bool IsUniqueIdOnline( const FUniqueNetId& ControllerId ) const; + + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override; + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override; + virtual void OnFocusLost( const FFocusEvent& InFocusEvent ) override; + + void UpdateSlots(); + void ConditionallyReadyPlayer( const int ControllerId, const bool bCanShowUI ); + void ReadyPlayer( const int ControllerId ); + void UnreadyPlayer( const int ControllerId ); + + virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) override; + + void HandleLoginUIClosedAndReady(TSharedPtr UniqueId, const int UserIndex, const FOnlineError& Error = FOnlineError()); + + UShooterGameInstance* GetGameInstance() const; + + FText GetPlayFindText() const; +#if PLATFORM_SWITCH + FText GetPlayAsGuestText() const; +#endif + + FReply OnOkOrCancel(); + bool ConfirmSponsorsSatisfied() const; + void OnUserCanPlay(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults); + + static const int MAX_POSSIBLE_SLOTS = 4; + + /** The player that owns the Lobby. */ + TWeakObjectPtr PlayerOwner; + + FOnClicked MasterUserBack; + FOnClicked MasterUserPlay; + + TSharedPtr UserTextWidgets[MAX_POSSIBLE_SLOTS]; + TSharedPtr UserSlots[MAX_POSSIBLE_SLOTS]; + + /** used for holding on to the splitscreen lobby widget so we can switch back to that UI after the LoginFailure UI pops up */ + TSharedPtr SplitScreenLobbyWidget; + + FText PressToPlayText; + FText PressToFindText; + FText PressToStartMatchText; +#if PLATFORM_SWITCH + FText PlayAsGuestText; +#endif + + void OnLoginComplete(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error); + FDelegateHandle OnLoginCompleteDelegateHandle; + + int PendingControllerId; + + /** True if we joining a match */ + bool bIsJoining; +}; diff --git a/Source/ShooterGame/Private/Weapons/ShooterDamageType.cpp b/Source/ShooterGame/Private/Weapons/ShooterDamageType.cpp new file mode 100644 index 0000000..962f9a6 --- /dev/null +++ b/Source/ShooterGame/Private/Weapons/ShooterDamageType.cpp @@ -0,0 +1,8 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Weapons/ShooterDamageType.h" + +UShooterDamageType::UShooterDamageType(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Weapons/ShooterProjectile.cpp b/Source/ShooterGame/Private/Weapons/ShooterProjectile.cpp new file mode 100644 index 0000000..0c5f313 --- /dev/null +++ b/Source/ShooterGame/Private/Weapons/ShooterProjectile.cpp @@ -0,0 +1,151 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Weapons/ShooterProjectile.h" +#include "Particles/ParticleSystemComponent.h" +#include "Effects/ShooterExplosionEffect.h" + +AShooterProjectile::AShooterProjectile(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + CollisionComp = ObjectInitializer.CreateDefaultSubobject(this, TEXT("SphereComp")); + CollisionComp->InitSphereRadius(5.0f); + CollisionComp->AlwaysLoadOnClient = true; + CollisionComp->AlwaysLoadOnServer = true; + CollisionComp->bTraceComplexOnMove = true; + CollisionComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + CollisionComp->SetCollisionObjectType(COLLISION_PROJECTILE); + CollisionComp->SetCollisionResponseToAllChannels(ECR_Ignore); + CollisionComp->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block); + CollisionComp->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Block); + CollisionComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block); + RootComponent = CollisionComp; + + ParticleComp = ObjectInitializer.CreateDefaultSubobject(this, TEXT("ParticleComp")); + ParticleComp->bAutoActivate = false; + ParticleComp->bAutoDestroy = false; + ParticleComp->SetupAttachment(RootComponent); + + MovementComp = ObjectInitializer.CreateDefaultSubobject(this, TEXT("ProjectileComp")); + MovementComp->UpdatedComponent = CollisionComp; + MovementComp->InitialSpeed = 2000.0f; + MovementComp->MaxSpeed = 2000.0f; + MovementComp->bRotationFollowsVelocity = true; + MovementComp->ProjectileGravityScale = 0.f; + + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.TickGroup = TG_PrePhysics; + SetRemoteRoleForBackwardsCompat(ROLE_SimulatedProxy); + bReplicates = true; + SetReplicatingMovement(true); +} + +void AShooterProjectile::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + MovementComp->OnProjectileStop.AddDynamic(this, &AShooterProjectile::OnImpact); + CollisionComp->MoveIgnoreActors.Add(GetInstigator()); + + AShooterWeapon_Projectile* OwnerWeapon = Cast(GetOwner()); + if (OwnerWeapon) + { + OwnerWeapon->ApplyWeaponConfig(WeaponConfig); + } + + SetLifeSpan( WeaponConfig.ProjectileLife ); + MyController = GetInstigatorController(); +} + +void AShooterProjectile::InitVelocity(FVector& ShootDirection) +{ + if (MovementComp) + { + MovementComp->Velocity = ShootDirection * MovementComp->InitialSpeed; + } +} + +void AShooterProjectile::OnImpact(const FHitResult& HitResult) +{ + if (GetLocalRole() == ROLE_Authority && !bExploded) + { + Explode(HitResult); + DisableAndDestroy(); + } +} + +void AShooterProjectile::Explode(const FHitResult& Impact) +{ + if (ParticleComp) + { + ParticleComp->Deactivate(); + } + + // effects and damage origin shouldn't be placed inside mesh at impact point + const FVector NudgedImpactLocation = Impact.ImpactPoint + Impact.ImpactNormal * 10.0f; + + if (WeaponConfig.ExplosionDamage > 0 && WeaponConfig.ExplosionRadius > 0 && WeaponConfig.DamageType) + { + UGameplayStatics::ApplyRadialDamage(this, WeaponConfig.ExplosionDamage, NudgedImpactLocation, WeaponConfig.ExplosionRadius, WeaponConfig.DamageType, TArray(), this, MyController.Get()); + } + + if (ExplosionTemplate) + { + FTransform const SpawnTransform(Impact.ImpactNormal.Rotation(), NudgedImpactLocation); + AShooterExplosionEffect* const EffectActor = GetWorld()->SpawnActorDeferred(ExplosionTemplate, SpawnTransform); + if (EffectActor) + { + EffectActor->SurfaceHit = Impact; + UGameplayStatics::FinishSpawningActor(EffectActor, SpawnTransform); + } + } + + bExploded = true; +} + +void AShooterProjectile::DisableAndDestroy() +{ + UAudioComponent* ProjAudioComp = FindComponentByClass(); + if (ProjAudioComp && ProjAudioComp->IsPlaying()) + { + ProjAudioComp->FadeOut(0.1f, 0.f); + } + + MovementComp->StopMovementImmediately(); + + // give clients some time to show explosion + SetLifeSpan( 2.0f ); +} + +///CODE_SNIPPET_START: AActor::GetActorLocation AActor::GetActorRotation +void AShooterProjectile::OnRep_Exploded() +{ + FVector ProjDirection = GetActorForwardVector(); + + const FVector StartTrace = GetActorLocation() - ProjDirection * 200; + const FVector EndTrace = GetActorLocation() + ProjDirection * 150; + FHitResult Impact; + + if (!GetWorld()->LineTraceSingleByChannel(Impact, StartTrace, EndTrace, COLLISION_PROJECTILE, FCollisionQueryParams(SCENE_QUERY_STAT(ProjClient), true, GetInstigator()))) + { + // failsafe + Impact.ImpactPoint = GetActorLocation(); + Impact.ImpactNormal = -ProjDirection; + } + + Explode(Impact); +} +///CODE_SNIPPET_END + +void AShooterProjectile::PostNetReceiveVelocity(const FVector& NewVelocity) +{ + if (MovementComp) + { + MovementComp->Velocity = NewVelocity; + } +} + +void AShooterProjectile::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const +{ + Super::GetLifetimeReplicatedProps( OutLifetimeProps ); + + DOREPLIFETIME( AShooterProjectile, bExploded ); +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Weapons/ShooterWeapon.cpp b/Source/ShooterGame/Private/Weapons/ShooterWeapon.cpp new file mode 100644 index 0000000..581d2e9 --- /dev/null +++ b/Source/ShooterGame/Private/Weapons/ShooterWeapon.cpp @@ -0,0 +1,992 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Weapons/ShooterWeapon.h" +#include "Player/ShooterCharacter.h" +#include "Particles/ParticleSystemComponent.h" +#include "Bots/ShooterAIController.h" +#include "Online/ShooterPlayerState.h" +#include "UI/ShooterHUD.h" +#include "Camera/CameraShake.h" + +AShooterWeapon::AShooterWeapon(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + Mesh1P = ObjectInitializer.CreateDefaultSubobject(this, TEXT("WeaponMesh1P")); + Mesh1P->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered; + Mesh1P->bReceivesDecals = false; + Mesh1P->CastShadow = false; + Mesh1P->SetCollisionObjectType(ECC_WorldDynamic); + Mesh1P->SetCollisionEnabled(ECollisionEnabled::NoCollision); + Mesh1P->SetCollisionResponseToAllChannels(ECR_Ignore); + RootComponent = Mesh1P; + + Mesh3P = ObjectInitializer.CreateDefaultSubobject(this, TEXT("WeaponMesh3P")); + Mesh3P->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered; + Mesh3P->bReceivesDecals = false; + Mesh3P->CastShadow = true; + Mesh3P->SetCollisionObjectType(ECC_WorldDynamic); + Mesh3P->SetCollisionEnabled(ECollisionEnabled::NoCollision); + Mesh3P->SetCollisionResponseToAllChannels(ECR_Ignore); + Mesh3P->SetCollisionResponseToChannel(COLLISION_WEAPON, ECR_Block); + Mesh3P->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block); + Mesh3P->SetCollisionResponseToChannel(COLLISION_PROJECTILE, ECR_Block); + Mesh3P->SetupAttachment(Mesh1P); + + bLoopedMuzzleFX = false; + bLoopedFireAnim = false; + bPlayingFireAnim = false; + bIsEquipped = false; + bWantsToFire = false; + bPendingReload = false; + bPendingEquip = false; + CurrentState = EWeaponState::Idle; + + CurrentAmmo = 0; + CurrentAmmoInClip = 0; + BurstCounter = 0; + LastFireTime = 0.0f; + + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.TickGroup = TG_PrePhysics; + SetRemoteRoleForBackwardsCompat(ROLE_SimulatedProxy); + bReplicates = true; + bNetUseOwnerRelevancy = true; +} + +void AShooterWeapon::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + + if (WeaponConfig.InitialClips > 0) + { + CurrentAmmoInClip = WeaponConfig.AmmoPerClip; + CurrentAmmo = WeaponConfig.AmmoPerClip * WeaponConfig.InitialClips; + } + + DetachMeshFromPawn(); +} + +void AShooterWeapon::Destroyed() +{ + Super::Destroyed(); + + StopSimulatingWeaponFire(); +} + +////////////////////////////////////////////////////////////////////////// +// Inventory + +void AShooterWeapon::OnEquip(const AShooterWeapon* LastWeapon) +{ + AttachMeshToPawn(); + + bPendingEquip = true; + DetermineWeaponState(); + + // Only play animation if last weapon is valid + if (LastWeapon) + { + float Duration = PlayWeaponAnimation(EquipAnim); + if (Duration <= 0.0f) + { + // failsafe + Duration = 0.5f; + } + EquipStartedTime = GetWorld()->GetTimeSeconds(); + EquipDuration = Duration; + + GetWorldTimerManager().SetTimer(TimerHandle_OnEquipFinished, this, &AShooterWeapon::OnEquipFinished, Duration, false); + } + else + { + OnEquipFinished(); + } + + if (MyPawn && MyPawn->IsLocallyControlled()) + { + PlayWeaponSound(EquipSound); + } + + AShooterCharacter::NotifyEquipWeapon.Broadcast(MyPawn, this); +} + +void AShooterWeapon::OnEquipFinished() +{ + AttachMeshToPawn(); + + bIsEquipped = true; + bPendingEquip = false; + + // Determine the state so that the can reload checks will work + DetermineWeaponState(); + + if (MyPawn) + { + // try to reload empty clip + if (MyPawn->IsLocallyControlled() && + CurrentAmmoInClip <= 0 && + CanReload()) + { + StartReload(); + } + } + + +} + +void AShooterWeapon::OnUnEquip() +{ + DetachMeshFromPawn(); + bIsEquipped = false; + StopFire(); + + if (bPendingReload) + { + StopWeaponAnimation(ReloadAnim); + bPendingReload = false; + + GetWorldTimerManager().ClearTimer(TimerHandle_StopReload); + GetWorldTimerManager().ClearTimer(TimerHandle_ReloadWeapon); + } + + if (bPendingEquip) + { + StopWeaponAnimation(EquipAnim); + bPendingEquip = false; + + GetWorldTimerManager().ClearTimer(TimerHandle_OnEquipFinished); + } + + AShooterCharacter::NotifyUnEquipWeapon.Broadcast(MyPawn, this); + + DetermineWeaponState(); +} + +void AShooterWeapon::OnEnterInventory(AShooterCharacter* NewOwner) +{ + SetOwningPawn(NewOwner); +} + +void AShooterWeapon::OnLeaveInventory() +{ + if (IsAttachedToPawn()) + { + OnUnEquip(); + } + + if (GetLocalRole() == ROLE_Authority) + { + SetOwningPawn(NULL); + } +} + +void AShooterWeapon::AttachMeshToPawn() +{ + if (MyPawn) + { + // Remove and hide both first and third person meshes + DetachMeshFromPawn(); + + // For locally controller players we attach both weapons and let the bOnlyOwnerSee, bOwnerNoSee flags deal with visibility. + FName AttachPoint = MyPawn->GetWeaponAttachPoint(); + if( MyPawn->IsLocallyControlled() == true ) + { + USkeletalMeshComponent* PawnMesh1p = MyPawn->GetSpecifcPawnMesh(true); + USkeletalMeshComponent* PawnMesh3p = MyPawn->GetSpecifcPawnMesh(false); + Mesh1P->SetHiddenInGame( false ); + Mesh3P->SetHiddenInGame( false ); + Mesh1P->AttachToComponent(PawnMesh1p, FAttachmentTransformRules::KeepRelativeTransform, AttachPoint); + Mesh3P->AttachToComponent(PawnMesh3p, FAttachmentTransformRules::KeepRelativeTransform, AttachPoint); + } + else + { + USkeletalMeshComponent* UseWeaponMesh = GetWeaponMesh(); + USkeletalMeshComponent* UsePawnMesh = MyPawn->GetPawnMesh(); + UseWeaponMesh->AttachToComponent(UsePawnMesh, FAttachmentTransformRules::KeepRelativeTransform, AttachPoint); + UseWeaponMesh->SetHiddenInGame( false ); + } + } +} + +void AShooterWeapon::DetachMeshFromPawn() +{ + Mesh1P->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + Mesh1P->SetHiddenInGame(true); + + Mesh3P->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + Mesh3P->SetHiddenInGame(true); +} + + +////////////////////////////////////////////////////////////////////////// +// Input + +void AShooterWeapon::StartFire() +{ + if (GetLocalRole() < ROLE_Authority) + { + ServerStartFire(); + } + + if (!bWantsToFire) + { + bWantsToFire = true; + DetermineWeaponState(); + } +} + +void AShooterWeapon::StopFire() +{ + if ((GetLocalRole() < ROLE_Authority) && MyPawn && MyPawn->IsLocallyControlled()) + { + ServerStopFire(); + } + + if (bWantsToFire) + { + bWantsToFire = false; + DetermineWeaponState(); + } +} + +void AShooterWeapon::StartReload(bool bFromReplication) +{ + if (!bFromReplication && GetLocalRole() < ROLE_Authority) + { + ServerStartReload(); + } + + if (bFromReplication || CanReload()) + { + bPendingReload = true; + DetermineWeaponState(); + + float AnimDuration = PlayWeaponAnimation(ReloadAnim); + if (AnimDuration <= 0.0f) + { + AnimDuration = WeaponConfig.NoAnimReloadDuration; + } + + GetWorldTimerManager().SetTimer(TimerHandle_StopReload, this, &AShooterWeapon::StopReload, AnimDuration, false); + if (GetLocalRole() == ROLE_Authority) + { + GetWorldTimerManager().SetTimer(TimerHandle_ReloadWeapon, this, &AShooterWeapon::ReloadWeapon, FMath::Max(0.1f, AnimDuration - 0.1f), false); + } + + if (MyPawn && MyPawn->IsLocallyControlled()) + { + PlayWeaponSound(ReloadSound); + } + } +} + +void AShooterWeapon::StopReload() +{ + if (CurrentState == EWeaponState::Reloading) + { + bPendingReload = false; + DetermineWeaponState(); + StopWeaponAnimation(ReloadAnim); + } +} + +bool AShooterWeapon::ServerStartFire_Validate() +{ + return true; +} + +void AShooterWeapon::ServerStartFire_Implementation() +{ + StartFire(); +} + +bool AShooterWeapon::ServerStopFire_Validate() +{ + return true; +} + +void AShooterWeapon::ServerStopFire_Implementation() +{ + StopFire(); +} + +bool AShooterWeapon::ServerStartReload_Validate() +{ + return true; +} + +void AShooterWeapon::ServerStartReload_Implementation() +{ + StartReload(); +} + +bool AShooterWeapon::ServerStopReload_Validate() +{ + return true; +} + +void AShooterWeapon::ServerStopReload_Implementation() +{ + StopReload(); +} + +void AShooterWeapon::ClientStartReload_Implementation() +{ + StartReload(); +} + +////////////////////////////////////////////////////////////////////////// +// Control + +bool AShooterWeapon::CanFire() const +{ + bool bCanFire = MyPawn && MyPawn->CanFire(); + bool bStateOKToFire = ( ( CurrentState == EWeaponState::Idle ) || ( CurrentState == EWeaponState::Firing) ); + return (( bCanFire == true ) && ( bStateOKToFire == true ) && ( bPendingReload == false )); +} + +bool AShooterWeapon::CanReload() const +{ + bool bCanReload = (!MyPawn || MyPawn->CanReload()); + bool bGotAmmo = ( CurrentAmmoInClip < WeaponConfig.AmmoPerClip) && (CurrentAmmo - CurrentAmmoInClip > 0 || HasInfiniteClip()); + bool bStateOKToReload = ( ( CurrentState == EWeaponState::Idle ) || ( CurrentState == EWeaponState::Firing) ); + return ( ( bCanReload == true ) && ( bGotAmmo == true ) && ( bStateOKToReload == true) ); +} + + +////////////////////////////////////////////////////////////////////////// +// Weapon usage + +void AShooterWeapon::GiveAmmo(int AddAmount) +{ + const int32 MissingAmmo = FMath::Max(0, WeaponConfig.MaxAmmo - CurrentAmmo); + AddAmount = FMath::Min(AddAmount, MissingAmmo); + CurrentAmmo += AddAmount; + + AShooterAIController* BotAI = MyPawn ? Cast(MyPawn->GetController()) : NULL; + if (BotAI) + { + BotAI->CheckAmmo(this); + } + + // start reload if clip was empty + if (GetCurrentAmmoInClip() <= 0 && + CanReload() && + MyPawn && (MyPawn->GetWeapon() == this)) + { + ClientStartReload(); + } +} + +void AShooterWeapon::UseAmmo() +{ + if (!HasInfiniteAmmo()) + { + CurrentAmmoInClip--; + } + + if (!HasInfiniteAmmo() && !HasInfiniteClip()) + { + CurrentAmmo--; + } + + AShooterAIController* BotAI = MyPawn ? Cast(MyPawn->GetController()) : NULL; + AShooterPlayerController* PlayerController = MyPawn ? Cast(MyPawn->GetController()) : NULL; + if (BotAI) + { + BotAI->CheckAmmo(this); + } + else if(PlayerController) + { + AShooterPlayerState* PlayerState = Cast(PlayerController->PlayerState); + switch (GetAmmoType()) + { + case EAmmoType::ERocket: + PlayerState->AddRocketsFired(1); + break; + case EAmmoType::EBullet: + default: + PlayerState->AddBulletsFired(1); + break; + } + } +} + +void AShooterWeapon::HandleReFiring() +{ + // Update TimerIntervalAdjustment + UWorld* MyWorld = GetWorld(); + + float SlackTimeThisFrame = FMath::Max(0.0f, (MyWorld->TimeSeconds - LastFireTime) - WeaponConfig.TimeBetweenShots); + + if (bAllowAutomaticWeaponCatchup) + { + TimerIntervalAdjustment -= SlackTimeThisFrame; + } + + HandleFiring(); +} + +void AShooterWeapon::HandleFiring() +{ + if ((CurrentAmmoInClip > 0 || HasInfiniteClip() || HasInfiniteAmmo()) && CanFire()) + { + if (GetNetMode() != NM_DedicatedServer) + { + SimulateWeaponFire(); + } + + if (MyPawn && MyPawn->IsLocallyControlled()) + { + FireWeapon(); + + UseAmmo(); + + // update firing FX on remote clients if function was called on server + BurstCounter++; + } + } + else if (CanReload()) + { + StartReload(); + } + else if (MyPawn && MyPawn->IsLocallyControlled()) + { + if (GetCurrentAmmo() == 0 && !bRefiring) + { + PlayWeaponSound(OutOfAmmoSound); + AShooterPlayerController* MyPC = Cast(MyPawn->Controller); + AShooterHUD* MyHUD = MyPC ? Cast(MyPC->GetHUD()) : NULL; + if (MyHUD) + { + MyHUD->NotifyOutOfAmmo(); + } + } + + // stop weapon fire FX, but stay in Firing state + if (BurstCounter > 0) + { + OnBurstFinished(); + } + } + else + { + OnBurstFinished(); + } + + if (MyPawn && MyPawn->IsLocallyControlled()) + { + // local client will notify server + if (GetLocalRole() < ROLE_Authority) + { + ServerHandleFiring(); + } + + // reload after firing last round + if (CurrentAmmoInClip <= 0 && CanReload()) + { + StartReload(); + } + + // setup refire timer + bRefiring = (CurrentState == EWeaponState::Firing && WeaponConfig.TimeBetweenShots > 0.0f); + if (bRefiring) + { + GetWorldTimerManager().SetTimer(TimerHandle_HandleFiring, this, &AShooterWeapon::HandleReFiring, FMath::Max(WeaponConfig.TimeBetweenShots + TimerIntervalAdjustment, SMALL_NUMBER), false); + TimerIntervalAdjustment = 0.f; + } + } + + LastFireTime = GetWorld()->GetTimeSeconds(); +} + +bool AShooterWeapon::ServerHandleFiring_Validate() +{ + return true; +} + +void AShooterWeapon::ServerHandleFiring_Implementation() +{ + const bool bShouldUpdateAmmo = (CurrentAmmoInClip > 0 && CanFire()); + + HandleFiring(); + + if (bShouldUpdateAmmo) + { + // update ammo + UseAmmo(); + + // update firing FX on remote clients + BurstCounter++; + } +} + +void AShooterWeapon::ReloadWeapon() +{ + int32 ClipDelta = FMath::Min(WeaponConfig.AmmoPerClip - CurrentAmmoInClip, CurrentAmmo - CurrentAmmoInClip); + + if (HasInfiniteClip()) + { + ClipDelta = WeaponConfig.AmmoPerClip - CurrentAmmoInClip; + } + + if (ClipDelta > 0) + { + CurrentAmmoInClip += ClipDelta; + } + + if (HasInfiniteClip()) + { + CurrentAmmo = FMath::Max(CurrentAmmoInClip, CurrentAmmo); + } +} + +void AShooterWeapon::SetWeaponState(EWeaponState::Type NewState) +{ + const EWeaponState::Type PrevState = CurrentState; + + if (PrevState == EWeaponState::Firing && NewState != EWeaponState::Firing) + { + OnBurstFinished(); + } + + CurrentState = NewState; + + if (PrevState != EWeaponState::Firing && NewState == EWeaponState::Firing) + { + OnBurstStarted(); + } +} + +void AShooterWeapon::DetermineWeaponState() +{ + EWeaponState::Type NewState = EWeaponState::Idle; + + if (bIsEquipped) + { + if( bPendingReload ) + { + if( CanReload() == false ) + { + NewState = CurrentState; + } + else + { + NewState = EWeaponState::Reloading; + } + } + else if ( (bPendingReload == false ) && ( bWantsToFire == true ) && ( CanFire() == true )) + { + NewState = EWeaponState::Firing; + } + } + else if (bPendingEquip) + { + NewState = EWeaponState::Equipping; + } + + SetWeaponState(NewState); +} + +void AShooterWeapon::OnBurstStarted() +{ + // start firing, can be delayed to satisfy TimeBetweenShots + const float GameTime = GetWorld()->GetTimeSeconds(); + if (LastFireTime > 0 && WeaponConfig.TimeBetweenShots > 0.0f && + LastFireTime + WeaponConfig.TimeBetweenShots > GameTime) + { + GetWorldTimerManager().SetTimer(TimerHandle_HandleFiring, this, &AShooterWeapon::HandleFiring, LastFireTime + WeaponConfig.TimeBetweenShots - GameTime, false); + } + else + { + HandleFiring(); + } +} + +void AShooterWeapon::OnBurstFinished() +{ + // stop firing FX on remote clients + BurstCounter = 0; + + // stop firing FX locally, unless it's a dedicated server + //if (GetNetMode() != NM_DedicatedServer) + //{ + StopSimulatingWeaponFire(); + //} + + GetWorldTimerManager().ClearTimer(TimerHandle_HandleFiring); + bRefiring = false; + + // reset firing interval adjustment + TimerIntervalAdjustment = 0.0f; +} + + +////////////////////////////////////////////////////////////////////////// +// Weapon usage helpers + +UAudioComponent* AShooterWeapon::PlayWeaponSound(USoundCue* Sound) +{ + UAudioComponent* AC = NULL; + if (Sound && MyPawn) + { + AC = UGameplayStatics::SpawnSoundAttached(Sound, MyPawn->GetRootComponent()); + } + + return AC; +} + +float AShooterWeapon::PlayWeaponAnimation(const FWeaponAnim& Animation) +{ + float Duration = 0.0f; + if (MyPawn) + { + UAnimMontage* UseAnim = MyPawn->IsFirstPerson() ? Animation.Pawn1P : Animation.Pawn3P; + if (UseAnim) + { + Duration = MyPawn->PlayAnimMontage(UseAnim); + } + } + + return Duration; +} + +void AShooterWeapon::StopWeaponAnimation(const FWeaponAnim& Animation) +{ + if (MyPawn) + { + UAnimMontage* UseAnim = MyPawn->IsFirstPerson() ? Animation.Pawn1P : Animation.Pawn3P; + if (UseAnim) + { + MyPawn->StopAnimMontage(UseAnim); + } + } +} + +FVector AShooterWeapon::GetCameraAim() const +{ + AShooterPlayerController* const PlayerController = GetInstigatorController(); + FVector FinalAim = FVector::ZeroVector; + + if (PlayerController) + { + FVector CamLoc; + FRotator CamRot; + PlayerController->GetPlayerViewPoint(CamLoc, CamRot); + FinalAim = CamRot.Vector(); + } + else if (GetInstigator()) + { + FinalAim = GetInstigator()->GetBaseAimRotation().Vector(); + } + + return FinalAim; +} + +FVector AShooterWeapon::GetAdjustedAim() const +{ + AShooterPlayerController* const PlayerController = GetInstigatorController(); + FVector FinalAim = FVector::ZeroVector; + // If we have a player controller use it for the aim + if (PlayerController) + { + FVector CamLoc; + FRotator CamRot; + PlayerController->GetPlayerViewPoint(CamLoc, CamRot); + FinalAim = CamRot.Vector(); + } + else if (GetInstigator()) + { + // Now see if we have an AI controller - we will want to get the aim from there if we do + AShooterAIController* AIController = MyPawn ? Cast(MyPawn->Controller) : NULL; + if(AIController != NULL ) + { + FinalAim = AIController->GetControlRotation().Vector(); + } + else + { + FinalAim = GetInstigator()->GetBaseAimRotation().Vector(); + } + } + + return FinalAim; +} + +FVector AShooterWeapon::GetCameraDamageStartLocation(const FVector& AimDir) const +{ + AShooterPlayerController* PC = MyPawn ? Cast(MyPawn->Controller) : NULL; + AShooterAIController* AIPC = MyPawn ? Cast(MyPawn->Controller) : NULL; + FVector OutStartTrace = FVector::ZeroVector; + + if (PC) + { + // use player's camera + FRotator UnusedRot; + PC->GetPlayerViewPoint(OutStartTrace, UnusedRot); + + // Adjust trace so there is nothing blocking the ray between the camera and the pawn, and calculate distance from adjusted start + OutStartTrace = OutStartTrace + AimDir * ((GetInstigator()->GetActorLocation() - OutStartTrace) | AimDir); + } + else if (AIPC) + { + OutStartTrace = GetMuzzleLocation(); + } + + return OutStartTrace; +} + +FVector AShooterWeapon::GetMuzzleLocation() const +{ + USkeletalMeshComponent* UseMesh = GetWeaponMesh(); + return UseMesh->GetSocketLocation(MuzzleAttachPoint); +} + +FVector AShooterWeapon::GetMuzzleDirection() const +{ + USkeletalMeshComponent* UseMesh = GetWeaponMesh(); + return UseMesh->GetSocketRotation(MuzzleAttachPoint).Vector(); +} + +FHitResult AShooterWeapon::WeaponTrace(const FVector& StartTrace, const FVector& EndTrace) const +{ + + // Perform trace to retrieve hit info + FCollisionQueryParams TraceParams(SCENE_QUERY_STAT(WeaponTrace), true, GetInstigator()); + TraceParams.bReturnPhysicalMaterial = true; + + FHitResult Hit(ForceInit); + GetWorld()->LineTraceSingleByChannel(Hit, StartTrace, EndTrace, COLLISION_WEAPON, TraceParams); + + return Hit; +} + +void AShooterWeapon::SetOwningPawn(AShooterCharacter* NewOwner) +{ + if (MyPawn != NewOwner) + { + SetInstigator(NewOwner); + MyPawn = NewOwner; + // net owner for RPC calls + SetOwner(NewOwner); + } +} + +////////////////////////////////////////////////////////////////////////// +// Replication & effects + +void AShooterWeapon::OnRep_MyPawn() +{ + if (MyPawn) + { + OnEnterInventory(MyPawn); + } + else + { + OnLeaveInventory(); + } +} + +void AShooterWeapon::OnRep_BurstCounter() +{ + if (BurstCounter > 0) + { + SimulateWeaponFire(); + } + else + { + StopSimulatingWeaponFire(); + } +} + +void AShooterWeapon::OnRep_Reload() +{ + if (bPendingReload) + { + StartReload(true); + } + else + { + StopReload(); + } +} + +void AShooterWeapon::SimulateWeaponFire() +{ + if (GetLocalRole() == ROLE_Authority && CurrentState != EWeaponState::Firing) + { + return; + } + + if (MuzzleFX) + { + USkeletalMeshComponent* UseWeaponMesh = GetWeaponMesh(); + if (!bLoopedMuzzleFX || MuzzlePSC == NULL) + { + // Split screen requires we create 2 effects. One that we see and one that the other player sees. + if( (MyPawn != NULL ) && ( MyPawn->IsLocallyControlled() == true ) ) + { + AController* PlayerCon = MyPawn->GetController(); + if( PlayerCon != NULL ) + { + Mesh1P->GetSocketLocation(MuzzleAttachPoint); + MuzzlePSC = UGameplayStatics::SpawnEmitterAttached(MuzzleFX, Mesh1P, MuzzleAttachPoint); + MuzzlePSC->bOwnerNoSee = false; + MuzzlePSC->bOnlyOwnerSee = true; + + Mesh3P->GetSocketLocation(MuzzleAttachPoint); + MuzzlePSCSecondary = UGameplayStatics::SpawnEmitterAttached(MuzzleFX, Mesh3P, MuzzleAttachPoint); + MuzzlePSCSecondary->bOwnerNoSee = true; + MuzzlePSCSecondary->bOnlyOwnerSee = false; + } + } + else + { + MuzzlePSC = UGameplayStatics::SpawnEmitterAttached(MuzzleFX, UseWeaponMesh, MuzzleAttachPoint); + } + } + } + + if (!bLoopedFireAnim || !bPlayingFireAnim) + { + PlayWeaponAnimation(FireAnim); + bPlayingFireAnim = true; + } + + if (bLoopedFireSound) + { + if (FireAC == NULL) + { + FireAC = PlayWeaponSound(FireLoopSound); + } + } + else + { + PlayWeaponSound(FireSound); + } + + AShooterPlayerController* PC = (MyPawn != NULL) ? Cast(MyPawn->Controller) : NULL; + if (PC != NULL && PC->IsLocalController()) + { + if (FireCameraShake != NULL) + { + PC->ClientStartCameraShake(FireCameraShake, 1); + } + if (FireForceFeedback != NULL && PC->IsVibrationEnabled()) + { + FForceFeedbackParameters FFParams; + FFParams.Tag = "Weapon"; + PC->ClientPlayForceFeedback(FireForceFeedback, FFParams); + } + } +} + +void AShooterWeapon::StopSimulatingWeaponFire() +{ + if (bLoopedMuzzleFX ) + { + if( MuzzlePSC != NULL ) + { + MuzzlePSC->DeactivateSystem(); + MuzzlePSC = NULL; + } + if( MuzzlePSCSecondary != NULL ) + { + MuzzlePSCSecondary->DeactivateSystem(); + MuzzlePSCSecondary = NULL; + } + } + + if (bLoopedFireAnim && bPlayingFireAnim) + { + StopWeaponAnimation(FireAnim); + bPlayingFireAnim = false; + } + + if (FireAC) + { + FireAC->FadeOut(0.1f, 0.0f); + FireAC = NULL; + + PlayWeaponSound(FireFinishSound); + } +} + +void AShooterWeapon::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const +{ + Super::GetLifetimeReplicatedProps( OutLifetimeProps ); + + DOREPLIFETIME( AShooterWeapon, MyPawn ); + + DOREPLIFETIME_CONDITION( AShooterWeapon, CurrentAmmo, COND_OwnerOnly ); + DOREPLIFETIME_CONDITION( AShooterWeapon, CurrentAmmoInClip, COND_OwnerOnly ); + + DOREPLIFETIME_CONDITION( AShooterWeapon, BurstCounter, COND_SkipOwner ); + DOREPLIFETIME_CONDITION( AShooterWeapon, bPendingReload, COND_SkipOwner ); +} + +USkeletalMeshComponent* AShooterWeapon::GetWeaponMesh() const +{ + return (MyPawn != NULL && MyPawn->IsFirstPerson()) ? Mesh1P : Mesh3P; +} + +class AShooterCharacter* AShooterWeapon::GetPawnOwner() const +{ + return MyPawn; +} + +bool AShooterWeapon::IsEquipped() const +{ + return bIsEquipped; +} + +bool AShooterWeapon::IsAttachedToPawn() const +{ + return bIsEquipped || bPendingEquip; +} + +EWeaponState::Type AShooterWeapon::GetCurrentState() const +{ + return CurrentState; +} + +int32 AShooterWeapon::GetCurrentAmmo() const +{ + return CurrentAmmo; +} + +int32 AShooterWeapon::GetCurrentAmmoInClip() const +{ + return CurrentAmmoInClip; +} + +int32 AShooterWeapon::GetAmmoPerClip() const +{ + return WeaponConfig.AmmoPerClip; +} + +int32 AShooterWeapon::GetMaxAmmo() const +{ + return WeaponConfig.MaxAmmo; +} + +bool AShooterWeapon::HasInfiniteAmmo() const +{ + const AShooterPlayerController* MyPC = (MyPawn != NULL) ? Cast(MyPawn->Controller) : NULL; + return WeaponConfig.bInfiniteAmmo || (MyPC && MyPC->HasInfiniteAmmo()); +} + +bool AShooterWeapon::HasInfiniteClip() const +{ + const AShooterPlayerController* MyPC = (MyPawn != NULL) ? Cast(MyPawn->Controller) : NULL; + return WeaponConfig.bInfiniteClip || (MyPC && MyPC->HasInfiniteClip()); +} + +float AShooterWeapon::GetEquipStartedTime() const +{ + return EquipStartedTime; +} + +float AShooterWeapon::GetEquipDuration() const +{ + return EquipDuration; +} diff --git a/Source/ShooterGame/Private/Weapons/ShooterWeapon_Instant.cpp b/Source/ShooterGame/Private/Weapons/ShooterWeapon_Instant.cpp new file mode 100644 index 0000000..e4591c6 --- /dev/null +++ b/Source/ShooterGame/Private/Weapons/ShooterWeapon_Instant.cpp @@ -0,0 +1,312 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Weapons/ShooterWeapon_Instant.h" +#include "Particles/ParticleSystemComponent.h" +#include "Effects/ShooterImpactEffect.h" + +AShooterWeapon_Instant::AShooterWeapon_Instant(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + CurrentFiringSpread = 0.0f; +} + +////////////////////////////////////////////////////////////////////////// +// Weapon usage + +void AShooterWeapon_Instant::FireWeapon() +{ + const int32 RandomSeed = FMath::Rand(); + FRandomStream WeaponRandomStream(RandomSeed); + const float CurrentSpread = GetCurrentSpread(); + const float ConeHalfAngle = FMath::DegreesToRadians(CurrentSpread * 0.5f); + + const FVector AimDir = GetAdjustedAim(); + const FVector StartTrace = GetCameraDamageStartLocation(AimDir); + const FVector ShootDir = WeaponRandomStream.VRandCone(AimDir, ConeHalfAngle, ConeHalfAngle); + const FVector EndTrace = StartTrace + ShootDir * InstantConfig.WeaponRange; + + const FHitResult Impact = WeaponTrace(StartTrace, EndTrace); + ProcessInstantHit(Impact, StartTrace, ShootDir, RandomSeed, CurrentSpread); + + CurrentFiringSpread = FMath::Min(InstantConfig.FiringSpreadMax, CurrentFiringSpread + InstantConfig.FiringSpreadIncrement); +} + +bool AShooterWeapon_Instant::ServerNotifyHit_Validate(const FHitResult& Impact, FVector_NetQuantizeNormal ShootDir, int32 RandomSeed, float ReticleSpread) +{ + return true; +} + +void AShooterWeapon_Instant::ServerNotifyHit_Implementation(const FHitResult& Impact, FVector_NetQuantizeNormal ShootDir, int32 RandomSeed, float ReticleSpread) +{ + const float WeaponAngleDot = FMath::Abs(FMath::Sin(ReticleSpread * PI / 180.f)); + + // if we have an instigator, calculate dot between the view and the shot + if (GetInstigator() && (Impact.GetActor() || Impact.bBlockingHit)) + { + const FVector Origin = GetMuzzleLocation(); + const FVector ViewDir = (Impact.Location - Origin).GetSafeNormal(); + + // is the angle between the hit and the view within allowed limits (limit + weapon max angle) + const float ViewDotHitDir = FVector::DotProduct(GetInstigator()->GetViewRotation().Vector(), ViewDir); + if (ViewDotHitDir > InstantConfig.AllowedViewDotHitDir - WeaponAngleDot) + { + if (CurrentState != EWeaponState::Idle) + { + if (Impact.GetActor() == NULL) + { + if (Impact.bBlockingHit) + { + ProcessInstantHit_Confirmed(Impact, Origin, ShootDir, RandomSeed, ReticleSpread); + } + } + // assume it told the truth about static things because the don't move and the hit + // usually doesn't have significant gameplay implications + else if (Impact.GetActor()->IsRootComponentStatic() || Impact.GetActor()->IsRootComponentStationary()) + { + ProcessInstantHit_Confirmed(Impact, Origin, ShootDir, RandomSeed, ReticleSpread); + } + else + { + // Get the component bounding box + const FBox HitBox = Impact.GetActor()->GetComponentsBoundingBox(); + + // calculate the box extent, and increase by a leeway + FVector BoxExtent = 0.5 * (HitBox.Max - HitBox.Min); + BoxExtent *= InstantConfig.ClientSideHitLeeway; + + // avoid precision errors with really thin objects + BoxExtent.X = FMath::Max(20.0f, BoxExtent.X); + BoxExtent.Y = FMath::Max(20.0f, BoxExtent.Y); + BoxExtent.Z = FMath::Max(20.0f, BoxExtent.Z); + + // Get the box center + const FVector BoxCenter = (HitBox.Min + HitBox.Max) * 0.5; + + // if we are within client tolerance + if (FMath::Abs(Impact.Location.Z - BoxCenter.Z) < BoxExtent.Z && + FMath::Abs(Impact.Location.X - BoxCenter.X) < BoxExtent.X && + FMath::Abs(Impact.Location.Y - BoxCenter.Y) < BoxExtent.Y) + { + ProcessInstantHit_Confirmed(Impact, Origin, ShootDir, RandomSeed, ReticleSpread); + } + else + { + UE_LOG(LogShooterWeapon, Log, TEXT("%s Rejected client side hit of %s (outside bounding box tolerance)"), *GetNameSafe(this), *GetNameSafe(Impact.GetActor())); + } + } + } + } + else if (ViewDotHitDir <= InstantConfig.AllowedViewDotHitDir) + { + UE_LOG(LogShooterWeapon, Log, TEXT("%s Rejected client side hit of %s (facing too far from the hit direction)"), *GetNameSafe(this), *GetNameSafe(Impact.GetActor())); + } + else + { + UE_LOG(LogShooterWeapon, Log, TEXT("%s Rejected client side hit of %s"), *GetNameSafe(this), *GetNameSafe(Impact.GetActor())); + } + } +} + +bool AShooterWeapon_Instant::ServerNotifyMiss_Validate(FVector_NetQuantizeNormal ShootDir, int32 RandomSeed, float ReticleSpread) +{ + return true; +} + +void AShooterWeapon_Instant::ServerNotifyMiss_Implementation(FVector_NetQuantizeNormal ShootDir, int32 RandomSeed, float ReticleSpread) +{ + const FVector Origin = GetMuzzleLocation(); + + // play FX on remote clients + HitNotify.Origin = Origin; + HitNotify.RandomSeed = RandomSeed; + HitNotify.ReticleSpread = ReticleSpread; + + // play FX locally + if (GetNetMode() != NM_DedicatedServer) + { + const FVector EndTrace = Origin + ShootDir * InstantConfig.WeaponRange; + SpawnTrailEffect(EndTrace); + } +} + +void AShooterWeapon_Instant::ProcessInstantHit(const FHitResult& Impact, const FVector& Origin, const FVector& ShootDir, int32 RandomSeed, float ReticleSpread) +{ + if (MyPawn && MyPawn->IsLocallyControlled() && GetNetMode() == NM_Client) + { + // if we're a client and we've hit something that is being controlled by the server + if (Impact.GetActor() && Impact.GetActor()->GetRemoteRole() == ROLE_Authority) + { + // notify the server of the hit + ServerNotifyHit(Impact, ShootDir, RandomSeed, ReticleSpread); + } + else if (Impact.GetActor() == NULL) + { + if (Impact.bBlockingHit) + { + // notify the server of the hit + ServerNotifyHit(Impact, ShootDir, RandomSeed, ReticleSpread); + } + else + { + // notify server of the miss + ServerNotifyMiss(ShootDir, RandomSeed, ReticleSpread); + } + } + } + + // process a confirmed hit + ProcessInstantHit_Confirmed(Impact, Origin, ShootDir, RandomSeed, ReticleSpread); +} + +void AShooterWeapon_Instant::ProcessInstantHit_Confirmed(const FHitResult& Impact, const FVector& Origin, const FVector& ShootDir, int32 RandomSeed, float ReticleSpread) +{ + // handle damage + if (ShouldDealDamage(Impact.GetActor())) + { + DealDamage(Impact, ShootDir); + } + + // play FX on remote clients + if (GetLocalRole() == ROLE_Authority) + { + HitNotify.Origin = Origin; + HitNotify.RandomSeed = RandomSeed; + HitNotify.ReticleSpread = ReticleSpread; + } + + // play FX locally + if (GetNetMode() != NM_DedicatedServer) + { + const FVector EndTrace = Origin + ShootDir * InstantConfig.WeaponRange; + const FVector EndPoint = Impact.GetActor() ? Impact.ImpactPoint : EndTrace; + + SpawnTrailEffect(EndPoint); + SpawnImpactEffects(Impact); + } +} + +bool AShooterWeapon_Instant::ShouldDealDamage(AActor* TestActor) const +{ + // if we're an actor on the server, or the actor's role is authoritative, we should register damage + if (TestActor) + { + if (GetNetMode() != NM_Client || + TestActor->GetLocalRole() == ROLE_Authority || + TestActor->GetTearOff()) + { + return true; + } + } + + return false; +} + +void AShooterWeapon_Instant::DealDamage(const FHitResult& Impact, const FVector& ShootDir) +{ + FPointDamageEvent PointDmg; + PointDmg.DamageTypeClass = InstantConfig.DamageType; + PointDmg.HitInfo = Impact; + PointDmg.ShotDirection = ShootDir; + PointDmg.Damage = InstantConfig.HitDamage; + + Impact.GetActor()->TakeDamage(PointDmg.Damage, PointDmg, MyPawn->Controller, this); +} + +void AShooterWeapon_Instant::OnBurstFinished() +{ + Super::OnBurstFinished(); + + CurrentFiringSpread = 0.0f; +} + + +////////////////////////////////////////////////////////////////////////// +// Weapon usage helpers + +float AShooterWeapon_Instant::GetCurrentSpread() const +{ + float FinalSpread = InstantConfig.WeaponSpread + CurrentFiringSpread; + if (MyPawn && MyPawn->IsTargeting()) + { + FinalSpread *= InstantConfig.TargetingSpreadMod; + } + + return FinalSpread; +} + + +////////////////////////////////////////////////////////////////////////// +// Replication & effects + +void AShooterWeapon_Instant::OnRep_HitNotify() +{ + SimulateInstantHit(HitNotify.Origin, HitNotify.RandomSeed, HitNotify.ReticleSpread); +} + +void AShooterWeapon_Instant::SimulateInstantHit(const FVector& ShotOrigin, int32 RandomSeed, float ReticleSpread) +{ + FRandomStream WeaponRandomStream(RandomSeed); + const float ConeHalfAngle = FMath::DegreesToRadians(ReticleSpread * 0.5f); + + const FVector StartTrace = ShotOrigin; + const FVector AimDir = GetAdjustedAim(); + const FVector ShootDir = WeaponRandomStream.VRandCone(AimDir, ConeHalfAngle, ConeHalfAngle); + const FVector EndTrace = StartTrace + ShootDir * InstantConfig.WeaponRange; + + FHitResult Impact = WeaponTrace(StartTrace, EndTrace); + if (Impact.bBlockingHit) + { + SpawnImpactEffects(Impact); + SpawnTrailEffect(Impact.ImpactPoint); + } + else + { + SpawnTrailEffect(EndTrace); + } +} + +void AShooterWeapon_Instant::SpawnImpactEffects(const FHitResult& Impact) +{ + if (ImpactTemplate && Impact.bBlockingHit) + { + FHitResult UseImpact = Impact; + + // trace again to find component lost during replication + if (!Impact.Component.IsValid()) + { + const FVector StartTrace = Impact.ImpactPoint + Impact.ImpactNormal * 10.0f; + const FVector EndTrace = Impact.ImpactPoint - Impact.ImpactNormal * 10.0f; + FHitResult Hit = WeaponTrace(StartTrace, EndTrace); + UseImpact = Hit; + } + + FTransform const SpawnTransform(Impact.ImpactNormal.Rotation(), Impact.ImpactPoint); + AShooterImpactEffect* EffectActor = GetWorld()->SpawnActorDeferred(ImpactTemplate, SpawnTransform); + if (EffectActor) + { + EffectActor->SurfaceHit = UseImpact; + UGameplayStatics::FinishSpawningActor(EffectActor, SpawnTransform); + } + } +} + +void AShooterWeapon_Instant::SpawnTrailEffect(const FVector& EndPoint) +{ + if (TrailFX) + { + const FVector Origin = GetMuzzleLocation(); + + UParticleSystemComponent* TrailPSC = UGameplayStatics::SpawnEmitterAtLocation(this, TrailFX, Origin); + if (TrailPSC) + { + TrailPSC->SetVectorParameter(TrailTargetParam, EndPoint); + } + } +} + +void AShooterWeapon_Instant::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const +{ + Super::GetLifetimeReplicatedProps( OutLifetimeProps ); + + DOREPLIFETIME_CONDITION( AShooterWeapon_Instant, HitNotify, COND_SkipOwner ); +} \ No newline at end of file diff --git a/Source/ShooterGame/Private/Weapons/ShooterWeapon_Projectile.cpp b/Source/ShooterGame/Private/Weapons/ShooterWeapon_Projectile.cpp new file mode 100644 index 0000000..efda8ad --- /dev/null +++ b/Source/ShooterGame/Private/Weapons/ShooterWeapon_Projectile.cpp @@ -0,0 +1,89 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGame.h" +#include "Weapons/ShooterWeapon_Projectile.h" +#include "Weapons/ShooterProjectile.h" + +AShooterWeapon_Projectile::AShooterWeapon_Projectile(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ +} + +////////////////////////////////////////////////////////////////////////// +// Weapon usage + +void AShooterWeapon_Projectile::FireWeapon() +{ + FVector ShootDir = GetAdjustedAim(); + FVector Origin = GetMuzzleLocation(); + + // trace from camera to check what's under crosshair + const float ProjectileAdjustRange = 10000.0f; + const FVector StartTrace = GetCameraDamageStartLocation(ShootDir); + const FVector EndTrace = StartTrace + ShootDir * ProjectileAdjustRange; + FHitResult Impact = WeaponTrace(StartTrace, EndTrace); + + // and adjust directions to hit that actor + if (Impact.bBlockingHit) + { + const FVector AdjustedDir = (Impact.ImpactPoint - Origin).GetSafeNormal(); + bool bWeaponPenetration = false; + + const float DirectionDot = FVector::DotProduct(AdjustedDir, ShootDir); + if (DirectionDot < 0.0f) + { + // shooting backwards = weapon is penetrating + bWeaponPenetration = true; + } + else if (DirectionDot < 0.5f) + { + // check for weapon penetration if angle difference is big enough + // raycast along weapon mesh to check if there's blocking hit + + FVector MuzzleStartTrace = Origin - GetMuzzleDirection() * 150.0f; + FVector MuzzleEndTrace = Origin; + FHitResult MuzzleImpact = WeaponTrace(MuzzleStartTrace, MuzzleEndTrace); + + if (MuzzleImpact.bBlockingHit) + { + bWeaponPenetration = true; + } + } + + if (bWeaponPenetration) + { + // spawn at crosshair position + Origin = Impact.ImpactPoint - ShootDir * 10.0f; + } + else + { + // adjust direction to hit + ShootDir = AdjustedDir; + } + } + + ServerFireProjectile(Origin, ShootDir); +} + +bool AShooterWeapon_Projectile::ServerFireProjectile_Validate(FVector Origin, FVector_NetQuantizeNormal ShootDir) +{ + return true; +} + +void AShooterWeapon_Projectile::ServerFireProjectile_Implementation(FVector Origin, FVector_NetQuantizeNormal ShootDir) +{ + FTransform SpawnTM(ShootDir.Rotation(), Origin); + AShooterProjectile* Projectile = Cast(UGameplayStatics::BeginDeferredActorSpawnFromClass(this, ProjectileConfig.ProjectileClass, SpawnTM)); + if (Projectile) + { + Projectile->SetInstigator(GetInstigator()); + Projectile->SetOwner(this); + Projectile->InitVelocity(ShootDir); + + UGameplayStatics::FinishSpawningActor(Projectile, SpawnTM); + } +} + +void AShooterWeapon_Projectile::ApplyWeaponConfig(FProjectileWeaponData& Data) +{ + Data = ProjectileConfig; +} diff --git a/Source/ShooterGame/Public/Bots/BTDecorator_HasLoSTo.h b/Source/ShooterGame/Public/Bots/BTDecorator_HasLoSTo.h new file mode 100644 index 0000000..2596bd8 --- /dev/null +++ b/Source/ShooterGame/Public/Bots/BTDecorator_HasLoSTo.h @@ -0,0 +1,23 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once +#include "BehaviorTree/BTDecorator.h" +#include "BTDecorator_HasLoSTo.generated.h" + + +// Checks if the AI pawn has Line of sight to the specified Actor or Location(Vector). +UCLASS() +class UBTDecorator_HasLoSTo : public UBTDecorator +{ + GENERATED_UCLASS_BODY() + + virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override; + +protected: + + UPROPERTY(EditAnywhere, Category = Condition) + struct FBlackboardKeySelector EnemyKey; + +private: + bool LOSTrace(AActor* InActor, AActor* InEnemyActor, const FVector& EndLocation) const; +}; diff --git a/Source/ShooterGame/Public/Bots/BTTask_FindPickup.h b/Source/ShooterGame/Public/Bots/BTTask_FindPickup.h new file mode 100644 index 0000000..9bb7e4a --- /dev/null +++ b/Source/ShooterGame/Public/Bots/BTTask_FindPickup.h @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once +#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h" +#include "BTTask_FindPickup.generated.h" + +// Bot AI Task that attempts to locate a pickup +UCLASS() +class UBTTask_FindPickup : public UBTTask_BlackboardBase +{ + GENERATED_UCLASS_BODY() + + virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; +}; diff --git a/Source/ShooterGame/Public/Bots/BTTask_FindPointNearEnemy.h b/Source/ShooterGame/Public/Bots/BTTask_FindPointNearEnemy.h new file mode 100644 index 0000000..f758b8c --- /dev/null +++ b/Source/ShooterGame/Public/Bots/BTTask_FindPointNearEnemy.h @@ -0,0 +1,15 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once +#include "BehaviorTree/BTNode.h" +#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h" +#include "BTTask_FindPointNearEnemy.generated.h" + +// Bot AI task that tries to find a location near the current enemy +UCLASS() +class UBTTask_FindPointNearEnemy : public UBTTask_BlackboardBase +{ + GENERATED_UCLASS_BODY() + + virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; +}; diff --git a/Source/ShooterGame/Public/Bots/ShooterAIController.h b/Source/ShooterGame/Public/Bots/ShooterAIController.h new file mode 100644 index 0000000..771e6e1 --- /dev/null +++ b/Source/ShooterGame/Public/Bots/ShooterAIController.h @@ -0,0 +1,75 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once +#include "AIController.h" +#include "ShooterAIController.generated.h" + +class UBehaviorTreeComponent; +class UBlackboardComponent; + +UCLASS(config=Game) +class AShooterAIController : public AAIController +{ + GENERATED_UCLASS_BODY() + +private: + UPROPERTY(transient) + UBlackboardComponent* BlackboardComp; + + /* Cached BT component */ + UPROPERTY(transient) + UBehaviorTreeComponent* BehaviorComp; +public: + + // Begin AController interface + virtual void GameHasEnded(class AActor* EndGameFocus = NULL, bool bIsWinner = false) override; + virtual void BeginInactiveState() override; + +protected: + virtual void OnPossess(class APawn* InPawn) override; + virtual void OnUnPossess() override; + // End APlayerController interface + +public: + void Respawn(); + + void CheckAmmo(const class AShooterWeapon* CurrentWeapon); + + void SetEnemy(class APawn* InPawn); + + class AShooterCharacter* GetEnemy() const; + + /* If there is line of sight to current enemy, start firing at them */ + UFUNCTION(BlueprintCallable, Category=Behavior) + void ShootEnemy(); + + /* Finds the closest enemy and sets them as current target */ + UFUNCTION(BlueprintCallable, Category=Behavior) + void FindClosestEnemy(); + + UFUNCTION(BlueprintCallable, Category = Behavior) + bool FindClosestEnemyWithLOS(AShooterCharacter* ExcludeEnemy); + + bool HasWeaponLOSToEnemy(AActor* InEnemyActor, const bool bAnyEnemy) const; + + // Begin AAIController interface + /** Update direction AI is looking based on FocalPoint */ + virtual void UpdateControlRotation(float DeltaTime, bool bUpdatePawn = true) override; + // End AAIController interface + +protected: + // Check of we have LOS to a character + bool LOSTrace(AShooterCharacter* InEnemyChar) const; + + int32 EnemyKeyID; + int32 NeedAmmoKeyID; + + /** Handle for efficient management of Respawn timer */ + FTimerHandle TimerHandle_Respawn; + +public: + /** Returns BlackboardComp subobject **/ + FORCEINLINE UBlackboardComponent* GetBlackboardComp() const { return BlackboardComp; } + /** Returns BehaviorComp subobject **/ + FORCEINLINE UBehaviorTreeComponent* GetBehaviorComp() const { return BehaviorComp; } +}; diff --git a/Source/ShooterGame/Public/Bots/ShooterBot.h b/Source/ShooterGame/Public/Bots/ShooterBot.h new file mode 100644 index 0000000..50de9f8 --- /dev/null +++ b/Source/ShooterGame/Public/Bots/ShooterBot.h @@ -0,0 +1,19 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterCharacter.h" +#include "ShooterBot.generated.h" + +UCLASS() +class AShooterBot : public AShooterCharacter +{ + GENERATED_UCLASS_BODY() + + UPROPERTY(EditAnywhere, Category=Behavior) + class UBehaviorTree* BotBehavior; + + virtual bool IsFirstPerson() const override; + + virtual void FaceRotation(FRotator NewRotation, float DeltaTime = 0.f) override; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Effects/ShooterExplosionEffect.h b/Source/ShooterGame/Public/Effects/ShooterExplosionEffect.h new file mode 100644 index 0000000..e43e8e7 --- /dev/null +++ b/Source/ShooterGame/Public/Effects/ShooterExplosionEffect.h @@ -0,0 +1,58 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterTypes.h" +#include "ShooterExplosionEffect.generated.h" + +// +// Spawnable effect for explosion - NOT replicated to clients +// Each explosion type should be defined as separate blueprint +// +UCLASS(Abstract, Blueprintable) +class AShooterExplosionEffect : public AActor +{ + GENERATED_UCLASS_BODY() + + /** explosion FX */ + UPROPERTY(EditDefaultsOnly, Category=Effect) + UParticleSystem* ExplosionFX; + +private: + /** explosion light */ + UPROPERTY(VisibleDefaultsOnly, Category=Effect) + UPointLightComponent* ExplosionLight; +public: + + /** how long keep explosion light on? */ + UPROPERTY(EditDefaultsOnly, Category=Effect) + float ExplosionLightFadeOut; + + /** explosion sound */ + UPROPERTY(EditDefaultsOnly, Category=Effect) + USoundCue* ExplosionSound; + + /** explosion decals */ + UPROPERTY(EditDefaultsOnly, Category=Effect) + struct FDecalData Decal; + + /** surface data for spawning */ + UPROPERTY(BlueprintReadOnly, Category=Surface) + FHitResult SurfaceHit; + + /** update fading light */ + virtual void Tick(float DeltaSeconds) override; + +protected: + /** spawn explosion */ + virtual void BeginPlay() override; + +private: + + /** Point light component name */ + FName ExplosionLightComponentName; + +public: + /** Returns ExplosionLight subobject **/ + FORCEINLINE UPointLightComponent* GetExplosionLight() const { return ExplosionLight; } +}; diff --git a/Source/ShooterGame/Public/Effects/ShooterImpactEffect.h b/Source/ShooterGame/Public/Effects/ShooterImpactEffect.h new file mode 100644 index 0000000..99547a9 --- /dev/null +++ b/Source/ShooterGame/Public/Effects/ShooterImpactEffect.h @@ -0,0 +1,107 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterTypes.h" +#include "ShooterImpactEffect.generated.h" + +// +// Spawnable effect for weapon hit impact - NOT replicated to clients +// Each impact type should be defined as separate blueprint +// +UCLASS(Abstract, Blueprintable) +class AShooterImpactEffect : public AActor +{ + GENERATED_UCLASS_BODY() + + /** default impact FX used when material specific override doesn't exist */ + UPROPERTY(EditDefaultsOnly, Category=Defaults) + UParticleSystem* DefaultFX; + + /** impact FX on concrete */ + UPROPERTY(EditDefaultsOnly, Category=Visual) + UParticleSystem* ConcreteFX; + + /** impact FX on dirt */ + UPROPERTY(EditDefaultsOnly, Category=Visual) + UParticleSystem* DirtFX; + + /** impact FX on water */ + UPROPERTY(EditDefaultsOnly, Category=Visual) + UParticleSystem* WaterFX; + + /** impact FX on metal */ + UPROPERTY(EditDefaultsOnly, Category=Visual) + UParticleSystem* MetalFX; + + /** impact FX on wood */ + UPROPERTY(EditDefaultsOnly, Category=Visual) + UParticleSystem* WoodFX; + + /** impact FX on glass */ + UPROPERTY(EditDefaultsOnly, Category=Visual) + UParticleSystem* GlassFX; + + /** impact FX on grass */ + UPROPERTY(EditDefaultsOnly, Category=Visual) + UParticleSystem* GrassFX; + + /** impact FX on flesh */ + UPROPERTY(EditDefaultsOnly, Category=Visual) + UParticleSystem* FleshFX; + + /** default impact sound used when material specific override doesn't exist */ + UPROPERTY(EditDefaultsOnly, Category=Defaults) + USoundCue* DefaultSound; + + /** impact FX on concrete */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* ConcreteSound; + + /** impact FX on dirt */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* DirtSound; + + /** impact FX on water */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* WaterSound; + + /** impact FX on metal */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* MetalSound; + + /** impact FX on wood */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* WoodSound; + + /** impact FX on glass */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* GlassSound; + + /** impact FX on grass */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* GrassSound; + + /** impact FX on flesh */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* FleshSound; + + /** default decal when material specific override doesn't exist */ + UPROPERTY(EditDefaultsOnly, Category=Defaults) + struct FDecalData DefaultDecal; + + /** surface data for spawning */ + UPROPERTY(BlueprintReadOnly, Category=Surface) + FHitResult SurfaceHit; + + /** spawn effect */ + virtual void PostInitializeComponents() override; + +protected: + + /** get FX for material type */ + UParticleSystem* GetImpactFX(TEnumAsByte SurfaceType) const; + + /** get sound for material type */ + USoundCue* GetImpactSound(TEnumAsByte SurfaceType) const; +}; diff --git a/Source/ShooterGame/Public/Online/ShooterGameMode.h b/Source/ShooterGame/Public/Online/ShooterGameMode.h new file mode 100644 index 0000000..475f0fe --- /dev/null +++ b/Source/ShooterGame/Public/Online/ShooterGameMode.h @@ -0,0 +1,183 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "OnlineIdentityInterface.h" +#include "ShooterPlayerController.h" +#include "A2S/A2SServer.h" +#include "A2S/A2SServerSettings.h" +#include "Discoverability/Discoverability.h" +#include "Discoverability/DiscoverabilitySettings.h" +#include "ShooterGameMode.generated.h" + +class AShooterAIController; +class AShooterPlayerState; +class AShooterPickup; +class FUniqueNetId; + +UCLASS(config=Game) +class AShooterGameMode : public AGameMode +{ + GENERATED_UCLASS_BODY() + + /** The bot pawn class */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=GameMode) + TSubclassOf BotPawnClass; + + UFUNCTION(exec) + void SetAllowBots(bool bInAllowBots, int32 InMaxBots = 8); + + virtual void PreInitializeComponents() override; + + /** Initialize the game. This is called before actors' PreInitializeComponents. */ + virtual void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override; + + /** Accept or reject a player attempting to join the server. Fails login if you set the ErrorMessage to a non-empty string. */ + virtual void PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) override; + + /** starts match warmup */ + virtual void PostLogin(APlayerController* NewPlayer) override; + + /** Tries to spawn the player's pawn */ + virtual void RestartPlayer(AController* NewPlayer) override; + + /** select best spawn point for player */ + virtual AActor* ChoosePlayerStart_Implementation(AController* Player) override; + + /** always pick new random spawn */ + virtual bool ShouldSpawnAtStartSpot(AController* Player) override; + + /** returns default pawn class for given controller */ + virtual UClass* GetDefaultPawnClassForController_Implementation(AController* InController) override; + + /** prevents friendly fire */ + virtual float ModifyDamage(float Damage, AActor* DamagedActor, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) const; + + /** notify about kills */ + virtual void Killed(AController* Killer, AController* KilledPlayer, APawn* KilledPawn, const UDamageType* DamageType); + + /** can players damage each other? */ + virtual bool CanDealDamage(AShooterPlayerState* DamageInstigator, AShooterPlayerState* DamagedPlayer) const; + + /** always create cheat manager */ + virtual bool AllowCheats(APlayerController* P) override; + + /** update remaining time */ + virtual void DefaultTimer(); + + /** called before startmatch */ + virtual void HandleMatchIsWaitingToStart() override; + + /** starts new match */ + virtual void HandleMatchHasStarted() override; + + /** new player joins */ + virtual void HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) override; + + /** hides the onscreen hud and restarts the map */ + virtual void RestartGame() override; + + /** Creates AIControllers for all bots */ + void CreateBotControllers(); + + /** Create a bot */ + AShooterAIController* CreateBot(int32 BotNum); + + virtual void PostInitProperties() override; + + virtual void BeginPlay() override; + + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + +protected: + + /** delay between first player login and starting match */ + UPROPERTY(config) + int32 WarmupTime; + + /** match duration */ + UPROPERTY(config) + int32 RoundTime; + + UPROPERTY(config) + int32 TimeBetweenMatches; + + /** score for kill */ + UPROPERTY(config) + int32 KillScore; + + /** score for death */ + UPROPERTY(config) + int32 DeathScore; + + /** scale for self instigated damage */ + UPROPERTY(config) + float DamageSelfScale; + + UPROPERTY(config) + int32 MaxBots; + + UPROPERTY() + TArray BotControllers; + + UPROPERTY(config) + TSubclassOf PlatformPlayerControllerClass; + + /** Handle for efficient management of DefaultTimer timer */ + FTimerHandle TimerHandle_DefaultTimer; + + bool bNeedsBotCreation; + + bool bAllowBots; + + UA2SServer* A2SServer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FA2SServerSettings A2SSettings; + + UDiscoverability* Discoverability; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FDiscoverabilitySettings DiscoverabilitySettings; + + /** spawning all bots for this game */ + void StartBots(); + + /** initialization for bot after creation */ + virtual void InitBot(AShooterAIController* AIC, int32 BotNum); + + /** check who won */ + virtual void DetermineMatchWinner(); + + /** check if PlayerState is a winner */ + virtual bool IsWinner(AShooterPlayerState* PlayerState) const; + + /** check if player can use spawnpoint */ + virtual bool IsSpawnpointAllowed(APlayerStart* SpawnPoint, AController* Player) const; + + /** check if player should use spawnpoint */ + virtual bool IsSpawnpointPreferred(APlayerStart* SpawnPoint, AController* Player) const; + + /** Returns game session class to use */ + virtual TSubclassOf GetGameSessionClass() const override; + + /** Tracks if a game has joined the server at least once */ + bool bHasPlayerConnected = false; + +public: + + /** finish current match and lock players */ + UFUNCTION(exec) + void FinishMatch(); + + /*Finishes the match and bumps everyone to main menu.*/ + /*Only GameInstance should call this function */ + void RequestFinishAndExitToMainMenu(); + + /** get the name of the bots count option used in server travel URL */ + static FString GetBotsCountOptionName(); + + UPROPERTY() + TArray LevelPickups; + +}; diff --git a/Source/ShooterGame/Public/Online/ShooterGameSession.h b/Source/ShooterGame/Public/Online/ShooterGameSession.h new file mode 100644 index 0000000..d189ce9 --- /dev/null +++ b/Source/ShooterGame/Public/Online/ShooterGameSession.h @@ -0,0 +1,264 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Online.h" +#include "ShooterLeaderboards.h" +#include "ShooterGameSession.generated.h" + +struct FShooterGameSessionParams +{ + /** Name of session settings are stored with */ + FName SessionName; + /** LAN Match */ + bool bIsLAN; + /** Presence enabled session */ + bool bIsPresence; + /** Id of player initiating lobby */ + TSharedPtr UserId; + /** Current search result choice to join */ + int32 BestSessionIdx; + + FShooterGameSessionParams() + : SessionName(NAME_None) + , bIsLAN(false) + , bIsPresence(false) + , BestSessionIdx(0) + { + } +}; + +UCLASS(config=Game) +class SHOOTERGAME_API AShooterGameSession : public AGameSession +{ + GENERATED_UCLASS_BODY() + +protected: + + /** Delegate for creating a new session */ + FOnCreateSessionCompleteDelegate OnCreateSessionCompleteDelegate; + /** Delegate after starting a session */ + FOnStartSessionCompleteDelegate OnStartSessionCompleteDelegate; + /** Delegate for destroying a session */ + FOnDestroySessionCompleteDelegate OnDestroySessionCompleteDelegate; + /** Delegate for searching for sessions */ + FOnFindSessionsCompleteDelegate OnFindSessionsCompleteDelegate; + /** Delegate after joining a session */ + FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate; + + /** Transient properties of a session during game creation/matchmaking */ + FShooterGameSessionParams CurrentSessionParams; + /** Current host settings */ + TSharedPtr HostSettings; + /** Current search settings */ + TSharedPtr SearchSettings; + + /** + * Delegate fired when a session create request has completed + * + * @param SessionName the name of the session this callback is for + * @param bWasSuccessful true if the async action completed without error, false if there was an error + */ + virtual void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful); + + /** + * Delegate fired when a session start request has completed + * + * @param SessionName the name of the session this callback is for + * @param bWasSuccessful true if the async action completed without error, false if there was an error + */ + void OnStartOnlineGameComplete(FName SessionName, bool bWasSuccessful); + + /** + * Delegate fired when a session search query has completed + * + * @param bWasSuccessful true if the async action completed without error, false if there was an error + */ + void OnFindSessionsComplete(bool bWasSuccessful); + + /** + * Delegate fired when a session join request has completed + * + * @param SessionName the name of the session this callback is for + * @param bWasSuccessful true if the async action completed without error, false if there was an error + */ + void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); + + /** + * Delegate fired when a destroying an online session has completed + * + * @param SessionName the name of the session this callback is for + * @param bWasSuccessful true if the async action completed without error, false if there was an error + */ + virtual void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful); + + /** + * Reset the variables the are keeping track of session join attempts + */ + void ResetBestSessionVars(); + + /** + * Choose the best session from a list of search results based on game criteria + */ + void ChooseBestSession(); + + /** + * Entry point for matchmaking after search results are returned + */ + void StartMatchmaking(); + + /** + * Return point after each attempt to join a search result + */ + void ContinueMatchmaking(); + + /** + * Delegate triggered when no more search results are available + */ + void OnNoMatchesAvailable(); + + + /** + * Called when this instance is starting up as a dedicated server + */ + virtual void RegisterServer() override; + + /* + * Event triggered when a presence session is created + * + * @param SessionName name of session that was created + * @param bWasSuccessful was the create successful + */ + DECLARE_EVENT_TwoParams(AShooterGameSession, FOnCreatePresenceSessionComplete, FName /*SessionName*/, bool /*bWasSuccessful*/); + FOnCreatePresenceSessionComplete CreatePresenceSessionCompleteEvent; + + /* + * Event triggered when a session is joined + * + * @param SessionName name of session that was joined + * @param bWasSuccessful was the create successful + */ + //DECLARE_DELEGATE_RetVal_TwoParams(bool, FOnJoinSessionComplete, FName /*SessionName*/, bool /*bWasSuccessful*/); + DECLARE_EVENT_OneParam(AShooterGameSession, FOnJoinSessionComplete, EOnJoinSessionCompleteResult::Type /*Result*/); + FOnJoinSessionComplete JoinSessionCompleteEvent; + + /* + * Event triggered after session search completes + */ + //DECLARE_EVENT(AShooterGameSession, FOnFindSessionsComplete); + DECLARE_EVENT_OneParam(AShooterGameSession, FOnFindSessionsComplete, bool /*bWasSuccessful*/); + FOnFindSessionsComplete FindSessionsCompleteEvent; + +public: + + /** Default number of players allowed in a game */ + static const int32 DEFAULT_NUM_PLAYERS = 8; + + /** + * Host a new online session + * + * @param UserId user that initiated the request + * @param SessionName name of session + * @param bIsLAN is this going to hosted over LAN + * @param bIsPresence is the session to create a presence session + * @param MaxNumPlayers Maximum number of players to allow in the session + * + * @return bool true if successful, false otherwise + */ + bool HostSession(TSharedPtr UserId, FName SessionName, const FString& GameType, const FString& MapName, bool bIsLAN, bool bIsPresence, int32 MaxNumPlayers); + + /** + * Host a new online session with specified settings + * + * @param UserId user that initiated the request + * @param SessionName name of session + * @param SessionSettings settings to create session with + * + * @return bool true if successful, false otherwise + */ + bool HostSession(const TSharedPtr UserId, const FName SessionName, const FOnlineSessionSettings& SessionSettings); + + /** + * Find an online session + * + * @param UserId user that initiated the request + * @param SessionName name of session this search will generate + * @param bIsLAN are we searching LAN matches + * @param bIsPresence are we searching presence sessions + */ + void FindSessions(TSharedPtr UserId, FName SessionName, bool bIsLAN, bool bIsPresence); + + /** + * Joins one of the session in search results + * + * @param UserId user that initiated the request + * @param SessionName name of session + * @param SessionIndexInSearchResults Index of the session in search results + * + * @return bool true if successful, false otherwise + */ + bool JoinSession(TSharedPtr UserId, FName SessionName, int32 SessionIndexInSearchResults); + + /** + * Joins a session via a search result + * + * @param SessionName name of session + * @param SearchResult Session to join + * + * @return bool true if successful, false otherwise + */ + bool JoinSession(TSharedPtr UserId, FName SessionName, const FOnlineSessionSearchResult& SearchResult); + + /** @return true if any online async work is in progress, false otherwise */ + bool IsBusy() const; + + /** + * Get the search results found and the current search result being probed + * + * @param SearchResultIdx idx of current search result accessed + * @param NumSearchResults number of total search results found in FindGame() + * + * @return State of search result query + */ + EOnlineAsyncTaskState::Type GetSearchResultStatus(int32& SearchResultIdx, int32& NumSearchResults); + + /** + * Get the search results. + * + * @return Search results + */ + const TArray & GetSearchResults() const; + + /** @return the delegate fired when creating a presence session */ + FOnCreatePresenceSessionComplete& OnCreatePresenceSessionComplete() { return CreatePresenceSessionCompleteEvent; } + + /** @return the delegate fired when joining a session */ + FOnJoinSessionComplete& OnJoinSessionComplete() { return JoinSessionCompleteEvent; } + + /** @return the delegate fired when search of session completes */ + FOnFindSessionsComplete& OnFindSessionsComplete() { return FindSessionsCompleteEvent; } + + /** Handle starting the match */ + virtual void HandleMatchHasStarted() override; + + /** Handles when the match has ended */ + virtual void HandleMatchHasEnded() override; + + /** + * Travel to a session URL (as client) for a given session + * + * @param ControllerId controller initiating the session travel + * @param SessionName name of session to travel to + * + * @return true if successful, false otherwise + */ + bool TravelToSession(int32 ControllerId, FName SessionName); + + /** Handles to various registered delegates */ + FDelegateHandle OnStartSessionCompleteDelegateHandle; + FDelegateHandle OnCreateSessionCompleteDelegateHandle; + FDelegateHandle OnDestroySessionCompleteDelegateHandle; + FDelegateHandle OnFindSessionsCompleteDelegateHandle; + FDelegateHandle OnJoinSessionCompleteDelegateHandle; +}; + diff --git a/Source/ShooterGame/Public/Online/ShooterGameState.h b/Source/ShooterGame/Public/Online/ShooterGameState.h new file mode 100644 index 0000000..5e81aa3 --- /dev/null +++ b/Source/ShooterGame/Public/Online/ShooterGameState.h @@ -0,0 +1,50 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterOnlineGameMatches.h" +#include "ShooterGameState.generated.h" + +/** ranked PlayerState map, created from the GameState */ +typedef TMap > RankedPlayerMap; + +UCLASS() +class AShooterGameState : public AGameState +{ + GENERATED_UCLASS_BODY() + +public: + + /** number of teams in current game (doesn't deprecate when no players are left in a team) */ + UPROPERTY(Transient, Replicated) + int32 NumTeams; + + /** accumulated score per team */ + UPROPERTY(Transient, Replicated) + TArray TeamScores; + + /** time left for warmup / match */ + UPROPERTY(Transient, Replicated) + int32 RemainingTime; + + /** is timer paused? */ + UPROPERTY(Transient, Replicated) + bool bTimerPaused; + + /** gets ranked PlayerState map for specific team */ + void GetRankedMap(int32 TeamIndex, RankedPlayerMap& OutRankedMap) const; + + void RequestFinishAndExitToMainMenu(); + + virtual void HandleMatchHasStarted() override; + virtual void HandleMatchHasEnded() override; + +protected: + UPROPERTY(config) + FString ActivityId; + + UPROPERTY(config) + bool bEnableGameFeedback; + + FShooterOnlineGameMatches GameMatches; +}; diff --git a/Source/ShooterGame/Public/Online/ShooterGame_FreeForAll.h b/Source/ShooterGame/Public/Online/ShooterGame_FreeForAll.h new file mode 100644 index 0000000..4939899 --- /dev/null +++ b/Source/ShooterGame/Public/Online/ShooterGame_FreeForAll.h @@ -0,0 +1,25 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterGame_FreeForAll.generated.h" + +class AShooterPlayerState; + +UCLASS() +class AShooterGame_FreeForAll : public AShooterGameMode +{ + GENERATED_UCLASS_BODY() + +protected: + + /** best player */ + UPROPERTY(transient) + AShooterPlayerState* WinnerPlayerState; + + /** check who won */ + virtual void DetermineMatchWinner() override; + + /** check if PlayerState is a winner */ + virtual bool IsWinner(AShooterPlayerState* PlayerState) const override; +}; diff --git a/Source/ShooterGame/Public/Online/ShooterGame_Menu.h b/Source/ShooterGame/Public/Online/ShooterGame_Menu.h new file mode 100644 index 0000000..f1cac4e --- /dev/null +++ b/Source/ShooterGame/Public/Online/ShooterGame_Menu.h @@ -0,0 +1,29 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterGame_Menu.generated.h" + +UCLASS() +class AShooterGame_Menu : public AGameModeBase +{ + GENERATED_UCLASS_BODY() + +public: + + // Begin AGameModeBase interface + /** skip it, menu doesn't require player start or pawn */ + virtual void RestartPlayer(class AController* NewPlayer) override; + + /** Returns game session class to use */ + virtual TSubclassOf GetGameSessionClass() const override; + // End AGameModeBase interface + +protected: + + /** Perform some final tasks before hosting/joining a session. Remove menus, set king state etc */ + void BeginSession(); + + /** Display a loading screen */ + void ShowLoadingScreen(); +}; diff --git a/Source/ShooterGame/Public/Online/ShooterGame_TeamDeathMatch.h b/Source/ShooterGame/Public/Online/ShooterGame_TeamDeathMatch.h new file mode 100644 index 0000000..5f8a759 --- /dev/null +++ b/Source/ShooterGame/Public/Online/ShooterGame_TeamDeathMatch.h @@ -0,0 +1,46 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterGame_TeamDeathMatch.generated.h" + +class AShooterPlayerState; +class AShooterAIController; + +UCLASS() +class AShooterGame_TeamDeathMatch : public AShooterGameMode +{ + GENERATED_UCLASS_BODY() + + /** setup team changes at player login */ + void PostLogin(APlayerController* NewPlayer) override; + + /** initialize replicated game data */ + virtual void InitGameState() override; + + /** can players damage each other? */ + virtual bool CanDealDamage(AShooterPlayerState* DamageInstigator, AShooterPlayerState* DamagedPlayer) const override; + +protected: + + /** number of teams */ + int32 NumTeams; + + /** best team */ + int32 WinnerTeam; + + /** pick team with least players in or random when it's equal */ + int32 ChooseTeam(AShooterPlayerState* ForPlayerState) const; + + /** check who won */ + virtual void DetermineMatchWinner() override; + + /** check if PlayerState is a winner */ + virtual bool IsWinner(AShooterPlayerState* PlayerState) const override; + + /** check team constraints */ + virtual bool IsSpawnpointAllowed(APlayerStart* SpawnPoint, AController* Player) const; + + /** initialization for bot after spawning */ + virtual void InitBot(AShooterAIController* AIC, int32 BotNum) override; +}; diff --git a/Source/ShooterGame/Public/Online/ShooterOnlineGameMatches.h b/Source/ShooterGame/Public/Online/ShooterOnlineGameMatches.h new file mode 100644 index 0000000..44ba108 --- /dev/null +++ b/Source/ShooterGame/Public/Online/ShooterOnlineGameMatches.h @@ -0,0 +1,62 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once +#include "OnlineGameMatchesInterface.h" +#include "ShooterGameState.h" +#include "ShooterGameInstance.h" + +class AShooterGameState; + +class SHOOTERGAME_API FShooterOnlineGameMatches +{ +public: + FShooterOnlineGameMatches() + { + } + + ~FShooterOnlineGameMatches() + { + } + void Initialize(AShooterGameState* InGameState, UShooterGameInstance* InGameInstance); + + void CreateMatch(const FUniqueNetId& LocalOwnerId, const FString& ActivitId, const TArray& Players, const TArray& Teams); + void StartMatch(const FUniqueNetId& LocalUserId, const FString& MatchId); + void EndMatch(const FUniqueNetId& LocalOwnerId, const FString& MatchId, const FFinalGameMatchReport& FinalReport); + void LeaveGameMatchFeedback(const FUniqueNetId& LocalUserId, const FString& MatchId, const bool bRequestReview); + + void HandleMatchHasStarted(const FString& ActivityId, int32& NumTeams); + void HandleMatchHasEnded(const bool bEnableGameFeedback, const int32 NumTeams, TArrayView TeamScores); + +protected: + TSharedPtr GetMatchOwnerId(); + bool GetMatchOwnerAndId(TSharedPtr& LocalOwnerId, FString& MatchId); + bool GetMatchId(FString& MatchId); + + void BuildTeamGameMatchResults(const TArrayView TeamScores, const int32& NumTeams, TArray& TeamResults); + void BuildPlayerGameMatchResults(const TSharedRef PlayerNetId, const int32 TeamId, FGameMatchPlayerResult PlayerResult); + void BuildTeamPlayerGameMatchStats(const TSharedRef PlayerNetId, const int32 TeamId, const FString& StatKey, const FString& StatValue); + void BuildTeamGameMatchStats(TArray& TeamStats); + +protected: + void OnCreateMatchComplete(const FUniqueNetId& LocalUserId, const FString& MatchId, const FOnlineError& Result); + void OnEndMatchComplete(const FUniqueNetId& LocalUserId, const FOnlineError& Result, FString MatchId, bool bRequestReview); + void OnMatchStatusUpdateComplete(const FUniqueNetId& LocalUserId, const EUpdateGameMatchStatus& Status, const FOnlineError& Result); + void OnLeaveGameMatchFeedbackComplete(const FUniqueNetId& LocalUserId, const FOnlineError& Result); + + bool IsClientRepresentative(const TSharedPtr& RepresentativeId); + bool GetNamedSessionInfo(FNamedOnlineSession*& NamedSession); + +protected: + // TeamID, Players result. + TMap> TeamPlayerResultsMap; + // Map for creating stats for teams + TMap> TeamToPlayerStatsMap; + + // Player to index mapping for PlayerNetIds + TMap PlayerIdToNetIdIndexMap; + // Holds player's net ids + TArray> PlayerNetIds; + + AShooterGameState* GameState = nullptr; + UShooterGameInstance* GameInstance = nullptr; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Online/ShooterOnlineSessionClient.h b/Source/ShooterGame/Public/Online/ShooterOnlineSessionClient.h new file mode 100644 index 0000000..e2c6082 --- /dev/null +++ b/Source/ShooterGame/Public/Online/ShooterOnlineSessionClient.h @@ -0,0 +1,24 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "OnlineSessionClient.h" +#include "ShooterOnlineSessionClient.generated.h" + +UCLASS(Config = Game) +class UShooterOnlineSessionClient : public UOnlineSessionClient +{ + GENERATED_BODY() + +public: + /** Ctor */ + UShooterOnlineSessionClient(); + + virtual void OnSessionUserInviteAccepted( + const bool bWasSuccess, + const int32 ControllerId, + TSharedPtr< const FUniqueNetId > UserId, + const FOnlineSessionSearchResult & InviteResult + ) override; + +}; diff --git a/Source/ShooterGame/Public/Online/ShooterPlayerState.h b/Source/ShooterGame/Public/Online/ShooterPlayerState.h new file mode 100644 index 0000000..7cea1a2 --- /dev/null +++ b/Source/ShooterGame/Public/Online/ShooterPlayerState.h @@ -0,0 +1,124 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterPlayerState.generated.h" + +UCLASS() +class AShooterPlayerState : public APlayerState +{ + GENERATED_UCLASS_BODY() + + + // Begin APlayerState interface + /** clear scores */ + virtual void Reset() override; + + /** + * Set the team + * + * @param InController The controller to initialize state with + */ + virtual void ClientInitialize(class AController* InController) override; + + virtual void RegisterPlayerWithSession(bool bWasFromInvite) override; + virtual void UnregisterPlayerWithSession() override; + + // End APlayerState interface + + /** + * Set new team and update pawn. Also updates player character team colors. + * + * @param NewTeamNumber Team we want to be on. + */ + void SetTeamNum(int32 NewTeamNumber); + + /** player killed someone */ + void ScoreKill(AShooterPlayerState* Victim, int32 Points); + + /** player died */ + void ScoreDeath(AShooterPlayerState* KilledBy, int32 Points); + + /** get current team */ + int32 GetTeamNum() const; + + /** get number of kills */ + int32 GetKills() const; + + /** get number of deaths */ + int32 GetDeaths() const; + + /** get number of bullets fired this match */ + int32 GetNumBulletsFired() const; + + /** get number of rockets fired this match */ + int32 GetNumRocketsFired() const; + + /** get whether the player quit the match */ + bool IsQuitter() const; + + /** get match id that the player is in */ + FString GetMatchId() const; + + /** gets truncated player name to fit in death log and scoreboards */ + FString GetShortPlayerName() const; + + /** Sends kill (excluding self) to clients */ + UFUNCTION(Reliable, Client) + void InformAboutKill(class AShooterPlayerState* KillerPlayerState, const UDamageType* KillerDamageType, class AShooterPlayerState* KilledPlayerState); + + /** broadcast death to local clients */ + UFUNCTION(Reliable, NetMulticast) + void BroadcastDeath(class AShooterPlayerState* KillerPlayerState, const UDamageType* KillerDamageType, class AShooterPlayerState* KilledPlayerState); + + /** replicate team colors. Updated the players mesh colors appropriately */ + UFUNCTION() + void OnRep_TeamColor(); + + //We don't need stats about amount of ammo fired to be server authenticated, so just increment these with local functions + void AddBulletsFired(int32 NumBullets); + void AddRocketsFired(int32 NumRockets); + + /** Set whether the player is a quitter */ + void SetQuitter(bool bInQuitter); + + /** Set the player's current match id */ + void SetMatchId(const FString& CurrentMatchId); + + virtual void CopyProperties(class APlayerState* PlayerState) override; +protected: + + /** Set the mesh colors based on the current teamnum variable */ + void UpdateTeamColors(); + + /** team number */ + UPROPERTY(Transient, ReplicatedUsing=OnRep_TeamColor) + int32 TeamNumber; + + /** number of kills */ + UPROPERTY(Transient, Replicated) + int32 NumKills; + + /** number of deaths */ + UPROPERTY(Transient, Replicated) + int32 NumDeaths; + + /** number of bullets fired this match */ + UPROPERTY() + int32 NumBulletsFired; + + /** number of rockets fired this match */ + UPROPERTY() + int32 NumRocketsFired; + + /** whether the user quit the match */ + UPROPERTY() + uint8 bQuitter : 1; + + /** Match id */ + UPROPERTY(Replicated) + FString MatchId; + + /** helper for scoring points */ + void ScorePoints(int32 Points); +}; diff --git a/Source/ShooterGame/Public/Pickups/ShooterPickup.h b/Source/ShooterGame/Public/Pickups/ShooterPickup.h new file mode 100644 index 0000000..65ad7f7 --- /dev/null +++ b/Source/ShooterGame/Public/Pickups/ShooterPickup.h @@ -0,0 +1,89 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterPickup.generated.h" + +// Base class for pickup objects that can be placed in the world +UCLASS(abstract) +class AShooterPickup : public AActor +{ + GENERATED_UCLASS_BODY() + + /** pickup on touch */ + virtual void NotifyActorBeginOverlap(class AActor* Other) override; + + /** check if pawn can use this pickup */ + virtual bool CanBePickedUp(class AShooterCharacter* TestPawn) const; + +protected: + /** initial setup */ + virtual void BeginPlay() override; + +private: + /** FX component */ + UPROPERTY(VisibleDefaultsOnly, Category=Effects) + UParticleSystemComponent* PickupPSC; + +protected: + /** FX of active pickup */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + UParticleSystem* ActiveFX; + + /** FX of pickup on respawn timer */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + UParticleSystem* RespawningFX; + + /** sound played when player picks it up */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + USoundCue* PickupSound; + + /** sound played on respawn */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + USoundCue* RespawnSound; + + /** how long it takes to respawn? */ + UPROPERTY(EditDefaultsOnly, Category=Pickup) + float RespawnTime; + + /** is it ready for interactions? */ + UPROPERTY(Transient, ReplicatedUsing=OnRep_IsActive) + uint32 bIsActive:1; + + /* The character who has picked up this pickup */ + UPROPERTY(Transient, Replicated) + AShooterCharacter* PickedUpBy; + + /** Handle for efficient management of RespawnPickup timer */ + FTimerHandle TimerHandle_RespawnPickup; + + UFUNCTION() + void OnRep_IsActive(); + + /** give pickup */ + virtual void GivePickupTo(class AShooterCharacter* Pawn); + + /** handle touches */ + void PickupOnTouch(class AShooterCharacter* Pawn); + + /** show and enable pickup */ + virtual void RespawnPickup(); + + /** show effects when pickup disappears */ + virtual void OnPickedUp(); + + /** show effects when pickup appears */ + virtual void OnRespawned(); + + /** blueprint event: pickup disappears */ + UFUNCTION(BlueprintImplementableEvent) + void OnPickedUpEvent(); + + /** blueprint event: pickup appears */ + UFUNCTION(BlueprintImplementableEvent) + void OnRespawnEvent(); + +protected: + /** Returns PickupPSC subobject **/ + FORCEINLINE UParticleSystemComponent* GetPickupPSC() const { return PickupPSC; } +}; diff --git a/Source/ShooterGame/Public/Pickups/ShooterPickup_Ammo.h b/Source/ShooterGame/Public/Pickups/ShooterPickup_Ammo.h new file mode 100644 index 0000000..ea5c19e --- /dev/null +++ b/Source/ShooterGame/Public/Pickups/ShooterPickup_Ammo.h @@ -0,0 +1,34 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterPickup.h" +#include "ShooterPickup_Ammo.generated.h" + +class AShooterCharacter; +class AShooterWeapon; + +// A pickup object that replenishes ammunition for a weapon +UCLASS(Abstract, Blueprintable) +class AShooterPickup_Ammo : public AShooterPickup +{ + GENERATED_UCLASS_BODY() + + /** check if pawn can use this pickup */ + virtual bool CanBePickedUp(AShooterCharacter* TestPawn) const override; + + bool IsForWeapon(UClass* WeaponClass); + +protected: + + /** how much ammo does it give? */ + UPROPERTY(EditDefaultsOnly, Category=Pickup) + int32 AmmoClips; + + /** which weapon gets ammo? */ + UPROPERTY(EditDefaultsOnly, Category=Pickup) + TSubclassOf WeaponType; + + /** give pickup */ + virtual void GivePickupTo(AShooterCharacter* Pawn) override; +}; diff --git a/Source/ShooterGame/Public/Pickups/ShooterPickup_Health.h b/Source/ShooterGame/Public/Pickups/ShooterPickup_Health.h new file mode 100644 index 0000000..6686f96 --- /dev/null +++ b/Source/ShooterGame/Public/Pickups/ShooterPickup_Health.h @@ -0,0 +1,27 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterPickup.h" +#include "ShooterPickup_Health.generated.h" + +class AShooterCharacter; + +// A pickup object that replenishes character health +UCLASS(Abstract, Blueprintable) +class AShooterPickup_Health : public AShooterPickup +{ + GENERATED_UCLASS_BODY() + + /** check if pawn can use this pickup */ + virtual bool CanBePickedUp(AShooterCharacter* TestPawn) const override; + +protected: + + /** how much health does it give? */ + UPROPERTY(EditDefaultsOnly, Category=Pickup) + int32 Health; + + /** give pickup */ + virtual void GivePickupTo(AShooterCharacter* Pawn) override; +}; diff --git a/Source/ShooterGame/Public/Player/ShooterCharacter.h b/Source/ShooterGame/Public/Player/ShooterCharacter.h new file mode 100644 index 0000000..c15409c --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterCharacter.h @@ -0,0 +1,490 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterTypes.h" +#include "ShooterCharacter.generated.h" + +DECLARE_MULTICAST_DELEGATE_TwoParams(FOnShooterCharacterEquipWeapon, AShooterCharacter*, AShooterWeapon* /* new */); +DECLARE_MULTICAST_DELEGATE_TwoParams(FOnShooterCharacterUnEquipWeapon, AShooterCharacter*, AShooterWeapon* /* old */); + +UCLASS(Abstract) +class AShooterCharacter : public ACharacter +{ + GENERATED_UCLASS_BODY() + + virtual void BeginDestroy() override; + + /** spawn inventory, setup initial variables */ + virtual void PostInitializeComponents() override; + + /** Update the character. (Running, health etc). */ + virtual void Tick(float DeltaSeconds) override; + + /** cleanup inventory */ + virtual void Destroyed() override; + + /** update mesh for first person view */ + virtual void PawnClientRestart() override; + + /** [server] perform PlayerState related setup */ + virtual void PossessedBy(class AController* C) override; + + /** [client] perform PlayerState related setup */ + virtual void OnRep_PlayerState() override; + + /** [server] called to determine if we should pause replication this actor to a specific player */ + virtual bool IsReplicationPausedForConnection(const FNetViewer& ConnectionOwnerNetViewer) override; + + /** [client] called when replication is paused for this actor */ + virtual void OnReplicationPausedChanged(bool bIsReplicationPaused) override; + + /** + * Add camera pitch to first person mesh. + * + * @param CameraLocation Location of the Camera. + * @param CameraRotation Rotation of the Camera. + */ + void OnCameraUpdate(const FVector& CameraLocation, const FRotator& CameraRotation); + + /** get aim offsets */ + UFUNCTION(BlueprintCallable, Category = "Game|Weapon") + FRotator GetAimOffsets() const; + + /** + * Check if pawn is enemy if given controller. + * + * @param TestPC Controller to check against. + */ + bool IsEnemyFor(AController* TestPC) const; + + ////////////////////////////////////////////////////////////////////////// + // Inventory + + /** + * [server] add weapon to inventory + * + * @param Weapon Weapon to add. + */ + void AddWeapon(class AShooterWeapon* Weapon); + + /** + * [server] remove weapon from inventory + * + * @param Weapon Weapon to remove. + */ + void RemoveWeapon(class AShooterWeapon* Weapon); + + /** + * Find in inventory + * + * @param WeaponClass Class of weapon to find. + */ + class AShooterWeapon* FindWeapon(TSubclassOf WeaponClass); + + /** + * [server + local] equips weapon from inventory + * + * @param Weapon Weapon to equip + */ + void EquipWeapon(class AShooterWeapon* Weapon); + + ////////////////////////////////////////////////////////////////////////// + // Weapon usage + + /** [local] starts weapon fire */ + void StartWeaponFire(); + + /** [local] stops weapon fire */ + void StopWeaponFire(); + + /** check if pawn can fire weapon */ + bool CanFire() const; + + /** check if pawn can reload weapon */ + bool CanReload() const; + + /** [server + local] change targeting state */ + void SetTargeting(bool bNewTargeting); + + ////////////////////////////////////////////////////////////////////////// + // Movement + + /** [server + local] change running state */ + void SetRunning(bool bNewRunning, bool bToggle); + + ////////////////////////////////////////////////////////////////////////// + // Animations + + /** play anim montage */ + virtual float PlayAnimMontage(class UAnimMontage* AnimMontage, float InPlayRate = 1.f, FName StartSectionName = NAME_None) override; + + /** stop playing montage */ + virtual void StopAnimMontage(class UAnimMontage* AnimMontage) override; + + /** stop playing all montages */ + void StopAllAnimMontages(); + + ////////////////////////////////////////////////////////////////////////// + // Input handlers + + /** setup pawn specific input handlers */ + virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override; + + /** + * Handle analog trigger for firing + * + * @param Val trigger input to apply + */ + void FireTrigger(float Val); + + /** + * Move forward/back + * + * @param Val Movment input to apply + */ + void MoveForward(float Val); + + /** + * Strafe right/left + * + * @param Val Movment input to apply + */ + void MoveRight(float Val); + + /** + * Move Up/Down in allowed movement modes. + * + * @param Val Movment input to apply + */ + void MoveUp(float Val); + + /* Frame rate independent turn */ + void TurnAtRate(float Val); + + /* Frame rate independent lookup */ + void LookUpAtRate(float Val); + + /** player pressed start fire action */ + void OnStartFire(); + + /** player released start fire action */ + void OnStopFire(); + + /** player pressed targeting action */ + void OnStartTargeting(); + + /** player released targeting action */ + void OnStopTargeting(); + + /** player pressed next weapon action */ + void OnNextWeapon(); + + /** player pressed prev weapon action */ + void OnPrevWeapon(); + + /** player pressed reload action */ + void OnReload(); + + /** player pressed jump action */ + void OnStartJump(); + + /** player released jump action */ + void OnStopJump(); + + /** player pressed run action */ + void OnStartRunning(); + + /** player pressed toggled run action */ + void OnStartRunningToggle(); + + /** player released run action */ + void OnStopRunning(); + + ////////////////////////////////////////////////////////////////////////// + // Reading data + + /** get mesh component */ + USkeletalMeshComponent* GetPawnMesh() const; + + /** get currently equipped weapon */ + UFUNCTION(BlueprintCallable, Category = "Game|Weapon") + class AShooterWeapon* GetWeapon() const; + + /** Global notification when a character equips a weapon. Needed for replication graph. */ + SHOOTERGAME_API static FOnShooterCharacterEquipWeapon NotifyEquipWeapon; + + /** Global notification when a character un-equips a weapon. Needed for replication graph. */ + SHOOTERGAME_API static FOnShooterCharacterUnEquipWeapon NotifyUnEquipWeapon; + + /** get weapon attach point */ + FName GetWeaponAttachPoint() const; + + /** get total number of inventory items */ + int32 GetInventoryCount() const; + + /** + * get weapon from inventory at index. Index validity is not checked. + * + * @param Index Inventory index + */ + class AShooterWeapon* GetInventoryWeapon(int32 index) const; + + /** get weapon taget modifier speed */ + UFUNCTION(BlueprintCallable, Category = "Game|Weapon") + float GetTargetingSpeedModifier() const; + + /** get targeting state */ + UFUNCTION(BlueprintCallable, Category = "Game|Weapon") + bool IsTargeting() const; + + /** get firing state */ + UFUNCTION(BlueprintCallable, Category = "Game|Weapon") + bool IsFiring() const; + + /** get the modifier value for running speed */ + UFUNCTION(BlueprintCallable, Category = Pawn) + float GetRunningSpeedModifier() const; + + /** get running state */ + UFUNCTION(BlueprintCallable, Category = Pawn) + bool IsRunning() const; + + /** get camera view type */ + UFUNCTION(BlueprintCallable, Category = Mesh) + virtual bool IsFirstPerson() const; + + /** get max health */ + int32 GetMaxHealth() const; + + /** check if pawn is still alive */ + bool IsAlive() const; + + /** returns percentage of health when low health effects should start */ + float GetLowHealthPercentage() const; + + /* + * Get either first or third person mesh. + * + * @param WantFirstPerson If true returns the first peron mesh, else returns the third + */ + USkeletalMeshComponent* GetSpecifcPawnMesh(bool WantFirstPerson) const; + + /** Update the team color of all player meshes. */ + void UpdateTeamColorsAllMIDs(); +private: + + /** pawn mesh: 1st person view */ + UPROPERTY(VisibleDefaultsOnly, Category = Mesh) + USkeletalMeshComponent* Mesh1P; +protected: + + /** socket or bone name for attaching weapon mesh */ + UPROPERTY(EditDefaultsOnly, Category = Inventory) + FName WeaponAttachPoint; + + /** default inventory list */ + UPROPERTY(EditDefaultsOnly, Category = Inventory) + TArray > DefaultInventoryClasses; + + /** weapons in inventory */ + UPROPERTY(Transient, Replicated) + TArray Inventory; + + /** currently equipped weapon */ + UPROPERTY(Transient, ReplicatedUsing = OnRep_CurrentWeapon) + class AShooterWeapon* CurrentWeapon; + + /** Replicate where this pawn was last hit and damaged */ + UPROPERTY(Transient, ReplicatedUsing = OnRep_LastTakeHitInfo) + struct FTakeHitInfo LastTakeHitInfo; + + /** Time at which point the last take hit info for the actor times out and won't be replicated; Used to stop join-in-progress effects all over the screen */ + float LastTakeHitTimeTimeout; + + /** modifier for max movement speed */ + UPROPERTY(EditDefaultsOnly, Category = Inventory) + float TargetingSpeedModifier; + + /** current targeting state */ + UPROPERTY(Transient, Replicated) + uint8 bIsTargeting : 1; + + /** modifier for max movement speed */ + UPROPERTY(EditDefaultsOnly, Category = Pawn) + float RunningSpeedModifier; + + /** current running state */ + UPROPERTY(Transient, Replicated) + uint8 bWantsToRun : 1; + + /** from gamepad running is toggled */ + uint8 bWantsToRunToggled : 1; + + /** current firing state */ + uint8 bWantsToFire : 1; + + /** when low health effects should start */ + float LowHealthPercentage; + + /** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */ + float BaseTurnRate; + + /** Base lookup rate, in deg/sec. Other scaling may affect final lookup rate. */ + float BaseLookUpRate; + + /** material instances for setting team color in mesh (3rd person view) */ + UPROPERTY(Transient) + TArray MeshMIDs; + + /** animation played on death */ + UPROPERTY(EditDefaultsOnly, Category = Animation) + UAnimMontage* DeathAnim; + + /** sound played on death, local player only */ + UPROPERTY(EditDefaultsOnly, Category = Pawn) + USoundCue* DeathSound; + + /** effect played on respawn */ + UPROPERTY(EditDefaultsOnly, Category = Pawn) + UParticleSystem* RespawnFX; + + /** sound played on respawn */ + UPROPERTY(EditDefaultsOnly, Category = Pawn) + USoundCue* RespawnSound; + + /** sound played when health is low */ + UPROPERTY(EditDefaultsOnly, Category = Pawn) + USoundCue* LowHealthSound; + + /** sound played when running */ + UPROPERTY(EditDefaultsOnly, Category = Pawn) + USoundCue* RunLoopSound; + + /** sound played when stop running */ + UPROPERTY(EditDefaultsOnly, Category = Pawn) + USoundCue* RunStopSound; + + /** sound played when targeting state changes */ + UPROPERTY(EditDefaultsOnly, Category = Pawn) + USoundCue* TargetingSound; + + /** used to manipulate with run loop sound */ + UPROPERTY() + UAudioComponent* RunLoopAC; + + /** hook to looped low health sound used to stop/adjust volume */ + UPROPERTY() + UAudioComponent* LowHealthWarningPlayer; + + /** handles sounds for running */ + void UpdateRunSounds(); + + /** handle mesh visibility and updates */ + void UpdatePawnMeshes(); + + /** handle mesh colors on specified material instance */ + void UpdateTeamColors(UMaterialInstanceDynamic* UseMID); + + /** Responsible for cleaning up bodies on clients. */ + virtual void TornOff(); + +private: + + /** Whether or not the character is moving (based on movement input). */ + bool IsMoving(); + + ////////////////////////////////////////////////////////////////////////// + // Damage & death + +public: + + /** Identifies if pawn is in its dying state */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Health) + uint32 bIsDying : 1; + + // Current health of the Pawn + UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = Health) + float Health; + + /** Take damage, handle death */ + virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, class AActor* DamageCauser) override; + + /** Pawn suicide */ + virtual void Suicide(); + + /** Kill this pawn */ + virtual void KilledBy(class APawn* EventInstigator); + + /** Returns True if the pawn can die in the current state */ + virtual bool CanDie(float KillingDamage, FDamageEvent const& DamageEvent, AController* Killer, AActor* DamageCauser) const; + + /** + * Kills pawn. Server/authority only. + * @param KillingDamage - Damage amount of the killing blow + * @param DamageEvent - Damage event of the killing blow + * @param Killer - Who killed this pawn + * @param DamageCauser - the Actor that directly caused the damage (i.e. the Projectile that exploded, the Weapon that fired, etc) + * @returns true if allowed + */ + virtual bool Die(float KillingDamage, struct FDamageEvent const& DamageEvent, class AController* Killer, class AActor* DamageCauser); + + // Die when we fall out of the world. + virtual void FellOutOfWorld(const class UDamageType& dmgType) override; + + /** Called on the actor right before replication occurs */ + virtual void PreReplication(IRepChangedPropertyTracker & ChangedPropertyTracker) override; +protected: + /** notification when killed, for both the server and client. */ + virtual void OnDeath(float KillingDamage, struct FDamageEvent const& DamageEvent, class APawn* InstigatingPawn, class AActor* DamageCauser); + + /** play effects on hit */ + virtual void PlayHit(float DamageTaken, struct FDamageEvent const& DamageEvent, class APawn* PawnInstigator, class AActor* DamageCauser); + + /** switch to ragdoll */ + void SetRagdollPhysics(); + + /** sets up the replication for taking a hit */ + void ReplicateHit(float Damage, struct FDamageEvent const& DamageEvent, class APawn* InstigatingPawn, class AActor* DamageCauser, bool bKilled); + + /** play hit or death on client */ + UFUNCTION() + void OnRep_LastTakeHitInfo(); + + ////////////////////////////////////////////////////////////////////////// + // Inventory + + /** updates current weapon */ + void SetCurrentWeapon(class AShooterWeapon* NewWeapon, class AShooterWeapon* LastWeapon = NULL); + + /** current weapon rep handler */ + UFUNCTION() + void OnRep_CurrentWeapon(class AShooterWeapon* LastWeapon); + + /** [server] spawns default inventory */ + void SpawnDefaultInventory(); + + /** [server] remove all weapons from inventory and destroy them */ + void DestroyInventory(); + + /** equip weapon */ + UFUNCTION(reliable, server, WithValidation) + void ServerEquipWeapon(class AShooterWeapon* NewWeapon); + + /** update targeting state */ + UFUNCTION(reliable, server, WithValidation) + void ServerSetTargeting(bool bNewTargeting); + + /** update targeting state */ + UFUNCTION(reliable, server, WithValidation) + void ServerSetRunning(bool bNewRunning, bool bToggle); + + /** Builds list of points to check for pausing replication for a connection*/ + void BuildPauseReplicationCheckPoints(TArray& RelevancyCheckPoints); + +protected: + /** Returns Mesh1P subobject **/ + FORCEINLINE USkeletalMeshComponent* GetMesh1P() const { return Mesh1P; } +}; + + diff --git a/Source/ShooterGame/Public/Player/ShooterCharacterMovement.h b/Source/ShooterGame/Public/Player/ShooterCharacterMovement.h new file mode 100644 index 0000000..a6e74a7 --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterCharacterMovement.h @@ -0,0 +1,17 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +/** + * Movement component meant for use with Pawns. + */ + +#pragma once +#include "ShooterCharacterMovement.generated.h" + +UCLASS() +class UShooterCharacterMovement : public UCharacterMovementComponent +{ + GENERATED_UCLASS_BODY() + + virtual float GetMaxSpeed() const override; +}; + diff --git a/Source/ShooterGame/Public/Player/ShooterCheatManager.h b/Source/ShooterGame/Public/Player/ShooterCheatManager.h new file mode 100644 index 0000000..7336dfc --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterCheatManager.h @@ -0,0 +1,32 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterCheatManager.generated.h" + +UCLASS(Within=ShooterPlayerController) +class UShooterCheatManager : public UCheatManager +{ + GENERATED_UCLASS_BODY() + + UFUNCTION(exec) + void ToggleInfiniteAmmo(); + + UFUNCTION(exec) + void ToggleInfiniteClip(); + + UFUNCTION(exec) + void ToggleMatchTimer(); + + UFUNCTION(exec) + void ForceMatchStart(); + + UFUNCTION(exec) + void ChangeTeam(int32 NewTeamNumber); + + UFUNCTION(exec) + void Cheat(const FString& Msg); + + UFUNCTION(exec) + void SpawnBot(); +}; diff --git a/Source/ShooterGame/Public/Player/ShooterDemoSpectator.h b/Source/ShooterGame/Public/Player/ShooterDemoSpectator.h new file mode 100644 index 0000000..c44c937 --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterDemoSpectator.h @@ -0,0 +1,31 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterDemoSpectator.generated.h" + +class SShooterDemoHUD; + +UCLASS(config=Game) +class AShooterDemoSpectator : public APlayerController +{ + GENERATED_UCLASS_BODY() + +public: + /** shooter in-game menu */ + TSharedPtr ShooterDemoPlaybackMenu; + + virtual void SetupInputComponent() override; + virtual void SetPlayer( UPlayer* Player ) override; + virtual void Destroyed() override; + + void OnToggleInGameMenu(); + void OnIncreasePlaybackSpeed(); + void OnDecreasePlaybackSpeed(); + + int32 PlaybackSpeed; + +private: + TSharedPtr DemoHUD; +}; + diff --git a/Source/ShooterGame/Public/Player/ShooterLocalPlayer.h b/Source/ShooterGame/Public/Player/ShooterLocalPlayer.h new file mode 100644 index 0000000..8068e5d --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterLocalPlayer.h @@ -0,0 +1,31 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterPersistentUser.h" +#include "ShooterLocalPlayer.generated.h" + +UCLASS(config=Engine, transient) +class UShooterLocalPlayer : public ULocalPlayer +{ + GENERATED_UCLASS_BODY() + +public: + + virtual void SetControllerId(int32 NewControllerId) override; + + virtual FString GetNickname() const; + + class UShooterPersistentUser* GetPersistentUser() const; + + /** Initializes the PersistentUser */ + void LoadPersistentUser(); + +private: + /** Persistent user data stored between sessions (i.e. the user's savegame) */ + UPROPERTY() + class UShooterPersistentUser* PersistentUser; +}; + + + diff --git a/Source/ShooterGame/Public/Player/ShooterPersistentUser.h b/Source/ShooterGame/Public/Player/ShooterPersistentUser.h new file mode 100644 index 0000000..7a07f95 --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterPersistentUser.h @@ -0,0 +1,175 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once +#include "ShooterPersistentUser.generated.h" + +UCLASS() +class UShooterPersistentUser : public USaveGame +{ + GENERATED_UCLASS_BODY() + +public: + /** Loads user persistence data if it exists, creates an empty record otherwise. */ + static UShooterPersistentUser* LoadPersistentUser(FString SlotName, const int32 UserIndex); + + /** Saves data if anything has changed. */ + void SaveIfDirty(); + + /** Records the result of a match. */ + void AddMatchResult(int32 MatchKills, int32 MatchDeaths, int32 MatchBulletsFired, int32 MatchRocketsFired, bool bIsMatchWinner); + + /** needed because we can recreate the subsystem that stores it */ + void TellInputAboutKeybindings(); + + int32 GetUserIndex() const; + + FORCEINLINE int32 GetKills() const + { + return Kills; + } + + FORCEINLINE int32 GetDeaths() const + { + return Deaths; + } + + FORCEINLINE int32 GetWins() const + { + return Wins; + } + + FORCEINLINE int32 GetLosses() const + { + return Losses; + } + + FORCEINLINE int32 GetBulletsFired() const + { + return BulletsFired; + } + + FORCEINLINE int32 GetRocketsFired() const + { + return RocketsFired; + } + + /** Is controller vibration turned on? */ + FORCEINLINE bool GetVibration() const + { + return bVibrationOpt; + } + + /** Is the y axis inverted? */ + FORCEINLINE bool GetInvertedYAxis() const + { + return bInvertedYAxis; + } + + /** Setter for controller vibration option */ + void SetVibration(bool bVibration); + + /** Setter for inverted y axis */ + void SetInvertedYAxis(bool bInvert); + + /** Getter for the aim sensitivity */ + FORCEINLINE float GetAimSensitivity() const + { + return AimSensitivity; + } + + void SetAimSensitivity(float InSensitivity); + + /** Getter for the gamma correction */ + FORCEINLINE float GetGamma() const + { + return Gamma; + } + + void SetGamma(float InGamma); + + FORCEINLINE int32 GetBotsCount() const + { + return BotsCount; + } + + void SetBotsCount(int32 InCount); + + FORCEINLINE bool IsRecordingDemos() const + { + return bIsRecordingDemos; + } + + void SetIsRecordingDemos(const bool InbIsRecordingDemos); + + FORCEINLINE FString GetName() const + { + return SlotName; + } + +protected: + void SetToDefaults(); + + /** Checks if the Mouse Sensitivity user setting is different from current */ + bool IsAimSensitivityDirty() const; + + /** Checks if the Inverted Mouse user setting is different from current */ + bool IsInvertedYAxisDirty() const; + + /** Triggers a save of this data. */ + void SavePersistentUser(); + + /** Lifetime count of kills */ + UPROPERTY() + int32 Kills; + + /** Lifetime count of deaths */ + UPROPERTY() + int32 Deaths; + + /** Lifetime count of match wins */ + UPROPERTY() + int32 Wins; + + /** Lifetime count of match losses */ + UPROPERTY() + int32 Losses; + + /** Lifetime count of bullets fired */ + UPROPERTY() + int32 BulletsFired; + + /** Lifetime count of rockets fired */ + UPROPERTY() + int32 RocketsFired; + + /** how many bots join hosted game */ + UPROPERTY() + int32 BotsCount; + + /** is recording demos? */ + UPROPERTY() + bool bIsRecordingDemos; + + /** Holds the gamma correction setting */ + UPROPERTY() + float Gamma; + + /** Holds the mouse sensitivity */ + UPROPERTY() + float AimSensitivity; + + /** Is the y axis inverted or not? */ + UPROPERTY() + bool bInvertedYAxis; + + UPROPERTY() + bool bVibrationOpt; + +private: + /** Internal. True if data is changed but hasn't been saved. */ + bool bIsDirty; + + /** The string identifier used to save/load this persistent user. */ + FString SlotName; + int32 UserIndex; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Player/ShooterPlayerCameraManager.h b/Source/ShooterGame/Public/Player/ShooterPlayerCameraManager.h new file mode 100644 index 0000000..8351934 --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterPlayerCameraManager.h @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterPlayerCameraManager.generated.h" + +UCLASS() +class AShooterPlayerCameraManager : public APlayerCameraManager +{ + GENERATED_UCLASS_BODY() + +public: + + /** normal FOV */ + float NormalFOV; + + /** targeting FOV */ + float TargetingFOV; + + /** After updating camera, inform pawn to update 1p mesh to match camera's location&rotation */ + virtual void UpdateCamera(float DeltaTime) override; +}; diff --git a/Source/ShooterGame/Public/Player/ShooterPlayerController.h b/Source/ShooterGame/Public/Player/ShooterPlayerController.h new file mode 100644 index 0000000..dba93f0 --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterPlayerController.h @@ -0,0 +1,334 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Online.h" +#include "ShooterLeaderboards.h" +#include "ShooterPlayerController.generated.h" + +class AShooterHUD; + +UCLASS(config=Game) +class AShooterPlayerController : public APlayerController +{ + GENERATED_UCLASS_BODY() + +public: + /** sets spectator location and rotation */ + UFUNCTION(reliable, client) + void ClientSetSpectatorCamera(FVector CameraLocation, FRotator CameraRotation); + + /** notify player about started match */ + UFUNCTION(reliable, client) + void ClientGameStarted(); + + /** Starts the online game using the session name in the PlayerState */ + UFUNCTION(reliable, client) + void ClientStartOnlineGame(); + + /** Ends the online game using the session name in the PlayerState */ + UFUNCTION(reliable, client) + void ClientEndOnlineGame(); + + /** notify player about finished match */ + virtual void ClientGameEnded_Implementation(class AActor* EndGameFocus, bool bIsWinner); + + /** Notifies clients to send the end-of-round event */ + UFUNCTION(reliable, client) + void ClientSendRoundEndEvent(bool bIsWinner, int32 ExpendedTimeInSeconds); + + /** used for input simulation from blueprint (for automatic perf tests) */ + UFUNCTION(BlueprintCallable, Category="Input") + void SimulateInputKey(FKey Key, bool bPressed = true); + + /** sends cheat message */ + UFUNCTION(reliable, server, WithValidation) + void ServerCheat(const FString& Msg); + + /* Overriden Message implementation. */ + virtual void ClientTeamMessage_Implementation( APlayerState* SenderPlayerState, const FString& S, FName Type, float MsgLifeTime ) override; + + /* Tell the HUD to toggle the chat window. */ + void ToggleChatWindow(); + + /** Local function say a string */ + UFUNCTION(exec) + virtual void Say(const FString& Msg); + + /** RPC for clients to talk to server */ + UFUNCTION(unreliable, server, WithValidation) + void ServerSay(const FString& Msg); + + /** Local function run an emote */ +// UFUNCTION(exec) +// virtual void Emote(const FString& Msg); + + /** notify local client about deaths */ + void OnDeathMessage(class AShooterPlayerState* KillerPlayerState, class AShooterPlayerState* KilledPlayerState, const UDamageType* KillerDamageType); + + /** toggle InGameMenu handler */ + void OnToggleInGameMenu(); + + /** Show the in-game menu if it's not already showing */ + void ShowInGameMenu(); + + /** Hides scoreboard if currently diplayed */ + void OnConditionalCloseScoreboard(); + + /** Toggles scoreboard */ + void OnToggleScoreboard(); + + /** shows scoreboard */ + void OnShowScoreboard(); + + /** hides scoreboard */ + void OnHideScoreboard(); + + /** set infinite ammo cheat */ + void SetInfiniteAmmo(bool bEnable); + + /** set infinite clip cheat */ + void SetInfiniteClip(bool bEnable); + + /** set health regen cheat */ + void SetHealthRegen(bool bEnable); + + /** set god mode cheat */ + UFUNCTION(exec) + void SetGodMode(bool bEnable); + + /** sets the produce force feedback flag. */ + void SetIsVibrationEnabled(bool bEnable); + + /** get infinite ammo cheat */ + bool HasInfiniteAmmo() const; + + /** get infinite clip cheat */ + bool HasInfiniteClip() const; + + /** get health regen cheat */ + bool HasHealthRegen() const; + + /** get gode mode cheat */ + bool HasGodMode() const; + + /** should produce force feedback? */ + bool IsVibrationEnabled() const; + + /** check if gameplay related actions (movement, weapon usage, etc) are allowed right now */ + bool IsGameInputAllowed() const; + + /** is game menu currently active? */ + bool IsGameMenuVisible() const; + + /** Ends and/or destroys game session */ + void CleanupSessionOnReturnToMenu(); + + /** + * Called when the read achievements request from the server is complete + * + * @param PlayerId The player id who is responsible for this delegate being fired + * @param bWasSuccessful true if the server responded successfully to the request + */ + void OnQueryAchievementsComplete(const FUniqueNetId& PlayerId, const bool bWasSuccessful ); + + UFUNCTION() + void OnLeaderboardReadComplete(bool bWasSuccessful); + + // Begin APlayerController interface + + /** handle weapon visibility */ + virtual void SetCinematicMode(bool bInCinematicMode, bool bHidePlayer, bool bAffectsHUD, bool bAffectsMovement, bool bAffectsTurning) override; + + /** Returns true if movement input is ignored. Overridden to always allow spectators to move. */ + virtual bool IsMoveInputIgnored() const override; + + /** Returns true if look input is ignored. Overridden to always allow spectators to look around. */ + virtual bool IsLookInputIgnored() const override; + + /** initialize the input system from the player settings */ + virtual void InitInputSystem() override; + + virtual bool SetPause(bool bPause, FCanUnpause CanUnpauseDelegate = FCanUnpause()) override; + + virtual FVector GetFocalLocation() const override; + + // End APlayerController interface + + // begin AShooterPlayerController-specific + + /** + * Reads achievements to precache them before first use + */ + void QueryAchievements(); + + /** + * Reads backend stats to precache them before first use + */ + void QueryStats(); + + /** + * Writes a single achievement (unless another write is in progress). + * + * @param Id achievement id (string) + * @param Percent number 1 to 100 + */ + void UpdateAchievementProgress( const FString& Id, float Percent ); + + /** Returns a pointer to the shooter game hud. May return NULL. */ + AShooterHUD* GetShooterHUD() const; + + /** Returns the persistent user record associated with this player, or null if there is't one. */ + class UShooterPersistentUser* GetPersistentUser() const; + + /** Informs that player fragged someone */ + void OnKill(); + + /** Cleans up any resources necessary to return to main menu. Does not modify GameInstance state. */ + virtual void HandleReturnToMainMenu(); + + /** Associate a new UPlayer with this PlayerController. */ + virtual void SetPlayer(UPlayer* Player); + + // end AShooterPlayerController-specific + + virtual void PreClientTravel(const FString& PendingURL, ETravelType TravelType, bool bIsSeamlessTravel) override; + +protected: + + /** infinite ammo cheat */ + UPROPERTY(Transient, Replicated) + uint8 bInfiniteAmmo : 1; + + /** infinite clip cheat */ + UPROPERTY(Transient, Replicated) + uint8 bInfiniteClip : 1; + + /** health regen cheat */ + UPROPERTY(Transient, Replicated) + uint8 bHealthRegen : 1; + + /** god mode cheat */ + UPROPERTY(Transient) + uint8 bGodMode : 1; + + /** should produce force feedback? */ + uint8 bIsVibrationEnabled : 1; + + /** if set, gameplay related actions (movement, weapn usage, etc) are allowed */ + uint8 bAllowGameActions : 1; + + /** true for the first frame after the game has ended */ + uint8 bGameEndedFrame : 1; + + /** stores pawn location at last player death, used where player scores a kill after they died **/ + FVector LastDeathLocation; + + /** shooter in-game menu */ + TSharedPtr ShooterIngameMenu; + + /** Achievements write object */ + FOnlineAchievementsWritePtr WriteObject; + + /** try to find spot for death cam */ + bool FindDeathCameraSpot(FVector& CameraLocation, FRotator& CameraRotation); + + virtual void BeginDestroy() override; + + //Begin AActor interface + + /** after all game elements are created */ + virtual void PostInitializeComponents() override; + + /** Internal. Used to store stats from the online interface. These increment as matches are written */ + int32 StatMatchesPlayed; + int32 StatKills; + int32 StatDeaths; + bool bHasQueriedPlatformStats; + bool bHasQueriedPlatformAchievements; + + /** Internal. Reads the stats from the platform backend to sync online status with local */ + FOnlineLeaderboardReadPtr ReadObject; + FDelegateHandle LeaderboardReadCompleteDelegateHandle; + void ClearLeaderboardDelegate(); + + /* Flag to prevent duplicate input bindings when using the same player controller for multiple maps */ + bool bHasInitializedInputComponent; + +public: + virtual void TickActor(float DeltaTime, enum ELevelTick TickType, FActorTickFunction& ThisTickFunction) override; + //End AActor interface + + //Begin AController interface + + /** transition to dead state, retries spawning later */ + virtual void FailedToSpawnPawn() override; + + /** update camera when pawn dies */ + virtual void PawnPendingDestroy(APawn* P) override; + + //End AController interface + + // Begin APlayerController interface + + /** respawn after dying */ + virtual void UnFreeze() override; + + /** sets up input */ + virtual void SetupInputComponent() override; + + /** + * Called from game info upon end of the game, used to transition to proper state. + * + * @param EndGameFocus Actor to set as the view target on end game + * @param bIsWinner true if this controller is on winning team + */ + virtual void GameHasEnded(class AActor* EndGameFocus = NULL, bool bIsWinner = false) override; + + /** Return the client to the main menu gracefully. ONLY sets GI state. */ + void ClientReturnToMainMenuWithTextReason_Implementation(const FText& ReturnReason) override; + + /** Causes the player to commit suicide */ + UFUNCTION(exec) + virtual void Suicide(); + + /** Notifies the server that the client has suicided */ + UFUNCTION(reliable, server, WithValidation) + void ServerSuicide(); + + /** Updates achievements based on the PersistentUser stats at the end of a round */ + void UpdateAchievementsOnGameEnd(); + + /** Updates leaderboard stats at the end of a round */ + void UpdateLeaderboardsOnGameEnd(); + + /** Updates stats at the end of a round */ + void UpdateStatsOnGameEnd(bool bIsWinner); + + /** Updates the save file at the end of a round */ + void UpdateSaveFileOnGameEnd(bool bIsWinner); + + // End APlayerController interface + + FName ServerSayString; + + // Timer used for updating friends in the player tick. + float ShooterFriendUpdateTimer; + + // For tracking whether or not to send the end event + bool bHasSentStartEvents; + + /** enable analog fire trigger mode */ + UPROPERTY(config) + bool bAnalogFireTrigger; + + /** threshold trigger fires */ + UPROPERTY(config) + float FireTriggerThreshold; + +private: + + /** Handle for efficient management of ClientStartOnlineGame timer */ + FTimerHandle TimerHandle_ClientStartOnlineGame; +}; + diff --git a/Source/ShooterGame/Public/Player/ShooterPlayerController_Menu.h b/Source/ShooterGame/Public/Player/ShooterPlayerController_Menu.h new file mode 100644 index 0000000..08bfe79 --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterPlayerController_Menu.h @@ -0,0 +1,16 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterTypes.h" +#include "ShooterPlayerController_Menu.generated.h" + +UCLASS() +class AShooterPlayerController_Menu : public APlayerController +{ + GENERATED_UCLASS_BODY() + + /** After game is initialized */ + virtual void PostInitializeComponents() override; +}; + diff --git a/Source/ShooterGame/Public/Player/ShooterSpectatorPawn.h b/Source/ShooterGame/Public/Player/ShooterSpectatorPawn.h new file mode 100644 index 0000000..3f887e4 --- /dev/null +++ b/Source/ShooterGame/Public/Player/ShooterSpectatorPawn.h @@ -0,0 +1,19 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once +#include "ShooterSpectatorPawn.generated.h" + + +UCLASS(config = Game, Blueprintable, BlueprintType) +class AShooterSpectatorPawn : public ASpectatorPawn +{ + GENERATED_UCLASS_BODY() + + // Begin ASpectatorPawn overrides + /** Overridden to implement Key Bindings the match the player controls */ + virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override; + // End Pawn overrides + + // Frame rate linked look + void LookUpAtRate(float Val); +}; diff --git a/Source/ShooterGame/Public/ShooterEngine.h b/Source/ShooterGame/Public/ShooterEngine.h new file mode 100644 index 0000000..ba266e1 --- /dev/null +++ b/Source/ShooterGame/Public/ShooterEngine.h @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterEngine.generated.h" + +UCLASS() +class SHOOTERGAME_API UShooterEngine : public UGameEngine +{ + GENERATED_UCLASS_BODY() + + /* Hook up specific callbacks */ + virtual void Init(IEngineLoop* InEngineLoop); + +public: + + /** + * All regular engine handling, plus update ShooterKing state appropriately. + */ + virtual void HandleNetworkFailure(UWorld *World, UNetDriver *NetDriver, ENetworkFailure::Type FailureType, const FString& ErrorString) override; +}; + diff --git a/Source/ShooterGame/Public/ShooterGame.h b/Source/ShooterGame/Public/ShooterGame.h new file mode 100644 index 0000000..9614018 --- /dev/null +++ b/Source/ShooterGame/Public/ShooterGame.h @@ -0,0 +1,64 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine.h" +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "ParticleDefinitions.h" +#include "SoundDefinitions.h" +#include "Net/UnrealNetwork.h" +#include "ShooterGameMode.h" +#include "ShooterGameState.h" +#include "ShooterCharacter.h" +#include "ShooterCharacterMovement.h" +#include "ShooterPlayerController.h" +#include "ShooterGameClasses.h" + + +class UBehaviorTreeComponent; + +DECLARE_LOG_CATEGORY_EXTERN(LogShooter, Log, All); +DECLARE_LOG_CATEGORY_EXTERN(LogShooterWeapon, Log, All); + +/** when you modify this, please note that this information can be saved with instances + * also DefaultEngine.ini [/Script/Engine.CollisionProfile] should match with this list **/ +#define COLLISION_WEAPON ECC_GameTraceChannel1 +#define COLLISION_PROJECTILE ECC_GameTraceChannel2 +#define COLLISION_PICKUP ECC_GameTraceChannel3 + +#define MAX_PLAYER_NAME_LENGTH 16 + + +#ifndef SHOOTER_CONSOLE_UI +/** Set to 1 to pretend we're building for console even on a PC, for testing purposes */ +#define SHOOTER_SIMULATE_CONSOLE_UI 0 + +#if PLATFORM_PS4 || PLATFORM_SWITCH || SHOOTER_SIMULATE_CONSOLE_UI + #define SHOOTER_CONSOLE_UI 1 +#else + #define SHOOTER_CONSOLE_UI 0 +#endif +#endif + +#ifndef SHOOTER_XBOX_STRINGS + #define SHOOTER_XBOX_STRINGS 0 +#endif + +#ifndef SHOOTER_SHOW_QUIT_MENU_ITEM + #define SHOOTER_SHOW_QUIT_MENU_ITEM (!SHOOTER_CONSOLE_UI) +#endif + +#ifndef SHOOTER_SUPPORTS_OFFLINE_SPLIT_SCREEEN + #define SHOOTER_SUPPORTS_OFFLINE_SPLIT_SCREEEN 1 +#endif + +// whether the platform will signal a controller pairing change on a controller disconnect. if not, we need to treat the pairing change as a request to switch profiles when the destination profile is not specified +#ifndef SHOOTER_CONTROLLER_PAIRING_ON_DISCONNECT + #define SHOOTER_CONTROLLER_PAIRING_ON_DISCONNECT 1 +#endif + +// whether the game should display an account picker when a new input device is connected, while the "please reconnect controller" message is on screen. +#ifndef SHOOTER_CONTROLLER_PAIRING_PROMPT_FOR_NEW_USER_WHEN_RECONNECTING + #define SHOOTER_CONTROLLER_PAIRING_PROMPT_FOR_NEW_USER_WHEN_RECONNECTING 0 +#endif diff --git a/Source/ShooterGame/Public/ShooterGameInstance.h b/Source/ShooterGame/Public/ShooterGameInstance.h new file mode 100644 index 0000000..98cc67f --- /dev/null +++ b/Source/ShooterGame/Public/ShooterGameInstance.h @@ -0,0 +1,400 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterGame.h" +#include "OnlineIdentityInterface.h" +#include "OnlineSessionInterface.h" +#include "OnlineGameActivityInterface.h" +#include "Engine/GameInstance.h" +#include "Engine/NetworkDelegates.h" +#include "ShooterGameInstance.generated.h" + +class FVariantData; +class FShooterMainMenu; +class FShooterWelcomeMenu; +class FShooterMessageMenu; +class AShooterGameSession; + +namespace ShooterGameInstanceState +{ + extern const FName None; + extern const FName PendingInvite; + extern const FName WelcomeScreen; + extern const FName MainMenu; + extern const FName MessageMenu; + extern const FName Playing; +} + +/** This class holds the value of what message to display when we are in the "MessageMenu" state */ +class FShooterPendingMessage +{ +public: + FText DisplayString; // This is the display message in the main message body + FText OKButtonString; // This is the ok button text + FText CancelButtonString; // If this is not empty, it will be the cancel button text + FName NextState; // Final destination state once message is discarded + + TWeakObjectPtr< ULocalPlayer > PlayerOwner; // Owner of dialog who will have focus (can be NULL) +}; + +class FShooterPendingInvite +{ +public: + FShooterPendingInvite() : ControllerId(-1), UserId(nullptr), bPrivilegesCheckedAndAllowed(false) {} + + int32 ControllerId; + TSharedPtr< const FUniqueNetId > UserId; + FOnlineSessionSearchResult InviteResult; + bool bPrivilegesCheckedAndAllowed; +}; + +class SShooterWaitDialog : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterWaitDialog) + {} + SLATE_ARGUMENT(FText, MessageText) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + +private: + + /** our curve sequence and the related handles */ + FCurveSequence WidgetAnimation; + + /** used for animating the text color. */ + FCurveHandle TextColorCurve; + + /** Gets the animated text color */ + FSlateColor GetTextColor() const; +}; + +UENUM() +enum class EOnlineMode : uint8 +{ + Offline, + LAN, + Online +}; + + +UCLASS(config=Game) +class UShooterGameInstance : public UGameInstance +{ +public: + GENERATED_UCLASS_BODY() + +public: + + bool Tick(float DeltaSeconds); + + AShooterGameSession* GetGameSession() const; + + virtual void Init() override; + virtual void Shutdown() override; + virtual void StartGameInstance() override; +#if WITH_EDITOR + virtual FGameInstancePIEResult StartPlayInEditorGameInstance(ULocalPlayer* LocalPlayer, const FGameInstancePIEParameters& Params) override; +#endif // WITH_EDITOR + + virtual void ReceivedNetworkEncryptionToken(const FString& EncryptionToken, const FOnEncryptionKeyResponse& Delegate) override; + virtual void ReceivedNetworkEncryptionAck(const FOnEncryptionKeyResponse& Delegate) override; + + bool HostGame(ULocalPlayer* LocalPlayer, const FString& GameType, const FString& InTravelURL); + bool JoinSession(ULocalPlayer* LocalPlayer, int32 SessionIndexInSearchResults); + bool JoinSession(ULocalPlayer* LocalPlayer, const FOnlineSessionSearchResult& SearchResult); + void SetPendingInvite(const FShooterPendingInvite& InPendingInvite); + + bool PlayDemo(ULocalPlayer* LocalPlayer, const FString& DemoName); + + /** Travel directly to the named session */ + void TravelToSession(const FName& SessionName); + + /** Directly connect to an address */ + void DirectConnectToSession(const FString& Address); + + /** Get the Travel URL for a quick match */ + static FString GetQuickMatchUrl(); + + /** Begin a hosted quick match */ + void BeginHostingQuickMatch(); + + /** Initiates the session searching */ + bool FindSessions(ULocalPlayer* PlayerOwner, bool bIsDedicatedServer, bool bLANMatch); + + /** Sends the game to the specified state. */ + void GotoState(FName NewState); + + /** Obtains the initial welcome state, which can be different based on platform */ + FName GetInitialState(); + + /** Sends the game to the initial startup/frontend state */ + void GotoInitialState(); + + /** Gets the current state of the GameInstance */ + const FName GetCurrentState() const; + + /** + * Creates the message menu, clears other menus and sets the KingState to Message. + * + * @param Message Main message body + * @param OKButtonString String to use for 'OK' button + * @param CancelButtonString String to use for 'Cancel' button + * @param NewState Final state to go to when message is discarded + */ + void ShowMessageThenGotoState( const FText& Message, const FText& OKButtonString, const FText& CancelButtonString, const FName& NewState, const bool OverrideExisting = true, TWeakObjectPtr< ULocalPlayer > PlayerOwner = nullptr ); + + void RemoveExistingLocalPlayer(ULocalPlayer* ExistingPlayer); + + void RemoveSplitScreenPlayers(); + + TSharedPtr< const FUniqueNetId > GetUniqueNetIdFromControllerId( const int ControllerId ); + + /** Returns true if the game is in online mode */ + EOnlineMode GetOnlineMode() const { return OnlineMode; } + + /** Sets the online mode of the game */ + void SetOnlineMode(EOnlineMode InOnlineMode); + + /** Updates the status of using multiplayer features */ + void UpdateUsingMultiplayerFeatures(bool bIsUsingMultiplayerFeatures); + + /** Sets the controller to ignore for pairing changes. Useful when we are showing external UI for manual profile switching. */ + void SetIgnorePairingChangeForControllerId( const int32 ControllerId ); + + /** Returns true if the passed in local player is signed in and online */ + bool IsLocalPlayerOnline(ULocalPlayer* LocalPlayer); + + /** Returns true if the passed in local player is signed in*/ + bool IsLocalPlayerSignedIn(ULocalPlayer* LocalPlayer); + + /** Returns true if owning player is online. Displays proper messaging if the user can't play */ + bool ValidatePlayerForOnlinePlay(ULocalPlayer* LocalPlayer); + + /** Returns true if owning player is signed in. Displays proper messaging if the user can't play */ + bool ValidatePlayerIsSignedIn(ULocalPlayer* LocalPlayer); + + /** Shuts down the session, and frees any net driver */ + void CleanupSessionOnReturnToMenu(); + + /** Flag the local player when they quit the game */ + void LabelPlayerAsQuitter(ULocalPlayer* LocalPlayer) const; + + // Generic confirmation handling (just hide the dialog) + FReply OnConfirmGeneric(); + bool HasLicense() const { return bIsLicensed; } + + /** Start task to get user privileges. */ + void StartOnlinePrivilegeTask(const IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate& Delegate, EUserPrivileges::Type Privilege, TSharedPtr< const FUniqueNetId > UserId); + + /** Common cleanup code for any Privilege task delegate */ + void CleanupOnlinePrivilegeTask(); + + /** Show approved dialogs for various privileges failures */ + void DisplayOnlinePrivilegeFailureDialogs(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults); + + /** @return OnlineSession class to use for this player */ + TSubclassOf GetOnlineSessionClass() override; + + /** Create a session with the default map and game-type with the selected online settings */ + bool HostQuickSession(ULocalPlayer& LocalPlayer, const FOnlineSessionSettings& SessionSettings); + + /** Sets a rich presence string for all local players. */ + void SetPresenceForLocalPlayers(const FString& StatusStr, const FVariantData& PresenceData); + + /** Handle game activity requests */ + void OnGameActivityActivationRequestComplete(const FUniqueNetId& PlayerId, const FString& ActivityId, const FOnlineSessionSearchResult* SessionInfo); + + +private: + + UPROPERTY(config) + FString WelcomeScreenMap; + + UPROPERTY(config) + FString MainMenuMap; + + UPROPERTY(config) + FString MatchmakerEndpoint; + + + FName CurrentState; + FName PendingState; + + FShooterPendingMessage PendingMessage; + + FShooterPendingInvite PendingInvite; + + /** URL to travel to after pending network operations */ + FString TravelURL; + + /** Current online mode of the game (offline, LAN, or online) */ + EOnlineMode OnlineMode; + + /** If true, enable splitscreen when map starts loading */ + bool bPendingEnableSplitscreen; + + /** Whether the user has an active license to play the game */ + bool bIsLicensed; + + /** Main menu UI */ + TSharedPtr MainMenuUI; + + /** Message menu (Shown in the even of errors - unable to connect etc) */ + TSharedPtr MessageMenuUI; + + /** Welcome menu UI (for consoles) */ + TSharedPtr WelcomeMenuUI; + + /** Dialog widget to show non-interactive waiting messages for network timeouts and such. */ + TSharedPtr WaitMessageWidget; + + /** Controller to ignore for pairing changes. -1 to skip ignore. */ + int32 IgnorePairingChangeForControllerId; + + /** Last connection status that was passed into the HandleNetworkConnectionStatusChanged hander */ + EOnlineServerConnectionStatus::Type CurrentConnectionStatus; + + /** Delegate for callbacks to Tick */ + FTickerDelegate TickDelegate; + + /** Handle to various registered delegates */ + FDelegateHandle TickDelegateHandle; + FDelegateHandle TravelLocalSessionFailureDelegateHandle; + FDelegateHandle OnJoinSessionCompleteDelegateHandle; + FDelegateHandle OnSearchSessionsCompleteDelegateHandle; + FDelegateHandle OnStartSessionCompleteDelegateHandle; + FDelegateHandle OnEndSessionCompleteDelegateHandle; + FDelegateHandle OnDestroySessionCompleteDelegateHandle; + FDelegateHandle OnCreatePresenceSessionCompleteDelegateHandle; + FDelegateHandle OnGameActivityActivationRequestedDelegateHandle; + + FOnGameActivityActivationRequestedDelegate OnGameActivityActivationRequestedDelegate; + + /** Local player login status when the system is suspended */ + TArray LocalPlayerOnlineStatus; + + /** A hard-coded encryption key used to try out the encryption code. This is NOT SECURE, do not use this technique in production! */ + TArray DebugTestEncryptionKey; + + void HandleNetworkConnectionStatusChanged(const FString& ServiceName, EOnlineServerConnectionStatus::Type LastConnectionStatus, EOnlineServerConnectionStatus::Type ConnectionStatus ); + + void HandleSessionFailure( const FUniqueNetId& NetId, ESessionFailure::Type FailureType ); + + void OnPreLoadMap(const FString& MapName); + void OnPostLoadMap(UWorld*); + void OnPostDemoPlay(); + + virtual void HandleDemoPlaybackFailure( EDemoPlayFailure::Type FailureType, const FString& ErrorString ) override; + + /** Delegate function executed after checking privileges for starting quick match */ + void OnUserCanPlayInvite(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults); + + /** Delegate for ending a session */ + FOnEndSessionCompleteDelegate OnEndSessionCompleteDelegate; + + void OnEndSessionComplete( FName SessionName, bool bWasSuccessful ); + + void MaybeChangeState(); + void EndCurrentState(FName NextState); + void BeginNewState(FName NewState, FName PrevState); + + void BeginPendingInviteState(); + void BeginWelcomeScreenState(); + void BeginMainMenuState(); + void BeginMessageMenuState(); + void BeginPlayingState(); + + void EndPendingInviteState(); + void EndWelcomeScreenState(); + void EndMainMenuState(); + void EndMessageMenuState(); + void EndPlayingState(); + + void ShowLoadingScreen(); + void AddNetworkFailureHandlers(); + void RemoveNetworkFailureHandlers(); + + /** Called when there is an error trying to travel to a local session */ + void TravelLocalSessionFailure(UWorld *World, ETravelFailure::Type FailureType, const FString& ErrorString); + + /** Callback which is intended to be called upon joining session */ + void OnJoinSessionComplete(EOnJoinSessionCompleteResult::Type Result); + + /** Callback which is intended to be called upon session creation */ + void OnCreatePresenceSessionComplete(FName SessionName, bool bWasSuccessful); + + /** Callback which is called after adding local users to a session */ + void OnRegisterLocalPlayerComplete(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result); + + /** Called after all the local players are registered */ + void FinishSessionCreation(EOnJoinSessionCompleteResult::Type Result); + + /** Callback which is called after adding local users to a session we're joining */ + void OnRegisterJoiningLocalPlayerComplete(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result); + + /** Called after all the local players are registered in a session we're joining */ + void FinishJoinSession(EOnJoinSessionCompleteResult::Type Result); + + /** + * Creates the message menu, clears other menus and sets the KingState to Message. + * + * @param Message Main message body + * @param OKButtonString String to use for 'OK' button + * @param CancelButtonString String to use for 'Cancel' button + */ + void ShowMessageThenGoMain(const FText& Message, const FText& OKButtonString, const FText& CancelButtonString); + + /** Callback which is intended to be called upon finding sessions */ + void OnSearchSessionsComplete(bool bWasSuccessful); + + bool LoadFrontEndMap(const FString& MapName); + + /** Travel directly to the named session */ + void InternalTravelToSession(const FName& SessionName); + + /** Show messaging and punt to welcome screen */ + void HandleSignInChangeMessaging(); + + // OSS delegates to handle + void HandleUserLoginChanged(int32 GameUserIndex, ELoginStatus::Type PreviousLoginStatus, ELoginStatus::Type LoginStatus, const FUniqueNetId& UserId); + + // Callback to pause the game when the OS has constrained our app. + void HandleAppWillDeactivate(); + + // Callback occurs when game being suspended + void HandleAppSuspend(); + + // Callback occurs when game resuming + void HandleAppResume(); + + // Callback to process game licensing change notifications. + void HandleAppLicenseUpdate(); + + // Callback to handle safe frame size changes. + void HandleSafeFrameChanged(); + + // Callback to handle controller connection changes. + void HandleControllerConnectionChange(bool bIsConnection, FPlatformUserId Unused, int32 GameUserIndex); + + // Callback to handle controller pairing changes. + FReply OnPairingUsePreviousProfile(); + + // Callback to handle controller pairing changes. + FReply OnPairingUseNewProfile(); + + // Callback to handle controller pairing changes. + void HandleControllerPairingChanged(int GameUserIndex, FControllerPairingChangedUserInfo PreviousUserInfo, FControllerPairingChangedUserInfo NewUserInfo); + + // Handle confirming the controller disconnected dialog. + FReply OnControllerReconnectConfirm(); + +protected: + bool HandleOpenCommand(const TCHAR* Cmd, FOutputDevice& Ar, UWorld* InWorld); + bool HandleDisconnectCommand(const TCHAR* Cmd, FOutputDevice& Ar, UWorld* InWorld); + bool HandleTravelCommand(const TCHAR* Cmd, FOutputDevice& Ar, UWorld* InWorld); +}; + + diff --git a/Source/ShooterGame/Public/ShooterGameUserSettings.h b/Source/ShooterGame/Public/ShooterGameUserSettings.h new file mode 100644 index 0000000..2a90f64 --- /dev/null +++ b/Source/ShooterGame/Public/ShooterGameUserSettings.h @@ -0,0 +1,78 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterGameUserSettings.generated.h" + +UCLASS() +class UShooterGameUserSettings : public UGameUserSettings +{ + GENERATED_UCLASS_BODY() + + /** Applies all current user settings to the game and saves to permanent storage (e.g. file), optionally checking for command line overrides. */ + virtual void ApplySettings(bool bCheckForCommandLineOverrides) override; + + int32 GetGraphicsQuality() const + { + return GraphicsQuality; + } + + void SetGraphicsQuality(int32 InGraphicsQuality) + { + GraphicsQuality = InGraphicsQuality; + } + + bool IsLanMatch() const + { + return bIsLanMatch; + } + + bool IsForceSystemResolution() const + { + return bIsForceSystemResolution; + } + + void SetLanMatch(bool InbIsLanMatch) + { + bIsLanMatch = InbIsLanMatch; + } + + bool IsDedicatedServer() const + { + return bIsDedicatedServer; + } + + void SetDedicatedServer(bool InbIsDedicatedServer) + { + bIsDedicatedServer = InbIsDedicatedServer; + } + + void SetForceSystemResolution(bool InbIsForceSystemResolution) + { + bIsForceSystemResolution = InbIsForceSystemResolution; + } + + // interface UGameUserSettings + virtual void SetToDefaults() override; + +private: + /** + * Graphics Quality + * 0 = Low + * 1 = High + */ + UPROPERTY(config) + int32 GraphicsQuality; + + /** is lan match? */ + UPROPERTY(config) + bool bIsLanMatch; + + /** is dedicated server? */ + UPROPERTY(config) + bool bIsDedicatedServer; + + /** Enable if UShooterGameUserSettings is not the authority on resolution */ + UPROPERTY(config) + bool bIsForceSystemResolution; +}; diff --git a/Source/ShooterGame/Public/ShooterGameViewportClient.h b/Source/ShooterGame/Public/ShooterGameViewportClient.h new file mode 100644 index 0000000..0958228 --- /dev/null +++ b/Source/ShooterGame/Public/ShooterGameViewportClient.h @@ -0,0 +1,94 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterTypes.h" +#include "ShooterGameViewportClient.generated.h" + +class SShooterConfirmationDialog; + +struct FShooterGameLoadingScreenBrush : public FSlateDynamicImageBrush, public FGCObject +{ + FShooterGameLoadingScreenBrush( const FName InTextureName, const FVector2D& InImageSize ) + : FSlateDynamicImageBrush( InTextureName, InImageSize ) + { + SetResourceObject(LoadObject( nullptr, *InTextureName.ToString() )); + } + + virtual void AddReferencedObjects(FReferenceCollector& Collector) + { + FSlateBrush::AddReferencedObjects(Collector); + } +}; + +class SShooterLoadingScreen : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterLoadingScreen) {} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + +private: + EVisibility GetLoadIndicatorVisibility() const + { + return EVisibility::Visible; + } + + /** loading screen image brush */ + TSharedPtr LoadingScreenBrush; +}; + +UCLASS(Within=Engine, transient, config=Engine) +class UShooterGameViewportClient : public UGameViewportClient +{ + GENERATED_UCLASS_BODY() + +public: + + // start UGameViewportClient interface + void NotifyPlayerAdded( int32 PlayerIndex, ULocalPlayer* AddedPlayer ) override; + void AddViewportWidgetContent( TSharedRef ViewportContent, const int32 ZOrder = 0 ) override; + void RemoveViewportWidgetContent( TSharedRef ViewportContent ) override; + + void ShowDialog(TWeakObjectPtr PlayerOwner, EShooterDialogType::Type DialogType, const FText& Message, const FText& Confirm, const FText& Cancel, const FOnClicked& OnConfirm, const FOnClicked& OnCancel); + void HideDialog(); + + void ShowLoadingScreen(); + void HideLoadingScreen(); + + bool IsShowingDialog() const { return DialogWidget.IsValid(); } + + EShooterDialogType::Type GetDialogType() const; + TWeakObjectPtr GetDialogOwner() const; + + TSharedPtr GetDialogWidget() { return DialogWidget; } + + //FTicker Funcs + virtual void Tick(float DeltaSeconds) override; + + virtual void BeginDestroy() override; + virtual void DetachViewportClient() override; + void ReleaseSlateResources(); + +#if WITH_EDITOR + virtual void DrawTransition(class UCanvas* Canvas) override; +#endif //WITH_EDITOR + // end UGameViewportClient interface + +protected: + void HideExistingWidgets(); + void ShowExistingWidgets(); + + /** List of viewport content that the viewport is tracking */ + TArray> ViewportContentStack; + + TArray> HiddenViewportContentStack; + + TSharedPtr OldFocusWidget; + + /** Dialog widget to show temporary messages ("Controller disconnected", "Parental Controls don't allow you to play online", etc) */ + TSharedPtr DialogWidget; + + TSharedPtr LoadingScreenWidget; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/ShooterTeamStart.h b/Source/ShooterGame/Public/ShooterTeamStart.h new file mode 100644 index 0000000..d8b1cbf --- /dev/null +++ b/Source/ShooterGame/Public/ShooterTeamStart.h @@ -0,0 +1,23 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterTeamStart.generated.h" + +UCLASS() +class AShooterTeamStart : public APlayerStart +{ + GENERATED_UCLASS_BODY() + + /** Which team can start at this point */ + UPROPERTY(EditInstanceOnly, Category=Team) + int32 SpawnTeam; + + /** Whether players can start at this point */ + UPROPERTY(EditInstanceOnly, Category=Team) + uint32 bNotForPlayers:1; + + /** Whether bots can start at this point */ + UPROPERTY(EditInstanceOnly, Category=Team) + uint32 bNotForBots:1; +}; diff --git a/Source/ShooterGame/Public/ShooterTypes.h b/Source/ShooterGame/Public/ShooterTypes.h new file mode 100644 index 0000000..d4440eb --- /dev/null +++ b/Source/ShooterGame/Public/ShooterTypes.h @@ -0,0 +1,162 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterTypes.generated.h" +#pragma once + +namespace EShooterMatchState +{ + enum Type + { + Warmup, + Playing, + Won, + Lost, + }; +} + +namespace EShooterCrosshairDirection +{ + enum Type + { + Left=0, + Right=1, + Top=2, + Bottom=3, + Center=4 + }; +} + +namespace EShooterHudPosition +{ + enum Type + { + Left=0, + FrontLeft=1, + Front=2, + FrontRight=3, + Right=4, + BackRight=5, + Back=6, + BackLeft=7, + }; +} + +/** keep in sync with ShooterImpactEffect */ +UENUM() +namespace EShooterPhysMaterialType +{ + enum Type + { + Unknown, + Concrete, + Dirt, + Water, + Metal, + Wood, + Grass, + Glass, + Flesh, + }; +} + +namespace EShooterDialogType +{ + enum Type + { + None, + Generic, + ControllerDisconnected + }; +} + +#define SHOOTER_SURFACE_Default SurfaceType_Default +#define SHOOTER_SURFACE_Concrete SurfaceType1 +#define SHOOTER_SURFACE_Dirt SurfaceType2 +#define SHOOTER_SURFACE_Water SurfaceType3 +#define SHOOTER_SURFACE_Metal SurfaceType4 +#define SHOOTER_SURFACE_Wood SurfaceType5 +#define SHOOTER_SURFACE_Grass SurfaceType6 +#define SHOOTER_SURFACE_Glass SurfaceType7 +#define SHOOTER_SURFACE_Flesh SurfaceType8 + +USTRUCT() +struct FDecalData +{ + GENERATED_USTRUCT_BODY() + + /** material */ + UPROPERTY(EditDefaultsOnly, Category=Decal) + UMaterial* DecalMaterial; + + /** quad size (width & height) */ + UPROPERTY(EditDefaultsOnly, Category=Decal) + float DecalSize; + + /** lifespan */ + UPROPERTY(EditDefaultsOnly, Category=Decal) + float LifeSpan; + + /** defaults */ + FDecalData() + : DecalMaterial(nullptr) + , DecalSize(256.f) + , LifeSpan(10.f) + { + } +}; + +/** replicated information on a hit we've taken */ +USTRUCT() +struct FTakeHitInfo +{ + GENERATED_USTRUCT_BODY() + + /** The amount of damage actually applied */ + UPROPERTY() + float ActualDamage; + + /** The damage type we were hit with. */ + UPROPERTY() + UClass* DamageTypeClass; + + /** Who hit us */ + UPROPERTY() + TWeakObjectPtr PawnInstigator; + + /** Who actually caused the damage */ + UPROPERTY() + TWeakObjectPtr DamageCauser; + + /** Specifies which DamageEvent below describes the damage received. */ + UPROPERTY() + int32 DamageEventClassID; + + /** Rather this was a kill */ + UPROPERTY() + uint32 bKilled:1; + +private: + + /** A rolling counter used to ensure the struct is dirty and will replicate. */ + UPROPERTY() + uint8 EnsureReplicationByte; + + /** Describes general damage. */ + UPROPERTY() + FDamageEvent GeneralDamageEvent; + + /** Describes point damage, if that is what was received. */ + UPROPERTY() + FPointDamageEvent PointDamageEvent; + + /** Describes radial damage, if that is what was received. */ + UPROPERTY() + FRadialDamageEvent RadialDamageEvent; + +public: + FTakeHitInfo(); + + FDamageEvent& GetDamageEvent(); + void SetDamageEvent(const FDamageEvent& DamageEvent); + void EnsureReplication(); +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Sound/SoundNodeLocalPlayer.h b/Source/ShooterGame/Public/Sound/SoundNodeLocalPlayer.h new file mode 100644 index 0000000..913de16 --- /dev/null +++ b/Source/ShooterGame/Public/Sound/SoundNodeLocalPlayer.h @@ -0,0 +1,34 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Sound/SoundNode.h" +#include "SoundNodeLocalPlayer.generated.h" + +/** + * Choose different branch for sounds attached to locally controlled player + */ +UCLASS(hidecategories=Object, editinlinenew) +class USoundNodeLocalPlayer : public USoundNode +{ + GENERATED_UCLASS_BODY() + + // Begin USoundNode interface. + virtual void ParseNodes( FAudioDevice* AudioDevice, const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound, const FSoundParseParameters& ParseParams, TArray& WaveInstances ) override; + virtual int32 GetMaxChildNodes() const override; + virtual int32 GetMinChildNodes() const override; +#if WITH_EDITOR + virtual FText GetInputPinName(int32 PinIndex) const override; +#endif + // End USoundNode interface. + + static TMap& GetLocallyControlledActorCache() + { + check(IsInAudioThread()); + return LocallyControlledActorCache; + } + +private: + + static TMap LocallyControlledActorCache; +}; diff --git a/Source/ShooterGame/Public/Tests/ShooterTestControllerBase.h b/Source/ShooterGame/Public/Tests/ShooterTestControllerBase.h new file mode 100644 index 0000000..5dce4a8 --- /dev/null +++ b/Source/ShooterGame/Public/Tests/ShooterTestControllerBase.h @@ -0,0 +1,77 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#pragma once + +#include "GauntletTestController.h" +#include "ShooterGameInstance.h" +#include "SharedPointer.h" +#include "OnlineSessionSettings.h" +#include "ShooterTestControllerBase.generated.h" + +// Based on LOGIN_REQUIRED_FOR_ONLINE_PLAY in ShooterMainMenu.cpp +#if PLATFORM_SWITCH +#define LOGIN_REQUIRED_FOR_ONLINE_PLAY 1 +#else +#define LOGIN_REQUIRED_FOR_ONLINE_PLAY 0 +#endif + +UCLASS() +class UShooterTestControllerBase : public UGauntletTestController, public TSharedFromThis +{ + GENERATED_BODY() + +public: + virtual void OnInit() override; + virtual void OnPostMapChange(UWorld* World) override; + +protected: + // Login + uint8 bIsLoggedIn : 1; + uint8 bIsLoggingIn : 1; + FDelegateHandle OnLoginCompleteDelegateHandle; + + // Quick Match + uint8 bInQuickMatchSearch : 1; + uint8 bFoundQuickMatchGame : 1; + TSharedPtr QuickMatchSearchSettings; + + // Game Search + uint8 bIsSearchingForGame : 1; + uint8 bFoundGame : 1; + + // Match Cycling + uint8 NumOfCycledMatches; + uint8 TargetNumOfCycledMatches; + + virtual void OnTick(float TimeDelta) override; + + // Login + virtual void StartPlayerLoginProcess(); + virtual void OnLoginUIClosed(TSharedPtr UniqueId, const int ControllerIndex, const FOnlineError& Error = FOnlineError()); + virtual void OnUserCanPlay(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults); + + virtual void StartLoginTask(); + virtual void OnLoginTaskComplete(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error); + + virtual void StartOnlinePrivilegeTask(); + virtual void OnUserCanPlayOnline(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults); + + void CheckApplicationLicenseValid(); + + // Host Game + virtual void HostGame(); + + // Quick Match + virtual void StartQuickMatch(); + void OnMatchmakingComplete(FName SessionName, const FOnlineError& ErrorDetails, const FSessionMatchmakingResults& Results); + + // Game Search + virtual void StartSearchingForGame(); + virtual void UpdateSearchStatus(); + + // Helper Functions + virtual UShooterGameInstance* GetGameInstance() const; + virtual const FName GetGameInstanceState() const; + virtual AShooterGameSession* GetGameSession() const; + virtual bool IsInGame() const; + virtual ULocalPlayer* GetFirstLocalPlayer() const; +}; diff --git a/Source/ShooterGame/Public/Tests/ShooterTestControllerBasicDedicatedServerTest.h b/Source/ShooterGame/Public/Tests/ShooterTestControllerBasicDedicatedServerTest.h new file mode 100644 index 0000000..3af0c79 --- /dev/null +++ b/Source/ShooterGame/Public/Tests/ShooterTestControllerBasicDedicatedServerTest.h @@ -0,0 +1,17 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#pragma once + +#include "Tests/ShooterTestControllerBase.h" +#include "ShooterTestControllerBasicDedicatedServerTest.generated.h" + +UCLASS() +class UShooterTestControllerBasicDedicatedServerTest : public UShooterTestControllerBase +{ + GENERATED_BODY() + +protected: + virtual void OnTick(float TimeDelta) override; + +public: + virtual void OnPostMapChange(UWorld* World) override; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Tests/ShooterTestControllerBootTest.h b/Source/ShooterGame/Public/Tests/ShooterTestControllerBootTest.h new file mode 100644 index 0000000..dd41d41 --- /dev/null +++ b/Source/ShooterGame/Public/Tests/ShooterTestControllerBootTest.h @@ -0,0 +1,18 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#pragma once + +#include "GauntletTestControllerBootTest.h" +#include "ShooterTestControllerBootTest.generated.h" + +UCLASS() +class UShooterTestControllerBootTest : public UGauntletTestControllerBootTest +{ + GENERATED_BODY() + +protected: + + // This test needs a delay as the test can be over before focus is returned to Gauntlet after launching the game on XBox. + // This can cause the test to be over before Gauntlet can even know that it is running and will cause the test to fail. + const double TestDelay = 20.0f; + virtual bool IsBootProcessComplete() const override; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Tests/ShooterTestControllerDedicatedServerTest.h b/Source/ShooterGame/Public/Tests/ShooterTestControllerDedicatedServerTest.h new file mode 100644 index 0000000..66d1ae6 --- /dev/null +++ b/Source/ShooterGame/Public/Tests/ShooterTestControllerDedicatedServerTest.h @@ -0,0 +1,15 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#pragma once + +#include "ShooterTestControllerBase.h" +#include "SharedPointer.h" +#include "ShooterTestControllerDedicatedServerTest.generated.h" + +UCLASS() +class UShooterTestControllerDedicatedServerTest : public UShooterTestControllerBase +{ + GENERATED_BODY() + +protected: + virtual void OnTick(float TimeDelta) override; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Tests/ShooterTestControllerListenServerClient.h b/Source/ShooterGame/Public/Tests/ShooterTestControllerListenServerClient.h new file mode 100644 index 0000000..d2995f6 --- /dev/null +++ b/Source/ShooterGame/Public/Tests/ShooterTestControllerListenServerClient.h @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#pragma once + +#include "ShooterTestControllerBase.h" +#include "ShooterTestControllerListenServerClient.generated.h" + +UCLASS() +class UShooterTestControllerListenServerClient : public UShooterTestControllerBase +{ + GENERATED_BODY() + +protected: + virtual void OnTick(float TimeDelta) override; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Tests/ShooterTestControllerListenServerHost.h b/Source/ShooterGame/Public/Tests/ShooterTestControllerListenServerHost.h new file mode 100644 index 0000000..cf388dd --- /dev/null +++ b/Source/ShooterGame/Public/Tests/ShooterTestControllerListenServerHost.h @@ -0,0 +1,17 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#pragma once + +#include "ShooterTestControllerBase.h" +#include "ShooterTestControllerListenServerHost.generated.h" + +UCLASS() +class UShooterTestControllerListenServerHost : public UShooterTestControllerBase +{ + GENERATED_BODY() + +public: + virtual void OnPostMapChange(UWorld* World) override {} + +protected: + virtual void OnUserCanPlayOnline(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults) override; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Tests/ShooterTestControllerListenServerQuickMatchClient.h b/Source/ShooterGame/Public/Tests/ShooterTestControllerListenServerQuickMatchClient.h new file mode 100644 index 0000000..a5baddc --- /dev/null +++ b/Source/ShooterGame/Public/Tests/ShooterTestControllerListenServerQuickMatchClient.h @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc.All Rights Reserved. +#pragma once + +#include "ShooterTestControllerBase.h" +#include "ShooterTestControllerListenServerQuickMatchClient.generated.h" + +UCLASS() +class UShooterTestControllerListenServerQuickMatchClient : public UShooterTestControllerBase +{ + GENERATED_BODY() + +protected: + virtual void OnTick(float TimeDelta) override; +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/UI/ShooterHUD.h b/Source/ShooterGame/Public/UI/ShooterHUD.h new file mode 100644 index 0000000..cdc392a --- /dev/null +++ b/Source/ShooterGame/Public/UI/ShooterHUD.h @@ -0,0 +1,383 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterTypes.h" +#include "ShooterHUD.generated.h" + +struct FHitData +{ + /** Last hit time. */ + float HitTime; + + /** strength of hit icon */ + float HitPercentage; + + /** Initialise defaults. */ + FHitData() + { + HitTime = 0.0f; + HitPercentage = 0.0f; + } +}; + +struct FDeathMessage +{ + /** Name of player scoring kill. */ + FString KillerDesc; + + /** Name of killed player. */ + FString VictimDesc; + + /** Killer is local player. */ + uint8 bKillerIsOwner : 1; + + /** Victim is local player. */ + uint8 bVictimIsOwner : 1; + + /** Team number of the killer. */ + int32 KillerTeamNum; + + /** Team number of the victim. */ + int32 VictimTeamNum; + + /** timestamp for removing message */ + float HideTime; + + /** What killed the player. */ + TWeakObjectPtr DamageType; + + /** Initialise defaults. */ + FDeathMessage() + : bKillerIsOwner(false) + , bVictimIsOwner(false) + , KillerTeamNum(0) + , VictimTeamNum(0) + , HideTime(0.f) + { + } +}; + +UCLASS() +class AShooterHUD : public AHUD +{ + GENERATED_UCLASS_BODY() + +public: + + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** Main HUD update loop. */ + virtual void DrawHUD() override; + + /** + * Sent from pawn hit, used to calculate hit notification overlay for drawing. + * + * @param DamageTaken The amount of damage. + * @param DamageEvent The actual damage event. + * @param PawnInstigator The pawn that did the damage. + */ + void NotifyWeaponHit(float DamageTaken, struct FDamageEvent const& DamageEvent, class APawn* PawnInstigator); + + /** Sent from ShooterWeapon, shows NO AMMO text. */ + void NotifyOutOfAmmo(); + + /** Notifies we have hit the enemy. */ + void NotifyEnemyHit(); + + /** + * Set state of current match. + * + * @param NewState The new match state. + */ + void SetMatchState(EShooterMatchState::Type NewState); + + /** Get state of current match. */ + EShooterMatchState::Type GetMatchState() const; + + /** Turns off scoreboard if it is being displayed */ + void ConditionalCloseScoreboard(bool bFocus = false); + + /** Toggles scoreboard */ + void ToggleScoreboard(); + + /** + * Toggles in game scoreboard. + * Note:Will not display if the game menu is visible. + + * @param bEnable Required scoreboard display state. + * @param bFocus Give keyboard focus to the scoreboard. + * @return true, if the scoreboard visibility changed + */ + bool ShowScoreboard(bool bEnable, bool bFocus = false); + + /** + * Add death message. + * + * @param KillerPlayerState Player that did the killings state. + * @param VictimPlayerState Played that was killed state. + * @param KillerDamageType The type of damaged that caused the death. + */ + void ShowDeathMessage(class AShooterPlayerState* KillerPlayerState, class AShooterPlayerState* VictimPlayerState, const UDamageType* KillerDamageType); + + /* + * Toggle chat window visibility. + * + */ + void ToggleChat(); + + /** + * Set chat window visibility. + * + * @param RequiredVisibility The required visibility. + */ + void SetChatVisibilty( const EVisibility RequiredVisibility ); + + /* + * Add a string to the chat window. + * + * @param ChatString The string to add. + * @param bWantFocus Should we set the chat window to focus + */ + void AddChatLine(const FText& ChatString, bool bWantFocus); + + /* Is the match over (IE Is the state Won or Lost). */ + bool IsMatchOver() const; + +protected: + /** Floor for automatic hud scaling. */ + static const float MinHudScale; + + /** Lighter HUD color. */ + FColor HUDLight; + + /** Darker HUD color. */ + FColor HUDDark; + + /** When we got last notice about out of ammo. */ + float NoAmmoNotifyTime; + + /** How long notice is fading out. */ + float NoAmmoFadeOutTime; + + /** Most recent hit time, used to check if we need to draw hit indicator at all. */ + float LastHitTime; + + /** How long till hit notify fades out completely. */ + float HitNotifyDisplayTime; + + /** When we last time hit the enemy. */ + float LastEnemyHitTime; + + /** How long till enemy hit notify fades out completely. */ + float LastEnemyHitDisplayTime; + + /** Icons for hit indicator. */ + UPROPERTY() + FCanvasIcon HitNotifyIcon[8]; + + /** kills background icon. */ + UPROPERTY() + FCanvasIcon KillsBg; + + /** Match timer and player position background icon. */ + UPROPERTY() + FCanvasIcon TimePlaceBg; + + /** Primary weapon background icon. */ + UPROPERTY() + FCanvasIcon PrimaryWeapBg; + + /** Secondary weapon background icon */ + UPROPERTY() + FCanvasIcon SecondaryWeapBg; + + /** Crosshair icons (left, top, right, bottom and center). */ + UPROPERTY() + FCanvasIcon Crosshair[5]; + + /** On crosshair indicator that we hit someone. */ + UPROPERTY() + FCanvasIcon HitNotifyCrosshair; + + /** Death messages background icon. */ + UPROPERTY() + FCanvasIcon DeathMessagesBg; + + /** Health bar background icon. */ + UPROPERTY() + FCanvasIcon HealthBarBg; + + /** Health bar icon. */ + UPROPERTY() + FCanvasIcon HealthBar; + + /** Health icon on the health bar. */ + UPROPERTY() + FCanvasIcon HealthIcon; + + /** Kills icon. */ + UPROPERTY() + FCanvasIcon KillsIcon; + + /** Bigger killed icon. */ + UPROPERTY() + FCanvasIcon KilledIcon; + + /** Timer icon. */ + UPROPERTY() + FCanvasIcon TimerIcon; + + /** Podium icon. */ + UPROPERTY() + FCanvasIcon PlaceIcon; + + /** UI scaling factor for other resolutions than Full HD. */ + float ScaleUI; + + /** Current animation pulse value. */ + float PulseValue; + + /** FontRenderInfo enabling casting shadow.s */ + FFontRenderInfo ShadowedFont; + + /** Big "KILLED [PLAYER]" message text above the crosshair. */ + FText CenteredKillMessage; + + /** last time we killed someone. */ + float LastKillTime; + + /** How long the message will fade out. */ + float KillFadeOutTime; + + /** Offsets to display hit indicator parts. */ + FVector2D Offsets[8]; + + /** Texture for hit indicator. */ + UPROPERTY() + UTexture2D* HitNotifyTexture; + + /** texture for HUD elements. */ + UPROPERTY() + UTexture2D* HUDMainTexture; + + /** Texture for HUD elements. */ + UPROPERTY() + UTexture2D* HUDAssets02Texture; + + /** Overlay shown when health is low. */ + UPROPERTY() + UTexture2D* LowHealthOverlayTexture; + + /** Large font - used for ammo display etc. */ + UPROPERTY() + UFont* BigFont; + + /** Normal font - used for death messages and such. */ + UPROPERTY() + UFont* NormalFont; + + /** General offset for HUD elements. */ + float Offset; + + /** Runtime data for hit indicator. */ + FHitData HitNotifyData[8]; + + /** Active death messages. */ + TArray DeathMessages; + + /** State of match. */ + EShooterMatchState::Type MatchState; + + /** Is the scoreboard widget on screen? */ + uint32 bIsScoreBoardVisible:1; + + /** Scoreboard widget. */ + TSharedPtr ScoreboardWidget; + + /** Scoreboard widget overlay. */ + TSharedPtr ScoreboardWidgetOverlay; + + /** Scoreboard widget container - used for removing */ + TSharedPtr ScoreboardWidgetContainer; + + /** Chatbox widget. */ + TSharedPtr ChatWidget; + + /** Array of information strings to render (Waiting to respawn etc) */ + TArray InfoItems; + + /** Called every time game is started. */ + virtual void PostInitializeComponents() override; + + /** + * Converts floating point seconds to MM:SS string. + * + * @param TimeSeconds The time to get a string for. + */ + FString GetTimeString(float TimeSeconds); + + /** Draws weapon HUD. */ + void DrawWeaponHUD(); + + /** Draws kills information. */ + void DrawKills(); + + /** Draw player's health bar. */ + void DrawHealth(); + + /** Draws match timer and player position. */ + void DrawMatchTimerAndPosition(); + + /** Draws weapon crosshair. */ + void DrawCrosshair(); + + /** Draws hit indicator. */ + void DrawHitIndicator(); + + /** Draw death messages. */ + void DrawDeathMessages(); + + /** Delegate for telling other methods when players have started/stopped talking */ + FOnPlayerTalkingStateChangedDelegate OnPlayerTalkingStateChangedDelegate; + void OnPlayerTalkingStateChanged(TSharedRef TalkingPlayerId, bool bIsTalking); + + /* + * Draw the most recently killed player if needed + * + * @return Returns the bottom of the message or 0 if not drawn. + */ + float DrawRecentlyKilledPlayer(); + + /** Temporary helper for drawing text-in-a-box. */ + void DrawDebugInfoString(const FString& Text, float PosX, float PosY, bool bAlignLeft, bool bAlignTop, const FColor& TextColor); + + /** helper for getting uv coords in normalized top,left, bottom, right format */ + void MakeUV(FCanvasIcon& Icon, FVector2D& UV0, FVector2D& UV1, uint16 U, uint16 V, uint16 UL, uint16 VL); + + /* + * Create the chat widget if it doesn't already exist. + * + * @return true if the widget was created. + */ + bool TryCreateChatWidget(); + + /* + * Add information string that will be displayed on the hud. They are added as required and rendered together to prevent overlaps + * + * @param InInfoString InInfoString + */ + void AddMatchInfoString(const FCanvasTextItem InfoItem); + + /* + * Render the info messages. + * + * @param YOffset YOffset from top of canvas to start drawing the messages + * @param ScaleUI UI Scale factor + * @param TextScale Text scale factor + * + * @returns The next Y position to draw any further strings + */ + float ShowInfoItems(float YOffset, float TextScale); + +}; \ No newline at end of file diff --git a/Source/ShooterGame/Public/Weapons/ShooterDamageType.h b/Source/ShooterGame/Public/Weapons/ShooterDamageType.h new file mode 100644 index 0000000..2242a1f --- /dev/null +++ b/Source/ShooterGame/Public/Weapons/ShooterDamageType.h @@ -0,0 +1,27 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterDamageType.generated.h" + +// DamageType class that specifies an icon to display +UCLASS(const, Blueprintable, BlueprintType) +class UShooterDamageType : public UDamageType +{ + GENERATED_UCLASS_BODY() + + /** icon displayed in death messages log when killed with this weapon */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + FCanvasIcon KillIcon; + + /** force feedback effect to play on a player hit by this damage type */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + UForceFeedbackEffect *HitForceFeedback; + + /** force feedback effect to play on a player killed by this damage type */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + UForceFeedbackEffect *KilledForceFeedback; +}; + + + diff --git a/Source/ShooterGame/Public/Weapons/ShooterProjectile.h b/Source/ShooterGame/Public/Weapons/ShooterProjectile.h new file mode 100644 index 0000000..0597948 --- /dev/null +++ b/Source/ShooterGame/Public/Weapons/ShooterProjectile.h @@ -0,0 +1,75 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameFramework/Actor.h" +#include "ShooterWeapon_Projectile.h" +#include "ShooterProjectile.generated.h" + +class UProjectileMovementComponent; +class USphereComponent; + +// +UCLASS(Abstract, Blueprintable) +class AShooterProjectile : public AActor +{ + GENERATED_UCLASS_BODY() + + /** initial setup */ + virtual void PostInitializeComponents() override; + + /** setup velocity */ + void InitVelocity(FVector& ShootDirection); + + /** handle hit */ + UFUNCTION() + void OnImpact(const FHitResult& HitResult); + +private: + /** movement component */ + UPROPERTY(VisibleDefaultsOnly, Category=Projectile) + UProjectileMovementComponent* MovementComp; + + /** collisions */ + UPROPERTY(VisibleDefaultsOnly, Category=Projectile) + USphereComponent* CollisionComp; + + UPROPERTY(VisibleDefaultsOnly, Category=Projectile) + UParticleSystemComponent* ParticleComp; +protected: + + /** effects for explosion */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + TSubclassOf ExplosionTemplate; + + /** controller that fired me (cache for damage calculations) */ + TWeakObjectPtr MyController; + + /** projectile data */ + struct FProjectileWeaponData WeaponConfig; + + /** did it explode? */ + UPROPERTY(Transient, ReplicatedUsing=OnRep_Exploded) + bool bExploded; + + /** [client] explosion happened */ + UFUNCTION() + void OnRep_Exploded(); + + /** trigger explosion */ + void Explode(const FHitResult& Impact); + + /** shutdown projectile and prepare for destruction */ + void DisableAndDestroy(); + + /** update velocity on client */ + virtual void PostNetReceiveVelocity(const FVector& NewVelocity) override; + +protected: + /** Returns MovementComp subobject **/ + FORCEINLINE UProjectileMovementComponent* GetMovementComp() const { return MovementComp; } + /** Returns CollisionComp subobject **/ + FORCEINLINE USphereComponent* GetCollisionComp() const { return CollisionComp; } + /** Returns ParticleComp subobject **/ + FORCEINLINE UParticleSystemComponent* GetParticleComp() const { return ParticleComp; } +}; diff --git a/Source/ShooterGame/Public/Weapons/ShooterWeapon.h b/Source/ShooterGame/Public/Weapons/ShooterWeapon.h new file mode 100644 index 0000000..f366fb9 --- /dev/null +++ b/Source/ShooterGame/Public/Weapons/ShooterWeapon.h @@ -0,0 +1,539 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameFramework/Actor.h" +#include "Engine/Canvas.h" // for FCanvasIcon +#include "ShooterWeapon.generated.h" + +class UAnimMontage; +class AShooterCharacter; +class UAudioComponent; +class UParticleSystemComponent; +class UForceFeedbackEffect; +class USoundCue; +class UMatineeCameraShake; + +namespace EWeaponState +{ + enum Type + { + Idle, + Firing, + Reloading, + Equipping, + }; +} + +USTRUCT() +struct FWeaponData +{ + GENERATED_USTRUCT_BODY() + + /** inifite ammo for reloads */ + UPROPERTY(EditDefaultsOnly, Category=Ammo) + bool bInfiniteAmmo; + + /** infinite ammo in clip, no reload required */ + UPROPERTY(EditDefaultsOnly, Category=Ammo) + bool bInfiniteClip; + + /** max ammo */ + UPROPERTY(EditDefaultsOnly, Category=Ammo) + int32 MaxAmmo; + + /** clip size */ + UPROPERTY(EditDefaultsOnly, Category=Ammo) + int32 AmmoPerClip; + + /** initial clips */ + UPROPERTY(EditDefaultsOnly, Category=Ammo) + int32 InitialClips; + + /** time between two consecutive shots */ + UPROPERTY(EditDefaultsOnly, Category=WeaponStat) + float TimeBetweenShots; + + /** failsafe reload duration if weapon doesn't have any animation for it */ + UPROPERTY(EditDefaultsOnly, Category=WeaponStat) + float NoAnimReloadDuration; + + /** defaults */ + FWeaponData() + { + bInfiniteAmmo = false; + bInfiniteClip = false; + MaxAmmo = 100; + AmmoPerClip = 20; + InitialClips = 4; + TimeBetweenShots = 0.2f; + NoAnimReloadDuration = 1.0f; + } +}; + +USTRUCT() +struct FWeaponAnim +{ + GENERATED_USTRUCT_BODY() + + /** animation played on pawn (1st person view) */ + UPROPERTY(EditDefaultsOnly, Category=Animation) + UAnimMontage* Pawn1P; + + /** animation played on pawn (3rd person view) */ + UPROPERTY(EditDefaultsOnly, Category=Animation) + UAnimMontage* Pawn3P; +}; + +UCLASS(Abstract, Blueprintable) +class AShooterWeapon : public AActor +{ + GENERATED_UCLASS_BODY() + + /** perform initial setup */ + virtual void PostInitializeComponents() override; + + virtual void Destroyed() override; + + ////////////////////////////////////////////////////////////////////////// + // Ammo + + enum class EAmmoType + { + EBullet, + ERocket, + EMax, + }; + + /** [server] add ammo */ + void GiveAmmo(int AddAmount); + + /** consume a bullet */ + void UseAmmo(); + + /** query ammo type */ + virtual EAmmoType GetAmmoType() const + { + return EAmmoType::EBullet; + } + + ////////////////////////////////////////////////////////////////////////// + // Inventory + + /** weapon is being equipped by owner pawn */ + virtual void OnEquip(const AShooterWeapon* LastWeapon); + + /** weapon is now equipped by owner pawn */ + virtual void OnEquipFinished(); + + /** weapon is holstered by owner pawn */ + virtual void OnUnEquip(); + + /** [server] weapon was added to pawn's inventory */ + virtual void OnEnterInventory(AShooterCharacter* NewOwner); + + /** [server] weapon was removed from pawn's inventory */ + virtual void OnLeaveInventory(); + + /** check if it's currently equipped */ + bool IsEquipped() const; + + /** check if mesh is already attached */ + bool IsAttachedToPawn() const; + + + ////////////////////////////////////////////////////////////////////////// + // Input + + /** [local + server] start weapon fire */ + virtual void StartFire(); + + /** [local + server] stop weapon fire */ + virtual void StopFire(); + + /** [all] start weapon reload */ + virtual void StartReload(bool bFromReplication = false); + + /** [local + server] interrupt weapon reload */ + virtual void StopReload(); + + /** [server] performs actual reload */ + virtual void ReloadWeapon(); + + /** trigger reload from server */ + UFUNCTION(reliable, client) + void ClientStartReload(); + + + ////////////////////////////////////////////////////////////////////////// + // Control + + /** check if weapon can fire */ + bool CanFire() const; + + /** check if weapon can be reloaded */ + bool CanReload() const; + + + ////////////////////////////////////////////////////////////////////////// + // Reading data + + /** get current weapon state */ + EWeaponState::Type GetCurrentState() const; + + /** get current ammo amount (total) */ + int32 GetCurrentAmmo() const; + + /** get current ammo amount (clip) */ + int32 GetCurrentAmmoInClip() const; + + /** get clip size */ + int32 GetAmmoPerClip() const; + + /** get max ammo amount */ + int32 GetMaxAmmo() const; + + /** get weapon mesh (needs pawn owner to determine variant) */ + USkeletalMeshComponent* GetWeaponMesh() const; + + /** get pawn owner */ + UFUNCTION(BlueprintCallable, Category="Game|Weapon") + class AShooterCharacter* GetPawnOwner() const; + + /** icon displayed on the HUD when weapon is equipped as primary */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + FCanvasIcon PrimaryIcon; + + /** icon displayed on the HUD when weapon is secondary */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + FCanvasIcon SecondaryIcon; + + /** bullet icon used to draw current clip (left side) */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + FCanvasIcon PrimaryClipIcon; + + /** bullet icon used to draw secondary clip (left side) */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + FCanvasIcon SecondaryClipIcon; + + /** how many icons to draw per clip */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + float AmmoIconsCount; + + /** defines spacing between primary ammo icons (left side) */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + int32 PrimaryClipIconOffset; + + /** defines spacing between secondary ammo icons (left side) */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + int32 SecondaryClipIconOffset; + + /** crosshair parts icons (left, top, right, bottom and center) */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + FCanvasIcon Crosshair[5]; + + /** crosshair parts icons when targeting (left, top, right, bottom and center) */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + FCanvasIcon AimingCrosshair[5]; + + /** only use red colored center part of aiming crosshair */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + bool UseLaserDot; + + /** false = default crosshair */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + bool UseCustomCrosshair; + + /** false = use custom one if set, otherwise default crosshair */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + bool UseCustomAimingCrosshair; + + /** true - crosshair will not be shown unless aiming with the weapon */ + UPROPERTY(EditDefaultsOnly, Category=HUD) + bool bHideCrosshairWhileNotAiming; + + /** Adjustment to handle frame rate affecting actual timer interval. */ + UPROPERTY(Transient) + float TimerIntervalAdjustment; + + /** Whether to allow automatic weapons to catch up with shorter refire cycles */ + UPROPERTY(Config) + bool bAllowAutomaticWeaponCatchup = true; + + /** check if weapon has infinite ammo (include owner's cheats) */ + bool HasInfiniteAmmo() const; + + /** check if weapon has infinite clip (include owner's cheats) */ + bool HasInfiniteClip() const; + + /** set the weapon's owning pawn */ + void SetOwningPawn(AShooterCharacter* AShooterCharacter); + + /** gets last time when this weapon was switched to */ + float GetEquipStartedTime() const; + + /** gets the duration of equipping weapon*/ + float GetEquipDuration() const; + +protected: + + /** pawn owner */ + UPROPERTY(Transient, ReplicatedUsing=OnRep_MyPawn) + class AShooterCharacter* MyPawn; + + /** weapon data */ + UPROPERTY(EditDefaultsOnly, Category=Config) + FWeaponData WeaponConfig; + +private: + /** weapon mesh: 1st person view */ + UPROPERTY(VisibleDefaultsOnly, Category=Mesh) + USkeletalMeshComponent* Mesh1P; + + /** weapon mesh: 3rd person view */ + UPROPERTY(VisibleDefaultsOnly, Category=Mesh) + USkeletalMeshComponent* Mesh3P; +protected: + + /** firing audio (bLoopedFireSound set) */ + UPROPERTY(Transient) + UAudioComponent* FireAC; + + /** name of bone/socket for muzzle in weapon mesh */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + FName MuzzleAttachPoint; + + /** FX for muzzle flash */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + UParticleSystem* MuzzleFX; + + /** spawned component for muzzle FX */ + UPROPERTY(Transient) + UParticleSystemComponent* MuzzlePSC; + + /** spawned component for second muzzle FX (Needed for split screen) */ + UPROPERTY(Transient) + UParticleSystemComponent* MuzzlePSCSecondary; + + /** camera shake on firing */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + TSubclassOf FireCameraShake; + + /** force feedback effect to play when the weapon is fired */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + UForceFeedbackEffect *FireForceFeedback; + + /** single fire sound (bLoopedFireSound not set) */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* FireSound; + + /** looped fire sound (bLoopedFireSound set) */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* FireLoopSound; + + /** finished burst sound (bLoopedFireSound set) */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* FireFinishSound; + + /** out of ammo sound */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* OutOfAmmoSound; + + /** reload sound */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* ReloadSound; + + /** reload animations */ + UPROPERTY(EditDefaultsOnly, Category=Animation) + FWeaponAnim ReloadAnim; + + /** equip sound */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + USoundCue* EquipSound; + + /** equip animations */ + UPROPERTY(EditDefaultsOnly, Category=Animation) + FWeaponAnim EquipAnim; + + /** fire animations */ + UPROPERTY(EditDefaultsOnly, Category=Animation) + FWeaponAnim FireAnim; + + /** is muzzle FX looped? */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + uint32 bLoopedMuzzleFX : 1; + + /** is fire sound looped? */ + UPROPERTY(EditDefaultsOnly, Category=Sound) + uint32 bLoopedFireSound : 1; + + /** is fire animation looped? */ + UPROPERTY(EditDefaultsOnly, Category=Animation) + uint32 bLoopedFireAnim : 1; + + /** is fire animation playing? */ + uint32 bPlayingFireAnim : 1; + + /** is weapon currently equipped? */ + uint32 bIsEquipped : 1; + + /** is weapon fire active? */ + uint32 bWantsToFire : 1; + + /** is reload animation playing? */ + UPROPERTY(Transient, ReplicatedUsing=OnRep_Reload) + uint32 bPendingReload : 1; + + /** is equip animation playing? */ + uint32 bPendingEquip : 1; + + /** weapon is refiring */ + uint32 bRefiring; + + /** current weapon state */ + EWeaponState::Type CurrentState; + + /** time of last successful weapon fire */ + float LastFireTime; + + /** last time when this weapon was switched to */ + float EquipStartedTime; + + /** how much time weapon needs to be equipped */ + float EquipDuration; + + /** current total ammo */ + UPROPERTY(Transient, Replicated) + int32 CurrentAmmo; + + /** current ammo - inside clip */ + UPROPERTY(Transient, Replicated) + int32 CurrentAmmoInClip; + + /** burst counter, used for replicating fire events to remote clients */ + UPROPERTY(Transient, ReplicatedUsing=OnRep_BurstCounter) + int32 BurstCounter; + + /** Handle for efficient management of OnEquipFinished timer */ + FTimerHandle TimerHandle_OnEquipFinished; + + /** Handle for efficient management of StopReload timer */ + FTimerHandle TimerHandle_StopReload; + + /** Handle for efficient management of ReloadWeapon timer */ + FTimerHandle TimerHandle_ReloadWeapon; + + /** Handle for efficient management of HandleFiring timer */ + FTimerHandle TimerHandle_HandleFiring; + + ////////////////////////////////////////////////////////////////////////// + // Input - server side + + UFUNCTION(reliable, server, WithValidation) + void ServerStartFire(); + + UFUNCTION(reliable, server, WithValidation) + void ServerStopFire(); + + UFUNCTION(reliable, server, WithValidation) + void ServerStartReload(); + + UFUNCTION(reliable, server, WithValidation) + void ServerStopReload(); + + + ////////////////////////////////////////////////////////////////////////// + // Replication & effects + + UFUNCTION() + void OnRep_MyPawn(); + + UFUNCTION() + void OnRep_BurstCounter(); + + UFUNCTION() + void OnRep_Reload(); + + /** Called in network play to do the cosmetic fx for firing */ + virtual void SimulateWeaponFire(); + + /** Called in network play to stop cosmetic fx (e.g. for a looping shot). */ + virtual void StopSimulatingWeaponFire(); + + + ////////////////////////////////////////////////////////////////////////// + // Weapon usage + + /** [local] weapon specific fire implementation */ + virtual void FireWeapon() PURE_VIRTUAL(AShooterWeapon::FireWeapon,); + + /** [server] fire & update ammo */ + UFUNCTION(reliable, server, WithValidation) + void ServerHandleFiring(); + + /** [local + server] handle weapon refire, compensating for slack time if the timer can't sample fast enough */ + void HandleReFiring(); + + /** [local + server] handle weapon fire */ + void HandleFiring(); + + /** [local + server] firing started */ + virtual void OnBurstStarted(); + + /** [local + server] firing finished */ + virtual void OnBurstFinished(); + + /** update weapon state */ + void SetWeaponState(EWeaponState::Type NewState); + + /** determine current weapon state */ + void DetermineWeaponState(); + + + ////////////////////////////////////////////////////////////////////////// + // Inventory + + /** attaches weapon mesh to pawn's mesh */ + void AttachMeshToPawn(); + + /** detaches weapon mesh from pawn */ + void DetachMeshFromPawn(); + + + ////////////////////////////////////////////////////////////////////////// + // Weapon usage helpers + + /** play weapon sounds */ + UAudioComponent* PlayWeaponSound(USoundCue* Sound); + + /** play weapon animations */ + float PlayWeaponAnimation(const FWeaponAnim& Animation); + + /** stop playing weapon animations */ + void StopWeaponAnimation(const FWeaponAnim& Animation); + + /** Get the aim of the weapon, allowing for adjustments to be made by the weapon */ + virtual FVector GetAdjustedAim() const; + + /** Get the aim of the camera */ + FVector GetCameraAim() const; + + /** get the originating location for camera damage */ + FVector GetCameraDamageStartLocation(const FVector& AimDir) const; + + /** get the muzzle location of the weapon */ + FVector GetMuzzleLocation() const; + + /** get direction of weapon's muzzle */ + FVector GetMuzzleDirection() const; + + /** find hit */ + FHitResult WeaponTrace(const FVector& TraceFrom, const FVector& TraceTo) const; + +protected: + /** Returns Mesh1P subobject **/ + FORCEINLINE USkeletalMeshComponent* GetMesh1P() const { return Mesh1P; } + /** Returns Mesh3P subobject **/ + FORCEINLINE USkeletalMeshComponent* GetMesh3P() const { return Mesh3P; } +}; + diff --git a/Source/ShooterGame/Public/Weapons/ShooterWeapon_Instant.h b/Source/ShooterGame/Public/Weapons/ShooterWeapon_Instant.h new file mode 100644 index 0000000..22af64d --- /dev/null +++ b/Source/ShooterGame/Public/Weapons/ShooterWeapon_Instant.h @@ -0,0 +1,164 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterWeapon.h" +#include "ShooterWeapon_Instant.generated.h" + +class AShooterImpactEffect; + +USTRUCT() +struct FInstantHitInfo +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY() + FVector Origin; + + UPROPERTY() + float ReticleSpread; + + UPROPERTY() + int32 RandomSeed; +}; + +USTRUCT() +struct FInstantWeaponData +{ + GENERATED_USTRUCT_BODY() + + /** base weapon spread (degrees) */ + UPROPERTY(EditDefaultsOnly, Category=Accuracy) + float WeaponSpread; + + /** targeting spread modifier */ + UPROPERTY(EditDefaultsOnly, Category=Accuracy) + float TargetingSpreadMod; + + /** continuous firing: spread increment */ + UPROPERTY(EditDefaultsOnly, Category=Accuracy) + float FiringSpreadIncrement; + + /** continuous firing: max increment */ + UPROPERTY(EditDefaultsOnly, Category=Accuracy) + float FiringSpreadMax; + + /** weapon range */ + UPROPERTY(EditDefaultsOnly, Category=WeaponStat) + float WeaponRange; + + /** damage amount */ + UPROPERTY(EditDefaultsOnly, Category=WeaponStat) + int32 HitDamage; + + /** type of damage */ + UPROPERTY(EditDefaultsOnly, Category=WeaponStat) + TSubclassOf DamageType; + + /** hit verification: scale for bounding box of hit actor */ + UPROPERTY(EditDefaultsOnly, Category=HitVerification) + float ClientSideHitLeeway; + + /** hit verification: threshold for dot product between view direction and hit direction */ + UPROPERTY(EditDefaultsOnly, Category=HitVerification) + float AllowedViewDotHitDir; + + /** defaults */ + FInstantWeaponData() + { + WeaponSpread = 5.0f; + TargetingSpreadMod = 0.25f; + FiringSpreadIncrement = 1.0f; + FiringSpreadMax = 10.0f; + WeaponRange = 10000.0f; + HitDamage = 10; + DamageType = UDamageType::StaticClass(); + ClientSideHitLeeway = 200.0f; + AllowedViewDotHitDir = 0.8f; + } +}; + +// A weapon where the damage impact occurs instantly upon firing +UCLASS(Abstract) +class AShooterWeapon_Instant : public AShooterWeapon +{ + GENERATED_UCLASS_BODY() + + /** get current spread */ + float GetCurrentSpread() const; + +protected: + + virtual EAmmoType GetAmmoType() const override + { + return EAmmoType::EBullet; + } + + /** weapon config */ + UPROPERTY(EditDefaultsOnly, Category=Config) + FInstantWeaponData InstantConfig; + + /** impact effects */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + TSubclassOf ImpactTemplate; + + /** smoke trail */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + UParticleSystem* TrailFX; + + /** param name for beam target in smoke trail */ + UPROPERTY(EditDefaultsOnly, Category=Effects) + FName TrailTargetParam; + + /** instant hit notify for replication */ + UPROPERTY(Transient, ReplicatedUsing=OnRep_HitNotify) + FInstantHitInfo HitNotify; + + /** current spread from continuous firing */ + float CurrentFiringSpread; + + ////////////////////////////////////////////////////////////////////////// + // Weapon usage + + /** server notified of hit from client to verify */ + UFUNCTION(reliable, server, WithValidation) + void ServerNotifyHit(const FHitResult& Impact, FVector_NetQuantizeNormal ShootDir, int32 RandomSeed, float ReticleSpread); + + /** server notified of miss to show trail FX */ + UFUNCTION(unreliable, server, WithValidation) + void ServerNotifyMiss(FVector_NetQuantizeNormal ShootDir, int32 RandomSeed, float ReticleSpread); + + /** process the instant hit and notify the server if necessary */ + void ProcessInstantHit(const FHitResult& Impact, const FVector& Origin, const FVector& ShootDir, int32 RandomSeed, float ReticleSpread); + + /** continue processing the instant hit, as if it has been confirmed by the server */ + void ProcessInstantHit_Confirmed(const FHitResult& Impact, const FVector& Origin, const FVector& ShootDir, int32 RandomSeed, float ReticleSpread); + + /** check if weapon should deal damage to actor */ + bool ShouldDealDamage(AActor* TestActor) const; + + /** handle damage */ + void DealDamage(const FHitResult& Impact, const FVector& ShootDir); + + /** [local] weapon specific fire implementation */ + virtual void FireWeapon() override; + + /** [local + server] update spread on firing */ + virtual void OnBurstFinished() override; + + + ////////////////////////////////////////////////////////////////////////// + // Effects replication + + UFUNCTION() + void OnRep_HitNotify(); + + /** called in network play to do the cosmetic fx */ + void SimulateInstantHit(const FVector& Origin, int32 RandomSeed, float ReticleSpread); + + /** spawn effects for impact */ + void SpawnImpactEffects(const FHitResult& Impact); + + /** spawn trail effect */ + void SpawnTrailEffect(const FVector& EndPoint); +}; diff --git a/Source/ShooterGame/Public/Weapons/ShooterWeapon_Projectile.h b/Source/ShooterGame/Public/Weapons/ShooterWeapon_Projectile.h new file mode 100644 index 0000000..29af206 --- /dev/null +++ b/Source/ShooterGame/Public/Weapons/ShooterWeapon_Projectile.h @@ -0,0 +1,74 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ShooterWeapon.h" +#include "GameFramework/DamageType.h" // for UDamageType::StaticClass() +#include "ShooterWeapon_Projectile.generated.h" + +USTRUCT() +struct FProjectileWeaponData +{ + GENERATED_USTRUCT_BODY() + + /** projectile class */ + UPROPERTY(EditDefaultsOnly, Category=Projectile) + TSubclassOf ProjectileClass; + + /** life time */ + UPROPERTY(EditDefaultsOnly, Category=Projectile) + float ProjectileLife; + + /** damage at impact point */ + UPROPERTY(EditDefaultsOnly, Category=WeaponStat) + int32 ExplosionDamage; + + /** radius of damage */ + UPROPERTY(EditDefaultsOnly, Category=WeaponStat) + float ExplosionRadius; + + /** type of damage */ + UPROPERTY(EditDefaultsOnly, Category=WeaponStat) + TSubclassOf DamageType; + + /** defaults */ + FProjectileWeaponData() + { + ProjectileClass = NULL; + ProjectileLife = 10.0f; + ExplosionDamage = 100; + ExplosionRadius = 300.0f; + DamageType = UDamageType::StaticClass(); + } +}; + +// A weapon that fires a visible projectile +UCLASS(Abstract) +class AShooterWeapon_Projectile : public AShooterWeapon +{ + GENERATED_UCLASS_BODY() + + /** apply config on projectile */ + void ApplyWeaponConfig(FProjectileWeaponData& Data); + +protected: + + virtual EAmmoType GetAmmoType() const override + { + return EAmmoType::ERocket; + } + + /** weapon config */ + UPROPERTY(EditDefaultsOnly, Category=Config) + FProjectileWeaponData ProjectileConfig; + + ////////////////////////////////////////////////////////////////////////// + // Weapon usage + + /** [local] weapon specific fire implementation */ + virtual void FireWeapon() override; + + /** spawn projectile on server */ + UFUNCTION(reliable, server, WithValidation) + void ServerFireProjectile(FVector Origin, FVector_NetQuantizeNormal ShootDir); +}; diff --git a/Source/ShooterGame/Resources/Mac/Info.plist b/Source/ShooterGame/Resources/Mac/Info.plist new file mode 100644 index 0000000..2573cee --- /dev/null +++ b/Source/ShooterGame/Resources/Mac/Info.plist @@ -0,0 +1,41 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + ${ICON_NAME} + CFBundleIdentifier + ${APP_NAME} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${EXECUTABLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${BUNDLE_VERSION} + CFBundleSignature + ???? + CFBundleVersion + ${BUNDLE_VERSION} + LSMinimumSystemVersion + ${MACOSX_DEPLOYMENT_TARGET} + NSPrincipalClass + NSApplication + NSHighResolutionCapable + + NSHighResolutionMagnifyAllowed + + NSMicrophoneUsageDescription + ShooterGame requires microphone access for voice chat. + + diff --git a/Source/ShooterGame/Resources/Mac/ShooterGame.icns b/Source/ShooterGame/Resources/Mac/ShooterGame.icns new file mode 100644 index 0000000..eadca7b Binary files /dev/null and b/Source/ShooterGame/Resources/Mac/ShooterGame.icns differ diff --git a/Source/ShooterGame/Resources/Windows/ShooterGame.ico b/Source/ShooterGame/Resources/Windows/ShooterGame.ico new file mode 100644 index 0000000..a8a7430 Binary files /dev/null and b/Source/ShooterGame/Resources/Windows/ShooterGame.ico differ diff --git a/Source/ShooterGame/Resources/Windows/ShooterGame.rc b/Source/ShooterGame/Resources/Windows/ShooterGame.rc new file mode 100644 index 0000000..a07d509 --- /dev/null +++ b/Source/ShooterGame/Resources/Windows/ShooterGame.rc @@ -0,0 +1,84 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include +#include "Runtime/Launch/Resources/Version.h" +#include "Runtime/Launch/Resources/Windows/resource.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION ENGINE_MAJOR_VERSION,ENGINE_MINOR_VERSION,ENGINE_PATCH_VERSION,0 + PRODUCTVERSION ENGINE_MAJOR_VERSION,ENGINE_MINOR_VERSION,ENGINE_PATCH_VERSION,0 + FILEFLAGSMASK 0x17L +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", EPIC_COMPANY_NAME + VALUE "LegalCopyright", EPIC_COPYRIGHT_STRING + VALUE "ProductName", EPIC_PRODUCT_NAME + VALUE "ProductVersion", ENGINE_VERSION_STRING + VALUE "FileDescription", "ShooterGame" + VALUE "InternalName", "ShooterGame" + VALUE "OriginalFilename", "ShooterGame.exe" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDICON_UE4Game ICON "ShooterGame.ico" + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +//#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +//#endif // not APSTUDIO_INVOKED diff --git a/Source/ShooterGame/ShooterGame.Build.cs b/Source/ShooterGame/ShooterGame.Build.cs new file mode 100644 index 0000000..96568a4 --- /dev/null +++ b/Source/ShooterGame/ShooterGame.Build.cs @@ -0,0 +1,119 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class ShooterGame : ModuleRules +{ + public ShooterGame(ReadOnlyTargetRules Target) : base(Target) + { + PrivatePCHHeaderFile = "Public/ShooterGame.h"; + + PublicDefinitions.Add("HOST_ONLINE_GAMEMODE_ENABLED=" + HostOnlineGameEnabled); + PublicDefinitions.Add("JOIN_ONLINE_GAME_ENABLED=" + JoinOnlineGameEnabled); + PublicDefinitions.Add("INVITE_ONLINE_GAME_ENABLED=" + InviteOnlineGameEnabled); + PublicDefinitions.Add("ONLINE_STORE_ENABLED=" + OnlineStoreEnabled); + + PrivateIncludePaths.AddRange( + new string[] { + "ShooterGame/Private", + "ShooterGame/Private/Online", + "ShooterGame/Private/UI", + "ShooterGame/Private/UI/Menu", + "ShooterGame/Private/UI/Style", + "ShooterGame/Private/UI/Widgets", + } + ); + + PublicDependencyModuleNames.AddRange( + new string[] { + "Core", + "CoreUObject", + "Engine", + "OnlineSubsystem", + "OnlineSubsystemUtils", + "AssetRegistry", + "NavigationSystem", + "AIModule", + "GameplayTasks", + "Gauntlet", + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] { + "InputCore", + "Slate", + "SlateCore", + "ShooterGameLoadingScreen", + "Json", + "ApplicationCore", + "ReplicationGraph", + "PakFile", + "RHI", + "PhysicsCore", + "Networking", + "Sockets", + "Http", + "Json" + } + ); + + DynamicallyLoadedModuleNames.AddRange( + new string[] { + "OnlineSubsystemNull", + "NetworkReplayStreaming", + "NullNetworkReplayStreaming", + "HttpNetworkReplayStreaming", + "LocalFileNetworkReplayStreaming" + } + ); + + PrivateIncludePathModuleNames.AddRange( + new string[] { + "NetworkReplayStreaming" + } + ); + + if (Target.bBuildDeveloperTools || (Target.Configuration != UnrealTargetConfiguration.Shipping && Target.Configuration != UnrealTargetConfiguration.Test)) + { + PrivateDependencyModuleNames.Add("GameplayDebugger"); + PublicDefinitions.Add("WITH_GAMEPLAY_DEBUGGER=1"); + } + else + { + PublicDefinitions.Add("WITH_GAMEPLAY_DEBUGGER=0"); + } + } + + protected virtual int HostOnlineGameEnabled + { + get + { + return 1; + } + } + + protected virtual int JoinOnlineGameEnabled + { + get + { + return 1; + } + } + + protected virtual int InviteOnlineGameEnabled + { + get + { + return 1; + } + } + + protected virtual int OnlineStoreEnabled + { + get + { + return 1; + } + } +} diff --git a/Source/ShooterGameEditor.Target.cs b/Source/ShooterGameEditor.Target.cs new file mode 100644 index 0000000..2a9d032 --- /dev/null +++ b/Source/ShooterGameEditor.Target.cs @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class ShooterGameEditorTarget : TargetRules +{ + public ShooterGameEditorTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Editor; + + ExtraModuleNames.Add("ShooterGame"); + } +} diff --git a/Source/ShooterGameLoadingScreen/Private/ShooterGameLoadingScreen.cpp b/Source/ShooterGameLoadingScreen/Private/ShooterGameLoadingScreen.cpp new file mode 100644 index 0000000..7a5d2b3 --- /dev/null +++ b/Source/ShooterGameLoadingScreen/Private/ShooterGameLoadingScreen.cpp @@ -0,0 +1,114 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ShooterGameLoadingScreen.h" +#include "GenericApplication.h" +#include "GenericApplicationMessageHandler.h" +#include "SlateBasics.h" +#include "SlateExtras.h" +#include "MoviePlayer.h" + +// This module must be loaded "PreLoadingScreen" in the .uproject file, otherwise it will not hook in time! + +struct FShooterGameLoadingScreenBrush : public FSlateDynamicImageBrush, public FGCObject +{ + FShooterGameLoadingScreenBrush( const FName InTextureName, const FVector2D& InImageSize ) + : FSlateDynamicImageBrush( InTextureName, InImageSize ) + { + SetResourceObject(LoadObject( NULL, *InTextureName.ToString() )); + } + + virtual void AddReferencedObjects(FReferenceCollector& Collector) + { + FSlateBrush::AddReferencedObjects(Collector); + } +}; + +class SShooterLoadingScreen2 : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SShooterLoadingScreen2) {} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs) + { + static const FName LoadingScreenName(TEXT("/Game/UI/Menu/LoadingScreen.LoadingScreen")); + + //since we are not using game styles here, just load one image + LoadingScreenBrush = MakeShareable( new FShooterGameLoadingScreenBrush( LoadingScreenName, FVector2D(1920,1080) ) ); + + ChildSlot + [ + SNew(SOverlay) + +SOverlay::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + [ + SNew(SImage) + .Image(LoadingScreenBrush.Get()) + ] + +SOverlay::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + [ + SNew(SSafeZone) + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .Padding(10.0f) + .IsTitleSafe(true) + [ + SNew(SThrobber) + .Visibility(this, &SShooterLoadingScreen2::GetLoadIndicatorVisibility) + ] + ] + ]; + } + +private: + EVisibility GetLoadIndicatorVisibility() const + { + return EVisibility::Visible; + } + + /** loading screen image brush */ + TSharedPtr LoadingScreenBrush; +}; + +class FShooterGameLoadingScreenModule : public IShooterGameLoadingScreenModule +{ +public: + virtual void StartupModule() override + { + // Load for cooker reference + LoadObject(NULL, TEXT("/Game/UI/Menu/LoadingScreen.LoadingScreen") ); + + + // Previously, we set up our startup movie here to play while the engine was initially loading. By removing this behavior, + // the startup movie can be set up in DefaultGame.ini or in the project settings, and is no longer hard coded + + /* + if (IsMoviePlayerEnabled()) + { + FLoadingScreenAttributes LoadingScreen; + LoadingScreen.bAutoCompleteWhenLoadingCompletes = true; + LoadingScreen.MoviePaths.Add(TEXT("LoadingScreen")); + GetMoviePlayer()->SetupLoadingScreen(LoadingScreen); + } + */ + } + + virtual bool IsGameModule() const override + { + return true; + } + + virtual void StartInGameLoadingScreen() override + { + FLoadingScreenAttributes LoadingScreen; + LoadingScreen.bAutoCompleteWhenLoadingCompletes = true; + LoadingScreen.WidgetLoadingScreen = SNew(SShooterLoadingScreen2); + + GetMoviePlayer()->SetupLoadingScreen(LoadingScreen); + } +}; + +IMPLEMENT_GAME_MODULE(FShooterGameLoadingScreenModule, ShooterGameLoadingScreen); diff --git a/Source/ShooterGameLoadingScreen/Public/ShooterGameLoadingScreen.h b/Source/ShooterGameLoadingScreen/Public/ShooterGameLoadingScreen.h new file mode 100644 index 0000000..e65548d --- /dev/null +++ b/Source/ShooterGameLoadingScreen/Public/ShooterGameLoadingScreen.h @@ -0,0 +1,17 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#ifndef __SHOOTERGAMELOADINGSCREEN_H__ +#define __SHOOTERGAMELOADINGSCREEN_H__ + +#include "ModuleInterface.h" + + +/** Module interface for this game's loading screens */ +class IShooterGameLoadingScreenModule : public IModuleInterface +{ +public: + /** Kicks off the loading screen for in game loading (not startup) */ + virtual void StartInGameLoadingScreen() = 0; +}; + +#endif // __SHOOTERGAMELOADINGSCREEN_H__ diff --git a/Source/ShooterGameLoadingScreen/ShooterGameLoadingScreen.Build.cs b/Source/ShooterGameLoadingScreen/ShooterGameLoadingScreen.Build.cs new file mode 100644 index 0000000..c305d9e --- /dev/null +++ b/Source/ShooterGameLoadingScreen/ShooterGameLoadingScreen.Build.cs @@ -0,0 +1,26 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +// This module must be loaded "PreLoadingScreen" in the .uproject file, otherwise it will not hook in time! + +public class ShooterGameLoadingScreen : ModuleRules +{ + public ShooterGameLoadingScreen(ReadOnlyTargetRules Target) : base(Target) + { + PrivatePCHHeaderFile = "Public/ShooterGameLoadingScreen.h"; + + PCHUsage = PCHUsageMode.UseSharedPCHs; + + PrivateDependencyModuleNames.AddRange( + new string[] { + "Core", + "CoreUObject", + "MoviePlayer", + "Slate", + "SlateCore", + "InputCore" + } + ); + } +} diff --git a/Source/ShooterServer.Target.cs b/Source/ShooterServer.Target.cs new file mode 100644 index 0000000..226dffc --- /dev/null +++ b/Source/ShooterServer.Target.cs @@ -0,0 +1,16 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +[SupportedPlatforms(UnrealPlatformClass.Server)] +public class ShooterServerTarget : TargetRules +{ + public ShooterServerTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Server; + bUsesSteam = false; + + ExtraModuleNames.Add("ShooterGame"); + } +}