/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace UnitTest { using namespace AZ; using namespace AzFramework; class TestComponentExternalChunk : public AZ::Component , public NetBindable { public: AZ_COMPONENT(TestComponentExternalChunk, "{73BB3B15-7C4D-4BD5-9568-F3B2DCBC7725}", AZ::Component); static void Reflect(ReflectContext* context); void Init() override { NetBindable::NetInit(); } void Activate() override {} void Deactivate() override {} bool SetPos(float x, float y, const RpcContext&) { m_x = x; m_y = y; return true; } void OnFloatChanged(const float&, const TimeContext&) { m_floatChanged = true; } bool m_floatChanged = false; private: float m_x = 0, m_y = 0; }; class TestComponentReplicaChunk : public ReplicaChunkBase , public ReplicaChunkInterface { public: GM_CLASS_ALLOCATOR(TestComponentReplicaChunk); static const char* GetChunkName() { return "TestComponentReplicaChunk"; } bool IsReplicaMigratable() override { return true; } public: TestComponentReplicaChunk() : m_int("m_int", 42) , m_float("m_float", 96.4f) , SetInt("SetInt") , SetPos("SetPos") { } bool SetIntImpl(int newValue, const RpcContext&) { m_int.Set(newValue); return true; } DataSet m_int; DataSet::BindInterface m_float; GridMate::Rpc > >::BindInterface SetInt; GridMate::Rpc, GridMate::RpcArg >::BindInterface SetPos; }; void TestComponentExternalChunk::Reflect(ReflectContext* context) { NetworkContext* netContext = azrtti_cast(context); if (netContext) { netContext->Class() ->Chunk() ->Field("m_int", &TestComponentReplicaChunk::m_int) ->Field("m_float", &TestComponentReplicaChunk::m_float) ->RPC("SetInt", &TestComponentReplicaChunk::SetInt) ->RPC("SetPos", &TestComponentReplicaChunk::SetPos); } } class TestComponentAutoChunk : public AZ::Component , public NetBindable { public: enum TestEnum { TEST_Value0 = 0, TEST_Value1 = 1, TEST_Value255 = 255 }; AZ_COMPONENT(TestComponentAutoChunk, "{003FD1BC-8456-43D5-9879-1B3804327A4F}", AZ::Component); static void Reflect(ReflectContext* context) { NetworkContext* netContext = azrtti_cast(context); if (netContext) { netContext->Class() ->Field("m_int", &TestComponentAutoChunk::m_int) ->Field("m_float", &TestComponentAutoChunk::m_float) ->Field("m_enum", &TestComponentAutoChunk::m_enum) ->RPC("SetInt", &TestComponentAutoChunk::SetInt) ->CtorData("CtorInt", &TestComponentAutoChunk::GetCtorInt, &TestComponentAutoChunk::SetCtorInt) ->CtorData("CtorVec", &TestComponentAutoChunk::GetCtorVec, &TestComponentAutoChunk::SetCtorVec); } SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { serializeContext->Class() ->Version(1) ->Field("m_int", &TestComponentAutoChunk::m_int) ->Field("m_float", &TestComponentAutoChunk::m_float) ->Field("m_enum", &TestComponentAutoChunk::m_enum) ->Field("ctorInt", &TestComponentAutoChunk::m_ctorInt) ->Field("ctorVec", &TestComponentAutoChunk::m_ctorVec); } } void Init() override { NetBindable::NetInit(); } void Activate() override {} void Deactivate() override {} void SetNetworkBinding(ReplicaChunkPtr chunk) override {} void UnbindFromNetwork() override {} bool SetIntImpl(int val, const RpcContext&) { m_int = val; return true; } void OnFloatChanged(const float&, const TimeContext&) { } int GetCtorInt() const { return m_ctorInt; } void SetCtorInt(const int& ctorInt) { m_ctorInt = ctorInt; } AZStd::vector& GetCtorVec() { return m_ctorVec; } void SetCtorVec(const AZStd::vector& vec) { m_ctorVec = vec; } int m_ctorInt; AZStd::vector m_ctorVec; Field m_int; BoundField m_float; Field > m_enum; Rpc::Binder SetInt; }; class NetContextReflectionTest : public ::testing::Test { public: void run() { AzFramework::Application app; AzFramework::Application::Descriptor appDesc; appDesc.m_recordingMode = Debug::AllocationRecords::RECORD_NO_RECORDS; appDesc.m_allocationRecords = false; appDesc.m_enableDrilling = false; app.Start(appDesc); AzFramework::NetworkContext* netContext = nullptr; EBUS_EVENT_RESULT(netContext, NetSystemRequestBus, GetNetworkContext); AZ_TEST_ASSERT(netContext); AZ::ComponentDescriptor* descTestComponentExternalChunk = TestComponentExternalChunk::CreateDescriptor(); app.RegisterComponentDescriptor(descTestComponentExternalChunk); AZ::ComponentDescriptor* descTestComponentAutoChunk = TestComponentAutoChunk::CreateDescriptor(); app.RegisterComponentDescriptor(descTestComponentAutoChunk); AZ::Entity* testEntity = aznew AZ::Entity("TestEntity"); testEntity->Init(); testEntity->CreateComponent(); testEntity->CreateComponent(); testEntity->Activate(); // test field binding/auto reflection/creation { TestComponentAutoChunk* testComponent = testEntity->FindComponent(); AZ_TEST_ASSERT(testComponent); testComponent->SetInt(2048); // should happen locally AZ_TEST_ASSERT(testComponent->m_int == 2048); ReplicaChunkPtr chunk = testComponent->GetNetworkBinding(); AZ_TEST_ASSERT(chunk); GridMate::ReplicaChunkDescriptor* desc = chunk->GetDescriptor(); AZ_TEST_ASSERT(desc); testComponent->m_ctorInt = 8192; for (int n = 0; n < 16; ++n) { testComponent->m_ctorVec.push_back(n); } GridMate::WriteBufferDynamic wb(GridMate::EndianType::IgnoreEndian); desc->MarshalCtorData(chunk.get(), wb); { // Create a chunk from the recorded ctor data, ensure that it stores // the ctor data in preparation for copying it to the instance GridMate::TimeContext tc; GridMate::ReplicaContext rc(nullptr, tc); GridMate::ReadBuffer rb(wb.GetEndianType(), wb.Get(), wb.Size()); GridMate::UnmarshalContext ctx(rc); ctx.m_hasCtorData = true; ctx.m_iBuf = &rb; ReplicaChunkPtr chunk2 = desc->CreateFromStream(ctx); AZ_TEST_ASSERT(chunk2); // ensure a new chunk was created ReflectedReplicaChunkBase* refChunk = static_cast(chunk2.get()); AZ_TEST_ASSERT(refChunk->m_ctorBuffer.Size() == sizeof(int) + sizeof(AZ::u16) + (sizeof(int) * testComponent->m_ctorVec.size())); } { // discard a ctor data stream and ensure that the stream is emptied GridMate::TimeContext tc; GridMate::ReplicaContext rc(nullptr, tc); GridMate::ReadBuffer rb(wb.GetEndianType(), wb.Get(), wb.Size()); GridMate::UnmarshalContext ctx(rc); ctx.m_hasCtorData = true; ctx.m_iBuf = &rb; desc->DiscardCtorStream(ctx); AZ_TEST_ASSERT(rb.IsEmptyIgnoreTrailingBits()); // should have discarded the whole stream } { // Make another chunk and bind it to a new component and make sure the ctor data matches AZ::Entity* testEntity2 = aznew AZ::Entity("TestEntity2"); testEntity2->Init(); testEntity2->CreateComponent(); testEntity2->Activate(); GridMate::TimeContext tc; GridMate::ReplicaContext rc(nullptr, tc); GridMate::ReadBuffer rb(wb.GetEndianType(), wb.Get(), wb.Size()); GridMate::UnmarshalContext ctx(rc); ctx.m_hasCtorData = true; ctx.m_iBuf = &rb; ReplicaChunkPtr chunk2 = desc->CreateFromStream(ctx); TestComponentAutoChunk* testComponent2 = testEntity2->FindComponent(); netContext->Bind(testComponent2, chunk2, NetworkContextBindMode::NonAuthoritative); // Ensure values match after ctor data is applied AZ_TEST_ASSERT(testComponent2->m_ctorInt == testComponent->m_ctorInt); AZ_TEST_ASSERT(testComponent2->m_ctorVec == testComponent->m_ctorVec); } testComponent->SetInt(4096); AZ_TEST_ASSERT(testComponent->m_int == 4096); testComponent->m_int = 42; // now it should change AZ_TEST_ASSERT(testComponent->m_int == 42); testComponent->m_enum = TestComponentAutoChunk::TEST_Value1; chunk.reset(); // should cause netContext->DestroyReplicaChunk() } // test chunk binding/creation { TestComponentExternalChunk* testComponent = testEntity->FindComponent(); AZ_TEST_ASSERT(testComponent); ReplicaChunkPtr chunk = testComponent->GetNetworkBinding(); AZ_TEST_ASSERT(chunk); TestComponentReplicaChunk* testChunk = static_cast(chunk.get()); // for now, this will throw a warning, but will at least attempt the dispatch testChunk->SetPos(42.0f, 96.0f); AZ_TEST_ASSERT(testComponent->m_floatChanged == false); testChunk->m_float.Set(1024.0f); // I would like to test that the notify fired, but without a Replica, cant :( testComponent->UnbindFromNetwork(); chunk.reset(); ///// CRASHES FROM HERE } // test serialization of NetBindable::Fields { TestComponentAutoChunk* testComponent = testEntity->FindComponent(); AZStd::vector buffer; AZ::IO::ByteContainerStream > saveStream(&buffer); bool saved = AZ::Utils::SaveObjectToStream(saveStream, AZ::DataStream::ST_XML, testComponent); AZ_TEST_ASSERT(saved); AZ::IO::ByteContainerStream > loadStream(&buffer); TestComponentAutoChunk* testCopy = AZ::Utils::LoadObjectFromStream(loadStream); AZ_TEST_ASSERT(testCopy); delete testCopy; } testEntity->Deactivate(); delete testEntity; descTestComponentExternalChunk->ReleaseDescriptor(); descTestComponentAutoChunk->ReleaseDescriptor(); app.Stop(); } }; TEST_F(NetContextReflectionTest, Test) { run(); } template class NetContextFixture : public ::testing::Test { public: NetContextFixture() = default; ~NetContextFixture() = default; void SetUp() override { AZ::AllocatorInstance::Create(); m_app = AZStd::make_unique(); m_app->Start(AzFramework::Application::Descriptor()); AzFramework::NetworkContext* netContext = nullptr; EBUS_EVENT_RESULT(netContext, NetSystemRequestBus, GetNetworkContext); AZ_TEST_ASSERT(netContext); m_descTestComponentAutoChunk = ComponentType::CreateDescriptor(); m_app->RegisterComponentDescriptor(m_descTestComponentAutoChunk); m_entity = AZStd::make_unique("TestEntity"); m_entity->Init(); m_entity->CreateComponent(); m_entity->Activate(); } void TearDown() override { m_descTestComponentAutoChunk->ReleaseDescriptor(); m_entity->Deactivate(); m_entity.reset(); m_app->Stop(); m_app.reset(); AZ::AllocatorInstance::Destroy(); } void RunTest() { const ComponentType* testComponent = m_entity->FindComponent(); AZStd::vector buffer; AZ::IO::ByteContainerStream > saveStream(&buffer); const bool saved = AZ::Utils::SaveObjectToStream(saveStream, AZ::DataStream::ST_XML, testComponent); AZ_TEST_ASSERT(saved); AZ::IO::ByteContainerStream > loadStream(&buffer); const AZStd::unique_ptr testCopy(AZ::Utils::LoadObjectFromStream(loadStream)); AZ_TEST_ASSERT(testCopy); } AZStd::unique_ptr m_app; AZStd::unique_ptr m_entity; AZ::ComponentDescriptor* m_descTestComponentAutoChunk = nullptr; }; class TestComponent_EmptyNetContext : public AZ::Component , public NetBindable { public: AZ_COMPONENT(TestComponent_EmptyNetContext, "{B1E2E2DD-DA70-4D59-A185-AF9A5CCF1574}", AZ::Component, NetBindable); static void Reflect(ReflectContext* context) { if (SerializeContext* serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Version(1); } if (NetworkContext* netContext = azrtti_cast(context)) { netContext->Class(); } } void Activate() override {} void Deactivate() override {} }; using NetContextEmpty = NetContextFixture; TEST_F(NetContextEmpty, SerializationTests) { RunTest(); } template class TestComponent_OneField : public AZ::Component , public NetBindable { public: AZ_COMPONENT(TestComponent_OneField, "{A7BCDBEF-3D4F-4D04-A6FA-DF48D4B66ABE}", AZ::Component, NetBindable); using ThisComponentType = TestComponent_OneField; static void Reflect(ReflectContext* context) { if (SerializeContext* serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Field("Field", &TestComponent_OneField::m_field) ->Version(1); } if (NetworkContext* netContext = azrtti_cast(context)) { netContext->Class() ->Field("Field", &TestComponent_OneField::m_field); } } void Activate() override {} void Deactivate() override {} Field m_field; }; TYPED_TEST_CASE_P(NetContextFixture); TYPED_TEST_P(NetContextFixture, SerializationTests) { this->RunTest(); } REGISTER_TYPED_TEST_CASE_P(NetContextFixture, SerializationTests); /* * Testing the basic common types. */ using CommonTypes = ::testing::Types< TestComponent_OneField, TestComponent_OneField, TestComponent_OneField, TestComponent_OneField, TestComponent_OneField, TestComponent_OneField, TestComponent_OneField >; INSTANTIATE_TYPED_TEST_CASE_P(NetContextCommonSerialization, NetContextFixture, CommonTypes); /* * And some less common types. */ using LessCommonTypes = ::testing::Types< TestComponent_OneField, TestComponent_OneField, TestComponent_OneField, TestComponent_OneField>, TestComponent_OneField >; INSTANTIATE_TYPED_TEST_CASE_P(NetContextLessCommonSerialization, NetContextFixture, LessCommonTypes); /* * Next up are marshal and unmarshal tests. */ template class NetContextMarshalFixture : public ::testing::Test { public: NetContextMarshalFixture() = default; ~NetContextMarshalFixture() = default; void SetUp() override { AZ::AllocatorInstance::Create(); AZ::AllocatorInstance::Create(); m_app = AZStd::make_unique(); m_app->Start(AzFramework::Application::Descriptor()); AzFramework::NetworkContext* netContext = nullptr; EBUS_EVENT_RESULT(netContext, NetSystemRequestBus, GetNetworkContext); AZ_TEST_ASSERT(netContext); m_descTestComponentAutoChunk = ComponentType::CreateDescriptor(); m_app->RegisterComponentDescriptor(m_descTestComponentAutoChunk); m_entityFrom = AZStd::make_unique("TestEntityFrom"); m_entityFrom->Init(); m_componentFrom = m_entityFrom->CreateComponent(); m_entityFrom->Activate(); m_entityTo = AZStd::make_unique("TestEntityTo"); m_entityTo->Init(); m_componentTo = m_entityTo->CreateComponent(); m_entityTo->Activate(); } void MarshalUnMarshal() { AzFramework::NetworkContext* netContext = nullptr; NetSystemRequestBus::BroadcastResult(netContext, &NetSystemRequestBus::Events::GetNetworkContext); AZ_TEST_ASSERT(netContext); ComponentType* testComponent = m_entityFrom->FindComponent(); AZ_TEST_ASSERT(testComponent); ReplicaChunkPtr chunk = testComponent->GetNetworkBinding(); AZ_TEST_ASSERT(chunk); m_outReplica = AZStd::make_unique("ReplicaTo"); { m_outManager = AZStd::make_unique(); m_outPeer = AZStd::make_unique(m_outManager.get()); GridMate::WriteBufferDynamic wb(GridMate::EndianType::IgnoreEndian); { GridMate::TimeContext tc; const GridMate::ReplicaContext rc(nullptr, tc); GridMate::MarshalContext mc(GridMate::ReplicaMarshalFlags::FullSync, &wb, nullptr, rc); mc.m_peer = m_outPeer.get(); mc.m_rm = m_outManager.get(); chunk->Debug_PrepareData(wb.GetEndianType(), GridMate::ReplicaMarshalFlags::FullSync); chunk->Debug_Marshal(mc, 0); } // and now unmarshal into the other entity { GridMate::TimeContext tc; const GridMate::ReplicaContext rc(nullptr, tc); GridMate::ReadBuffer rb(wb.GetEndianType(), wb.Get(), wb.Size()); GridMate::UnmarshalContext ctx(rc); ctx.m_hasCtorData = false; ctx.m_iBuf = &rb; ctx.m_peer = m_outPeer.get(); ctx.m_rm = m_outManager.get(); m_outReplicaChunk = chunk->GetDescriptor()->CreateFromStream(ctx); m_outReplicaChunk->Debug_AttachedToReplica(m_outReplica.get()); ctx.m_peer->Debug_Add(m_outReplica.get()); m_outReplicaChunk->Debug_Unmarshal(ctx, 0); /* * Note the order: unmarshal first to populate the chunk with data, then apply it to a component. * The expectation is that the valid will apply to NetBindable::Field without being overwritten. */ m_componentTo->SetNetworkBinding(m_outReplicaChunk); // the main test body can now test for the equality } } } void TearDown() override { m_outReplicaChunk.reset(); m_outManager.reset(); m_outPeer.reset(); m_outReplica.release(); // Replica is held by as an intrusive pointer in @m_outPeer and is destroyed there. if (m_entityFrom) { m_entityFrom->Deactivate(); m_entityFrom.reset(); } if (m_entityTo) { m_entityTo->Deactivate(); m_entityTo.reset(); } m_descTestComponentAutoChunk->ReleaseDescriptor(); m_app->Stop(); m_app.reset(); AZ::AllocatorInstance::Destroy(); AZ::AllocatorInstance::Destroy(); } AZStd::unique_ptr m_app; AZStd::unique_ptr m_entityFrom; AZStd::unique_ptr m_entityTo; ComponentType* m_componentFrom = nullptr; ComponentType* m_componentTo = nullptr; AZ::ComponentDescriptor* m_descTestComponentAutoChunk = nullptr; GridMate::ReplicaChunkPtr m_outReplicaChunk; AZStd::unique_ptr m_outReplica; AZStd::unique_ptr m_outManager; AZStd::unique_ptr m_outPeer; }; using NetContextVector3 = NetContextMarshalFixture>; TEST_F(NetContextVector3, SerializationTests) { const Vector3 value = AZ::Vector3::CreateAxisZ( 1.f ); m_componentFrom->m_field = value; MarshalUnMarshal(); AZ_TEST_ASSERT(m_componentTo->m_field.Get() == value); } /* * Now the same test but with NetBindable::BoundField<> */ template class TestComponent_OneBoundField : public AZ::Component , public NetBindable { public: AZ_COMPONENT(TestComponent_OneBoundField, "{2B283821-41DF-46BB-BE8E-66EF7301B62A}", AZ::Component, NetBindable); using ThisComponentType = TestComponent_OneBoundField; static void Reflect(ReflectContext* context) { if (SerializeContext* serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Field("Field", &ThisComponentType::m_boundField) ->Version(1); } if (NetworkContext* netContext = azrtti_cast(context)) { netContext->Class() ->Field("Field", &ThisComponentType::m_boundField); } } void Activate() override {} void Deactivate() override {} void OnBoundFieldChanged( const FieldType&, const GridMate::TimeContext& ) {} BoundField m_boundField; }; using NetContextBoundVector2 = NetContextMarshalFixture>; TEST_F(NetContextBoundVector2, SerializationTests) { const Vector2 value = AZ::Vector2::CreateAxisX( 4.f ); m_componentFrom->m_boundField = value; MarshalUnMarshal(); AZ_TEST_ASSERT(m_componentTo->m_boundField.Get() == value); } TEST_F(NetContextBoundVector2, Delete_Authoritative_Entity) { using ThisComponentType = TestComponent_OneBoundField; AzFramework::NetworkContext* netContext = nullptr; NetSystemRequestBus::BroadcastResult(netContext, &NetSystemRequestBus::Events::GetNetworkContext); AZ_TEST_ASSERT(netContext); ThisComponentType* testComponent = m_entityFrom->FindComponent(); AZ_TEST_ASSERT(testComponent); ReplicaChunkPtr chunk = testComponent->GetNetworkBinding(); // Testing early deletion of an entity on the server. m_entityFrom->Deactivate(); m_entityFrom.reset(); // This test passes if it doesn't crash on cleanup. chunk.reset(); } template class TestComponent_OneBoundField_ServerCallback : public AZ::Component , public NetBindable { public: AZ_COMPONENT(TestComponent_OneBoundField_ServerCallback, "{74F5B232-0544-45CA-B207-9846052ED1AD}", AZ::Component, NetBindable); using ThisComponentType = TestComponent_OneBoundField_ServerCallback; static void Reflect(ReflectContext* context) { if (SerializeContext* serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Field("Field", &ThisComponentType::m_boundField) ->Version(1); } if (NetworkContext* netContext = azrtti_cast(context)) { netContext->Class() ->Field("Field", &ThisComponentType::m_boundField); } } void Activate() override {} void Deactivate() override {} void OnBoundFieldChanged( const FieldType&, const GridMate::TimeContext& ) { ++m_callbacksInvokeCount; } AZ::u8 m_callbacksInvokeCount = 0; BoundField m_boundField; }; using NetContextBoundVector2WithCallbackCount = NetContextMarshalFixture>; TEST_F(NetContextBoundVector2WithCallbackCount, BoundField_Invoke_OnServer_Test) { MarshalUnMarshal(); m_componentFrom->m_callbacksInvokeCount = 0; // resetting the count const Vector2 value = AZ::Vector2::CreateAxisX( 4.f ); m_componentFrom->m_boundField = value; AZ_TEST_ASSERT(m_componentFrom->m_callbacksInvokeCount == 1); } }