Объявления
Поздравляем
Roman2211


Друзья, если не получается зарегистрироваться, напишите на почту vdv_forever@bk.ru.
Я оторву свою задницу от всех дел и обязательно Вас активирую! :smile10:
Добро пожаловать на геройский форум! :smile25:

База данных IDA от void17

Герои Меча и Магии III: Возрождение Эрафии, Герои Меча и Магии III Дыхание Смерти, Герои Меча и Магии III Клинок Армагеддона, Герои Меча и Магии III Хроники Героев
offlineАватара пользователя
void_17  
имя: имя
Ветеран
Ветеран
 
Сообщения: 548
Зарегистрирован: 25 апр 2021, 15:05
Откуда: Оттуда
Пол: Мужчина
Поблагодарили: 132 раз.

Re: База данных IDA от void17

Сообщение void_17 » 23 ноя 2021, 04:25

А, ну да, есть же старые компы. Зажрался я со своим восьмиядерным Ryzen 7.

Приеду домой — кину вам Ghidra базу, у меня там уже все обработано. Единственное, имена не все деманглерные да и декомпилятор жутко кривой, но общий вид функции вполне может предоставить.
Последний раз редактировалось void_17 23 ноя 2021, 04:29, всего редактировалось 1 раз.
Вернуться к началу

offlineАватара пользователя
AlexSpl  
имя: Александр
Эксперт
Эксперт
 
Сообщения: 5587
Зарегистрирован: 17 сен 2010, 12:58
Пол: Мужчина
Награды: 14
Высшая медаль (1) Победителю турнира по HMM1_TE (2) Победителю этапа по HMM1 (1) Победителю этапа по HMM2 (1) Лучшему из лучших (1) 2 место 1 этапа по HMM1 (1)
3 место 1 этапа по HMM1 (1) 1 место 2 этапа по HMM2 (1) Победителю турнира по KB (2) Победителю турнира по KB (1) Грандмастер оффлайн-турниров (1) Боевой шлем (1)
Поблагодарили: 2185 раз.

Re: База данных IDA от void17

Сообщение AlexSpl » 23 ноя 2021, 04:29

Не, у меня Intel i5, на работе просто старый :smile2:

С предыдущей страницы:

Цитата:
А к ассемблеру для SH-4 сразу не привыкнешь. А как Вы описание команд добавили для SH-4, вручную комменты вбивали?

У меня без пояснений к командам дизассемблирует.
Вернуться к началу

offlineАватара пользователя
void_17  
имя: имя
Ветеран
Ветеран
 
Сообщения: 548
Зарегистрирован: 25 апр 2021, 15:05
Откуда: Оттуда
Пол: Мужчина
Поблагодарили: 132 раз.

Re: База данных IDA от void17

Сообщение void_17 » 23 ноя 2021, 04:36

Да я до сих пор сам не привык. Тяжело, но что-то разобрать можно. Ну, например, r0 — регистр возврата, что гораздо проще, чем с х86 ассемблером. Ну это наверное единственное, что там проще, чем с х86, а так, SH-4 ассемблер это прям пытка какая-то.

И да, не вручную вбивал описание комманд, это самоубийство, т.к. там слишком много. Там в настройках есть автоматические комменты для команд. (Не помню где, поковыряйтесь вообщем) Я скрин прилагал с теми мусорными функциями, это скрин из моей базы, где все удобные настройки уже есть.

Некоторые комменты связаны с функциями, "inline function"(индикатор того, что в ПК-SoD версии игры эта функция является встроенной, например army::Is(void) ) вот именно эти я вручную писал.
Вернуться к началу

offlineАватара пользователя
AlexSpl  
имя: Александр
Эксперт
Эксперт
 
Сообщения: 5587
Зарегистрирован: 17 сен 2010, 12:58
Пол: Мужчина
Награды: 14
Высшая медаль (1) Победителю турнира по HMM1_TE (2) Победителю этапа по HMM1 (1) Победителю этапа по HMM2 (1) Лучшему из лучших (1) 2 место 1 этапа по HMM1 (1)
3 место 1 этапа по HMM1 (1) 1 место 2 этапа по HMM2 (1) Победителю турнира по KB (2) Победителю турнира по KB (1) Грандмастер оффлайн-турниров (1) Боевой шлем (1)
Поблагодарили: 2185 раз.

