Scripting (ScriptDev2)

Z WoWResource Wiki
Přejít na: navigace, hledání

Aneb letmý úvod do světa skriptování MaNGOSu, na počátek budeme potřebovat:

  • Mít stážený MaNGOS a ScriptDev2 ze SVN
  • Umět zkompilovat MaNGOS a SD2
  • Základní znalosti programování v jazyce C++ (nebo jiném programovacím jazyku)

Poznámky před začátkem aneb teorie C++

Je vhodné znát základy programování ať už z Céčka nebo jiného programovacího jazyku. C++ je case-sensitive - záleží tedy na velkých a malých písmenech. Základní datové typy int (celá čísla), float a double (desetinná čísla), bool (logický typ), char (znak). Často se zde budete setkávat se strukturami (struct) a třídami (class), k prvkům těchto objektů se přistupuje pomocí operátoru tečky.

V praxi se setkáte hodně s tzv. pointery = ukazatel, v c++ je poznáte tak, že u definice proměnné mají hvězdičku. Ukazatel je vlastně v paměti pouze číslem, které ukazuje jinam do paměti, kde se ve skutečnosti nachází daná proměnná. Abychom se dostali na hodnotu ukazatele provádíme dereferenci: *ukazatel. V MaNGOSu se mnohem častěji setkáte se symbolem: ukazatel->prvek, což je dereference a přístup k prvku objektu (třídy/struktury) ve zjedodušeném zápisu. Samozřejmě to jde zapsat i jako: (*ukazatel).prvek. Je nutné dbát na bezpečnost programu, pokud někde přiřadíme pointer a vlastní proměnnou pak smažeme, přístupem skrze tento neplatný pointer dojde ke crashi programu.

Dalším možností jak předat proměnnou, aby byla modifikovatelná uvnitř metody a mělo to efekt na vnější svět je reference. U definice proměnné se použije značka &proměnná. Narozdíl od ukazatelů je bezpečná a nedereferencuje se.

Základní třídy a jejich struktura v MaNGOS API

Mangos-dedeni-trid.png

Základní třídou je Object ta drží informace o UpdateValues polích, WorldObject doplňuje funkce objektu ve 3D světě - umístění, počítaní vzdáleností... Unit je společný předek Creature a Player, zde se nacházejí společné věci jako jsou staty a práce s nimy, metody útoku, atd... Mangos disponuje velkou škálou funkcí, které můžete použít je proto vhodné mít přehled, kde je můžete hledat.

Přidání "Hello world" skriptu

  • Vytvořit soubor se skriptem a umístit ho do složek k ostatním skriptům (doporučuji volit složky s ostatnímy skripty, předejdete pozdějším problémům)
  • Náš skript prozatím bude vypadat takto (je umístěn ve složce scripts/creature):
#include "precompiled.h"

void AddSC_muj_mob()
{
    Script *newscript;
    newscript = new Script;
    // newscript je pointer (ukazatel) proto použijeme -> pro přístup k prvkům struktury
    newscript->Name="muj_mob";
    // ...
    newscript->RegisterSelf();
}
  • Poté ještě musíme skript přidat do souboru ScriptMgr.cpp, aby se načetl s ostatními skripty:
    • V horní části souboru umístíme mezi ostatní obdobné definice řádek: extern void AddSC_muj_mob();
    • O něco níže, uvnitř metody ScriptsInit() umístíme řádek: AddSC_muj_mob();
  • Tato kostra je prozatím dostačující a na vysvětlení nováčkům stačí. První řádek #include připojuje hlavičkový soubor sc_defines.h, zde je nutné udat adresu relativní k fyzickému umístění na disku! Poté následuje metoda AddSC_muj_mob(), která je bez parametrů a nic nevrací. První dva řádky v paměti vytvoří prostor pro strukturu Script, dalším řádkem dostane skript jméno (to co se používá v db), vynechané místo bude pro přidělení jednotlivých metod ke skriptu a na závěr náš skript přidáme do pole všech skriptů a inkrementujeme počet skriptů.
  • Abychom mohli použít metody dostupné pro skript je nutné se nejdříve seznámit, které jsou k dispozici: (Unk - existují, ale nejsou z MaNGOSe volány)
