BVB Source Codes

CRYENGINE Show VehicleViewFirstPerson.cpp Source code

Return Download CRYENGINE: download VehicleViewFirstPerson.cpp Source code - Download CRYENGINE Source code - Type:.cpp
  1. // Copyright 2001-2016 Crytek GmbH / Crytek Group. All rights reserved.
  2.  
  3. /*************************************************************************
  4.    -------------------------------------------------------------------------
  5.    $Id$
  6.    $DateTime$
  7.    Description: Implements the first person pit view for vehicles
  8.  
  9.    -------------------------------------------------------------------------
  10.    History:
  11.    - 29:01:2006: Created by Mathieu Pinard
  12.  
  13. *************************************************************************/
  14. #include "StdAfx.h"
  15. #include "CryAction.h"
  16. #include "IActorSystem.h"
  17. #include <CryAnimation/ICryAnimation.h>
  18. #include "IViewSystem.h"
  19. #include "IVehicleSystem.h"
  20. #include "VehicleViewFirstPerson.h"
  21. #include "VehicleSeat.h"
  22. #include "Vehicle.h"
  23.  
  24. const char* CVehicleViewFirstPerson::m_name = "FirstPerson";
  25.  
  26. //------------------------------------------------------------------------
  27. CVehicleViewFirstPerson::CVehicleViewFirstPerson()
  28.         : m_passengerId(0)
  29. {
  30.         m_hideVehicle = false;
  31.         m_pHelper = NULL;
  32.         m_sCharacterBoneName = "";
  33.         m_offset.zero();
  34.         m_fov = DEG2RAD(55.0f);
  35.         m_relToHorizon = 0.f;
  36.         m_frameSlot = -1;
  37.         m_invFrame.SetIdentity();
  38.         m_passengerId = 0;
  39.         m_speedRot = 0.f;
  40.         m_frameObjectOffset = Vec3(0, 0.18f, 0.01f);
  41. }
  42.  
  43. //------------------------------------------------------------------------
  44. bool CVehicleViewFirstPerson::Init(IVehicleSeat* pISeat, const CVehicleParams& table)
  45. {
  46.         CVehicleSeat* pSeat = static_cast<CVehicleSeat*>(pISeat);
  47.  
  48.         if (!CVehicleViewBase::Init(pSeat, table))
  49.                 return false;
  50.  
  51.         if (CVehicleParams paramsTable = table.findChild(m_name))
  52.         {
  53.                 paramsTable.getAttr("offset", m_offset);
  54.                 paramsTable.getAttr("hidePlayer", m_hidePlayer);
  55.                 paramsTable.getAttr("hideVehicle", m_hideVehicle);
  56.                 paramsTable.getAttr("relativeToHorizon", m_relToHorizon);
  57.                 paramsTable.getAttr("followSpeed", m_speedRot);
  58.  
  59.                 float viewFov;
  60.                 if (paramsTable.getAttr("fov", viewFov))
  61.                 {
  62.                         m_fov = DEG2RAD(viewFov);
  63.                 }
  64.  
  65.                 m_sCharacterBoneName = paramsTable.getAttr("characterBone");
  66.                 string helperName = paramsTable.getAttr("helper");
  67.  
  68.                 if (!helperName.empty())
  69.                 {
  70.                         if (helperName != "auto")
  71.                         {
  72.                                 m_pHelper = m_pVehicle->GetHelper(helperName);
  73.                         }
  74.                         else
  75.                         {
  76.                                 // create a helper with default viewpos above sithelper
  77.                                 const string& seatName = pSeat->GetName();
  78.                                 helperName = seatName + string("_ghostview_pos");
  79.  
  80.                                 if (IVehicleHelper* pSitHelper = pSeat->GetSitHelper())
  81.                                 {
  82.                                         Matrix34 tm;
  83.                                         pSitHelper->GetVehicleTM(tm);
  84.                                         Vec3 pos = tm.GetTranslation() + Vec3(0, 0, 0.625); // player eye height
  85.  
  86.                                         m_pVehicle->AddHelper(helperName.c_str(), pos, tm.GetColumn1(), pSitHelper->GetParentPart());
  87.                                         m_pHelper = m_pVehicle->GetHelper(helperName.c_str());
  88.                                 }
  89.                         }
  90.  
  91.                         if (!m_pHelper)
  92.                                 GameWarning("[%s, seat %s]: view helper %s not found, using character head", m_pVehicle->GetEntity()->GetName(), m_pSeat->GetName().c_str(), helperName.c_str());
  93.                 }
  94.  
  95.                 string frame = paramsTable.getAttr("frameObject");
  96.                 if (!frame.empty())
  97.                 {
  98.                         // todo: aspect ratio?
  99.                         if (strstr(frame, ".cgf"))
  100.                                 m_frameSlot = m_pVehicle->GetEntity()->LoadGeometry(-1, frame);
  101.                         else
  102.                                 m_frameSlot = m_pVehicle->GetEntity()->LoadCharacter(-1, frame);
  103.  
  104.                         if (m_frameSlot != -1)
  105.                         {
  106.                                 m_pVehicle->GetEntity()->SetSlotFlags(m_frameSlot, m_pVehicle->GetEntity()->GetSlotFlags(m_frameSlot) & ~(ENTITY_SLOT_RENDER | ENTITY_SLOT_RENDER_NEAREST));
  107.  
  108.                                 if (m_pHelper)
  109.                                 {
  110.                                         Matrix34 tm;
  111.                                         m_pHelper->GetVehicleTM(tm);
  112.                                         m_invFrame = tm.GetInverted();
  113.                                         m_pVehicle->GetEntity()->SetSlotLocalTM(m_frameSlot, tm);
  114.                                 }
  115.                         }
  116.                 }
  117.  
  118.                 paramsTable.getAttr("frameObjectOffset", m_frameObjectOffset);
  119.         }
  120.  
  121.         if (m_hideVehicle)
  122.                 m_hidePlayer = true;
  123.  
  124.         if (m_speedRot == 0.f)
  125.         {
  126.                 m_speedRot = 4.0f;
  127.  
  128.                 if (IVehicleMovement* pMovement = m_pVehicle->GetMovement())
  129.                 {
  130.                         if (pMovement->GetMovementType() == IVehicleMovement::eVMT_Air)
  131.                         {
  132.                                 m_speedRot *= 2.0f;
  133.                         }
  134.                 }
  135.         }
  136.  
  137.         Reset();
  138.         return true;
  139. }
  140.  
  141. //------------------------------------------------------------------------
  142. void CVehicleViewFirstPerson::Reset()
  143. {
  144.         CVehicleViewBase::Reset();
  145.  
  146.         if (m_hideVehicle)
  147.                 HideEntitySlots(m_pVehicle->GetEntity(), false);
  148. }
  149.  
  150. //------------------------------------------------------------------------
  151. void CVehicleViewFirstPerson::OnStartUsing(EntityId passengerId)
  152. {
  153.         CVehicleViewBase::OnStartUsing(passengerId);
  154.  
  155.         if (!m_sCharacterBoneName.empty())
  156.         {
  157.                 bool errorOccurred = false;
  158.                 IEntity* pEntity = gEnv->pEntitySystem->GetEntity(passengerId);
  159.                 ICharacterInstance* pCharacter = pEntity ? pEntity->GetCharacter(0) : NULL;
  160.                 if (pCharacter)
  161.                 {
  162.                         IDefaultSkeleton& rIDefaultSkeleton = pCharacter->GetIDefaultSkeleton();
  163.                         uint32 id = rIDefaultSkeleton.GetJointIDByName(m_sCharacterBoneName);
  164.                         uint32 numJoints = rIDefaultSkeleton.GetJointCount();
  165.                         if (numJoints <= id)
  166.                         {
  167.                                 errorOccurred = true;
  168.                         }
  169.                 }
  170.                 else
  171.                 {
  172.                         errorOccurred = true;
  173.                 }
  174.  
  175.                 if (errorOccurred)
  176.                 {
  177.                         GameWarning("[%s, seat %s]: character Bone name %s not found", m_pVehicle->GetEntity()->GetName(), m_pSeat->GetName().c_str(), m_sCharacterBoneName.c_str());
  178.                 }
  179.                 else if (m_pHelper)
  180.                 {
  181.                         GameWarning("[%s, seat %s]: helper and character Bone name found, helper will be used (please only use one of these options)", m_pVehicle->GetEntity()->GetName(), m_pSeat->GetName().c_str());
  182.                 }
  183.         }
  184.  
  185.         if (m_hideVehicle)
  186.         {
  187.                 m_slotFlags.clear();
  188.                 HideEntitySlots(m_pVehicle->GetEntity(), true);
  189.         }
  190.  
  191.         if (m_frameSlot != -1)
  192.         {
  193.                 m_pVehicle->GetEntity()->SetSlotFlags(m_frameSlot, m_pVehicle->GetEntity()->GetSlotFlags(m_frameSlot) | ENTITY_SLOT_RENDER | ENTITY_SLOT_RENDER_NEAREST);
  194.         }
  195.  
  196.         m_passengerId = passengerId;
  197.  
  198.         m_viewPosition = GetWorldPosGoal();
  199.         m_viewRotation = GetWorldRotGoal();
  200.  
  201.         if (m_passengerId && m_pSeat && !m_pSeat->IsDriver() && m_pSeat->IsPassengerShielded())
  202.         {
  203.                 // disable rendering of ocean for passenger of amphibious vehicles (we see water inside the cabin)
  204.                 if (IVehicleMovement* pMovement = m_pVehicle->GetMovement())
  205.                 {
  206.                         if (pMovement->GetMovementType() == IVehicleMovement::eVMT_Amphibious)
  207.                         {
  208.                                 I3DEngine* p3DEngine = gEnv->p3DEngine;
  209.                                 p3DEngine->SetOceanRenderFlags(OCR_NO_DRAW);
  210.                         }
  211.                 }
  212.         }
  213.  
  214.         if (m_hidePlayer)
  215.         {
  216.                 if (IEntity* pEntity = gEnv->pEntitySystem->GetEntity(m_passengerId))
  217.                         HideEntitySlots(pEntity, true);
  218.         }
  219.  
  220.         m_pVehicle->RegisterVehicleEventListener(this, "1stPersonView");
  221. }
  222.  
  223. //------------------------------------------------------------------------
  224. void CVehicleViewFirstPerson::OnStopUsing()
  225. {
  226.         CVehicleViewBase::OnStopUsing();
  227.  
  228.         m_pVehicle->UnregisterVehicleEventListener(this);
  229.  
  230.         if (m_hideVehicle)
  231.                 HideEntitySlots(m_pVehicle->GetEntity(), false);
  232.  
  233.         if (m_hidePlayer)
  234.         {
  235.                 if (IEntity* pEntity = gEnv->pEntitySystem->GetEntity(m_passengerId))
  236.                         HideEntitySlots(pEntity, false);
  237.         }
  238.  
  239.         if (m_frameSlot != -1)
  240.         {
  241.                 m_pVehicle->GetEntity()->SetSlotFlags(m_frameSlot, m_pVehicle->GetEntity()->GetSlotFlags(m_frameSlot) & ~(ENTITY_SLOT_RENDER | ENTITY_SLOT_RENDER_NEAREST));
  242.         }
  243.  
  244.         // enable rendering of ocean again, if it was a passenger in amphibious mode
  245.         I3DEngine* p3DEngine = gEnv->p3DEngine;
  246.         p3DEngine->SetOceanRenderFlags(OCR_OCEANVOLUME_VISIBLE);
  247.  
  248.         if (EntityId weaponId = m_pVehicle->GetCurrentWeaponId(m_passengerId))
  249.         {
  250.                 if (IItem* pItem = CCryAction::GetCryAction()->GetIItemSystem()->GetItem(weaponId))
  251.                 {
  252.                         if (IWeapon* pWeapon = pItem->GetIWeapon())
  253.                         {
  254.                                 if (pWeapon->IsZoomed() || pWeapon->IsZoomingInOrOut())
  255.                                         pWeapon->StopZoom(m_passengerId);
  256.                         }
  257.  
  258.                 }
  259.         }
  260.  
  261.         m_passengerId = 0;
  262. }
  263.  
  264. //------------------------------------------------------------------------
  265. void CVehicleViewFirstPerson::Update(float frameTimeIn)
  266. {
  267.         // Use the physics frame time, but only if non zero!
  268.         const float physFrameTime = static_cast<CVehicle*>(m_pVehicle)->GetPhysicsFrameTime();
  269.         const float frameTime = (physFrameTime > 0.f) ? min(physFrameTime, frameTimeIn) : frameTimeIn;
  270.  
  271.         CVehicleViewBase::Update(frameTime);
  272.  
  273.         if (m_frameSlot != -1 && m_pHelper)
  274.         {
  275.                 Matrix34 tm;
  276.                 m_pHelper->GetVehicleTM(tm);
  277.                 tm = tm * m_invFrame;
  278.                 tm.SetTranslation(tm.GetTranslation() + tm.TransformVector(m_frameObjectOffset));
  279.                 m_pVehicle->GetEntity()->SetSlotLocalTM(m_frameSlot, tm);
  280.         }
  281.  
  282.         m_viewPosition = GetWorldPosGoal();
  283. }
  284.  
  285. //------------------------------------------------------------------------
  286. void CVehicleViewFirstPerson::UpdateView(SViewParams& viewParams, EntityId playerId)
  287. {
  288.         // JB: Attaching vehicles to trackview causes the view position to be out of date
  289.         // by the time we reach Update view, introducing view lag. Need to re-update
  290.         m_viewPosition = GetWorldPosGoal();
  291.  
  292.         if (!m_passengerId)
  293.                 return;
  294.  
  295.         viewParams.nearplane = 0.02f;
  296.         viewParams.fov = m_fov;
  297.  
  298.         if (EntityId weaponId = m_pVehicle->GetCurrentWeaponId(m_passengerId))
  299.         {
  300.                 if (IItem* pItem = CCryAction::GetCryAction()->GetIItemSystem()->GetItem(weaponId))
  301.                 {
  302.                         if (pItem->FilterView(viewParams))
  303.                                 return;
  304.                 }
  305.         }
  306.  
  307.         viewParams.position = m_viewPosition;
  308.         viewParams.rotation = GetVehicleRotGoal() * Quat::CreateRotationXYZ(m_rotation);
  309.  
  310.         // set view direction on actor
  311.         IActor* pActor = CCryAction::GetCryAction()->GetIActorSystem()->GetActor(playerId);
  312.         if (pActor && pActor->IsClient())
  313.         {
  314.                 pActor->SetViewInVehicle(viewParams.rotation);
  315.         }
  316.  
  317.         // recoil
  318.         viewParams.rotation *= Quat::CreateRotationXYZ(m_viewAngleOffset);
  319. }
  320.  
  321. //------------------------------------------------------------------------
  322. Vec3 CVehicleViewFirstPerson::GetWorldPosGoal()
  323. {
  324.         Vec3 vehiclePos;
  325.  
  326.         if (m_pHelper)
  327.         {
  328.                 vehiclePos = m_pHelper->GetVehicleSpaceTranslation();
  329.         }
  330.         else if (!m_sCharacterBoneName.empty())
  331.         {
  332.                 Vec3 bonePos;
  333.                 IEntity* pEntity = gEnv->pEntitySystem->GetEntity(m_pSeat->GetPassenger());
  334.                 ICharacterInstance* pCharacter = pEntity ? pEntity->GetCharacter(0) : NULL;
  335.                 if (pCharacter)
  336.                 {
  337.                         IDefaultSkeleton& rIDefaultSkeleton = pCharacter->GetIDefaultSkeleton();
  338.                         uint32 id = rIDefaultSkeleton.GetJointIDByName(m_sCharacterBoneName);
  339.                         uint32 numJoints = rIDefaultSkeleton.GetJointCount();
  340.                         if (numJoints > id)
  341.                         {
  342.                                 bonePos = pCharacter->GetISkeletonPose()->GetAbsJointByID(id).t;
  343.                         }
  344.                 }
  345.                 vehiclePos = pEntity ? pEntity->GetWorldTM() * bonePos : Vec3(0, 0, 0);
  346.                 return vehiclePos;
  347.         }
  348.         else
  349.         {
  350.                 IActor* pActor = CCryAction::GetCryAction()->GetIActorSystem()->GetActor(m_passengerId);
  351.                 CRY_ASSERT(pActor);
  352.  
  353.                 vehiclePos = pActor->GetLocalEyePos() + m_offset;
  354.  
  355.                 IEntity* pActorEntity = pActor->GetEntity();
  356.  
  357.                 const Matrix34& slotTM = pActorEntity->GetSlotLocalTM(0, false);
  358.                 vehiclePos = pActorEntity->GetLocalTM() * slotTM * vehiclePos;
  359.         }
  360.  
  361.         return m_pVehicle->GetEntity()->GetWorldTM() * vehiclePos;
  362. }
  363.  
  364. //------------------------------------------------------------------------
  365. Quat CVehicleViewFirstPerson::GetWorldRotGoal()
  366. {
  367.         // now get fitting vehicle world pos/rot
  368.         Quat vehicleWorldRot = m_pVehicle->GetEntity()->GetWorldRotation();
  369.  
  370.         if (m_relToHorizon > 0.f)
  371.         {
  372.                 Quat vehicleRot = m_pVehicle->GetEntity()->GetRotation();
  373.                 Vec3 vx = vehicleRot * Vec3(1, 0, 0);
  374.                 Vec3 vy = vehicleRot * Vec3(0, 1, 0);
  375.  
  376.                 // vx is "correct"
  377.                 vy = (1.0f - m_relToHorizon) * Vec3(0, 0, 1).Cross(vx) + m_relToHorizon * vy;
  378.                 vy.NormalizeSafe(Vec3Constants<float>::fVec3_OneY);
  379.  
  380.                 Vec3 vz = vx.Cross(vy);
  381.                 vz.NormalizeSafe(Vec3Constants<float>::fVec3_OneZ);
  382.  
  383.                 vehicleWorldRot = Quat(Matrix33::CreateFromVectors(vx, vy, vz));
  384.         }
  385.  
  386.         return vehicleWorldRot;
  387. }
  388.  
  389. //------------------------------------------------------------------------
  390. Quat CVehicleViewFirstPerson::GetVehicleRotGoal()
  391. {
  392.         Quat vehicleRot = m_pVehicle->GetEntity()->GetRotation();
  393.  
  394.         if (m_pHelper)
  395.         {
  396.                 Matrix34 helperTM;
  397.                 m_pHelper->GetVehicleTM(helperTM);
  398.                 vehicleRot = vehicleRot * Quat(helperTM);
  399.         }
  400.         else
  401.         {
  402.                 IEntity* pEntity = gEnv->pEntitySystem->GetEntity(m_pSeat->GetPassenger());
  403.                 CRY_ASSERT(pEntity);
  404.  
  405.                 if (!m_sCharacterBoneName.empty())
  406.                 {
  407.                         if (ICharacterInstance* pCharacter = pEntity->GetCharacter(0))
  408.                         {
  409.                                 IDefaultSkeleton& rIDefaultSkeleton = pCharacter->GetIDefaultSkeleton();
  410.                                 const uint32 id = rIDefaultSkeleton.GetJointIDByName(m_sCharacterBoneName);
  411.                                 const uint32 numJoints = rIDefaultSkeleton.GetJointCount();
  412.                                 if (numJoints > id)
  413.                                 {
  414.                                         const Quat boneRot = pCharacter->GetISkeletonPose()->GetAbsJointByID(id).q;
  415.                                         vehicleRot *= (pEntity->GetRotation() * boneRot);
  416.                                 }
  417.                         }
  418.                 }
  419.                 else
  420.                 {
  421.                         vehicleRot *= pEntity->GetRotation();
  422.                 }
  423.         }
  424.         return vehicleRot;
  425. }
  426.  
  427. //------------------------------------------------------------------------
  428. void CVehicleViewFirstPerson::HideEntitySlots(IEntity* pEnt, bool hide)
  429. {
  430.         IActorSystem* pActorSystem = CCryAction::GetCryAction()->GetIActorSystem();
  431.         CRY_ASSERT(pActorSystem);
  432.  
  433.         if (hide)
  434.         {
  435.                 for (int i = 0; i < pEnt->GetSlotCount(); ++i)
  436.                 {
  437.                         if (pEnt->IsSlotValid(i) && pEnt->GetSlotFlags(i) & ENTITY_SLOT_RENDER)
  438.                         {
  439.                                 if (pEnt->GetId() == m_pVehicle->GetEntity()->GetId())
  440.                                 {
  441.                                         // set character to always update
  442.                                         if (ICharacterInstance* pCharInstance = pEnt->GetCharacter(i))
  443.                                         {
  444.                                                 pCharInstance->SetFlags(pCharInstance->GetFlags() | CS_FLAG_UPDATE_ALWAYS);
  445.  
  446.                                                 if (ISkeletonPose* pSkeletonPose = pCharInstance->GetISkeletonPose())
  447.                                                         pSkeletonPose->SetForceSkeletonUpdate(10);
  448.                                         }
  449.                                 }
  450.  
  451.                                 pEnt->SetSlotFlags(i, pEnt->GetSlotFlags(i) & ~ENTITY_SLOT_RENDER);
  452.  
  453.                                 if (IActor* pActor = pActorSystem->GetActor(pEnt->GetId()))
  454.                                 {
  455.                                         pActor->HideAllAttachments(true);
  456.                                 }
  457.  
  458.                                 // store slot; we must not reveal previously hidden slots later
  459.                                 m_slotFlags.insert(std::pair<EntityId, int>(pEnt->GetId(), i));
  460.                         }
  461.                 }
  462.  
  463.                 // hide all children
  464.                 for (int i = 0; i < pEnt->GetChildCount(); ++i)
  465.                         HideEntitySlots(pEnt->GetChild(i), hide);
  466.         }
  467.         else
  468.         {
  469.                 // unhide all stored slots
  470.                 for (TSlots::iterator it = m_slotFlags.begin(); it != m_slotFlags.end(); ++it)
  471.                 {
  472.                         IEntity* pEntity = gEnv->pEntitySystem->GetEntity(it->first);
  473.  
  474.                         if (pEntity && pEntity->IsSlotValid(it->second))
  475.                         {
  476.                                 pEntity->SetSlotFlags(it->second, pEntity->GetSlotFlags(it->second) | (ENTITY_SLOT_RENDER));
  477.  
  478.                                 if (IActor* pActor = pActorSystem->GetActor(pEnt->GetId()))
  479.                                 {
  480.                                         pActor->HideAllAttachments(false);
  481.                                 }
  482.  
  483.                                 if (pEntity->GetId() == m_pVehicle->GetEntity()->GetId())
  484.                                 {
  485.                                         // reset character flags
  486.                                         if (ICharacterInstance* pCharInstance = pEntity->GetCharacter(it->second))
  487.                                         {
  488.                                                 pCharInstance->SetFlags(pCharInstance->GetFlags() & ~CS_FLAG_UPDATE_ALWAYS);
  489.  
  490.                                                 if (ISkeletonPose* pSkeletonPose = pCharInstance->GetISkeletonPose())
  491.                                                         pSkeletonPose->SetForceSkeletonUpdate(0);
  492.                                         }
  493.                                 }
  494.                         }
  495.                 }
  496.  
  497.                 m_slotFlags.clear();
  498.         }
  499. }
  500.  
  501. //------------------------------------------------------------------------
  502. void CVehicleViewFirstPerson::OnVehicleEvent(EVehicleEvent event, const SVehicleEventParams& params)
  503. {
  504.         CVehicleViewBase::OnVehicleEvent(event, params);
  505.  
  506.         if (event == eVE_PassengerEnter && m_hideVehicle)
  507.         {
  508.                 if (IEntity* pPassengerEntity = gEnv->pEntitySystem->GetEntity(params.entityId))
  509.                 {
  510.                         HideEntitySlots(pPassengerEntity, true);
  511.                 }
  512.         }
  513. }
  514.  
  515. //------------------------------------------------------------------------
  516. void CVehicleViewFirstPerson::GetMemoryUsage(ICrySizer* s) const
  517. {
  518.         s->Add(*this);
  519.         s->AddObject(m_slotFlags);
  520. }
  521.  
  522. DEFINE_VEHICLEOBJECT(CVehicleViewFirstPerson);
  523.  
downloadVehicleViewFirstPerson.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