Re: База данных IDA от void17

Сообщение AlexSpl » 23 ноя 2021, 05:03

Переделал каст походных заклинаний:

Код: Выделить всё
int __stdcall castAdventureSpell(LoHook* h, HookContext* c)
{
   int SpellID = *(int*)(c->ebp + 8);
   Hero* hero = (Hero*)c->esi;

   int landModifier = hero->GetLandModifierUnder();
   int schoolLevel = hero->get_spell_level(SpellID, landModifier);

   switch (SpellID)
   {
   case SPL_MOBILITY:
      if (heroAdvInfoEx[hero->id].mobilityCastCount < mobilitySpellParams[schoolLevel])
      {
         ++heroAdvInfoEx[hero->id].mobilityCastCount;
         hero->UseSpell(hero->GetManaCost(SPL_MOBILITY, NULL, landModifier));
         hero->movement_points += o_Spell[SPL_MOBILITY].effect[schoolLevel];
         hero->movement_points_max += o_Spell[SPL_MOBILITY].effect[schoolLevel];
         pAdventureManager->ShowRoute(0, 0, 1);
         pAdventureManager->FullUpdate(true);
         playSound(o_Spell[SPL_MOBILITY].wav_name);
      }
      else
      {
         sprintf(o_TextBuffer, o_GENRLTXT_TXT->GetString(339), hero->name);
         b_MsgBox(o_TextBuffer, MBX_OK);
      }
      break;

   case SPL_EYE_OF_THE_MAGI:
      if (heroAdvInfoEx[hero->id].eyeOfTheMagiCastCount < eyeOfTheMagiSpellParams[schoolLevel])
      {
         HeroWindow* heroWindow = new HeroWindow();
         CALL_1(void, __thiscall, 0x41D326 + 5 + *(int*)0x41D327, heroWindow);
         eyeOfTheMagiCast = true;
         heroWindow->DoModal(false);
         eyeOfTheMagiCast = false;
         CALL_1(void, __thiscall, 0x41D346 + 5 + *(int*)0x41D347, heroWindow);

         type_point mapPoint;
         pAdventureManager->get_mouse_map_point(mapPoint);

         if (mapPoint.is_valid() && pWindowManager->Field<int>(0x38))
         {
            int x = mapPoint.unpack_x();
            int y = mapPoint.unpack_y();
            int z = mapPoint.unpack_z();
            pGame->SetVisibility(x, y, z, o_ActivePlayer->id, o_Spell[SPL_EYE_OF_THE_MAGI].effect[schoolLevel], 0);
           
            pAdventureManager->Field<int>(0xEC) = -1;
            pMouseManager->MouseCoords(x, y);
            pAdventureManager->ProcessHover(x, y);
         }
         else
         {
            sprintf(o_TextBuffer, o_GENRLTXT_TXT->GetString(732));
            b_MsgBox(o_TextBuffer, MBX_OK);
            break;
         }

         ++heroAdvInfoEx[hero->id].eyeOfTheMagiCastCount;
         hero->UseSpell(hero->GetManaCost(SPL_EYE_OF_THE_MAGI, NULL, landModifier));
         pAdventureManager->FullUpdate(true);
         playSound(o_Spell[SPL_EYE_OF_THE_MAGI].wav_name);
      }
      else
      {
         sprintf(o_TextBuffer, o_GENRLTXT_TXT->GetString(339), hero->name);
         b_MsgBox(o_TextBuffer, MBX_OK);
      }
      break;
   
   default:
      return EXEC_DEFAULT;
   }
   
   c->return_address = 0x41C67B;
   return NO_EXEC_DEFAULT;
}

Теперь читается лучше :smile20:
Вернуться к началу

offlineАватара пользователя
void_17  
имя: имя
Ветеран
Ветеран
 
Сообщения: 548
Зарегистрирован: 25 апр 2021, 15:05
Откуда: Оттуда
Пол: Мужчина
Поблагодарили: 132 раз.

Re: База данных IDA от void17

Сообщение void_17 » 23 ноя 2021, 05:20

Если что, .h файлик для удобной работы с type_point лежит тут
https://vk.com/topic-205297784_48046300
Вернуться к началу

offlineАватара пользователя
void_17  
имя: имя
Ветеран
Ветеран
 