bool (*pGossipHello         )(Player*, Creature*);
// Metoda zavolan při zahájení komunikace s mobem (viz npcflag - gossip)
// return false - zamezí následnou činnost mangosu

bool (*pQuestAccept         )(Player*, Creature*, Quest const* );
// Metoda zavolaná při akceptování questu
// return neovlivňuje

bool (*pGossipSelect        )(Player*, Creature*, uint32 , uint32  );
// Metoda zavolaná při potvrzení výběru z rozhovoru (viz GossipHello)
// return true - zamezí následnou činnost mangosu

bool (*pGossipSelectWithCode)(Player*, Creature*, uint32 , uint32 , char* );
// Unk

bool (*pQuestSelect         )(Player*, Creature*, Quest const* );
// Unk

bool (*pQuestComplete       )(Player*, Creature*, Quest const* );
// Unk

uint32 (*pNPCDialogStatus   )(Player*, Creature* );
// Zjištění druhu ikony nad mobem

bool (*pChooseReward        )(Player*, Creature*, Quest const*, uint32 );
// Zavoláno po výběru odměny za quest od moba
// return true - zamezí následnou činnost mangosu

bool (*pItemHello           )(Player*, Item*, Quest const* );
// Unk

bool (*pGOHello             )(Player*, GameObject* );
// Jako GossipHello, ale reakce s GameObjectem
// return false - zamezí následnou činnost mangosu

bool (*pAreaTrigger         )(Player*, Quest const*, uint32 );
// Unk

bool (*pItemQuestAccept     )(Player*, Item *, Quest const* );
// Zavoláno po akceptování questu od itemu
// return neovlivňuje

bool (*pGOQuestAccept       )(Player*, GameObject*, Quest const* );
// Zavoláno po akceptování questu od GameObjectu
// return neovlivňuje

bool (*pGOChooseReward      )(Player*, GameObject*_GO, Quest const*, uint32 );
// Zavoláno po výběru odměny za quest od GO
// return true - zamezí následnou činnost mangosu

bool (*pReceiveEmote        )(Player*, Creature*, uint32 );
// Zavoláno po emotování (/chicken) moba
// return neovlivňuje

bool (*pItemUse             )(Player*, Item*, SpellCastTargets const& );
// Zavoláno při použití předmětu
// return true - zamezí následnou činnost mangosu
  • Pro náš skript Hello World použijeme tedy GossipHello a přidáme podle definice novou metodu do skriptu a nastavíme ji ke skriptu:
#include "precompiled.h"

// O pár řádků výše byla definice funkcí s naznačenými parametry, zde je nutné je skutečně pojmenovat
bool GossipHello_muj_mob(Player* player, Creature* _creature)
{
    player->ADD_GOSSIP_ITEM(0, "Hello World", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1);
    player->SEND_GOSSIP_MENU(0, _creature->GetGUID());
}

void AddSC_muj_mob()
{
    Script *newscript;
    newscript = new Script;
    newscript->Name="muj_mob";
    newscript->pGossipHello = &GossipHello_muj_mob;
    newscript->RegisterSelf();
}
  • Jako hlavní přibyla metoda GossipHello_muj_mob, s parametry definovanými výše. ADD_GOSSIP_ITEM a SEND_GOSSIP_MENU se starají o vytvoření a zobrazení seznamu u hráče. Nejsou to přímo metody, ale makra, která zkracují zápis, jejich definici najdete v souboru sc_defines.h včetně dalších informací. Další změnou je nový řádek v metodě AddSC_muj_mob(), který přiřazuje adresu funkce GossipHello_muj_mob k funkci skriptu, bez toho by skript nevěděl, že má tuto metodu zavolat. Prvním parametrem u ADD_GOSSIP_ITEM je id ikonky, druhý je samotný text, pak následuje číselný identifikátor menu (abychom mohli reagovat na různá gossip menu, viz guardi ve městech) a poslední je číselný identifikátor položky. Po stisku položky se volá metoda GossipSelect. Tu teď přidáme, aby náš mob dále reagoval na hráče:
#include "precompiled.h"

bool GossipHello_muj_mob(Player* player, Creature* _creature)
{
    player->ADD_GOSSIP_ITEM(0, "Hello World", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1);
    player->SEND_GOSSIP_MENU(0, _creature->GetGUID());
}

bool GossipSelect_muj_mob(Player* player, Creature* _creature, uint32 sender, uint32 action)
{
    if (sender == GOSSIP_SENDER_MAIN)
    {
        if (action == GOSSIP_ACTION_INFO_DEF + 1)
        {
            _creature->CastSpell(player, 5, true);
        }
    }
}

void AddSC_muj_mob()
{
    Script *newscript;
    newscript = new Script;
    newscript->Name="muj_mob";
    newscript->pGossipHello = &GossipHello_muj_mob;
    newscript->pGossipSelect = &GossipSelect_muj_mob;
    newscript->RegisterSelf();
}   
  • Postup přidání metody je stejný jako u GossipHello, v metodě je provedena kontrola, zdali hráč opravdu vybral tu položku z menu, v praxi při mnoha položkách v menu se to řeší blokem switch. NPC zareaguje vykouzlením spellu 5 (Death Touch) na hráče, třetí parametr je tzv. triggered = něco jako vynucené kouzlení a npc okamžitě zakouzlí bez téměř všech bežných kontrol.

Soupis maker definovaných v sc_defines.h

Makro Použití na Popis
ADD_GOSSIP_ITEM(a,b,c,d) Player Přidá položku do Gossip menu, a - id ikony, b - text, c - identifikace menu, d - identifikace akce
SEND_GOSSIP_MENU(a,b) Player Odešle hráči Gossip menu, a - uint32 npctext id, b - uint64 GUID moba
CLOSE_GOSSIP_MENU() Player Uzavře gossip menu
SEND_POI(a,b,c,d,e,f) Player Zobrazí klientovi POI na mapě, a - pozice X, b - pozice Y, c - id ikony, d - flagy, e - data, f - název lokace
SEND_QUEST_DETAILS(a,b,c) Player (zrušeno?)
SEND_REQUESTEDITEMS(a,b,c,d) Player (zrušeno?)
SEND_VENDORLIST(a) Player Vendor list - věci k prodeji (a - npc GUID)
SEND_TRAINERLIST(a) Player Trainer list - spelly k naučení (a - npc GUID)
SEND_BANKERLIST(a) Player Zobrazení banky (a - npc GUID)
SEND_TABARDLIST(a) Player Nastavení barev tabardu (a - npc GUID)
SEND_ACTIONLIST(a) Player Zobrazení aukce (a - npc GUID)
SEND_TAXILIST(a) Player Seznam taxi lokací (a - npc GUID)
SEND_SPRESURRECT() Player Oživí hráče
GET_HONORRANK() Player (zrušeno?)
QUEST_DIALOG_STATUS(a,b) Creature Získá ikonku dialogu s NPC (a - Player*, b - defaultní ikona)

