From 35876e45bf4bb10530ba15f0e735f26854c821bd Mon Sep 17 00:00:00 2001 From: stilnat Date: Tue, 10 Oct 2023 00:23:42 +0200 Subject: [PATCH] Play mode tests (#1168) * Early work to create runtime test framework, and basics of input dependency injection. * Created a movement play mode test. * Divided Playmode Test organisation into Server, Client and Host. Created separate test proxies and helpers to be used by all types. * Created initial host game test. * Added server tests, and added stub methods to ReadyPlayersSystem and RoundSystem to imitate Broadcasts. * Restructured test scripts, and created tests for late join issue. * Initial work on pocket tests. * Finalized tests for Propagating Pocket Problem. * Streamlining Server creation of Clients. * Gave player prefab the player tag, for convenience in finding multiple. * Server-side test for remaining above station level. * Refactored server side "PlayersRemainAboveStationLevelAfterSpawn" test to set up in UnityTest rather than SetUp. * Attempt at placing client windows properly. Not yet functional. * Created FreePlayMultiplayer test to set up and display eight clients. * Restructured tests folder to include "Asset Audit" assembly and separate out "Known Issue Reproduction" * Replace Input Manager with Input Action * Add InputSystem to ToggleInternalClothingUI.cs and NetworkPhysicsDebug.cs * Delete redundant Update() in ToggleInternalClothingUI.cs and change mouse position read in ItemGrid.cs * Add checks if scripts are enabled * Remove Debug.Log(1) in InputSystem.cs * Add UnityEngine.InputSystem reference to SS3D.Networking.asmdef * Replace InputInput.GetButtonDown("Drop") in Hands.cs * Work in progress: Parameterizing scene and prefab tests. * More work on SceneTests. * OnDisable()s unsubscribe from controls * remove if (!enabled) * Fixed scene loading in SceneTests. * Completed merge from develop. - AssetDataTests moved to Asset Audit. - Assembly definitions updated. * Moved relevant TilemapTests to Asset Audit assembly, refactored a bit. * Rationalized EditMode TileMapTests. * Set up CI pipeline to run Edit Mode tests only. * Move controls intializations to OnStart() * Rearrange Swap Hands to X, add summary to InputSystem, add try catch to OnEnable in InteractionController * Lower InputSystem execution order and remove try catch blocks * fix conficlt issue * Moved OnEnable code to OnStart, OnDisable to OnDestroyed, .inputactions to Content/Input * Make .inputaction file * Added discrete PlayMode test runner. * Commence conversion to InputSystem. * Still trying to get TestInputFixture working * Working on getting InputTestFixture going... No luck yet. * Mock input actually working!! * Commented out most UnityTests. * Changed build to Linux. * Changed build back to Windows. * Changed how the filepath is checked. * Trying Linux64 now. * update station level to 0 * uncomment unityTest * add toEnum utility on string * add tryget method for assets database * add utility for item command * add spawn item command * remove helper method already present in testHelper * remove old test * remove move from testhelper * uncomment unity test attributes * Add moveInDirection test helper * remove useless namespaces * remove itemcommandUtilities remove itemcommandUtilities and add methods in entity and item system. Update command accordingly * partial working tests * try to fix itemdrop test * failed attempt at implementing mouse mockup * some progress : now the item get dropped at real cursor position * update mouse read position with new input system * fix last commit * make playerCanDropItem test functional and clean up * add a parameter to helper function * remove useless unworking test * add interaction controller as field * move set up inputaction to set up method * some cleanup * edit back edit mode test * Update editmodetestrunner.yml * remove play mode test runner * small fix for some tests * partial fix for server tests * put back issue reproduction tests * fix station test height * bandaid fix * fix late join tests * some doc and move kill to unityTearDown * move controllers to SpessPlayModeTests * remove some inputAction, put left mouse click in SpessPlayModeTest * add pick up item to late join tests * add some doc * running only edit mode tests * update editmodetestrunner * edit workflow * edit workflow * edit workflow * update workflow * update workflow * fix test failing in host game action * fix late join fails * move stuff to play mode repository * complete host game action * simplify hierarchy and clean up redundant stuff * small fix attribute forgotten * fixing built staying open at end of tests * update to fix exceptions * fixing build * fix play mode tests * fix overflow * Add logAssert ignore fail messages everywhere needed * Revert "Merge branch 'Fix-container-stack-overflow' into PlayModeTests" This reverts commit e7d7620aee681157ddb7d08fa3afa831c9817eff, reversing changes made to 83db4da49b3145bd136488e19f1f82b85d406683. * renaming tests classes --------- Co-authored-by: Ryan Co-authored-by: Mechar418 <77565416+Mechar418@users.noreply.github.com> --- ...{testrunner.yml => editmodetestrunner.yml} | 2 + Assets/Content/Scenes/Empty.unity | 125 +++++++ Assets/Content/Scenes/Empty.unity.meta | 7 + Assets/Content/Scenes/Game.unity | 2 +- .../Entities/Humanoids/Human/Human.prefab | 2 +- .../SS3D/Data/AssetDatabases/AssetDatabase.cs | 11 + Assets/Scripts/SS3D/Data/Assets.cs | 10 + .../SS3D/Systems/Entities/EntitySystem.cs | 18 +- .../Entities/Humanoid/HumanoidController.cs | 2 +- .../SS3D/Systems/Gamemodes/GamemodeSystem.cs | 2 +- .../Commands/ItemCommands/SpawnItemCommand.cs | 66 ++++ .../ItemCommands/SpawnItemCommand.cs.meta | 11 + .../Interactions/InteractionController.cs | 12 +- .../Systems/Inventory/Items/ItemSystem.cs | 26 ++ .../SS3D/Systems/Inventory/UI/ItemDisplay.cs | 7 +- .../SS3D/Systems/Rounds/ReadyPlayersSystem.cs | 18 + .../SS3D/Systems/Rounds/RoundSystem.cs | 18 + Assets/Scripts/SS3D/UI/Buttons/LabelButton.cs | 5 + Assets/Scripts/SS3D/Utils/StringUtility.cs | 14 +- .../Tests/Asset Audit/AssetAuditUtilities.cs | 2 +- Assets/Scripts/Tests/Common.meta | 8 + Assets/Scripts/Tests/Common/EditModeTest.cs | 16 + .../Scripts/Tests/Common/EditModeTest.cs.meta | 11 + .../Tests/Common/SS3D.Tests.Common.asmdef | 22 ++ .../Common/SS3D.Tests.Common.asmdef.meta | 7 + Assets/Scripts/Tests/Common/Test.cs | 46 +++ Assets/Scripts/Tests/Common/Test.cs.meta | 11 + .../Scripts/Tests/Edit Mode/GamemodeTests.cs | 19 +- .../Edit Mode/SS3D.Tests.EditMode.asmdef | 3 +- .../Tests/Known Issue Reproduction.meta | 8 + ...pagatingPocketProblem_ClientPerspective.cs | 36 ++ ...ingPocketProblem_ClientPerspective.cs.meta | 11 + ...ropagatingPocketProblem_HostPerspective.cs | 42 +++ ...atingPocketProblem_HostPerspective.cs.meta | 11 + ...Issue1002_LateJoinFails_HostPerspective.cs | 165 +++++++++ ...1002_LateJoinFails_HostPerspective.cs.meta | 11 + .../SS3D.Tests.PlayMode.asmdef | 31 ++ .../SS3D.Tests.PlayMode.asmdef.meta | 7 + Assets/Scripts/Tests/Play Mode.meta | 8 + Assets/Scripts/Tests/Play Mode/Framework.meta | 8 + .../Play Mode/Framework/Abstract Tests.meta | 8 + .../Framework/Abstract Tests/PlayModeTest.cs | 336 ++++++++++++++++++ .../Abstract Tests/PlayModeTest.cs.meta | 11 + .../Tests/Play Mode/Framework/Helpers.meta | 8 + .../Framework/Helpers/LoadFileHelpers.cs | 159 +++++++++ .../Framework/Helpers/LoadFileHelpers.cs.meta | 11 + .../Framework/Helpers/ServerHelpers.cs | 132 +++++++ .../Framework/Helpers/ServerHelpers.cs.meta | 11 + .../Framework/Helpers/TestHelpers.cs | 197 ++++++++++ .../Framework/Helpers/TestHelpers.cs.meta | 11 + .../Play Mode/Framework/Template Tests.meta | 8 + .../Template Tests/IssueReproduction.cs | 51 +++ .../Template Tests/IssueReproduction.cs.meta | 11 + .../Template Tests/PlaymodeTestRepository.cs | 205 +++++++++++ .../PlaymodeTestRepository.cs.meta | 11 + .../Tests/Play Mode/General Tests.meta | 8 + .../Tests/Play Mode/General Tests/Client.meta | 8 + .../General Tests/Client/ClientGameActions.cs | 65 ++++ .../Client/ClientGameActions.cs.meta | 11 + .../Client/ClientLateJoinActions.cs | 62 ++++ .../Client/ClientLateJoinActions.cs.meta | 11 + .../Client/ClientLobbyActions.cs | 56 +++ .../Client/ClientLobbyActions.cs.meta | 11 + .../Tests/Play Mode/General Tests/Host.meta | 8 + .../General Tests/Host/HostGameActions.cs | 52 +++ .../Host/HostGameActions.cs.meta | 11 + .../Tests/Play Mode/General Tests/Server.meta | 8 + .../General Tests/Server/ServerGameActions.cs | 101 ++++++ .../Server/ServerGameActions.cs.meta | 11 + .../Play Mode/SS3D.Tests.PlayMode.asmdef | 31 ++ .../Play Mode/SS3D.Tests.PlayMode.asmdef.meta | 7 + Documents/UnityLicense.ulf | 76 ++-- Packages/manifest.json | 5 +- ProjectSettings/EditorBuildSettings.asset | 4 +- 74 files changed, 2498 insertions(+), 59 deletions(-) rename .github/workflows/{testrunner.yml => editmodetestrunner.yml} (97%) create mode 100644 Assets/Content/Scenes/Empty.unity create mode 100644 Assets/Content/Scenes/Empty.unity.meta create mode 100644 Assets/Scripts/SS3D/Systems/IngameConsoleSystem/Commands/ItemCommands/SpawnItemCommand.cs create mode 100644 Assets/Scripts/SS3D/Systems/IngameConsoleSystem/Commands/ItemCommands/SpawnItemCommand.cs.meta create mode 100644 Assets/Scripts/Tests/Common.meta create mode 100644 Assets/Scripts/Tests/Common/EditModeTest.cs create mode 100644 Assets/Scripts/Tests/Common/EditModeTest.cs.meta create mode 100644 Assets/Scripts/Tests/Common/SS3D.Tests.Common.asmdef create mode 100644 Assets/Scripts/Tests/Common/SS3D.Tests.Common.asmdef.meta create mode 100644 Assets/Scripts/Tests/Common/Test.cs create mode 100644 Assets/Scripts/Tests/Common/Test.cs.meta create mode 100644 Assets/Scripts/Tests/Known Issue Reproduction.meta create mode 100644 Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_ClientPerspective.cs create mode 100644 Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_ClientPerspective.cs.meta create mode 100644 Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_HostPerspective.cs create mode 100644 Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_HostPerspective.cs.meta create mode 100644 Assets/Scripts/Tests/Known Issue Reproduction/Issue1002_LateJoinFails_HostPerspective.cs create mode 100644 Assets/Scripts/Tests/Known Issue Reproduction/Issue1002_LateJoinFails_HostPerspective.cs.meta create mode 100644 Assets/Scripts/Tests/Known Issue Reproduction/SS3D.Tests.PlayMode.asmdef create mode 100644 Assets/Scripts/Tests/Known Issue Reproduction/SS3D.Tests.PlayMode.asmdef.meta create mode 100644 Assets/Scripts/Tests/Play Mode.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests/PlayModeTest.cs create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests/PlayModeTest.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Helpers.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Helpers/LoadFileHelpers.cs create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Helpers/LoadFileHelpers.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Helpers/ServerHelpers.cs create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Helpers/ServerHelpers.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Helpers/TestHelpers.cs create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Helpers/TestHelpers.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Template Tests.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Template Tests/IssueReproduction.cs create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Template Tests/IssueReproduction.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Template Tests/PlaymodeTestRepository.cs create mode 100644 Assets/Scripts/Tests/Play Mode/Framework/Template Tests/PlaymodeTestRepository.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests.meta create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Client.meta create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Client/ClientGameActions.cs create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Client/ClientGameActions.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Client/ClientLateJoinActions.cs create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Client/ClientLateJoinActions.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Client/ClientLobbyActions.cs create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Client/ClientLobbyActions.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Host.meta create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Host/HostGameActions.cs create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Host/HostGameActions.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Server.meta create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Server/ServerGameActions.cs create mode 100644 Assets/Scripts/Tests/Play Mode/General Tests/Server/ServerGameActions.cs.meta create mode 100644 Assets/Scripts/Tests/Play Mode/SS3D.Tests.PlayMode.asmdef create mode 100644 Assets/Scripts/Tests/Play Mode/SS3D.Tests.PlayMode.asmdef.meta diff --git a/.github/workflows/testrunner.yml b/.github/workflows/editmodetestrunner.yml similarity index 97% rename from .github/workflows/testrunner.yml rename to .github/workflows/editmodetestrunner.yml index 6707f6007e..5ff47deff6 100644 --- a/.github/workflows/testrunner.yml +++ b/.github/workflows/editmodetestrunner.yml @@ -20,6 +20,8 @@ jobs: - run: npm install fs - name: Run tests uses: game-ci/unity-test-runner@v2 + with: + testMode: editmode env: UNITY_LICENSE_FILE: ../Documents/UnityLicense.ulf - name: Produce summary output diff --git a/Assets/Content/Scenes/Empty.unity b/Assets/Content/Scenes/Empty.unity new file mode 100644 index 0000000000..fe1ff710a2 --- /dev/null +++ b/Assets/Content/Scenes/Empty.unity @@ -0,0 +1,125 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.37311953, g: 0.38074014, b: 0.3587274, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} diff --git a/Assets/Content/Scenes/Empty.unity.meta b/Assets/Content/Scenes/Empty.unity.meta new file mode 100644 index 0000000000..eaf4c261d2 --- /dev/null +++ b/Assets/Content/Scenes/Empty.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a7a0fb29f301d364a822be64003a90cb +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Content/Scenes/Game.unity b/Assets/Content/Scenes/Game.unity index 2c70aba674..58af57e2d9 100644 --- a/Assets/Content/Scenes/Game.unity +++ b/Assets/Content/Scenes/Game.unity @@ -1288,7 +1288,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 4468402021075485018, guid: cc5383d385556f443b2a7d3202f2b2c1, type: 3} propertyPath: m_AnchoredPosition.y - value: -229.99997 + value: -230.00006 objectReference: {fileID: 0} - target: {fileID: 4468402022117316798, guid: cc5383d385556f443b2a7d3202f2b2c1, type: 3} propertyPath: m_AnchorMax.x diff --git a/Assets/Content/WorldObjects/Entities/Humanoids/Human/Human.prefab b/Assets/Content/WorldObjects/Entities/Humanoids/Human/Human.prefab index 0e74cae709..5aa52cb88d 100644 --- a/Assets/Content/WorldObjects/Entities/Humanoids/Human/Human.prefab +++ b/Assets/Content/WorldObjects/Entities/Humanoids/Human/Human.prefab @@ -8711,7 +8711,7 @@ GameObject: - component: {fileID: 6282983976573159881} m_Layer: 11 m_Name: Human - m_TagString: Untagged + m_TagString: Player m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 diff --git a/Assets/Scripts/SS3D/Data/AssetDatabases/AssetDatabase.cs b/Assets/Scripts/SS3D/Data/AssetDatabases/AssetDatabase.cs index 21f72f3234..b86f7acc05 100644 --- a/Assets/Scripts/SS3D/Data/AssetDatabases/AssetDatabase.cs +++ b/Assets/Scripts/SS3D/Data/AssetDatabases/AssetDatabase.cs @@ -73,6 +73,17 @@ public T Get(int index) where T : Object return Assets[index] as T; } + public bool TryGet(int index, out T asset) where T : Object + { + if(index >= 0 && index < Assets.Count) + { + asset = Assets[index] as T; + return true; + } + asset = null; + return false; + } + /// /// Adds abd asset to the asset database. Should be used only for additional content or runtime stuff. /// diff --git a/Assets/Scripts/SS3D/Data/Assets.cs b/Assets/Scripts/SS3D/Data/Assets.cs index 561ea95d18..96508a40ef 100644 --- a/Assets/Scripts/SS3D/Data/Assets.cs +++ b/Assets/Scripts/SS3D/Data/Assets.cs @@ -32,6 +32,16 @@ public static TAsset Get(int databaseId, int assetId) where TAsset : Obj return GetDatabase(databaseId).Get(assetId); } + public static bool TryGet(int databaseId, int assetId, out TAsset asset) where TAsset : Object + { + if(GetDatabase(databaseId).TryGet(assetId, out asset)) + { + return true; + } + asset= null; + return false; + } + /// /// Generic getter, supports all databases and all asset database enums. /// diff --git a/Assets/Scripts/SS3D/Systems/Entities/EntitySystem.cs b/Assets/Scripts/SS3D/Systems/Entities/EntitySystem.cs index 0b69a4d80e..83810e6378 100644 --- a/Assets/Scripts/SS3D/Systems/Entities/EntitySystem.cs +++ b/Assets/Scripts/SS3D/Systems/Entities/EntitySystem.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Coimbra; using Coimbra.Services.Events; @@ -7,8 +7,10 @@ using FishNet.Object.Synchronizing; using SS3D.Core; using SS3D.Core.Behaviours; +using SS3D.Core.Settings; using SS3D.Logging; using SS3D.Systems.Entities.Events; +using SS3D.Systems.PlayerControl; using SS3D.Systems.Rounds; using SS3D.Systems.Rounds.Events; using SS3D.Utils; @@ -148,6 +150,20 @@ public void CmdSpawnLatePlayer(Player player, NetworkConnection networkConnectio SpawnLatePlayer(player); } + public bool TryGetOwnedEntity(NetworkConnection conn, out Entity entity) + { + foreach(Entity e in SpawnedPlayers) + { + if(e.Owner == conn) + { + entity = e; + return true; + } + } + entity = null; + return false; + } + /// /// Spawns a player after the round has started /// diff --git a/Assets/Scripts/SS3D/Systems/Entities/Humanoid/HumanoidController.cs b/Assets/Scripts/SS3D/Systems/Entities/Humanoid/HumanoidController.cs index 7fb7e16129..d6e4c657a6 100644 --- a/Assets/Scripts/SS3D/Systems/Entities/Humanoid/HumanoidController.cs +++ b/Assets/Scripts/SS3D/Systems/Entities/Humanoid/HumanoidController.cs @@ -123,7 +123,7 @@ private void HandleUpdate(ref EventContext context, in UpdateEvent updateEvent) /// private void UpdateMousePositionTransforms() { - // Ray ray = _camera.ScreenPointToRay(Input.mousePosition); + // Ray ray = _camera.ScreenPointToRay(Mouse.current.position.ReadValue()); // Vector3 mousePos = ray.origin - ray.direction * (ray.origin.y / ray.direction.y); // mousePos = new Vector3(mousePos.x, transform.position.y, mousePos.z); diff --git a/Assets/Scripts/SS3D/Systems/Gamemodes/GamemodeSystem.cs b/Assets/Scripts/SS3D/Systems/Gamemodes/GamemodeSystem.cs index 5042bf324c..e30330dc7e 100644 --- a/Assets/Scripts/SS3D/Systems/Gamemodes/GamemodeSystem.cs +++ b/Assets/Scripts/SS3D/Systems/Gamemodes/GamemodeSystem.cs @@ -37,7 +37,7 @@ public sealed class GamemodeSystem : NetworkSystem protected override void OnStart() { base.OnStart(); - Setup(); + if (base.IsServer) Setup(); } public override void OnStartClient() diff --git a/Assets/Scripts/SS3D/Systems/IngameConsoleSystem/Commands/ItemCommands/SpawnItemCommand.cs b/Assets/Scripts/SS3D/Systems/IngameConsoleSystem/Commands/ItemCommands/SpawnItemCommand.cs new file mode 100644 index 0000000000..46f9428dea --- /dev/null +++ b/Assets/Scripts/SS3D/Systems/IngameConsoleSystem/Commands/ItemCommands/SpawnItemCommand.cs @@ -0,0 +1,66 @@ +using SS3D.Core; +using SS3D.Systems.Entities; +using SS3D.Systems.Permissions; +using SS3D.Systems.PlayerControl; +using SS3D.Systems; +using SS3D.Data; +using SS3D.Data.AssetDatabases; +using SS3D.Data.Enums; +using SS3D.Systems.Inventory.Items; +using SS3D.Utils; +using SS3D.Core.Settings; +using UnityEngine; +using UnityEngine.UIElements; +using FishNet.Managing.Server; +using FishNet; +using FishNet.Connection; +using UnityEngine.InputSystem; + +namespace SS3D.Systems.IngameConsoleSystem.Commands +{ + public class SpawnItemCommand : Command + { + public override string LongDescription => "Spawn item using item name at the same position as human or at position x,z"; + public override string ShortDescription => "Spawn item"; + public override ServerRoleTypes AccessLevel => ServerRoleTypes.User; + + public override CommandType Type => CommandType.Server; + + public override string Perform(string[] args, NetworkConnection conn) + { + CheckArgsResponse checkArgsResponse = CheckArgs(args); + if (checkArgsResponse.IsValid == false) + return checkArgsResponse.InvalidArgs; + string itemName = args[0]; + if(!Subsystems.Get().TryGetOwnedEntity(conn, out Entity entity)) + { + return "Connection does not own any entity registered in entity system."; + }; + + ItemSystem itemSystem = Subsystems.Get(); + itemSystem.CmdSpawnItem(itemName.ToEnum(), entity.transform.position, Quaternion.identity); + return $"item {itemName} spawned at position {entity.transform.position}"; + } + + protected override CheckArgsResponse CheckArgs(string[] args) + { + CheckArgsResponse response = new CheckArgsResponse(); + if (args.Length != 1 && args.Length != 3) + { + response.IsValid = false; + response.InvalidArgs = "Invalid number of arguments"; + return response; + } + string itemName = args[0]; + if(!Assets.TryGet((int) AssetDatabases.Items, (int) itemName.ToEnum(), out Item item)) + { + response.IsValid = false; + response.InvalidArgs = $"item with name {itemName} not found"; + return response; + } + + response.IsValid = true; + return response; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/SS3D/Systems/IngameConsoleSystem/Commands/ItemCommands/SpawnItemCommand.cs.meta b/Assets/Scripts/SS3D/Systems/IngameConsoleSystem/Commands/ItemCommands/SpawnItemCommand.cs.meta new file mode 100644 index 0000000000..1dd6ae7480 --- /dev/null +++ b/Assets/Scripts/SS3D/Systems/IngameConsoleSystem/Commands/ItemCommands/SpawnItemCommand.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 731c833576f90374a871632c098ed503 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SS3D/Systems/Interactions/InteractionController.cs b/Assets/Scripts/SS3D/Systems/Interactions/InteractionController.cs index 316d58debd..5cab942f5d 100644 --- a/Assets/Scripts/SS3D/Systems/Interactions/InteractionController.cs +++ b/Assets/Scripts/SS3D/Systems/Interactions/InteractionController.cs @@ -82,13 +82,15 @@ protected override void OnDestroyed() /// Runs the most prioritised interaction /// [Client] - private void HandleRunPrimary(InputAction.CallbackContext callbackContext) + public void HandleRunPrimary(InputAction.CallbackContext callbackContext) { + + Debug.Log("run primary : " + Mouse.current.position.ReadValue()); if (EventSystem.current.IsPointerOverGameObject()) { return; } - Ray ray = _camera.ScreenPointToRay(Input.mousePosition); + Ray ray = _camera.ScreenPointToRay(Mouse.current.position.ReadValue()); List viableInteractions = GetViableInteractions(ray, out InteractionEvent interactionEvent); if (viableInteractions.Count <= 0) @@ -112,7 +114,7 @@ private void HandleView(InputAction.CallbackContext callbackContext) { return; } - Ray ray = _camera.ScreenPointToRay(Input.mousePosition); + Ray ray = _camera.ScreenPointToRay(Mouse.current.position.ReadValue()); List viableInteractions = GetViableInteractions(ray, out InteractionEvent interactionEvent); ViewTargetInteractions(viableInteractions, interactionEvent, ray); @@ -156,7 +158,7 @@ void handleInteractionSelected(IInteraction interaction, RadialInteractionButton CmdRunInteraction(ray, interactionName); } - _radialView.SetInteractions(interactions, interactionEvent, Input.mousePosition); + _radialView.SetInteractions(interactions, interactionEvent, Mouse.current.position.ReadValue()); _radialView.OnInteractionSelected += handleInteractionSelected; _radialView.ShowInteractionsMenu(); } @@ -190,7 +192,7 @@ private void InteractInHand(GameObject target, GameObject sourceObject, bool sho if (showMenu && interactions.Count > 0) { - Vector3 mousePosition = Input.mousePosition; + Vector3 mousePosition = Mouse.current.position.ReadValue(); mousePosition.y = Mathf.Max(_radialView.RectTransform.rect.height, mousePosition.y); _radialView.SetInteractions(interactions, interactionEvent, mousePosition); diff --git a/Assets/Scripts/SS3D/Systems/Inventory/Items/ItemSystem.cs b/Assets/Scripts/SS3D/Systems/Inventory/Items/ItemSystem.cs index 50f8460b2c..884b5bbc12 100644 --- a/Assets/Scripts/SS3D/Systems/Inventory/Items/ItemSystem.cs +++ b/Assets/Scripts/SS3D/Systems/Inventory/Items/ItemSystem.cs @@ -5,6 +5,7 @@ using SS3D.Data.AssetDatabases; using SS3D.Data.Enums; using SS3D.Logging; +using SS3D.Systems.Entities; using SS3D.Systems.Inventory.Containers; using UnityEngine; @@ -89,6 +90,20 @@ public Item SpawnItem(ItemId id, Vector3 position, Quaternion rotation) return itemInstance; } + + /// + /// Requests to spawn an item in a given container. + /// + /// The item ID to spawn. + /// The desired position to spawn. + /// The desired rotation to apply. + [ServerRpc(RequireOwnership = false)] + public void CmdSpawnItemInContainer(ItemId id, AttachedContainer attachedContainer) + { + SpawnItemInContainer(id, attachedContainer); + } + + /// /// Spawns an Item inside a container. /// @@ -120,5 +135,16 @@ public Item SpawnItemInContainer(ItemId id, AttachedContainer attachedContainer) Log.Information(this, "Item {item} spawned in container {container}", Logs.ServerOnly, itemInstance.name, attachedContainer.ContainerName); return itemInstance; } + + /// + /// Return the item in the active hand of the given player entity. + /// + public Item GetItemInHand(Entity playerEntity) + { + Hands hands = playerEntity.GetComponentInParent().Hands; + return hands.SelectedHand.ItemInHand; + } + + } } \ No newline at end of file diff --git a/Assets/Scripts/SS3D/Systems/Inventory/UI/ItemDisplay.cs b/Assets/Scripts/SS3D/Systems/Inventory/UI/ItemDisplay.cs index ee282afc33..50cf427fb8 100644 --- a/Assets/Scripts/SS3D/Systems/Inventory/UI/ItemDisplay.cs +++ b/Assets/Scripts/SS3D/Systems/Inventory/UI/ItemDisplay.cs @@ -3,6 +3,7 @@ using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; +using UnityEngine.InputSystem; namespace SS3D.Systems.Inventory.UI { @@ -20,7 +21,7 @@ public class ItemDisplay : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndD [SerializeField] private Item _item; private Transform _oldParent; - private Vector3 _startMousePosition; + private Vector2 _startMousePosition; private Vector3 _startPosition; private Image _slotImage; private Outline _outlineInner; @@ -63,7 +64,7 @@ public virtual void OnDropAccepted() { } public void OnPointerDown(PointerEventData eventData) { _startPosition = transform.position; - _startMousePosition = Input.mousePosition; + _startMousePosition = Mouse.current.position.ReadValue(); } public void OnPointerClick(PointerEventData eventData) @@ -102,7 +103,7 @@ public void OnDrag(PointerEventData eventData) // Only allow to drag with a left click. if (eventData.button != PointerEventData.InputButton.Left) return; - Vector3 diff = Input.mousePosition - _startMousePosition; + Vector3 diff = Mouse.current.position.ReadValue() - _startMousePosition; transform.position = _startPosition + diff; } diff --git a/Assets/Scripts/SS3D/Systems/Rounds/ReadyPlayersSystem.cs b/Assets/Scripts/SS3D/Systems/Rounds/ReadyPlayersSystem.cs index 3a500477e1..294fbb1582 100644 --- a/Assets/Scripts/SS3D/Systems/Rounds/ReadyPlayersSystem.cs +++ b/Assets/Scripts/SS3D/Systems/Rounds/ReadyPlayersSystem.cs @@ -129,5 +129,23 @@ private void SyncReadyPlayers() ReadyPlayersChanged readyPlayersChanged = new(_readyPlayers.ToList()); readyPlayersChanged.Invoke(this); } + + public int Count => _readyPlayers.Count; + +#if UNITY_EDITOR + /// + /// This method facilitates automated testing, and is not to be used in production. + /// It simulates a ChangePlayerReadyMessage broadcast received from a client, and + /// is handled normally by the server. Method required because the server cannot + /// broadcast to itself. + /// + /// The client the message is apparently from + /// The ChangePlayerReadyMessage apparently broadcast + [Server] + public void ChangePlayerReadyMessageStubBroadcast(NetworkConnection sender, ChangePlayerReadyMessage m) + { + HandleChangePlayerReady(sender, m); + } +#endif } } \ No newline at end of file diff --git a/Assets/Scripts/SS3D/Systems/Rounds/RoundSystem.cs b/Assets/Scripts/SS3D/Systems/Rounds/RoundSystem.cs index 9382e712aa..662db649ed 100644 --- a/Assets/Scripts/SS3D/Systems/Rounds/RoundSystem.cs +++ b/Assets/Scripts/SS3D/Systems/Rounds/RoundSystem.cs @@ -104,5 +104,23 @@ protected override async UniTask StopRound() TimeSpan second = TimeSpan.FromMilliseconds(500); await UniTask.Delay(second); } + +#if UNITY_EDITOR + /// + /// This method facilitates automated testing, and is not to be used in production. + /// It simulates a ChangeRoundStateMessage broadcast received from a client, and + /// is handled normally by the server. Method required because the server cannot + /// broadcast to itself. Note that authentication in RoundSystemBase has been + /// bypassed by this method. + /// + /// The ChangeRoundStateMessage apparently broadcast + [Server] + public void ChangeRoundStateMessageStubBroadcast(ChangeRoundStateMessage m) + { + #pragma warning disable CS4014 + ProcessChangeRoundState(m); + #pragma warning restore CS4014 + } +#endif } } \ No newline at end of file diff --git a/Assets/Scripts/SS3D/UI/Buttons/LabelButton.cs b/Assets/Scripts/SS3D/UI/Buttons/LabelButton.cs index f5ec5090af..66ca36990e 100644 --- a/Assets/Scripts/SS3D/UI/Buttons/LabelButton.cs +++ b/Assets/Scripts/SS3D/UI/Buttons/LabelButton.cs @@ -131,6 +131,11 @@ public virtual void OnPointerUp(PointerEventData eventData) ProcessPress(MouseButtonType.MouseUp); } + public void Press() + { + OnPointerDown(null); + } + private void Highlight() { UpdateVisuals(); diff --git a/Assets/Scripts/SS3D/Utils/StringUtility.cs b/Assets/Scripts/SS3D/Utils/StringUtility.cs index 64c0b32093..3aa1bd412c 100644 --- a/Assets/Scripts/SS3D/Utils/StringUtility.cs +++ b/Assets/Scripts/SS3D/Utils/StringUtility.cs @@ -1,4 +1,5 @@ -using UnityEngine; +using System; +using UnityEngine; namespace SS3D.Utils { @@ -8,5 +9,16 @@ public static string Colorize(this string text, string color) { return $"{text}"; } + + /// + /// Extension method to return an enum value of type T for the given string. + /// + /// + /// + /// + public static T ToEnum(this string value) + { + return (T)Enum.Parse(typeof(T), value, true); + } } } \ No newline at end of file diff --git a/Assets/Scripts/Tests/Asset Audit/AssetAuditUtilities.cs b/Assets/Scripts/Tests/Asset Audit/AssetAuditUtilities.cs index 6672d7ee1c..88e1c586d3 100644 --- a/Assets/Scripts/Tests/Asset Audit/AssetAuditUtilities.cs +++ b/Assets/Scripts/Tests/Asset Audit/AssetAuditUtilities.cs @@ -110,4 +110,4 @@ public static bool CheckGameObjectForMissingScripts(GameObject gameobject, ref S return allScriptsExist; } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Tests/Common.meta b/Assets/Scripts/Tests/Common.meta new file mode 100644 index 0000000000..6606687ead --- /dev/null +++ b/Assets/Scripts/Tests/Common.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1b78efc202d433448abc6fc5704ab26a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Common/EditModeTest.cs b/Assets/Scripts/Tests/Common/EditModeTest.cs new file mode 100644 index 0000000000..70f3cfae7b --- /dev/null +++ b/Assets/Scripts/Tests/Common/EditModeTest.cs @@ -0,0 +1,16 @@ +using NUnit.Framework; + +namespace SS3D.Tests +{ + /// + /// Base for edit mode tests + /// + public abstract class EditModeTest: Test + { + [SetUp] + public override void SetUp() => base.SetUp(); + + [TearDown] + public override void TearDown() => base.TearDown(); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Tests/Common/EditModeTest.cs.meta b/Assets/Scripts/Tests/Common/EditModeTest.cs.meta new file mode 100644 index 0000000000..e93c4a2514 --- /dev/null +++ b/Assets/Scripts/Tests/Common/EditModeTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 10a858882aa9e8a44ac3fcf72db02d75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Common/SS3D.Tests.Common.asmdef b/Assets/Scripts/Tests/Common/SS3D.Tests.Common.asmdef new file mode 100644 index 0000000000..5f4c548bc4 --- /dev/null +++ b/Assets/Scripts/Tests/Common/SS3D.Tests.Common.asmdef @@ -0,0 +1,22 @@ +{ + "name": "SS3D.Tests.Common", + "rootNamespace": "", + "references": [ + "GUID:27619889b8ba8c24980f49ee34dbb44a", + "GUID:0acc523941302664db1f4e527237feb3", + "GUID:6e00473804df0f0468c07310fbce5737" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/Tests/Common/SS3D.Tests.Common.asmdef.meta b/Assets/Scripts/Tests/Common/SS3D.Tests.Common.asmdef.meta new file mode 100644 index 0000000000..b17b0ad73a --- /dev/null +++ b/Assets/Scripts/Tests/Common/SS3D.Tests.Common.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 438d2c813cdccaa4fa2dab0553d00c9d +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Common/Test.cs b/Assets/Scripts/Tests/Common/Test.cs new file mode 100644 index 0000000000..ee8802f045 --- /dev/null +++ b/Assets/Scripts/Tests/Common/Test.cs @@ -0,0 +1,46 @@ +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace SS3D.Tests +{ + /// + /// Base testing class, inherited by PlayModeTest and EditModeTest. + /// + [TestFixture] + public abstract class Test + { + /// + /// Keep track of any objects instantiated during tests. + /// + public List instantiated; + + public virtual void SetUp() + { + instantiated = new List(); + } + + public virtual void TearDown() + { + // Clear up anything we instantiated. + foreach (GameObject go in instantiated) + { + GameObject.DestroyImmediate(go); + } + } + + protected void CreateGameObject(out GameObject go) + { + go = new GameObject(); + instantiated.Add(go); + } + + protected void CreateGameObject(out GameObject go, out T component) where T : MonoBehaviour + { + CreateGameObject(out go); + component = go.AddComponent(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Tests/Common/Test.cs.meta b/Assets/Scripts/Tests/Common/Test.cs.meta new file mode 100644 index 0000000000..533bfc7c2d --- /dev/null +++ b/Assets/Scripts/Tests/Common/Test.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8071e4442aba62a4783600f8659a0353 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Edit Mode/GamemodeTests.cs b/Assets/Scripts/Tests/Edit Mode/GamemodeTests.cs index eb775d28b7..498ed24685 100644 --- a/Assets/Scripts/Tests/Edit Mode/GamemodeTests.cs +++ b/Assets/Scripts/Tests/Edit Mode/GamemodeTests.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NUnit.Framework; using SS3D.Systems.Gamemodes; using SS3D.Systems.GameModes.Modes; +using SS3D.Tests; using UnityEditor; using UnityEngine; namespace EditorTests { - public class GamemodeTests + public class GamemodeTests : EditModeTest { #region Class variables /// @@ -16,6 +17,20 @@ public class GamemodeTests private bool SHOW_DEBUG = false; #endregion + #region Test set up + [SetUp] + public override void SetUp() + { + base.SetUp(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + #endregion + #region Tests /// diff --git a/Assets/Scripts/Tests/Edit Mode/SS3D.Tests.EditMode.asmdef b/Assets/Scripts/Tests/Edit Mode/SS3D.Tests.EditMode.asmdef index 1b6ce9d9a9..a7685bb8bf 100644 --- a/Assets/Scripts/Tests/Edit Mode/SS3D.Tests.EditMode.asmdef +++ b/Assets/Scripts/Tests/Edit Mode/SS3D.Tests.EditMode.asmdef @@ -9,7 +9,8 @@ "Coimbra", "SS3D.Data", "SS3D.Logging", - "SS3D.Interactions" + "SS3D.Interactions", + "SS3D.Tests.Common" ], "includePlatforms": [ "Editor" diff --git a/Assets/Scripts/Tests/Known Issue Reproduction.meta b/Assets/Scripts/Tests/Known Issue Reproduction.meta new file mode 100644 index 0000000000..4a162b9a94 --- /dev/null +++ b/Assets/Scripts/Tests/Known Issue Reproduction.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 89d54f52c9cff924fb4cf809cd007bed +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_ClientPerspective.cs b/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_ClientPerspective.cs new file mode 100644 index 0000000000..5ffa88b48b --- /dev/null +++ b/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_ClientPerspective.cs @@ -0,0 +1,36 @@ +using SS3D.Core.Settings; +using System.Collections; +using UnityEngine.TestTools; + +namespace SS3D.Tests +{ + public class Issue0990_PropagatingPocketProblem_ClientPerspective : PlayModeTest + { + + [UnitySetUp] + public IEnumerator UnitySetUp() + { + LogAssert.ignoreFailingMessages = true; + yield return LoadAndSetInGame(NetworkType.Client); + } + + [UnityTearDown] + public IEnumerator UnityTearDown() + { + LogAssert.ignoreFailingMessages = true; + yield return TestHelpers.FinishAndExitRound(); + } + + [UnityTest] + public IEnumerator PlayerHasTheSameNumberOfPocketsAfterEndingRoundAndStartingNewOne() + { + LogAssert.ignoreFailingMessages = true; + yield return IssueReproduction.Issue0990_PlayerHasTheSameNumberOfPocketsAfterEndingRoundAndStartingNewOne(); + } + + protected override bool UseMockUpInputs() + { + return false; + } + } +} diff --git a/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_ClientPerspective.cs.meta b/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_ClientPerspective.cs.meta new file mode 100644 index 0000000000..9b54dcf3b9 --- /dev/null +++ b/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_ClientPerspective.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fe197ec5d3be371438cb2a189683e989 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_HostPerspective.cs b/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_HostPerspective.cs new file mode 100644 index 0000000000..9eaaaf48bc --- /dev/null +++ b/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_HostPerspective.cs @@ -0,0 +1,42 @@ +using NUnit.Framework; +using SS3D.Core.Settings; +using System.Collections; +using UnityEngine.TestTools; + +namespace SS3D.Tests +{ + public class Issue0990_PropagatingPocketProblem_HostPerspective : PlayModeTest + { + + [UnitySetUp] + public IEnumerator UnitySetUp() + { + LogAssert.ignoreFailingMessages = true; + if (!setUpOnce) + { + yield return LoadAndSetInLobby(NetworkType.Host); + setUpOnce = true; + } + yield return SetInGame(); + } + + [UnityTearDown] + public IEnumerator UnityTearDown() + { + LogAssert.ignoreFailingMessages = true; + yield return TestHelpers.FinishAndExitRound(); + } + + [UnityTest] + public IEnumerator PlayerHasTheSameNumberOfPocketsAfterEndingRoundAndStartingNewOne() + { + LogAssert.ignoreFailingMessages = true; + yield return IssueReproduction.Issue0990_PlayerHasTheSameNumberOfPocketsAfterEndingRoundAndStartingNewOne(); + } + + protected override bool UseMockUpInputs() + { + return false; + } + } +} diff --git a/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_HostPerspective.cs.meta b/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_HostPerspective.cs.meta new file mode 100644 index 0000000000..c48bcb2ad6 --- /dev/null +++ b/Assets/Scripts/Tests/Known Issue Reproduction/Issue0990_PropagatingPocketProblem_HostPerspective.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ba75b7d52af7d0429a8cee741470f33 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Known Issue Reproduction/Issue1002_LateJoinFails_HostPerspective.cs b/Assets/Scripts/Tests/Known Issue Reproduction/Issue1002_LateJoinFails_HostPerspective.cs new file mode 100644 index 0000000000..ccef6228ad --- /dev/null +++ b/Assets/Scripts/Tests/Known Issue Reproduction/Issue1002_LateJoinFails_HostPerspective.cs @@ -0,0 +1,165 @@ +using System.Collections; +using NUnit.Framework; +using SS3D.Core; +using SS3D.Core.Settings; +using SS3D.UI.Buttons; +using UnityEngine; +using UnityEngine.TestTools; +using UnityEngine.UI; +using System.Diagnostics; +using SS3D.Systems.Entities.Humanoid; +using System; +using SS3D.Systems.PlayerControl; +using System.Linq; + +namespace SS3D.Tests +{ + public class Issue1002_LateJoinFails_HostPerspective : PlayModeTest + { + + protected Process clientProcess; + + [UnitySetUp] + public IEnumerator UnitySetUp() + { + LogAssert.ignoreFailingMessages = true; + yield return LoadAndSetInLobby(NetworkType.Host); + } + + [UnityTearDown] + public IEnumerator UnityTearDown() + { + LogAssert.ignoreFailingMessages = true; + // Wait for a bit, to get some temporal separation. + yield return new WaitForSeconds(1f); + + // Shut down the client + clientProcess.CloseMainWindow(); + clientProcess.Close(); + + TestHelpers.FinishAndExitRound(); + + // Wait for a bit more + yield return new WaitForSeconds(1f); + } + + [UnityTest] + public IEnumerator ClientCanEmbarkAfterRoundStartWhenNoOneElseHasEmbarked() + { + LogAssert.ignoreFailingMessages = true; + string clientCkey = "client"; + + // Start the client running, and wait until they have entered the lobby. + clientProcess = LoadFileHelpers.OpenCompiledBuild(NetworkType.Client, clientCkey); + yield return WaitForClientSoulToAppearInLobby(clientCkey); + + + // Set the round to begin, and wait a few seconds for the countdown etc to finish. + yield return TestHelpers.StartRound(); + yield return new WaitForSeconds(5f); + + // Set the client as ready. + ServerHelpers.SpawnLatePlayer(clientCkey); + yield return new WaitForSeconds(3f); + + // We test that this is working by attempting to get the player mind. + // This contains an assertion to confirm the mind is in the scene. + yield return CheckMindIsInScene(clientCkey); + yield return new WaitForSeconds(1f); + } + + [UnityTest] + public IEnumerator ClientCanEmbarkAfterRoundStartWhenHostHasAlreadyEmbarked() + { + LogAssert.ignoreFailingMessages = true; + string clientCkey = "client"; + + // Start the client running, and wait until they have entered the lobby. + clientProcess = LoadFileHelpers.OpenCompiledBuild(NetworkType.Client, clientCkey); + yield return WaitForClientSoulToAppearInLobby(clientCkey); + + + // Set the round to begin, and wait a few seconds for the countdown etc to finish. + yield return TestHelpers.StartAndEnterRound(); + yield return new WaitForSeconds(5f); + + // Set the client as ready. + ServerHelpers.SpawnLatePlayer(clientCkey); + yield return new WaitForSeconds(3f); + + // We test that this is working by attempting to get the player mind. + // This contains an assertion to confirm the mind is in the scene. + yield return CheckMindIsInScene(clientCkey); + yield return new WaitForSeconds(1f); + } + + [UnityTest] + public IEnumerator ClientCanJoinAfterRoundStartWhenHostHasAlreadyEmbarked() + { + LogAssert.ignoreFailingMessages = true; + string clientCkey = "client"; + + // Set the round to begin, and wait a few seconds for the countdown etc to finish. + yield return TestHelpers.StartAndEnterRound(); + yield return new WaitForSeconds(5f); + + // Start the client running, and wait until they have entered the lobby. + // This contains assertion with timeout to confirm the soul has connected. + clientProcess = LoadFileHelpers.OpenCompiledBuild(NetworkType.Client, clientCkey); + yield return WaitForClientSoulToAppearInLobby(clientCkey); + yield return new WaitForSeconds(1f); + } + + [UnityTest] + public IEnumerator ClientCanJoinAfterRoundStartWhenNoOneHasEmbarked() + { + string clientCkey = "client"; + + // Set the round to begin, and wait a few seconds for the countdown etc to finish. + yield return TestHelpers.StartRound(); + yield return new WaitForSeconds(5f); + + // Start the client running, and wait until they have entered the lobby. + // This contains assertion with timeout to confirm the soul has connected. + clientProcess = LoadFileHelpers.OpenCompiledBuild(NetworkType.Client, clientCkey); + yield return WaitForClientSoulToAppearInLobby(clientCkey); + yield return new WaitForSeconds(1f); + + } + + protected IEnumerator WaitForClientSoulToAppearInLobby(string ckey, float timeout = 15f) + { + PlayerSystem playerSystem = Subsystems.Get(); + float startTime = Time.time; + + while (playerSystem.OnlinePlayers.ToList().Find(soul => soul.Ckey == ckey) == null) + { + yield return new WaitForSeconds(1f); + Assert.IsTrue(Time.time < startTime + timeout, $"Client '{ckey}' not loaded after timeout of {timeout} seconds."); + } + } + + public IEnumerator CheckMindIsInScene(string ckey, float timeout = 10f) + { + string mind_prefix = "Mind - "; + GameObject mindGO = null; + + float startTime = Time.time; + + while (mindGO == null) + { + yield return null; + mindGO = GameObject.Find($"{mind_prefix}{ckey}"); + if (Time.time - startTime > timeout) + { + throw new Exception($"{mind_prefix}{ckey} not found within timeout of {timeout} seconds."); + } + } + } + + protected override bool UseMockUpInputs() + { + return false; + } + } +} diff --git a/Assets/Scripts/Tests/Known Issue Reproduction/Issue1002_LateJoinFails_HostPerspective.cs.meta b/Assets/Scripts/Tests/Known Issue Reproduction/Issue1002_LateJoinFails_HostPerspective.cs.meta new file mode 100644 index 0000000000..3d2c0196f4 --- /dev/null +++ b/Assets/Scripts/Tests/Known Issue Reproduction/Issue1002_LateJoinFails_HostPerspective.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a0f8eb5116c19884886316d7d4cd170e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Known Issue Reproduction/SS3D.Tests.PlayMode.asmdef b/Assets/Scripts/Tests/Known Issue Reproduction/SS3D.Tests.PlayMode.asmdef new file mode 100644 index 0000000000..5afdb02f1b --- /dev/null +++ b/Assets/Scripts/Tests/Known Issue Reproduction/SS3D.Tests.PlayMode.asmdef @@ -0,0 +1,31 @@ +{ + "name": "SS3D.Tests.KnownIssueReproduction", + "rootNamespace": "", + "references": [ + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "SS3D.Systems", + "SS3D.Core", + "FishNet.Runtime", + "SS3D.UI", + "Coimbra", + "Coimbra.Editor", + "SS3D.Tests.PlayMode", + "SS3D.Tests.Common", + "Unity.InputSystem", + "Unity.InputSystem.TestFramework" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/Tests/Known Issue Reproduction/SS3D.Tests.PlayMode.asmdef.meta b/Assets/Scripts/Tests/Known Issue Reproduction/SS3D.Tests.PlayMode.asmdef.meta new file mode 100644 index 0000000000..982a0cafe0 --- /dev/null +++ b/Assets/Scripts/Tests/Known Issue Reproduction/SS3D.Tests.PlayMode.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f5c7e13e59a150e47b929db3d6c18fa6 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Play Mode.meta b/Assets/Scripts/Tests/Play Mode.meta new file mode 100644 index 0000000000..3812079e8b --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 019b51812e545da4596bc3189e1e540b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Play Mode/Framework.meta b/Assets/Scripts/Tests/Play Mode/Framework.meta new file mode 100644 index 0000000000..6af0d19dd1 --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ad2d5c27fa5375948bb705525b4ef370 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests.meta b/Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests.meta new file mode 100644 index 0000000000..7ff83eefe2 --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f6122e0b8b9990f40b2338883ed104d2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests/PlayModeTest.cs b/Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests/PlayModeTest.cs new file mode 100644 index 0000000000..9c39def965 --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests/PlayModeTest.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using Coimbra; +using NUnit.Framework; +using SS3D.Core; +using SS3D.Core.Settings; +using SS3D.Systems.Entities; +using SS3D.Systems.Entities.Humanoid; +using SS3D.Systems.Interactions; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Layouts; +using UnityEngine.SceneManagement; +using UnityEngine.TestTools; + +namespace SS3D.Tests +{ + /// + /// All play mode tests should inherit from this class. This class set up the mock up controls, contains some utilities for all play tests. + /// + [TestFixture] + public abstract class PlayModeTest : InputTestFixture + { + protected const string ExecutableName = "SS3D"; + protected const string CancelButton = "Cancel"; + protected const string ReadyButtonName = "Ready"; + protected const string ServerSettingsTabName = "Server Settings"; + protected const string StartRoundButtonName = "Start Round"; + protected const string StartClientCommand = "Start SS3D Server.bat"; + + // Use it in [UnitySetUp] method if you want to do special stuff during the first call to such method. + protected bool setUpOnce = false; + + // Whether the lobby scene has been loaded + protected bool lobbySceneLoaded = false; + + // Used to simulate mouse input + protected Mouse mouse; + protected InputAction leftMouseClick = new InputAction(); + + // Used to simulate all possible actions defined in SS3D + protected InputDevice inputDevice; + private List inputActions = new(); + + public InputDevice InputDevice => inputDevice; + + public Mouse Mouse => mouse; + + protected HumanoidController HumanoidController; + protected InteractionController InteractionController; + + protected abstract bool UseMockUpInputs(); + + /// + /// Set up input system and virtual devices for input. Should not contain anything else. + /// TODO : determine precisely the timing of this method call, as calling it too soon can lead to issues with controllers not set up. + /// + public override void Setup() + { + if (UseMockUpInputs()) + { + UnityEngine.Debug.Log("Calling InputTestFixture.Setup"); + base.Setup(); + // Don't set up a new input device when running multiple tests in a row + if (inputDevice == null) + { + inputDevice = SetUpMockInputForActions(ref inputActions); + InputSystem.AddDevice(inputDevice); + } + SetUpMouse(); + } + } + + /// + /// Put input system back to it's original state. Should not contain anything else. + /// + public override void TearDown() + { + base.TearDown(); + } + + /// + /// TODO : find a better way to set up the mouse device (not with free actions like this). + /// Only handle left click currently, and only does primary interaction when left clicking. + /// + private void SetUpMouse() + { + mouse = InputSystem.AddDevice(); + mouse.MakeCurrent(); + leftMouseClick.AddBinding(mouse.leftButton); + } + + + public IEnumerator GetHumanoidController(float timeout = 3f) + { + float startTime = Time.time; + HumanoidController = null; + while (HumanoidController == null) + { + yield return null; + HumanoidController = GameObject.FindWithTag("Player")?.GetComponent(); + if (Time.time - startTime > timeout) + { + throw new Exception($"Humanoid controller not found within timeout of {timeout} seconds."); + } + } + } + + public IEnumerator GetInteractionController(float timeout = 3f) + { + float startTime = Time.time; + InteractionController = null; + while (InteractionController == null) + { + yield return null; + InteractionController = TestHelpers.GetLocalInteractionController(); + if (Time.time - startTime > timeout) + { + throw new Exception($"Interaction controller not found within timeout of {timeout} seconds."); + } + } + + // Set up mouse click so it performs the primary interaction when pressed. + leftMouseClick.performed += InteractionController.HandleRunPrimary; + leftMouseClick.Enable(); + } + + protected InputAction GetAction(string name) + { + foreach (InputAction action in inputActions) + { + UnityEngine.Debug.Log(action.name); + if (action.name == name) + { + return action; + } + } + UnityEngine.Debug.Log($"ERROR! No action of name {name} found!"); + return null; + } + + protected void SetApplicationSettings(NetworkType type) + { + // Create new settings so that tests are run in the correct context. + NetworkSettings originalSettings = ScriptableSettings.GetOrFind(); + NetworkSettings newSettings = UnityEngine.Object.Instantiate(originalSettings); + newSettings.NetworkType = type; + newSettings.Ckey = "john"; + + // Apply the new settings + ScriptableSettings.SetOrOverwrite(newSettings); + } + + protected void ClientSceneLoaded(Scene scene, LoadSceneMode mode) + { + if (scene.name.Equals("Game")) + { + lobbySceneLoaded = true; + SceneManager.sceneLoaded -= ClientSceneLoaded; + } + } + + protected IEnumerator WaitForLobbyLoaded(float timeout = 20f) + { + float startTime = Time.time; + while (!lobbySceneLoaded) + { + yield return new WaitForSeconds(1f); + + if (Time.time - startTime > timeout) + { + throw new Exception($"Lobby not loaded within timeout of {timeout} seconds."); + } + } + } + + protected void LoadStartupScene() + { + // Start up the game. + lobbySceneLoaded = false; + SceneManager.sceneLoaded += ClientSceneLoaded; + SceneManager.LoadScene("Boot", LoadSceneMode.Single); + } + + protected void KillAllBuiltExecutables() + { + foreach (var process in Process.GetProcessesByName(ExecutableName)) + { + process.Kill(); + } + } + + private IEnumerator GetControllers() + { + yield return GetHumanoidController(); + yield return GetInteractionController(); + } + + /// + /// A simple means of running a UnityTest multiple times to see if it consistently works. + /// To use this, simply paste "[ValueSource("Iterations")] int iteration" as the argument + /// to the UnityTest you want to repeat. + /// + /// An array of the size specified by the Repetition constant. + protected static int[] Iterations() + { + const int Repetitions = 10; + + int[] result = new int[Repetitions]; + for (int i = 0; i < Repetitions; i++) + { + result[i] = i; + } + return result; + } + + /// + /// Take a set of actions and create an InputDevice for it that has a control for each + /// of the actions. Also binds the actions to that those controls. Gratefully adapted from + /// https://rene-damm.github.io/HowDoI.html#set-an-actions-value-programmatically + /// + /// A mock device for use in testing. + public static InputDevice SetUpMockInputForActions(ref List inputActions) + { + UnityEngine.Debug.Log("Entering SetUpMockInput"); + InputActionAsset actions = Subsystems.Get().Inputs.asset; + UnityEngine.Debug.Log(actions.ToString()); + + var layoutName = actions.name; + + // Build a device layout that simply has one control for each action in the asset. + InputSystem.RegisterLayoutBuilder(() => + { + var builder = new InputControlLayout.Builder() + .WithName(layoutName); + + foreach (var action in actions) + { + builder.AddControl(action.name) // Must not have actions in separate maps with the same name. + .WithLayout(action.expectedControlType); + } + + return builder.Build(); + }, name: layoutName); + + // Create the device. + var device = InputSystem.AddDevice(layoutName); + UnityEngine.Debug.Log(device.ToString()); + + + // Add a control scheme for it to the actions. + actions.AddControlScheme("MockInput") + .WithRequiredDevice($"<{layoutName}>"); + + // Bind the actions in the newly added control scheme. + foreach (var action in actions) + { + inputActions.Add(action); + action.AddBinding($"<{layoutName}>/{action.name}", groups: "MockInput"); + UnityEngine.Debug.Log($"Added binding <{layoutName}>/{action.name}"); + } + + + // Restrict the actions to bind only to our newly created + // device using the bindings we just added. + //actions.bindingMask = InputBinding.MaskByGroup("MockInput"); + actions.devices = new[] { device }; + + UnityEngine.Debug.Log("Returning device."); + + return device; + } + + /// + /// As host, simply open the game and wait for host to be correctly loaded in lobby. + /// As client, does the same, but open a dedicated server to connect to before. + /// + /// The network type we want to load in lobby. + /// + protected IEnumerator LoadAndSetInLobby(NetworkType type) + { + if(type is NetworkType.Client) + { + LoadFileHelpers.OpenCompiledBuild(); + + // Force wait for 10 seconds - this should be long enough for the server to load + yield return new WaitForSeconds(10f); + // Set to run as client + SetApplicationSettings(NetworkType.Client); + } + else + { + SetApplicationSettings(type); + } + + // Load the startup scene (which will subsequently load the lobby once connected) + LoadStartupScene(); + + yield return WaitForLobbyLoaded(); + + // Wait a bit to make sure UI is correctly Loaded + yield return new WaitForSeconds(3f); + } + + /// + /// This load player (either client or host) in lobby and then in game. + /// + /// Time between starting the game and joining it + /// + protected IEnumerator LoadAndSetInGame(NetworkType type, float joinDelay = 0f) + { + yield return LoadAndSetInLobby(type); + yield return SetInGame(joinDelay); + } + + /// + /// Assume the player is in lobby, then launch a round, embark and get the controllers. + /// + /// Time between starting the game and joining it. + /// + protected IEnumerator SetInGame(float joinDelay = 0f) + { + if (!lobbySceneLoaded) + { + throw new Exception("Don't try to get in game without loading the lobby scene first !"); + } + yield return TestHelpers.StartAndEnterRound(joinDelay); + yield return GetControllers(); + yield return new WaitForSeconds(1f); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests/PlayModeTest.cs.meta b/Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests/PlayModeTest.cs.meta new file mode 100644 index 0000000000..68ead55fff --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework/Abstract Tests/PlayModeTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 74175235ac472474ba219702deeed7f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Play Mode/Framework/Helpers.meta b/Assets/Scripts/Tests/Play Mode/Framework/Helpers.meta new file mode 100644 index 0000000000..673f7c7bd9 --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework/Helpers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: acb900cc0b2855448a15e2049160ebc9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Play Mode/Framework/Helpers/LoadFileHelpers.cs b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/LoadFileHelpers.cs new file mode 100644 index 0000000000..9f686b4f75 --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/LoadFileHelpers.cs @@ -0,0 +1,159 @@ +using System.Diagnostics; +using UnityEngine; +using SS3D.Core.Settings; +using SS3D.Core.Utils; +using System.Runtime.InteropServices; +using System; +using System.IO; + +namespace SS3D.Tests +{ + /// + /// This class manages all actions relating to opening and closing the built executable automatically. + /// + public static class LoadFileHelpers + { + public const string ExecutableName = "SS3D.exe"; + public const string IpAddress = "127.0.0.1"; + public const string Port = "1151"; + public const int MaxExpectedServerLoadTimeMillis = 10000; + + /// + /// Runs the compiled SS3D executable with the selected settings. + /// + /// Server (default), Host or Client. + /// Ckey. Applicable only for Client network type, but must not be empty if client + /// Whether minimized (default), maximized, normal or hidden + /// The process handle for the running SS3D build + public static Process OpenCompiledBuild(NetworkType networkType = NetworkType.DedicatedServer, string Ckey = "client", ProcessWindowStyle windowStyle = ProcessWindowStyle.Minimized) + { + // Confirm all arguments + string arguments = $"{CommandLineArgs.Ip}{IpAddress} {CommandLineArgs.Port}{Port} {CommandLineArgs.SkipIntro} "; + switch (networkType) + { + case NetworkType.DedicatedServer: arguments += CommandLineArgs.ServerOnly; break; + case NetworkType.Host: arguments += CommandLineArgs.Host; break; + case NetworkType.Client: arguments += CommandLineArgs.Ckey + Ckey; break; + } + + // Create a process that will be able to launch the build. + Process process = new Process(); + process.StartInfo.WindowStyle = windowStyle; + process.StartInfo.Arguments = arguments; + process.StartInfo.FileName = ExecutableName; + process.StartInfo.WorkingDirectory = GetFilePath(); + + UnityEngine.Debug.Log($"Attempting to load {ExecutableName} {arguments}" ); + + // Execute the process + process.Start(); + + // Enforce a delay if we are loading a server (either dedicated or host). + // This prevents us loading our client scene before server is ready to handle us. + if (networkType != NetworkType.Client) Sleep(MaxExpectedServerLoadTimeMillis); + + // Return the process handle so we can close it correctly later + return process; + } + + /// + /// Pauses execution. For example, used so the server has time to load. + /// + /// How long to pause for (in milliseconds) + public static void Sleep(int durationInMilliseconds) + { + System.Threading.Thread.Sleep(durationInMilliseconds); + } + + public static void PlaceQuadWindow(Process process, int windowNumber = 0) + { + const int ScreenWidth = 2000; + const int MaxWindowsPerRow = 4; + + int row = windowNumber / MaxWindowsPerRow; + int col = windowNumber % MaxWindowsPerRow; + int windowWidth = ScreenWidth / MaxWindowsPerRow; + + SetWindowPos(process.MainWindowHandle, new IntPtr((int)SpecialWindowHandles.HWND_TOP), col * windowWidth, row * windowWidth, windowWidth, windowWidth, SetWindowPosFlags.SWP_SHOWWINDOW); + } + + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool repaint); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags); + + + #region Private helper methods + /// + /// This method gets the filepath of the Builds directory where the compiled build is saved. + /// + /// The filepath of the Builds folder. + private static string GetFilePath() + { + // Get relevant executable file path + string filePath = Application.dataPath; + filePath = filePath.Substring(0, filePath.Length - 6); // Needed to remove the "Assets" folder. + filePath += "Builds"; // Needed to add the "Builds" folder. + + const string expectedFolderNameForContinuousIntegrationTesting = "StandaloneLinux64"; + + if (Directory.Exists($"{filePath}/{expectedFolderNameForContinuousIntegrationTesting}")) + { + filePath += $"/{expectedFolderNameForContinuousIntegrationTesting}"; + } + else + { + filePath += "/Game"; + } + return filePath; + } + #endregion + + #region Random Enums for managing loaded application windows + public enum SpecialWindowHandles : int + { + HWND_TOP = 0, + HWND_BOTTOM = 1, + HWND_TOPMOST = -1, + HWND_NOTOPMOST = -2 + } + + [Flags] + public enum SetWindowPosFlags : uint + { + SWP_ASYNCWINDOWPOS = 0x4000, + + SWP_DEFERERASE = 0x2000, + + SWP_DRAWFRAME = 0x0020, + + SWP_FRAMECHANGED = 0x0020, + + SWP_HIDEWINDOW = 0x0080, + + SWP_NOACTIVATE = 0x0010, + + SWP_NOCOPYBITS = 0x0100, + + SWP_NOMOVE = 0x0002, + + SWP_NOOWNERZORDER = 0x0200, + + SWP_NOREDRAW = 0x0008, + + SWP_NOREPOSITION = 0x0200, + + SWP_NOSENDCHANGING = 0x0400, + + SWP_NOSIZE = 0x0001, + + SWP_NOZORDER = 0x0004, + + SWP_SHOWWINDOW = 0x0040, + } + #endregion + } +} \ No newline at end of file diff --git a/Assets/Scripts/Tests/Play Mode/Framework/Helpers/LoadFileHelpers.cs.meta b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/LoadFileHelpers.cs.meta new file mode 100644 index 0000000000..7cdb16e856 --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/LoadFileHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bc5959161327c4e4ea3086870685a30b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Play Mode/Framework/Helpers/ServerHelpers.cs b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/ServerHelpers.cs new file mode 100644 index 0000000000..bdc6693dee --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/ServerHelpers.cs @@ -0,0 +1,132 @@ +using NUnit.Framework; +using SS3D.Core; +using SS3D.Systems.Entities; +using SS3D.Systems.PlayerControl; +using SS3D.Systems.Rounds; +using SS3D.Systems.Rounds.Messages; +using System.Collections; +using System.Diagnostics; +using UnityEngine; +using System.Linq; + +namespace SS3D.Tests +{ + /// + /// This class is simply a container for helper methods for the Server, for use + /// in UnityTests. + /// + public static class ServerHelpers + { + public static Process[] CreateClients(int amount, ProcessWindowStyle windowStyle = ProcessWindowStyle.Minimized) + { + string filePath; + Process[] result; + + // Initialize the process return array + result = new Process[amount]; + + // Get relevant executable file path + filePath = Application.dataPath; + filePath = filePath.Substring(0, filePath.Length - 6); // Needed to remove the "Assets" folder. + filePath += "Builds/Game"; // Needed to add the "Builds" folder. + + for (int i = 0; i < amount; i++) + { + // Fire up the client. + result[i] = new Process(); + result[i].StartInfo.WindowStyle = windowStyle; + result[i].StartInfo.Arguments = $"-ip=localhost -ckey=player_{i} -port=1151 -skipintro"; + result[i].StartInfo.FileName = "SS3D.exe"; + result[i].StartInfo.WorkingDirectory = filePath; + result[i].Start(); + } + + + + return result; + } + + + + public static IEnumerator SetWindowPositions(Process[] process) + { + for (int i = 0; i < process.Length;i++) + { + LoadFileHelpers.PlaceQuadWindow(process[i], i); + yield return new WaitForSeconds(0.5f); + } + } + + public static IEnumerator WaitUntilClientsLoaded(int amountOfClients, float timeout = 60f) + { + PlayerSystem playerSystem = Subsystems.Get(); + int currentOnlineSouls = 0; + float startTime = Time.time; + + // This loop simply waits until all players have joined. + while (currentOnlineSouls < amountOfClients) + { + // Allow timeout if needed. + Assert.IsTrue(Time.time < startTime + timeout, $"Only {currentOnlineSouls} of {amountOfClients} clients loaded after timeout of {timeout} seconds."); + + // Check whether all souls are online. + currentOnlineSouls = 0; + foreach (Player player in playerSystem.OnlinePlayers) + { + currentOnlineSouls++; + } + + yield return new WaitForSeconds(2f); + } + + yield return new WaitForSeconds(2f); + } + + + /// + /// Sets all players ready. + /// + public static void SetAllPlayersReady() + { + PlayerSystem playerSystem = Subsystems.Get(); + ReadyPlayersSystem readyPlayersSystem = Subsystems.Get(); + ChangePlayerReadyMessage msg; + + foreach (Player player in playerSystem.OnlinePlayers) + { + msg = new ChangePlayerReadyMessage(player.Ckey, true); + readyPlayersSystem.ChangePlayerReadyMessageStubBroadcast(player.LocalConnection, msg); + } + } + + public static void SetPlayerReadiness(string Ckey, bool readiness) + { + PlayerSystem playerSystem = Subsystems.Get(); + ReadyPlayersSystem readyPlayersSystem = Subsystems.Get(); + Player player = playerSystem.OnlinePlayers.ToList().Find(soul => soul.Ckey == Ckey); + ChangePlayerReadyMessage msg = new ChangePlayerReadyMessage(Ckey, readiness); + readyPlayersSystem.ChangePlayerReadyMessageStubBroadcast(player.LocalConnection, msg); + } + + /// + /// Turn the round on or off. + /// + public static void ChangeRoundState(bool running) + { + RoundSystem roundSystem = Subsystems.Get(); + ChangeRoundStateMessage msg = new ChangeRoundStateMessage(running); + roundSystem.ChangeRoundStateMessageStubBroadcast(msg); + } + + public static void SpawnLatePlayer(string Ckey) + { + PlayerSystem playerSystem = Subsystems.Get(); + EntitySystem entitySystem = Subsystems.Get(); + + Player player = playerSystem.GetPlayer(Ckey); + entitySystem.CmdSpawnLatePlayer(player); + } + + + } +} \ No newline at end of file diff --git a/Assets/Scripts/Tests/Play Mode/Framework/Helpers/ServerHelpers.cs.meta b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/ServerHelpers.cs.meta new file mode 100644 index 0000000000..bf5e1ef305 --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/ServerHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b1a3639fd7b16ff47bc6fa3f7050aeed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Tests/Play Mode/Framework/Helpers/TestHelpers.cs b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/TestHelpers.cs new file mode 100644 index 0000000000..241cca13b6 --- /dev/null +++ b/Assets/Scripts/Tests/Play Mode/Framework/Helpers/TestHelpers.cs @@ -0,0 +1,197 @@ +using FishNet; +using FishNet.Connection; +using FishNet.Managing.Client; +using NUnit.Framework; +using SS3D.Core; +using SS3D.Systems.Entities; +using SS3D.Systems.Interactions; +using SS3D.Systems.Inventory.Containers; +using SS3D.Systems.Inventory.Items; +using SS3D.UI.Buttons; +using System.Collections; +using UnityEditor; +using UnityEngine; +using UnityEngine.InputSystem.Controls; +using UnityEngine.UI; + +namespace SS3D.Tests +{ + /// + /// This class is simply a container for helper methods for UnityTests. + /// + public static class TestHelpers + { + #region Constants + private const string HorizontalAxis = "Horizontal"; + private const string VerticalAxis = "Vertical"; + private const string CancelButton = "Cancel"; + private const string ReadyButtonName = "Ready"; + private const string EmbarkButtonName = "Embark"; + + private const string ServerSettingsTabName = "Server Settings"; + private const string StartRoundButtonName = "Start Round"; + #endregion + + /// + /// Checks whether two floats are approximately equal, disregarding minor floating point imprecisions. + /// + /// The first number to check. + /// The second number to check. + /// Whether the input arguments are equal. + public static bool ApproximatelyEqual(float a, float b) + { + bool result = ((a - b) * (a - b)) < 0.001f; + return result; + } + + public static IEnumerator StartAndEnterRound(float delay = 0f) + { + yield return StartRound(); + yield return new WaitForSeconds(delay); + yield return PressButtonWhenAvailable(EmbarkButtonName); + } + + /// + /// This method simulates the user in the lobby switching to the "Server Settings" tab, and clicking "Start Round". + /// + /// Time delay between actions. + /// IEnumerator for yielding in UnityTest. + public static IEnumerator StartRound(float delay = 0.5f) + { + SetTabActive(ServerSettingsTabName); yield return new WaitForSeconds(delay); + PressButton(StartRoundButtonName); yield return new WaitForSeconds(delay); + } + + public static IEnumerator LateJoinRound() + { + // Fire up the game + SetTabActive(ServerSettingsTabName); yield return new WaitForSeconds(1f); + PressButton(StartRoundButtonName); yield return new WaitForSeconds(1f); + + // Give a few moments pause before we try and late join + yield return new WaitForSeconds(5f); + + // Now we join. + PressButton(EmbarkButtonName); yield return new WaitForSeconds(3f); + + } + + public static IEnumerator Embark() + { + PressButton(EmbarkButtonName); yield return new WaitForSeconds(3f); + } + + public static IEnumerator ContinueFreePlayUntilControlAltBackspacePressed() + { + bool controlPressed; + bool altPressed; + bool backspacePressed; + bool endTestKeyCombo = false; + + Debug.Log("Entering Free Play. Press Control + Alt + Backspace while focused on the game window in Unity to finish Free Play."); + + while (!endTestKeyCombo) + { + yield return null; + controlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); + altPressed = Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt); + backspacePressed = Input.GetKey(KeyCode.Backspace); + endTestKeyCombo = controlPressed && altPressed && backspacePressed; + } + + Debug.Log("Ending Free Play."); + } + + public static IEnumerator FinishAndExitRound() + { + // Press and release the Escape key + //TODO: ScriptedInput input = UserInput.GetInputService() as ScriptedInput; + //input.HandleButton(CancelButton, true); + yield return null; + //input.HandleButton(CancelButton, false); + + // Change to the server settings tab, and cancel the round + SetTabActive(ServerSettingsTabName); yield return new WaitForSeconds(1f); + PressButton(StartRoundButtonName); yield return new WaitForSeconds(1f); + + // Give a moment's pause before test formally concludes. (It takes a while for the round to reset). + yield return new WaitForSeconds(4f); + } + + public static void PressButton(string buttonName) + { + GetButton(buttonName).Press(); + } + + public static IEnumerator PressButtonWhenAvailable(string buttonName, float timeout = 15f) + { + LabelButton button = GetButton(buttonName); + float startTime = Time.time; + while (button == null) + { + Assert.IsTrue(Time.time < startTime + timeout, $"Timeout of {timeout} reached when trying to press {buttonName} button"); + yield return new WaitForSeconds(1f); + button = GetButton(buttonName); + } + button.Press(); + } + + public static LabelButton GetButton(string buttonName) + { + LabelButton button = GameObject.Find(buttonName)?.GetComponent(); + return button; + } + + public static void SetTabActive(string tabName) + { + GameObject.Find(tabName)?.GetComponent