Сообщения: 548
Зарегистрирован: 25 апр 2021, 15:05
Откуда: Оттуда
Пол: Мужчина
Поблагодарили: 132 раз.

Re: База данных IDA от void17

Сообщение void_17 » 23 ноя 2021, 05:23

@AlexSpl, типы в базе начинаются со строчных букв, а переменные хоть как. Например, heroWindowManager. Так в оригинале было..

С — Стандартизация :smile17:
Вернуться к началу

offlineХеромант  
имя: OL
Новичок
Новичок
 
Сообщения: 15
Зарегистрирован: 21 ноя 2021, 19:42
Пол: Мужчина
Поблагодарили: 2 раз.

Re: База данных IDA от void17

Сообщение Херомант » 23 ноя 2021, 06:39

AlexSpl писал(а):

А что такого в базе Sav'а, что могло бы придать смысл этой мусорной функции?


Имена функций конечно (например, для базы к редактору карт имён функций Сав не сливал, поэтому что-то новое добавлять в редактор карт - одна сплошная боль и мучение) - огромный шаг вперёд по пониманию кода Героев. И от чего Вы считаете её мусорной, если не знаете её назначения в игре? Сразу скажу, что у Вас на СИ-шном виде она показана некорректно (IDA не всегда правильно может "восстановить" сишный код из ассемблерного), лично я с СИ вообще никак не связан и работаю только на АСМ-е на низком уровне, чуть выше плинтуса, поэтому код игры вижу совершенно по-другому.

void_17 писал(а):

@ХЕРОМАНТ,
а что с моим тредом на вогфоруме стало?


В библиотеке он: http://wforum.heroes35.net/showthread.php?tid=6419

void_17 писал(а):

Если что, .h файлик для удобной работы с type_point лежит тут
https://vk.com/topic-205297784_48046300


Спасибо, полезно для лучшего понимания пакованных координат. И наглядно видно, что уровневость в Третьих Героях не была заложена с самого начала, а добавилась на этапе альфа-версий.
Вернуться к началу

offlineАватара пользователя
AlexSpl  
имя: Александр
Эксперт
Эксперт
 
Сообщения: 5587
Зарегистрирован: 17 сен 2010, 12:58
Пол: Мужчина
Награды: 14
Высшая медаль (1) Победителю турнира по HMM1_TE (2) Победителю этапа по HMM1 (1) Победителю этапа по HMM2 (1) Лучшему из лучших (1) 2 место 1 этапа по HMM1 (1)
3 место 1 этапа по HMM1 (1) 1 место 2 этапа по HMM2 (1) Победителю турнира по KB (2) Победителю турнира по KB (1) Грандмастер оффлайн-турниров (1) Боевой шлем (1)
Поблагодарили: 2185 раз.

Re: База данных IDA от void17

Сообщение AlexSpl » 23 ноя 2021, 11:00

У меня для type_point всё дёшево и сердито:

Код: Выделить всё
struct type_point
{
   USHORT x;
   USHORT yz;

   inline bool is_valid()
   {
      return CALL_1(bool, __thiscall, 0x4B1090, this);
   }
   inline USHORT unpack_x()
   {
      return (USHORT)(x << 6) >> 6;
   }
   inline USHORT unpack_y()
   {
      return (USHORT)(yz << 6) >> 6;
   }
   inline USHORT unpack_z()
   {
      return (USHORT)(yz << 2) >> 12;
   }
};


Цитата:
@AlexSpl, типы в базе начинаются со строчных букв, а переменные хоть как. Например, heroWindowManager. Так в оригинале было..

Это я как раз для того сделал, чтобы переменные можно было с маленькой писать: Hero* hero, а не hero* Hero. Просто привычка.

Цитата:
Имена функций конечно (например, для базы к редактору карт имён функций Сав не сливал, поэтому что-то новое добавлять в редактор карт - одна сплошная боль и мучение) - огромный шаг вперёд по пониманию кода Героев. И от чего Вы считаете её мусорной, если не знаете её назначения в игре? Сразу скажу, что у Вас на СИ-шном виде она показана некорректно (IDA не всегда правильно может "восстановить" сишный код из ассемблерного), лично я с СИ вообще никак не связан и работаю только на АСМ-е на низком уровне, чуть выше плинтуса, поэтому код игры вижу совершенно по-другому.