AI

  • Jako další rozebereme SkriptedAI dostupnou v ScriptDev2
 // definice hlavičky
 struct MANGOS_DLL_DECL ScriptedAI : public CreatureAI
 {
   // konstruktor, destruktor
   ScriptedAI(Creature* creature) : m_creature(creature) {}
   ~ScriptedAI() {}

   //*************
   //CreatureAI skriptovatelné funkce - volány MaNGOSem
   //*************

   // Volá se při každém pohybu jednotky pokud je viditelná (IsVisible(Unit *who))
   void MoveInLineOfSight(Unit *) {}

   // Volá se, když mob má zaútočit na jednotku
   void AttackStart(Unit *);

   // Volá se při ukončení útoku
   void EnterEvadeMode();

   // Volá se při heal kouzlu/použití itemu (zatím není implementováno v mangosu!!!
   void HealBy(Unit *healer, uint32 amount_healed) {}

   // Volá se, když tato jednotka uštědří damage jiné (volá se před vlastním zapsáním damage)
   void DamageDeal(Unit *done_to, uint32 &damage) {}

   // Volá se, když tato jednotka byla zraněna (volá se po zapsání damage)
   void DamageTaken(Unit *done_by, uint32 &damage) {}

   // Detekce viditelnosti (pro MoveInLIneOfSight)
   bool IsVisible(Unit *who) const;

   // Opakovaně volaná funkce, obsahuje vše co potřebuje být zpracováno časem a né událostí (damage,..)
   void UpdateAI(const uint32);

   // Zavoláno, když mob umře
   void JustDied(Unit*){}

   // Zavoláno, když mob někoho zabije
   void KilledUnit(Unit*){}

   // Ukazatel na moba, se kterým AI manipuluje
   Creature* m_creature;

   //*************
   //AI Pomocné funkce (definovány ve SD2)
   //*************

   // Zahájí útok na blízko na jednotku
   void DoStartMeleeAttack(Unit* victim);

   // Zahájí útok na dálku na jednotku
   void DoStartRangedAttack(Unit* victim);

   // Provede útok na blízko pokud je ready, nekouzlí a je v dosahu
   void DoMeleeAttackIfReady();

   // Ukončí útok na současného nepřítele
   void DoStopAttack();

   // Vykouzlí spell podle id
   void DoCast(Unit* victim, uint32 spellId, bool triggered = false);

   // Vykouzlí spell podle struktury SpellEntry
   void DoCastSpell(Unit* who,SpellEntry const *spellInfo, bool triggered = false);

   // Přidá aury podle spell id
   void DoAddAura(Unit *target, uint32 SpellId);

   // Promluví
   void DoSay(const char* text, uint32 language, Unit* target);

   // Yellne
   void DoYell(const char* text, uint32 language, Unit* target);

   // Emotne
   void DoTextEmote(const char* text, Unit* target);

   // Vrátí se na spawn
   void DoGoHome();

   // Zahraje zvuk všem hráčům poblíž
   void DoPlaySoundToSet(Unit* unit, uint32 sound);

   // Nastaví tvář na jednotku
   void DoFaceTarget(Unit* unit);

   // Spawne moba, pozice relativně k aktuálnímu mobu
   Creature* DoSpawnCreature(uint32 id, float x, float y, float z, float angle, uint32 type, uint32 despawntime);

   // Vybere jednotku z threat (aggro) seznamu
   Unit* SelectUnit(SelectAggroTarget target, uint32 position);

   // Vybere spell, který odpovídá kritériim ze spell listu moba
   SpellEntry const* SelectSpell(Unit* Target, int32 School, int32 Mechanic, SelectTarget Targets,  uint32 PowerCostMin, uint32 PowerCostMax, float RangeMin, float RangeMax, SelectEffect Effect);

   // Zkontroluje jestli mob může zakouzlit spell
   bool CanCast(Unit* Target, SpellEntry const *Spell);
   };
  • Skript GenericAI základní použití AI s popisem funkcí:
// Odvození generic_creatureAI od ScriptedAI
struct MANGOS_DLL_DECL generic_creatureAI : public ScriptedAI
{
   // Konstruktor - zavolá se kontruktor ScriptedAI a mob se uvede do "EvadeMode", tedy nebojový režim na spawn pointu
   generic_creatureAI(Creature *c) : ScriptedAI(c) {EnterEvadeMode();}

   uint32 GlobalCooldown;      //This variable acts like the global cooldown that players have (1.5 seconds)
   uint32 BuffTimer;           //This variable keeps track of buffs
   bool IsSelfRooted;
   bool InCombat;

