Guest User

Untitled

a guest
Sep 26th, 2024
133
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 15.45 KB | None | 0 0
  1. #define _cube_cpp_
  2.  
  3. #include "stdafx.h"
  4. #include "constants.h"
  5. #include "utils.h"
  6. #include "log.h"
  7. #include "char.h"
  8. #include "locale_service.h"
  9. #include "item.h"
  10. #include "item_manager.h"
  11. #include "questmanager.h"
  12. #include <sstream>
  13. #include "packet.h"
  14. #include "desc_client.h"
  15.  
  16. #ifdef ENABLE_CUBE_RENEWAL
  17. static std::vector<CUBE_RENEWAL_DATA*>  s_cube_proto;
  18.  
  19. typedef std::vector<CUBE_RENEWAL_VALUE> TCubeValueVector;
  20.  
  21. struct SCubeMaterialInfo
  22. {
  23.     SCubeMaterialInfo()
  24.     {
  25.         bHaveComplicateMaterial = false;
  26.     };
  27.  
  28.     CUBE_RENEWAL_VALUE          reward;                         // º¸»ó?? ¹¹³?
  29.     TCubeValueVector    material;                       // ?ç·áµé?º ¹¹³?
  30.     DWORD               gold;                           // µ·?º ¾ó¸¶µå³?
  31.     int                 percent;
  32.     std::string         category;
  33.     TCubeValueVector    complicateMaterial;             // º¹?â??-_- ?ç·áµé
  34.     // bool             allowCopyAttr;
  35.     std::string         infoText;      
  36.     bool                bHaveComplicateMaterial;        //
  37. };
  38.  
  39. struct SItemNameAndLevel
  40. {
  41.     SItemNameAndLevel() { level = 0; }
  42.  
  43.     std::string     name;
  44.     int             level;
  45. };
  46.  
  47.  
  48. typedef std::vector<SCubeMaterialInfo>                              TCubeResultList;
  49. typedef quest::unordered_map<DWORD, TCubeResultList>                TCubeMapByNPC;              // °¢°¢?? NPCº°·? ¾î¶² °? ¸¸µé ¼ö ??°í ?ç·á°¡ ¹º?ö...
  50.  
  51. TCubeMapByNPC cube_info_map;
  52.  
  53.  
  54. static bool FN_check_valid_npc( WORD vnum )
  55. {
  56.     for ( std::vector<CUBE_RENEWAL_DATA*>::iterator iter = s_cube_proto.begin(); iter != s_cube_proto.end(); iter++ )
  57.     {
  58.         if ( std::find((*iter)->npc_vnum.begin(), (*iter)->npc_vnum.end(), vnum) != (*iter)->npc_vnum.end() )
  59.             return true;
  60.     }
  61.  
  62.     return false;
  63. }
  64.  
  65. static bool FN_check_cube_data (CUBE_RENEWAL_DATA *cube_data)
  66. {
  67.     DWORD   i = 0;
  68.     DWORD   end_index = 0;
  69.  
  70.     end_index = cube_data->npc_vnum.size();
  71.     for (i=0; i<end_index; ++i)
  72.     {
  73.         if ( cube_data->npc_vnum[i] == 0 )  return false;
  74.     }
  75.  
  76.     end_index = cube_data->item.size();
  77.     for (i=0; i<end_index; ++i)
  78.     {
  79.         if ( cube_data->item[i].vnum == 0 )     return false;
  80.         if ( cube_data->item[i].count == 0 )    return false;
  81.     }
  82.  
  83.     end_index = cube_data->reward.size();
  84.     for (i=0; i<end_index; ++i)
  85.     {
  86.         if ( cube_data->reward[i].vnum == 0 )   return false;
  87.         if ( cube_data->reward[i].count == 0 )  return false;
  88.     }
  89.     return true;
  90. }
  91.  
  92. static int FN_check_cube_item_vnum_material(const SCubeMaterialInfo& materialInfo, int index)
  93. {
  94.     if (index <= materialInfo.material.size()){
  95.         return materialInfo.material[index-1].vnum;
  96.     }
  97.     return 0;
  98. }
  99.  
  100. static int FN_check_cube_item_count_material(const SCubeMaterialInfo& materialInfo,int index)
  101. {
  102.     if (index <= materialInfo.material.size()){
  103.         return materialInfo.material[index-1].count;
  104.     }
  105.  
  106.     return 0;
  107. }
  108.  
  109. CUBE_RENEWAL_DATA::CUBE_RENEWAL_DATA()
  110. {
  111.     this->gold = 0;
  112.     // this->allowCopyAttr = false;
  113.     this->category = "WORLDARD";
  114. }
  115.  
  116.  
  117. void Cube_init()
  118. {
  119.     CUBE_RENEWAL_DATA * p_cube = NULL;
  120.     std::vector<CUBE_RENEWAL_DATA*>::iterator iter;
  121.  
  122.     char file_name[256+1];
  123.     snprintf(file_name, sizeof(file_name), "%s/cube.txt", LocaleService_GetBasePath().c_str());
  124.  
  125.     sys_log(0, "Cube_Init %s", file_name);
  126.  
  127.     for (iter = s_cube_proto.begin(); iter!=s_cube_proto.end(); iter++)
  128.     {
  129.         p_cube = *iter;
  130.         M2_DELETE(p_cube);
  131.     }
  132.  
  133.     s_cube_proto.clear();
  134.  
  135.     if (false == Cube_load(file_name))
  136.         sys_err("Cube_Init failed");
  137. }
  138.  
  139. bool Cube_load (const char *file)
  140. {
  141.     FILE    *fp;
  142.  
  143.  
  144.     const char  *value_string;
  145.  
  146.     char    one_line[256];
  147.     int     value1, value2;
  148.     const char  *delim = " \t\r\n";
  149.     char    *v, *token_string;
  150.     char *v1;
  151.     CUBE_RENEWAL_DATA   *cube_data = NULL;
  152.     CUBE_RENEWAL_VALUE  cube_value = {0,0};
  153.  
  154.     if (0 == file || 0 == file[0])
  155.         return false;
  156.  
  157.     if ((fp = fopen(file, "r")) == 0)
  158.         return false;
  159.  
  160.     while (fgets(one_line, 256, fp))
  161.     {
  162.         value1 = value2 = 0;
  163.  
  164.         if (one_line[0] == '#')
  165.             continue;
  166.  
  167.         token_string = strtok(one_line, delim);
  168.  
  169.         if (NULL == token_string)
  170.             continue;
  171.  
  172.         // set value1, value2
  173.         if ((v = strtok(NULL, delim)))
  174.             str_to_number(value1, v);
  175.             value_string = v;
  176.  
  177.         if ((v = strtok(NULL, delim)))
  178.             str_to_number(value2, v);
  179.  
  180.         TOKEN("section")
  181.         {
  182.             cube_data = M2_NEW CUBE_RENEWAL_DATA;
  183.         }
  184.         else TOKEN("npc")
  185.         {
  186.             cube_data->npc_vnum.push_back((WORD)value1);
  187.         }
  188.         else TOKEN("item")
  189.         {
  190.             cube_value.vnum     = value1;
  191.             cube_value.count    = value2;
  192.  
  193.             cube_data->item.push_back(cube_value);
  194.         }
  195.         else TOKEN("reward")
  196.         {
  197.             cube_value.vnum     = value1;
  198.             cube_value.count    = value2;
  199.  
  200.             cube_data->reward.push_back(cube_value);
  201.         }
  202.         else TOKEN("percent")
  203.         {
  204.  
  205.             cube_data->percent = value1;
  206.         }
  207.  
  208.         else TOKEN("category")
  209.         {
  210.             cube_data->category = value_string;
  211.         }
  212.  
  213.         else TOKEN("gold")
  214.         {
  215.             // ?¦?¶¿¡ ??¿ä?? ±?¾?
  216.             cube_data->gold = value1;
  217.         }
  218.         // else TOKEN("allow_copy")
  219.         // {
  220.         //  cube_data->allowCopyAttr = (value1 == 1 ? true : false);
  221.         // }
  222.         else TOKEN("end")
  223.         {
  224.  
  225.             // TODO : check cube data
  226.             if (false == FN_check_cube_data(cube_data))
  227.             {
  228.                 M2_DELETE(cube_data);
  229.                 continue;
  230.             }
  231.             s_cube_proto.push_back(cube_data);
  232.         }
  233.     }
  234.  
  235.     fclose(fp);
  236.     return true;
  237. }
  238.  
  239.  
  240. SItemNameAndLevel SplitItemNameAndLevelFromName(const std::string& name)
  241. {
  242.     int level = 0;
  243.     SItemNameAndLevel info;
  244.     info.name = name;
  245.  
  246.     size_t pos = name.find("+");
  247.    
  248.     if (std::string::npos != pos)
  249.     {
  250.         const std::string levelStr = name.substr(pos + 1, name.size() - pos - 1);
  251.         str_to_number(level, levelStr.c_str());
  252.  
  253.         info.name = name.substr(0, pos);
  254.     }
  255.  
  256.     info.level = level;
  257.  
  258.     return info;
  259. };
  260.  
  261.  
  262. bool Cube_InformationInitialize()
  263. {
  264.     for (int i = 0; i < s_cube_proto.size(); ++i)
  265.     {
  266.         CUBE_RENEWAL_DATA* cubeData = s_cube_proto[i];
  267.  
  268.         const std::vector<CUBE_RENEWAL_VALUE>& rewards = cubeData->reward;
  269.  
  270.         if (1 != rewards.size())
  271.         {
  272.             sys_err("[CubeInfo] WARNING! Does not support multiple rewards (count: %d)", rewards.size());          
  273.             continue;
  274.         }
  275.  
  276.         const CUBE_RENEWAL_VALUE& reward = rewards.at(0);
  277.         const WORD& npcVNUM = cubeData->npc_vnum.at(0);
  278.         bool bComplicate = false;
  279.        
  280.         TCubeMapByNPC& cubeMap = cube_info_map;
  281.         TCubeResultList& resultList = cubeMap[npcVNUM];
  282.         SCubeMaterialInfo materialInfo;
  283.  
  284.         materialInfo.reward = reward;
  285.         materialInfo.gold = cubeData->gold;
  286.         materialInfo.percent = cubeData->percent;
  287.         materialInfo.material = cubeData->item;
  288.         materialInfo.category = cubeData->category;
  289.  
  290.         resultList.push_back(materialInfo);
  291.     }
  292.  
  293.     return true;
  294. }
  295.  
  296.  
  297. void Cube_open (LPCHARACTER ch)
  298. {
  299.     LPCHARACTER npc;
  300.     npc = ch->GetQuestNPC();
  301.  
  302.     DWORD npcVNUM = npc->GetRaceNum();
  303.  
  304.     if (NULL==npc)
  305.     {
  306.         return;
  307.     }
  308.  
  309.     if ( FN_check_valid_npc(npcVNUM) == false )
  310.     {
  311.         return;
  312.     }
  313.  
  314. // #ifdef ENABLE_OFFLINE_SHOP_SYSTEM
  315. //  if ( ch->GetExchange() || ch->GetMyShop() || ch->GetShopOwner() || ch->IsOpenSafebox() || ch->IsCubeOpen() || ch->GetOfflineShopOwner())
  316. //  {
  317. //      ch->ChatPacket(CHAT_TYPE_INFO, "Cannot open refinement window");
  318. //      return;
  319. //  }
  320. // #else
  321.     if (ch->GetExchange() || ch->GetMyShop() || ch->GetShopOwner() || ch->IsOpenSafebox() || ch->IsCubeOpen())
  322.     {
  323.         ch->ChatPacket(CHAT_TYPE_INFO, "Cannot open refinement window");
  324.         return;
  325.     }
  326. // #endif
  327.  
  328.     long distance = DISTANCE_APPROX(ch->GetX() - npc->GetX(), ch->GetY() - npc->GetY());
  329.  
  330.     if (distance >= CUBE_MAX_DISTANCE)
  331.     {
  332.         sys_log(1, "CUBE: TOO_FAR: %s distance %d", ch->GetName(), distance);
  333.         return;
  334.     }
  335.  
  336.  
  337.     SendDateCubeRenewalPackets(ch,CUBE_RENEWAL_SUB_HEADER_CLEAR_DATES_RECEIVE);
  338.     SendDateCubeRenewalPackets(ch,CUBE_RENEWAL_SUB_HEADER_DATES_RECEIVE,npcVNUM);
  339.     SendDateCubeRenewalPackets(ch,CUBE_RENEWAL_SUB_HEADER_DATES_LOADING);
  340.     SendDateCubeRenewalPackets(ch,CUBE_RENEWAL_SUB_HEADER_OPEN_RECEIVE);
  341.  
  342.     ch->SetCubeNpc(npc);
  343.    
  344. }
  345.  
  346. void Cube_close(LPCHARACTER ch)
  347. {
  348.     ch->SetCubeNpc(NULL);
  349. }
  350.  
  351. void Cube_Make(LPCHARACTER ch, int index, int count_item, int index_item_improve)
  352. {
  353.     LPCHARACTER npc;
  354.     CUBE_RENEWAL_DATA   *cube_proto;
  355.     LPITEM  *items;
  356.     LPITEM pItem;
  357.  
  358.     if (!ch->IsCubeOpen())
  359.     {
  360.         ch->ChatPacket(CHAT_TYPE_INFO, "To create an item you have to have the refinement window open");
  361.         return;
  362.     }
  363.  
  364.     npc = ch->GetQuestNPC();
  365.     if (NULL == npc)
  366.     {
  367.         return;
  368.     }
  369.  
  370.     items = ch->GetCubeItem();
  371.  
  372.     int iEmptyPos;
  373.     int index_value = 0;
  374.     bool material_check = true;
  375.     const TCubeResultList& resultList = cube_info_map[npc->GetRaceNum()];
  376.    
  377.     // DWORD copyAttr[ITEM_ATTRIBUTE_MAX_NUM][2];
  378.  
  379.     for (TCubeResultList::const_iterator iter = resultList.begin(); resultList.end() != iter; ++iter)
  380.     {
  381.         if (index_value == index)
  382.         {
  383.             const SCubeMaterialInfo& materialInfo = *iter;
  384.  
  385.             for (int i = 0; i < materialInfo.material.size(); ++i)
  386.             {
  387.                 if (ch->CountSpecifyItem(materialInfo.material[i].vnum) < (materialInfo.material[i].count*count_item))
  388.                 {
  389.                     material_check = false;
  390.                 }
  391.             }
  392.  
  393.             if (materialInfo.gold != 0){
  394.                 if (ch->GetGold() < (materialInfo.gold*count_item))
  395.                 {
  396.                     ch->ChatPacket(CHAT_TYPE_INFO, "He doesn't have the necessary amount of yang.");
  397.                     return;
  398.                 }
  399.             }
  400.  
  401.             if (material_check)
  402.             {
  403.                 int percent_number;
  404.                 int total_items_give = 0;
  405.                 int porcent_item_improve = 0;
  406.  
  407.                 if (index_item_improve != -1)
  408.                 {
  409.  
  410.                     LPITEM item = ch->GetInventoryItem(index_item_improve);
  411.                     if(item != NULL)
  412.                     {
  413.  
  414.                         if(item->GetCount() <= 40){
  415.                             if (materialInfo.percent+item->GetCount() <= 100){
  416.                                 porcent_item_improve = item->GetCount();
  417.                             }
  418.  
  419.                             if(materialInfo.percent < 100)
  420.                             {
  421.                                 if (materialInfo.percent+item->GetCount() > 100){
  422.                                     porcent_item_improve = 100 - materialInfo.percent;
  423.                                 }
  424.                             }
  425.                         }
  426.                     }
  427.  
  428.                     if(porcent_item_improve != 0)
  429.                     {
  430.                         item->SetCount(item->GetCount()-porcent_item_improve);
  431.                     }
  432.                 }
  433.  
  434.                 for (int i=0; i<CUBE_MAX_NUM; ++i)
  435.                 {
  436.                     if (NULL==items[i]) continue;
  437.  
  438.                     if (items[i]->GetType() == ITEM_WEAPON || items[i]->GetType() == ITEM_ARMOR)
  439.                     {
  440.                         bool hasElement = false;
  441.                         for (int j = 0; j < cube_proto->item.size(); ++j)
  442.                         {
  443.                             if(cube_proto->item[j].vnum == items[i]->GetVnum())
  444.                             {
  445.                                 hasElement = true;
  446.                                 break;
  447.                             }
  448.                         }
  449.                        
  450.                         if(hasElement == false)
  451.                             continue;
  452.                        
  453.                         // for (int a = 0; a < ITEM_ATTRIBUTE_MAX_NUM; a++)
  454.                         // {
  455.                         //  copyAttr[a][0] = items[i]->GetAttributeType(a);
  456.                         //  copyAttr[a][1] = items[i]->GetAttributeValue(a);
  457.                         // }
  458.  
  459.                         break;
  460.                     }
  461.                    
  462.                     continue;
  463.                 }
  464.  
  465.                 for (int i = 0; i < count_item; ++i)
  466.                 {
  467.                     percent_number = number(1,100);
  468.                     if ( percent_number<=materialInfo.percent+porcent_item_improve)
  469.                     {
  470.                         total_items_give++;
  471.                     }
  472.                 }
  473.  
  474.                 pItem = ITEM_MANAGER::instance().CreateItem(materialInfo.reward.vnum,(materialInfo.reward.count*count_item));
  475.                 if (pItem->IsDragonSoul())
  476.                 {
  477.                     iEmptyPos = ch->GetEmptyDragonSoulInventory(pItem);
  478.                 }
  479.  
  480. #ifdef ENABLE_SPECIAL_STORAGE
  481.                 else if (pItem->IsSpecialStorageItem())
  482.                     iEmptyPos = ch->GetEmptySpecialStorageSlot(pItem);
  483. #endif
  484.  
  485.                 else {
  486.                     iEmptyPos = ch->GetEmptyInventory(pItem->GetSize());
  487.                 }
  488.  
  489. #ifdef ENABLE_SPECIAL_STORAGE
  490.                 int new_pos = iEmptyPos;
  491.                 bool new_window = false;
  492. #endif
  493.                 if (iEmptyPos < 0)
  494.                 {
  495. #ifdef ENABLE_SPECIAL_STORAGE
  496.                     bool empty_slot = false;
  497.                     int iCell = ch->GetEmptyInventory(pItem->GetSize());
  498.  
  499.                     if (iCell != -1 && pItem->IsSpecialStorageItem())
  500.                         empty_slot = true;
  501.  
  502.                     if (empty_slot)
  503.                     {
  504.                         new_pos = iCell;
  505.                         new_window = true;
  506.                     }
  507.  
  508.                     if (!empty_slot)
  509. #endif
  510.                     {
  511.                         ch->ChatPacket(CHAT_TYPE_INFO, "You do not have enough space in your inventory.");
  512.                         return;
  513.                     }
  514.                 }
  515.  
  516.                 for (int i = 0; i < materialInfo.material.size(); ++i)
  517.                 {
  518.                     ch->RemoveSpecifyItem(materialInfo.material[i].vnum, (materialInfo.material[i].count*count_item));
  519.                 }
  520.  
  521.                 if (materialInfo.gold != 0){
  522.                     ch->PointChange(POINT_GOLD, -static_cast<long long>(materialInfo.gold*count_item), false);
  523.                        
  524.                 }
  525.  
  526.                 if(total_items_give <= 0)
  527.                 {
  528.                     ch->ChatPacket(CHAT_TYPE_INFO, "It has failed.");
  529.                     return;
  530.                 }
  531.  
  532.                 // if (cube_proto->allowCopyAttr == true && copyAttr != NULL)
  533.                 // {
  534.                 //  new_item->ClearAttribute();
  535.                    
  536.                 //  for (int a = 0; a < ITEM_ATTRIBUTE_MAX_NUM; a++)
  537.                 //  {
  538.                 //      new_item->SetForceAttribute(a, copyAttr[a][0], copyAttr[a][1]);
  539.                 //  }
  540.                 // }
  541.  
  542.                 pItem = ITEM_MANAGER::instance().CreateItem(materialInfo.reward.vnum,(materialInfo.reward.count*total_items_give));
  543.                 if (pItem->IsDragonSoul())
  544.                 {
  545.                     iEmptyPos = ch->GetEmptyDragonSoulInventory(pItem);
  546.                     pItem->AddToCharacter(ch, TItemPos(DRAGON_SOUL_INVENTORY, iEmptyPos));
  547.                 }
  548. #ifdef ENABLE_SPECIAL_STORAGE
  549.                 else if (pItem->IsSpecialStorageItem())
  550.                     pItem->AddToCharacter(ch, TItemPos(new_window ? INVENTORY : pItem->GetSpecialWindowType(), new_pos));
  551. #endif
  552.  
  553.                 else {
  554.                     iEmptyPos = ch->GetEmptyInventory(pItem->GetSize());
  555.                     pItem->AddToCharacter(ch, TItemPos(INVENTORY, iEmptyPos));
  556.                 }
  557.             }
  558.             else
  559.             {
  560.                 ch->ChatPacket(CHAT_TYPE_INFO,"You don't have the necessary materials.");
  561.             }
  562.         }
  563.  
  564.         index_value++;
  565.     }
  566. }
  567.  
  568.  
  569. void SendDateCubeRenewalPackets(LPCHARACTER ch, BYTE subheader, DWORD npcVNUM)
  570. {
  571.    
  572.     TPacketGCCubeRenewalReceive pack;
  573.     pack.subheader = subheader;
  574.  
  575.     if(subheader == CUBE_RENEWAL_SUB_HEADER_DATES_RECEIVE)
  576.     {
  577.         const TCubeResultList& resultList = cube_info_map[npcVNUM];
  578.         for (TCubeResultList::const_iterator iter = resultList.begin(); resultList.end() != iter; ++iter)
  579.         {
  580.  
  581.             const SCubeMaterialInfo& materialInfo = *iter;
  582.  
  583.             pack.date_cube_renewal.vnum_reward = materialInfo.reward.vnum;
  584.             pack.date_cube_renewal.count_reward = materialInfo.reward.count;
  585.  
  586.             LPITEM item = ITEM_MANAGER::instance().CreateItem(materialInfo.reward.vnum, materialInfo.reward.count);
  587.             if (item->IsStackable() || !IS_SET(item->GetAntiFlag(), ITEM_ANTIFLAG_STACK)){
  588.                 pack.date_cube_renewal.item_reward_stackable = true;
  589.             }else{
  590.                 pack.date_cube_renewal.item_reward_stackable = false;
  591.             }
  592.  
  593.             pack.date_cube_renewal.vnum_material_1 = FN_check_cube_item_vnum_material(materialInfo,1);
  594.             pack.date_cube_renewal.count_material_1 = FN_check_cube_item_count_material(materialInfo,1);
  595.  
  596.             pack.date_cube_renewal.vnum_material_2 = FN_check_cube_item_vnum_material(materialInfo,2);
  597.             pack.date_cube_renewal.count_material_2 = FN_check_cube_item_count_material(materialInfo,2);
  598.  
  599.             pack.date_cube_renewal.vnum_material_3 = FN_check_cube_item_vnum_material(materialInfo,3);
  600.             pack.date_cube_renewal.count_material_3 = FN_check_cube_item_count_material(materialInfo,3);
  601.  
  602.             pack.date_cube_renewal.vnum_material_4 = FN_check_cube_item_vnum_material(materialInfo,4);
  603.             pack.date_cube_renewal.count_material_4 = FN_check_cube_item_count_material(materialInfo,4);
  604.  
  605.             pack.date_cube_renewal.vnum_material_5 = FN_check_cube_item_vnum_material(materialInfo,5);
  606.             pack.date_cube_renewal.count_material_5 = FN_check_cube_item_count_material(materialInfo,5);
  607.  
  608.             pack.date_cube_renewal.gold = materialInfo.gold;
  609.             pack.date_cube_renewal.percent = materialInfo.percent;
  610.  
  611.             memcpy (pack.date_cube_renewal.category,    materialInfo.category.c_str(),      sizeof(pack.date_cube_renewal.category));
  612.  
  613.             LPDESC d = ch->GetDesc();
  614.  
  615.             if (NULL == d)
  616.             {
  617.                 sys_err ("User SendDateCubeRenewalPackets (%s)'s DESC is NULL POINT.", ch->GetName());
  618.                 return ;
  619.             }
  620.  
  621.             d->Packet(&pack, sizeof(pack));
  622.         }
  623.     }
  624.     else{
  625.  
  626.         LPDESC d = ch->GetDesc();
  627.  
  628.         if (NULL == d)
  629.         {
  630.             sys_err ("User SendDateCubeRenewalPackets (%s)'s DESC is NULL POINT.", ch->GetName());
  631.             return ;
  632.         }
  633.  
  634.         d->Packet(&pack, sizeof(pack));
  635.     }
  636. }
  637. #endif
  638.  
Advertisement
Add Comment
Please, Sign In to add comment