Я и ассемблерный код смотрел и даже дебаггером пошагово проходил. Эти функции генерируют степень двойки от 0 до 9, причём цикла нет (ebx = 1). Переменные, куда они сливают степени двойки, не используются.
Вернуться к началу

offlineАватара пользователя
void_17  
имя: имя
Ветеран
Ветеран
 
Сообщения: 548
Зарегистрирован: 25 апр 2021, 15:05
Откуда: Оттуда
Пол: Мужчина
Поблагодарили: 132 раз.

Re: База данных IDA от void17

Сообщение void_17 » 23 ноя 2021, 12:41

AlexSPL, не забудьте зайти на наш discord-сервер. Там вас ждет информация по совместной работе над базой.
Вернуться к началу

offlineАватара пользователя
AlexSpl  
имя: Александр
Эксперт
Эксперт
 
Сообщения: 5587
Зарегистрирован: 17 сен 2010, 12:58
Пол: Мужчина
Награды: 14
Высшая медаль (1) Победителю турнира по HMM1_TE (2) Победителю этапа по HMM1 (1) Победителю этапа по HMM2 (1) Лучшему из лучших (1) 2 место 1 этапа по HMM1 (1)
3 место 1 этапа по HMM1 (1) 1 место 2 этапа по HMM2 (1) Победителю турнира по KB (2) Победителю турнира по KB (1) Грандмастер оффлайн-турниров (1) Боевой шлем (1)
Поблагодарили: 2185 раз.

Re: База данных IDA от void17

Сообщение AlexSpl » 23 ноя 2021, 17:52

Почти полностью разобранная функция army::get_unit_combat_value(). Осталось только узнать сакральный смысл аргументов ArgVar_0 и ArgVar_1:

