Scripting (ScriptDev2)
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)
Obsah
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
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