BVB Source Codes

CRYENGINE Show ObjectContainer.cpp Source code

Return Download CRYENGINE: download ObjectContainer.cpp Source code - Download CRYENGINE Source code - Type:.cpp
  1. // Copyright 2001-2016 Crytek GmbH / Crytek Group. All rights reserved.
  2.  
  3. #include "StdAfx.h"
  4. #include "ObjectContainer.h"
  5.  
  6. // Annoying...
  7. #include "AIVehicle.h"
  8. #include "AIPlayer.h"
  9.  
  10. int CObjectContainer::m_snObjectsRegistered = 0;
  11. int CObjectContainer::m_snObjectsDeregistered = 0;
  12.  
  13. const char* GetNameFromType(EAIClass type)
  14. {
  15.         switch (type)
  16.         {
  17.         case eAIC_AIVehicle:
  18.                 return "CAIVehicle";
  19.         case eAIC_Puppet:
  20.                 return "CPuppet";
  21.         case eAIC_PipeUser:
  22.                 return "CPipeUser";
  23.         case eAIC_AIPlayer:
  24.                 return "CAIPlayer";
  25.         case eAIC_Leader:
  26.                 return "CLeader";
  27.         case eAIC_AIActor:
  28.                 return "CAIActor";
  29.         case eAIC_AIObject:
  30.                 return "CAIObject";
  31.         case eAIC_AIFlyingVehicle:
  32.                 return "CAIFlyingVehicle";
  33.         default:
  34.                 assert(false);
  35.                 return "<UNKNOWN>";
  36.         }
  37. }
  38.  
  39. CObjectContainer::CObjectContainer(void)
  40.         : m_objects(MAX_AI_OBJECTS)
  41. {
  42. }
  43.  
  44. void CObjectContainer::Reset()
  45. {
  46.         // unreserve all IDs first
  47.         for (size_t i = 0; i < m_reservedIDs.size(); ++i)
  48.         {
  49.                 m_objects.erase(m_reservedIDs[i]);
  50.                 m_snObjectsDeregistered++;
  51.         }
  52.         stl::free_container(m_reservedIDs);
  53.  
  54.         const int numRegistered = GetNumRegistered();
  55.         if (numRegistered != 0)
  56.         {
  57.                 DumpRegistered();
  58.         }
  59.         CRY_ASSERT_MESSAGE(numRegistered == 0, "Something has leaked AI objects, and we're about to create dangling pointers! Check the log for details");
  60.  
  61.         m_objects.clear();
  62.  
  63.         stl::free_container(m_DeregisteredBuffer);
  64.         stl::free_container(m_DeregisteredWorkingBuffer);
  65. }
  66.  
  67. // pObject is essential - have to register something of course
  68. // ref is to set a strong reference if we have one, otherwise we accept it is unowned
  69. // inId is optional, if specified will attempt to register the object under that ID. Mostly this is used for serialization.
  70. bool CObjectContainer::RegisterObject(CAIObject* pObject, CStrongRef<CAIObject>& ref, tAIObjectID inId /*=INVALID_AIOBJECTID*/)
  71. {
  72.         CCCPOINT(RegisterObjectUntyped);
  73.  
  74.         // First, check and release the reference if it is already used
  75.         // Recreating an object like this usually isn't necessary but the semantics make sense
  76.         if (!ref.IsNil())
  77.         {
  78.                 ref.Release();
  79.         }
  80.  
  81.         m_snObjectsRegistered++;
  82.  
  83. #if _DEBUG
  84.         if (pObject)
  85.         {
  86.                 uint16 capacity = static_cast<uint16>(m_objects.capacity());
  87.  
  88.                 tAIObjectID prevID = 0;
  89.                 for (uint16 i = 0; i < capacity; ++i)
  90.                 {
  91.                         if (!m_objects.index_free(i) && (m_objects.get_index(i) == pObject))
  92.                         {
  93.                                 prevID = m_objects.get_index_id(i);
  94.                                 break;
  95.                         }
  96.                 }
  97.  
  98.                 assert(!prevID);
  99.                 if (prevID)
  100.                 {
  101.                         gEnv->pLog->LogError("AI: CObjectContainer::RegisterObjectUntyped - Object already registered - %p @%6d \"%s\" ",
  102.                                              (pObject), prevID, pObject->GetName());
  103.                         // intentionally not calling pObject->SetSelfReference(ref); here, since that would be bad.
  104.                         return false;
  105.                 }
  106.         }
  107. #endif
  108.  
  109.         tAIObjectID id = inId;
  110.         if (id != INVALID_AIOBJECTID)
  111.         {
  112.                 // Registering an object with a specified ID. This usually means the object was serialized out
  113.                 //      with a specific ID (eg to a pool bookmark) and now needs to be recreated using the same ID.
  114.  
  115.                 // In this case the ID should have been reserved earlier, and there should be a null object
  116.                 //      in the object map. UnreserveID will remove that, so we can add the new object in its place.
  117.                 UnreserveID(id);
  118.  
  119.                 m_objects.insert(id, pObject);
  120.         }
  121.         else
  122.         {
  123.                 id = m_objects.insert(pObject);
  124.         }
  125.  
  126.         AIAssert(id != INVALID_AIOBJECTID);
  127.  
  128.         ref.Assign(id);
  129.  
  130.         AILogComment("Registered object %p @%6d \"%s\" ", pObject, id, pObject ? pObject->GetName() : "NULL");
  131.  
  132.         if (pObject)
  133.         {
  134.                 pObject->SetSelfReference(ref);
  135.         }
  136.  
  137.         return true;
  138. }
  139.  
  140. bool CObjectContainer::DeregisterObjectUntyped(CAIObject* pObject)
  141. {
  142.         CWeakRef<CAIObject> ref(GetWeakRef(pObject));
  143.         return DeregisterObjectUntyped(&ref);
  144. }
  145.  
  146. // Only strong refs should ever be passed in
  147. bool CObjectContainer::DeregisterObjectUntyped(CAbstractUntypedRef* ref)
  148. {
  149.         CCCPOINT(DeregisterObjectUntyped);
  150.  
  151.         // (MATT) Checks for double-deregister in debug might be helpful here - but if the mechanisms are enforced it shouldn't be possible {2009/03/30}
  152.  
  153.         // (MATT) Perhaps this isn't the right place to increment - they are only pushed on a list, after all {2009/04/07}
  154.         m_snObjectsDeregistered++;
  155.         assert(m_snObjectsRegistered >= m_snObjectsDeregistered);
  156.  
  157.         tAIObjectID id = ref->GetObjectID();
  158.         bool validID = m_objects.validate(id);
  159.  
  160.         CRY_ASSERT_TRACE(validID, ("Multiple AI objects with id %i, dangling pointers or corruption imminent", id));
  161.  
  162.         if (validID)
  163.         {
  164.                 CRY_ASSERT_MESSAGE(!stl::find(m_DeregisteredBuffer, id), "Double deregistering object!");
  165.                 m_DeregisteredBuffer.push_back(id);
  166.                 ref->Assign(INVALID_AIOBJECTID);
  167.  
  168. #ifdef _DEBUG
  169.                 const CAIObject* object = m_objects[id];
  170.                 const char* const name = object ? object->GetName() : "<NULL OBJECT>";
  171.  
  172.                 AILogComment("Deregistered object %p @%6d \"%s\" ", (object), id, name);
  173. #endif
  174.                 return true;
  175.         }
  176.         else
  177.         {
  178.                 int prevIndex = m_objects.get_index_for_id(id);
  179.                 CAIObject* pObject = m_objects.get_index(prevIndex);
  180.                 CRY_ASSERT_TRACE(false, ("Previous object was %s (%d)", pObject ? pObject->GetName() : "<NULL OBJECT>", pObject ? pObject->GetAIObjectID() : 0));
  181.  
  182.                 return false;
  183.         }
  184. }
  185.  
  186. // This implictly has to mark something for deletion
  187. // Or, at least, it does usually....
  188. // No point returning the pointer because that encourages people to delete it themselves, which is no use
  189.  
  190. // We (should!) call this at the end of each AI frame
  191. int CObjectContainer::ReleaseDeregisteredObjects(bool checkForLeaks)
  192. {
  193.         CCCPOINT(ReleaseDeregisteredObjects);
  194.         FUNCTION_PROFILER(GetISystem(), PROFILE_AI);
  195.  
  196.         int nReleased = 0;
  197.  
  198.         // We use a double-buffer approach
  199.         // Currently, deleting objects currently triggers deregistration of any sub-objects
  200.         // It could be made to work with one vector but this seems more debuggable
  201.         int loopLimit = 100;
  202.         while (!m_DeregisteredBuffer.empty() && loopLimit)
  203.         {
  204.                 assert(--loopLimit > 0);
  205.                 m_DeregisteredBuffer.swap(m_DeregisteredWorkingBuffer);
  206.  
  207.                 TVecAIObjects::iterator itO = m_DeregisteredWorkingBuffer.begin();
  208.                 TVecAIObjects::iterator itOEnd = m_DeregisteredWorkingBuffer.end();
  209.                 for (; itO != itOEnd; ++itO)
  210.                 {
  211.                         const tAIObjectID& id = *itO;
  212.                         const bool validHandle = m_objects.validate(id);
  213.  
  214.                         assert(validHandle);
  215.                         if (validHandle)
  216.                         {
  217.                                 CAIObject* object = m_objects[id];
  218.  
  219. #ifdef _DEBUG
  220.                                 // Before we start removing the object check for Proxy and release if necessary
  221.                                 // This allows us to prepare for any proxy queries during remove procedure, such as checking health
  222.                                 const CAIActor* actor = object ? object->CastToCAIActor() : NULL;
  223.                                 const IAIActorProxy* proxy = actor ? actor->GetProxy() : NULL;
  224.                                 const char* const name = object ? object->GetName() : "<NULL OBJECT>";
  225.  
  226.                                 AILogComment("Releasing object %p @%6d proxy %p \"%s\" ",
  227.                                              object, id, proxy, name);
  228. #endif
  229.                                 if (object)
  230.                                 {
  231.  
  232.                                         // For transitional purposes (at least) call the remove code now
  233.                                         gAIEnv.pAIObjectManager->OnObjectRemoved(object);
  234.  
  235.                                         // Delete the object. Past this point, weak refs will still function and you can still fetch them for a given pointer, but the object itself is gone
  236.                                         // It might be better to put off that delete until we wipe pointers from the stubs, but if GetWeakRef disappeared, so would the need for all that.
  237.                                         object->Release();
  238.                                         //m_DeregisteredObjects.push_back(*itO);
  239.                                 }
  240.  
  241.                                 m_objects.erase(id);
  242.  
  243.                                 // Special case: if this ID is in the reserve list, we now need to add a null object to reserve the ID.
  244.                                 // This is because the object's ID has been reserved while the object still existed.
  245.                                 if (stl::find(m_reservedIDs, id))
  246.                                 {
  247.                                         m_objects.insert(id, NULL);
  248.                                         m_snObjectsRegistered++;
  249.                                 }
  250.  
  251.                                 nReleased++;
  252.                         }
  253.                 }
  254.                 m_DeregisteredWorkingBuffer.clear();
  255.         }
  256.  
  257. #ifdef CRYAISYSTEM_DEBUG
  258.         if (checkForLeaks)
  259.         {
  260.                 size_t totalObjects = GetNumRegistered();
  261.                 size_t count = m_objects.size();
  262.  
  263.                 if (count != totalObjects)
  264.                         DumpRegistered();
  265.  
  266.                 CRY_ASSERT_MESSAGE(count == totalObjects, "Something has leaked AI objects! Check the log for details");
  267.         }
  268. #endif
  269.  
  270.         return nReleased;
  271. }
  272.  
  273. CWeakRef<CAIObject> CObjectContainer::GetWeakRef(tAIObjectID id)
  274. {
  275.         if (m_objects.validate(id))
  276.                 return CWeakRef<CAIObject>(id);
  277.         return NILREF;
  278. }
  279.  
  280. void CObjectContainer::DumpRegistered()
  281. {
  282. #ifdef _DEBUG
  283.         size_t count = 0;
  284.         gEnv->pLog->Log("Listing all current AI objects:");
  285.         for (uint16 i = 0; i < static_cast<uint16>(m_objects.capacity()); ++i)
  286.         {
  287.                 if (!m_objects.index_free(i))
  288.                 {
  289.                         CAIObject* object = m_objects.get_index(i);
  290.                         CWeakRef<CAIObject> weakRef = GetWeakRef(object);
  291.                         gEnv->pLog->Log("Slot %" PRISIZE_T ": Object %p @%6d \"%s\" ", i, (object), weakRef.GetObjectID(), object ? object->GetName() : "NULL");
  292.                         ++count;
  293.                 }
  294.         }
  295.  
  296.         gEnv->pLog->Log("Total object count %" PRISIZE_T, count);
  297. #endif
  298. }
  299.  
  300. void CObjectContainer::Serialize(TSerialize ser)
  301. {
  302.         MEMSTAT_CONTEXT(EMemStatContextTypes::MSC_Other, 0, "Object Container serialization");
  303.  
  304.         CAISystem* pAISystem = GetAISystem();
  305.  
  306.         // Deal with the deregistration list as it makes no sense to serialize those objects
  307.         ReleaseDeregisteredObjects(true);
  308.  
  309.         ser.BeginGroup("ObjectContainer");
  310.  
  311.         const bool bReading = ser.IsReading();
  312.  
  313.         uint32 totalObjects = (uint32)GetNumRegistered();
  314.         uint32 capacity = m_objects.capacity();
  315.  
  316.         if (ser.IsWriting())
  317.         {
  318.                 for (uint32 i = 0; i < capacity; ++i)
  319.                 {
  320.                         if (!m_objects.index_free(i))
  321.                         {
  322.                                 CAIObject* object = m_objects.get_index(i);
  323.  
  324.                                 // AI objects associated with pooled entities are serialized as
  325.                                 //       part of the entity bookmark: skip them here
  326.                                 if (!object || object->ShouldSerialize() == false)
  327.                                 {
  328.                                         if (object)
  329.                                         {
  330.                                                 AILogComment("Serialization skipping no-save object %p @%6d \"%s\" ", object, object->GetAIObjectID(), object->GetName());
  331.                                         }
  332.  
  333.                                         totalObjects--;
  334.                                         continue;
  335.                                 }
  336.                         }
  337.                 }
  338.         }
  339.  
  340.         ser.Value("total", totalObjects);
  341.  
  342.         for (uint32 i = 0, totalSerialised = 0; totalSerialised < totalObjects && i < capacity; ++i)
  343.         {
  344.                 CAIObject* object = 0;
  345.  
  346.                 if (ser.IsWriting())
  347.                 {
  348.                         // First, is there a valid object here?
  349.                         if (m_objects.index_free(i))
  350.                                 continue;
  351.  
  352.                         object = m_objects.get_index(i);
  353.  
  354.                         // AI objects associated with pooled entities are serialized as
  355.                         //       part of the entity bookmark: skip them here
  356.                         if (!object || !object->ShouldSerialize())
  357.                                 continue;
  358.                 }
  359.  
  360.                 tAIObjectID id = INVALID_AIOBJECTID;
  361.  
  362.                 if (ser.IsWriting())
  363.                         id = m_objects.get_index_id(i);
  364.  
  365.                 // If writing, we've established this is a valid index
  366.                 // If reading, we will read the next valid index and skip ahead
  367.                 ser.BeginGroup("Entry");
  368.                 ser.Value("index", i);    // Reading may change the loop iterator
  369.                 ser.Value("id", id);
  370.  
  371.                 // Serialize basic data (enough to allow recreation of the object)
  372.                 SAIObjectCreationHelper objHeader(object);
  373.                 objHeader.Serialize(ser);
  374.                 assert(id == objHeader.objectId);
  375.  
  376.                 if (bReading)
  377.                 {
  378.                         //Read type for creation, skipping if the object already exists
  379.                         if (!object && m_objects.get(id) == NULL)
  380.                         {
  381.                                 object = objHeader.RecreateObject();
  382.  
  383.                                 // all IDs for objects to serialize should have been reserved earlier
  384.                                 UnreserveID(id);
  385.  
  386.                                 m_objects.insert(id, object);
  387.                                 m_snObjectsRegistered++;
  388.                         }
  389.                         else
  390.                         {
  391.                                 object = m_objects.get(id);
  392.                         }
  393.                 }
  394.  
  395.                 if (object)
  396.                 {
  397.                         MEMSTAT_CONTEXT_FMT(EMemStatContextTypes::MSC_Other, 0, "AI object (%s) serialization", GetNameFromType(objHeader.aiClass));
  398.  
  399.                         // (MATT) Note that this call may reach CAIActor, which currently handles serialising the proxies {2009/04/30}
  400.                         object->Serialize(ser);
  401.                 }
  402.                 totalSerialised++;
  403.  
  404.                 // Simple back-and-forth test, should be valid at this point
  405.                 // cppcheck-suppress assertWithSideEffect
  406.                 assert(GetWeakRef(object).IsValid());
  407.  
  408.                 if (bReading)
  409.                         AILogComment("Serialisation created object %p @%6d \"%s\" ", object, id, object ? object->GetName() : "");
  410.  
  411.                 ser.EndGroup();
  412.         }
  413.  
  414.         ser.EndGroup();
  415. }
  416.  
  417. void CObjectContainer::SerializeObjectIDs(TSerialize ser)
  418. {
  419.         if (ser.IsReading())
  420.         {
  421.                 // AI flush should have reset everything by this point
  422.                 assert(m_objects.size() == 0);
  423.  
  424.                 // read in the reserved object IDs and the used IDs
  425.                 //      before the rest of the system is serialized
  426.  
  427.                 // add NULL objects for each of the reserved IDs
  428.                 ser.Value("reservedObjects", m_reservedIDs);
  429.                 for (size_t i = 0; i < m_reservedIDs.size(); ++i)
  430.                 {
  431.                         m_objects.insert(m_reservedIDs[i], NULL);
  432.                         m_snObjectsRegistered++;
  433.                 }
  434.  
  435.                 ser.BeginGroup("existingObjects");
  436.                 uint32 objectCount;
  437.                 ser.Value("count", objectCount);
  438.                 for (uint32 i = 0; i < objectCount; ++i)
  439.                 {
  440.                         ser.BeginGroup("object");
  441.                         tAIObjectID id = INVALID_AIOBJECTID;
  442.                         ser.Value("id", id);
  443.  
  444.                         //string name = object->GetName();
  445.                         //ser.Value("name", name);
  446.  
  447.                         // again, add a NULL object for each one.
  448.                         ReserveID(id);
  449.  
  450.                         ser.EndGroup();
  451.                 }
  452.                 ser.EndGroup();
  453.         }
  454.         else
  455.         {
  456.                 // write out:
  457.                 //      - the list of reserved object IDs
  458.                 //      - a list of all currently registered objects
  459.                 ser.Value("reservedObjects", m_reservedIDs);
  460.  
  461.                 ser.BeginGroup("existingObjects");
  462.                 uint32 objectCount = 0;
  463.                 uint32 totalObjects = (uint32)m_objects.size();
  464.                 uint32 capacity = (uint32)m_objects.capacity();
  465.                 for (uint32 i = 0; i < capacity && objectCount < totalObjects; ++i)
  466.                 {
  467.                         if (!m_objects.index_free(i))
  468.                         {
  469.                                 CAIObject* object = m_objects.get_index(i);
  470.                                 if (object)
  471.                                 {
  472.                                         ++objectCount;
  473.  
  474.                                         ser.BeginGroup("object");
  475.                                         tAIObjectID id = object->GetAIObjectID();
  476.                                         ser.Value("id", id);
  477.  
  478.                                         // useful for debugging
  479.                                         //string name = object->GetName();
  480.                                         //ser.Value("name", name);
  481.  
  482.                                         ser.EndGroup();
  483.                                 }
  484.                         }
  485.                 }
  486.                 ser.Value("count", objectCount);
  487.                 ser.EndGroup();
  488.         }
  489. }
  490.  
  491. void CObjectContainer::PostSerialize()
  492. {
  493.         size_t capacity = m_objects.capacity();
  494.         for (size_t i = 0; i < capacity; ++i)
  495.         {
  496.                 uint16 i16 = static_cast<uint16>(i);
  497.                 if (!m_objects.index_free(i16))
  498.                 {
  499.                         CAIObject* object = m_objects.get_index(i16);
  500.                         if (object)
  501.                         {
  502.                                 object->PostSerialize();
  503.                         }
  504.                         else
  505.                         {
  506.                                 // NULL object in m_objects map:
  507.                                 //      verify that this object is on the reserved list
  508.                                 assert(stl::find(m_reservedIDs, m_objects.get_index_id(i16)));
  509.                         }
  510.                 }
  511.         }
  512. }
  513.  
  514. void CObjectContainer::RebuildObjectMaps(std::multimap<short, CCountedRef<CAIObject>>& objectMap, std::multimap<short, CWeakRef<CAIObject>>& dummyMap)
  515. {
  516.         size_t capacity = m_objects.capacity();
  517.  
  518.         for (size_t i = 0; i < capacity; ++i)
  519.         {
  520.                 uint16 i16 = static_cast<uint16>(i);
  521.                 if (!m_objects.index_free(i16))
  522.                 {
  523.                         CAIObject* object = m_objects.get_index(i16);
  524.                         if (!object)
  525.                                 continue;
  526.  
  527.                         unsigned short type = object->GetType();
  528.  
  529.                         bool bIsDummy = false;
  530.                         if (type == AIOBJECT_DUMMY)
  531.                                 bIsDummy = true;
  532.                         else if (type == AIOBJECT_WAYPOINT && object->GetSubType() == IAIObject::STP_BEACON)
  533.                                 bIsDummy = true;
  534.  
  535.                         if (bIsDummy)
  536.                         {
  537.                                 CWeakRef<CAIObject> ref = GetWeakRef(object->GetAIObjectID());
  538.                                 dummyMap.insert(std::multimap<short, CWeakRef<CAIObject>>::iterator::value_type(object->GetSubType(), ref));
  539.                         }
  540.                         else
  541.                         {
  542.                                 CStrongRef<CAIObject> ref;
  543.                                 ref.Assign(object->GetAIObjectID());
  544.                                 objectMap.insert(std::multimap<short, CCountedRef<CAIObject>>::iterator::value_type(object->GetType(), ref));
  545.                         }
  546.                 }
  547.         }
  548. }
  549.  
  550. void CObjectContainer::ReserveID(tAIObjectID id)
  551. {
  552.         assert(id != INVALID_AIOBJECTID);
  553.  
  554.         // Objects may already be in the reserve list - this happens for instance when an AI is
  555.         //      active at a checkpoint save and then is later deactivated (returned to pool): both will
  556.         //      cause a serialize-to-bookmark, which will reserve the ID. So just ignore the second reserve.
  557.         stl::push_back_unique(m_reservedIDs, id);
  558.  
  559.         // If an object still exists using this ID, leave the existing one there.
  560.         //      When removed, ReleaseDeregisteredObjects will add the NULL entry.
  561.         if (m_objects.free(id))
  562.         {
  563.                 m_objects.insert(id, NULL);
  564.  
  565.                 m_snObjectsRegistered++;  // prevents asserts about leaking objects
  566.         }
  567. }
  568.  
  569. void CObjectContainer::UnreserveID(tAIObjectID id)
  570. {
  571.         assert(id != INVALID_AIOBJECTID);
  572.         assert(stl::find(m_reservedIDs, id));
  573.         assert(m_objects.get(id) == NULL);
  574.  
  575.         if (m_objects.get(id) != NULL)
  576.         {
  577.                 // this would suggest a code error: something was added to the reserve list, which should have then placed a NULL
  578.                 //      AI object in m_objects. That should mean that no other object can take that slot.
  579.                 CAIObject* pPrevObject = m_objects[id];
  580.                 AILogAlways("Error: Trying to unreserve existing AI Object id %d, object is %s", id, pPrevObject ? pPrevObject->GetName() : "NULL");
  581.  
  582.                 // Returning now probably means the request won't be able to register a new object using this ID.
  583.                 // That's bad, but probably better than removing some other object.
  584.                 return;
  585.         }
  586.  
  587.         stl::find_and_erase(m_reservedIDs, id);
  588.         m_objects.erase(id);
  589.         m_snObjectsDeregistered++;
  590. }
  591.  
downloadObjectContainer.cpp Source code - Download CRYENGINE Source code
Related Source Codes/Software:
postal - 2017-06-11
reactide - Reactide is the first dedicated IDE for React web ... 2017-06-11
rkt - rkt is a pod-native container engine for Linux. It... 2017-06-11
uWebSockets - Tiny WebSockets https://for... 2017-06-11
realworld - TodoMVC for the RealWorld - Exemplary fullstack Me... 2017-06-11
CRYENGINE - CRYENGINE is a powerful real-time game development... 2017-06-11
goreplay - GoReplay is an open-source tool for capturing and ... 2017-06-10
pyenv - Simple Python version management 2017-06-10
redux-saga - An alternative side effect model for Redux apps ... 2017-06-10
angular-starter - 2017-06-10

 Back to top