Код: Выделить всё
// ArgVar_0 - AI пенальти к атаке
// ArgVar_1 - AI пенальти к защите
// ArgVar_2 - стрелковая атака или нет
//
// ArgVar - это аргумент, который используется как переменная. С такими названиями меньше путаницы с локальными переменными
double __thiscall army::get_unit_combat_value(const army *this, int ArgVar_0, int ArgVar_1, int ArgVar_2, int a5)
{
  char isShooter; // bl
  int AdjustedAttack; // eax
  int AdjustedDefense; // eax
  double ShieldMultiplier; // st7
  int Side; // eax
  hero *Hero; // eax
  ECreatureType Type; // eax
  hero *TempHero; // eax
  int Side_0; // eax
  int SpellDurationHypnotize; // edx
  int Side_1; // ecx
  int Side_2; // ecx
  int Side_3; // ecx
  int SpellDurationBless; // edx
  int BaseMinDamage; // eax
  int BaseMaxDamage; // ecx
  int *damage; // eax
  signed int CurseDamagePenalty; // edx
  bool isDamageLessThan1; // cc
  FCreatureFlags *Flags; // eax
  double k; // st7
  int StackCurrentHealth; // edi
  int TotalHealth; // edx
  army *FirstStack; // ecx
  int Side_4; // esi
  int StacksNum; // esi
  int Side_5; // ecx
  FCreatureFlags *__shifted(army,0x84) pFlags; // ecx
  int StackCurrentHealth_0; // eax
  double TotalHealth_0; // st7
  double DefenseMod; // [esp+14h] [ebp-20h]
  double BaseAvgDamage; // [esp+1Ch] [ebp-18h]
  double Multiplier; // [esp+24h] [ebp-10h]
  double Damage; // [esp+24h] [ebp-10h]
  double EffectiveFightValue; // [esp+24h] [ebp-10h]
  double AttackMod; // [esp+2Ch] [ebp-8h]
  int DeltaAttack; // [esp+30h] [ebp-4h]
  int CurseDamage; // [esp+40h] [ebp+Ch] FORCED BYREF

  AdjustedAttack = army::get_adjusted_attack(this, 0, ArgVar_2);
  DeltaAttack = AdjustedAttack - akCreatureTypeTraits[this->Type].Attack - ArgVar_0;
  AdjustedDefense = army::get_adjusted_defense(this, 0, 1);
  isShooter = ArgVar_2;
  Multiplier = 1.0;
  ArgVar_0 = AdjustedDefense - akCreatureTypeTraits[this->Type].Defense - ArgVar_1;
  if ( ArgVar_2 )
  {
    if ( !this->SpellDuration[AIR_SHIELD] )
      goto labelSkipMultiplier;
    ShieldMultiplier = this->AirShield_Multiplier;
  }
  else
  {
    if ( !this->SpellDuration[SHIELD] )
      goto labelSkipMultiplier;
    ShieldMultiplier = this->Shield_Multiplier;
  }
  Multiplier = ShieldMultiplier;
labelSkipMultiplier:
  if ( this->SpellDuration[STONE] )
    Multiplier = Multiplier * 0.5;              // Половинный модификатор под окаменением
  if ( this->SpellDuration[HYPNOTIZE] )
    Side = 1 - this->Side;
  else
    Side = this->Side;
  Hero = gpCombatManager->Hero[Side];
  if ( Hero )
    Multiplier = hero::GetDefenseFactor(Hero) * Multiplier;
  DefenseMod = (ArgVar_0 * 0.05 + 1.0) * Multiplier;
  if ( isShooter )
  {
    Type = this->Type;
    if ( Type != _BALLISTA_
      && Type != ARROW_TOWER
      && ((this->Flags & SHOOTER) == 0
       || this->Shots <= 0
       || ((TempHero = gpCombatManager->Hero[combatMonster_GetSide(this)]) == 0
        || !hero::IsWieldingArtifact(TempHero, BOW_OF_THE_SHARPSHOOTER))
       && army::enemy_is_adjacent(this, 0)
       || this->SpellDuration[FORGETFULNESS] && this->ForgetfulnessLevel >= ADVANCED) )
    {
      isShooter = 0;
      LOBYTE(ArgVar_2) = 0;                     // Случаи, когда AI не считает отряд стрелком:
                                                // Баллиста не считается стрелком, а рассматривается отдельно ниже;
                                                // Стрелковая башня тоже не является стрелком при расчётах;
                                                // Отряд с дистанционной атакой, у которого закончились стрелы;
                                                // Блокированный отряд с дистанционной атакой;
                                                // Отряд с дистанционной атакой под действием заклинания "Забывчивость" продвинутого или экспертного уровня
                                                //
    }
  }
  AttackMod = DeltaAttack * 0.05 + 1.0;
  if ( !isShooter && (this->Flags & SHOOTER) != 0 )
    AttackMod = AttackMod * 0.5;                // Берём половинный модификатор для стрелков, которые не могут стрелять
  if ( this->Type == _BALLISTA_ )               // Целый отдельный случай для Баллисты
  {
    SpellDurationHypnotize = this->SpellDuration[HYPNOTIZE];
    Side_0 = this->Side;                        // Но не помню, чтобы Баллисту можно было загипнотизировать :P
    Side_1 = SpellDurationHypnotize ? 1 - Side_0 : this->Side;
    if ( gpCombatManager->Hero[Side_1] )
    {
      if ( SpellDurationHypnotize )
        Side_2 = 1 - Side_0;
      else
        Side_2 = this->Side;
      if ( gpCombatManager->Hero[Side_2]->SecondarySkills[ARTILLERY] > 1 )
        AttackMod = AttackMod + AttackMod;
      if ( SpellDurationHypnotize )
        Side_3 = 1 - Side_0;
      else
        Side_3 = this->Side;
      AttackMod = AttackMod * ArtilleryEfficiency[gpCombatManager->Hero[Side_3]->SecondarySkills[ARTILLERY]];
    }
  }
  SpellDurationBless = this->SpellDuration[BLESS];
  if ( SpellDurationBless || this->SpellDuration[CURSE] )
  {
    BaseMaxDamage = this->MaxDamage;
    BaseMinDamage = this->MinDamage;
    ArgVar_0 = BaseMinDamage + BaseMaxDamage;
    BaseAvgDamage = (BaseMinDamage + BaseMaxDamage) / 2.0;
    if ( SpellDurationBless )
    {
      ArgVar_0 = BaseMaxDamage + this->BlessDamageBonus;
      Damage = ArgVar_0;
    }
    else if ( this->SpellDuration[CURSE] )
    {
      CurseDamagePenalty = this->CurseDamagePenalty;
      ArgVar_0 = 1;
      CurseDamage = BaseMinDamage - CurseDamagePenalty;
      isDamageLessThan1 = BaseMinDamage - CurseDamagePenalty < 1;
      damage = &ArgVar_0;
      if ( !isDamageLessThan1 )
        damage = &CurseDamage;
      Damage = *damage;
    }
    else
    {
      Damage = (BaseMinDamage + BaseMaxDamage) / 2.0;
    }
    isShooter = ArgVar_2;
    AttackMod = Damage / BaseAvgDamage * AttackMod;
  }
  if ( isShooter && (this->Flags & DOUBLEATTACK) != 0 )
    AttackMod = AttackMod + AttackMod;          // Если стрелок с двойной атакой, удваиваем модификатор
  k = sqrt(AttackMod * DefenseMod);
  Flags = this->Flags;
  EffectiveFightValue = k * akCreatureTypeTraits[this->Type].FightValue;
  if ( (Flags & (SUMMON|SIEGEWEAPON)) != 0 )    // Если призванное существо или осадное орудие
  {
    if ( (Flags & CLONE) != 0 )
      StackCurrentHealth = 1;                   // Если клон, Здоровье = 1
    else
      StackCurrentHealth = this->AmountAlive * this->Health - this->HealthLost;
    Side_4 = this->Side;
    TotalHealth = 0;
    ArgVar_2 = 0;
    Side_5 = Side_4;
    StacksNum = gpCombatManager->StacksNum[Side_4];
    FirstStack = gpCombatManager->BattleStack[Side_5];
    if ( StacksNum > 0 )                        // BattleStack[Side_5][0], но не знаю, как сделать это в IDA
    {
      pFlags = &FirstStack->Flags;
      do                                        // Цикл по всем отрядам в армии героя
      {
        if ( (ADJ(pFlags)->Flags & (SUMMON|CANNOTMOVE|SIEGEWEAPON)) == 0 )
        {                                       // Если не призванное существо и не боевая машина
          if ( (ADJ(pFlags)->Flags & CLONE) != 0 )
            StackCurrentHealth_0 = 1;           // Если клон, Здоровье = 1
          else
            StackCurrentHealth_0 = ADJ(pFlags)->AmountAlive * ADJ(pFlags)->Health - ADJ(pFlags)->HealthLost;
          TotalHealth += StackCurrentHealth_0;
        }
        pFlags += 0x152;
        --StacksNum;
      }
      while ( StacksNum );
      ArgVar_2 = TotalHealth;
    }
    if ( !StackCurrentHealth )
      return 0.1;                               // Если отряд погиб, его ценность равна 0.1
                                                // Данная функция, кстати, используется также и для принятия решения о воскрешении
    TotalHealth_0 = ArgVar_2;
    ArgVar_2 = TotalHealth + StackCurrentHealth;
    return TotalHealth_0 * EffectiveFightValue / (TotalHealth + StackCurrentHealth);
  }                                             // Обретает смысл сумма в знаменателе:
                                                // данная формула только для призванных существ и Катапульты,
                                                // а они являются добавочным "здоровьем" для армии.
                                                // В цикле подсчёта суммарного здоровья армии выше такие отряды пропускаются.
                                                // Можете поделить числитель и знаменатель на TotalHealth (TotalHealth_0 = TotalHealth)
                                                // и получится красивая формула
  return EffectiveFightValue;
}                                               // А это уже эффективная боевая ценность "нормальных" отрядов

Кстати, как понимать вот это:

Код: Выделить всё
((TempHero = gpCombatManager->Hero[combatMonster_GetSide(this)]) == 0
        || !hero::IsWieldingArtifact(TempHero, BOW_OF_THE_SHARPSHOOTER)

Причём тут вообще Лук Снайпера и отсутствие героя?

Ааа, вспомнил. Отсутствие героя чисто техническая проверка, чтобы не закрашилась функция hero::IsWieldingArtifact(), а Лук Снайпера при том, что с ним стрелки стреляют в рукопашной :smile14:

* * *
Из этой функции делаем важный вывод: если заклинание изменяет урон отряда, то для того чтобы AI правильно считал эффективную боевую ценность отряда под таким заклинанием, нужно добавлять код, учитывающий это, в данную функцию (подобно Bless и Curse).
Вернуться к началу

Пред.След.

Вернуться в Общий раздел

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 3

cron