   // Metoda pro navrácení moba na spawn point
   void EnterEvadeMode()
   {
       // Vynuluje vnitřní proměnné
       GlobalCooldown = 0;
       BuffTimer = 0;          
       IsSelfRooted = false;
       InCombat = false;

       // odstraní aury
       m_creature->RemoveAllAuras();
       // odstraní threat list
       m_creature->DeleteThreatList();
       // zruší boj
       m_creature->CombatStop();
       // nravrátí moba na spawn point
       DoGoHome();
   }

   // (Voláno při zahájení útoku na někoho)
   void AttackStart(Unit *who)
   {
       // Kontrola platnosti ukazatele
       if (!who)
           return;

       // Kontrola zdali můžeme útočit a zdali cíl nejsem já sám
       if (who->isTargetableForAttack() && who!= m_creature)
       {
           // začni útočit
           DoStartMeleeAttack(who);
           // pokud není v dosahu útočení na danou jednotku nastaví se proměnná IsSelfRooted na true
           if (!m_creature->IsWithinDistInMap(who, ATTACK_DISTANCE))
           {
               IsSelfRooted = true;
           }

           InCombat = true;
       }
   }

   // (Spatřen pohyb před npc)
   void MoveInLineOfSight(Unit *who)
   {
       // Pokud ukazatel není platný nebo mob již má oběť (na kterou útočí) nedělej nic
       if (!who || m_creature->getVictim())
           return;

       // jeli zaútočitelný na dostupném místě a nepřátelský 
       if (who->isTargetableForAttack() && who->isInAccessablePlaceFor(m_creature) && m_creature->IsHostileTo(who))
       {
           //provede kontrolu vzdálenosti, vzdálenosti vertikální a viditelnosti (Line of sight)
           float attackRadius = m_creature->GetAttackDistance(who);
           if (m_creature->IsWithinDistInMap(who, attackRadius) && m_creature->GetDistanceZ(who) <= CREATURE_Z_ATTACK_RANGE && m_creature->IsWithinLOSInMap(who))
           {
               // Pokud má stealth auru, zruš ji
               if(who->HasStealthAura())
                   who->RemoveSpellsCausingAura(SPELL_AURA_MOD_STEALTH);

               // Začni útok
               DoStartMeleeAttack(who);
               // pokud není v dosahu útočení na danou jednotku nastaví se proměnná IsSelfRooted na true
               if (!m_creature->IsWithinDistInMap(who, ATTACK_DISTANCE))
               {
                   IsSelfRooted = true;
               }

               InCombat = true;
           }
       }
   }

   // (periodické obnovování ai)
   void UpdateAI(const uint32 diff)
   {
       // Snížení global cooldownu
       if (GlobalCooldown > diff)
           GlobalCooldown -= diff;
       else GlobalCooldown = 0;

       // Buffování - pouze mimo boj a když žiju
       if (!InCombat && m_creature->isAlive())
           // pokud je BuffTimer < diff - buffni jinak sniž BuffTimer o diff
           if (BuffTimer < diff )
           {
               // Najdi spell, který targetuje přítele a dává auru
               SpellEntry const *info = SelectSpell(m_creature, -1, -1, SELECT_TARGET_ANY_FRIEND, 0, 0, 0, 0, SELECT_EFFECT_AURA);

               // když je ukazatel platný a není nastaven globální cooldown
               if (info && !GlobalCooldown)
               {
                   // vykouzli buff
                   DoCastSpell(m_creature, info);

                   // nastav znovu globální cooldown
                   GlobalCooldown = GENERIC_CREATURE_COOLDOWN;

                   // nastav bufftimer na 10 minut
                   BuffTimer = 600000;
               }// spell nenalezen, zkus to za 30 sekund
               else BuffTimer = 30000;
           }else BuffTimer -= diff;

       // nepokračuj dále, pokud nemáme aktivního nepřítele
       if (!m_creature->SelectHostilTarget() || !m_creature->getVictim() )
           return;

       // Pokud jsme v dosahu
       if( m_creature->IsWithinDistInMap(m_creature->getVictim(), ATTACK_DISTANCE))
       {
           // Pokud jsme připraveni na útok a nekouzlíme 
           if( m_creature->isAttackReady() && !m_creature->IsNonMeleeSpellCasted(false))
           {
               bool Healing = false;
               SpellEntry const *info = NULL;

               // Pokud máme méně jak 30% hp zkus vybrat healovací spell
               if (m_creature->GetHealth()*100 / m_creature->GetMaxHealth() < 30)
                   info = SelectSpell(m_creature, -1, -1, SELECT_TARGET_ANY_FRIEND, 0, 0, 0, 0, SELECT_EFFECT_HEALING);

               // pokud není healovací spell, vyber útočný spell
               if (info) Healing = true;
               else info = SelectSpell(m_creature->getVictim(), -1, -1, SELECT_TARGET_ANY_ENEMY, 0, 0, 0, 0, SELECT_EFFECT_DONTCARE);

               //50% šance kouzlit pokud je to elita, 20% pokud elita nejsem ( + kontrola ukazatele a globalcooldownu)
               if (info && (rand() % (m_creature->GetCreatureInfo()->rank > 1 ? 2 : 5) == 0) && !GlobalCooldown)
               {
                   // Když healuju tak sebe, jinak kouzlím na protivníka
                   if (Healing)DoCastSpell(m_creature, info);
                   else DoCastSpell(m_creature->getVictim(), info);

                   // Nastav globalcooldown
                   GlobalCooldown = GENERIC_CREATURE_COOLDOWN;
               } // Jinak proveď update útočného stavu
               else m_creature->AttackerStateUpdate(m_creature->getVictim());

               // resetuj attact časovač
               m_creature->resetAttackTimer();
           }
       }
       else // nejsme v dosahu meele útoku
       {
           // Pokud nekouzlíme
           if (!m_creature->IsNonMeleeSpellCasted(false))
           {
               bool Healing = false;
               SpellEntry const *info = NULL;

               if (m_creature->GetHealth()*100 / m_creature->GetMaxHealth() < 30 && rand() % 3 == 0)
                   info = SelectSpell(m_creature, -1, -1, SELECT_TARGET_ANY_FRIEND, 0, 0, 0, 0, SELECT_EFFECT_HEALING);
 
               if (info) Healing = true;
               else info = SelectSpell(m_creature->getVictim(), -1, -1, SELECT_TARGET_ANY_ENEMY, 0, 0, ATTACK_DISTANCE, 0, SELECT_EFFECT_DONTCARE);

               if (info && !GlobalCooldown)
               {
                   // Zastaví pohyb, pokud bychom se hýbali
                   if (!IsSelfRooted)
                   {
                       IsSelfRooted = true;
                   }

                   // Kuk na nepřítele
                   DoFaceTarget(m_creature->getVictim());

                   // Zakouzlí
                   if (Healing) DoCastSpell(m_creature,info);
                   else DoCastSpell(m_creature->getVictim(),info);

                   GlobalCooldown = GENERIC_CREATURE_COOLDOWN;


               }// Spell není dostupný a my se nesmíme hejbat
               else if (IsSelfRooted)
               {
                   // Zruš kouzlení a odblokuj pohyb
                   m_creature->InterruptSpell(CURRENT_GENERIC_SPELL);
                   IsSelfRooted = false;
               }
           }
       }
   }
};
// Získá AI pro moba
// Pro každého moba se vytváří nová instance AI 
CreatureAI* GetAI_generic_creature(Creature *_Creature)
{
    return new generic_creatureAI (_Creature);
}

// Standartní vytvoření skriptu
void AddSC_generic_creature()
{
   Script *newscript;
   newscript = new Script;
   newscript->Name="generic_creature";
   // Přiřazení AI
   newscript->GetAI = GetAI_generic_creature;
   newscript->RegisterSelf();
}
  • další výhodnou jednoduchou věcí v SD2 je tzv. SimpleAI, která dělá většinu standartních věcích jenom na základě nadefinování textu, spellů. Příklad použití naleznete např. ve skriptu Boss_Ambassador_Hellmaw.cpp
  • Event AI - skriptování npc na základě eventů v db