Pull to refresh

Пишем AddOn к World of Warcraft

Game development *
Tutorial
На хабре как-то писали о создании ботов к WoW, а вот статей о написании аддонов я не нашел и решил описать этот процесс на примере одной поделки, которую я писал когда-то давно, когда я был еще студентом и моего времени хватало на игру в World of Warcraft и создание аддонов к нему. С деньгами тогда было туго и играл я не на официальном сервере, а на одном из бесплатных, имеющем кучу недоделок, багов, фич и аномалий.


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

Я выложил аддон на Google Code и форум серевера и он стал достаточно популярным (как для пиратского сервера). Кому интересно, как делаются аддоны — велкам под кат.

Введение


Итак, начнём с определения. Аддон в WoW — это набор некоторых файлов, которые позволяют пользователю изменить интерфейс клиентской части игры, но никак не влияют на серверную игровую механику. Аддоны были в WoW изначально, что выгодно отличает эту ММОРПГ от некоторых других, кричащих о своём превосходстве, но не имеющих возможности создания официальных аддонов и по сей день. Аддонов есть тысячи, если не десятки тысяч. Почему так много? А потому, что стилей игры и игровых задач ровно столько же. И конечно же, разработчики молодцы, что с самого начала дали способ каждому создать что-то своё, а не навязывать единый интерфейс, требуя считать его удобным.

Главным источником документации при создании аддона у нас будет портал WoWWiki. Пару ссылок по теме:

Полезной информации там еще очень много, но до неё можно добраться по ссылкам с этих четырёх страниц в пару кликов.

Инструментарий


Никаких специальных инструментов для создания аддонов не нужно. Все они представляют собой текстовые файлы, так что берите свой любимый текстовый редактор — и вперёд! Если он имеет подсветку синтаксиса языка Lua (именно на нём создаются аддоны) — вообще прекрасно. Этим требованиям отвечает, например, Notepad++.
А еще, энтузиасты создали на базе движка Free Visual Studio Shell специальную open-source IDE, которая называется AddOn Studio.



Штука прикольная и берет на себя ряд функций, таких как:
  • Дизайнер интерфейса
  • Просмотре интерфейса в виде XML-дерева
  • Браузер ресурсов WoW
  • Автодополнение по WoW API
  • Автогенерацию некоторых файлов

+ пару других прикольных вещей. В общем, можно пользоваться. Ну и еще можно просмотреть эту страницу, может быть найдете плагин к своей любимой IDE или просто что-то знакомое.

Пару концептуальных моментов


  • Есть такая штука, как политика Blizzard, требующая от аддона бесплатности, соответствия EULA, отсутствия вреда для других пользователей и пару других скучных, но идейно верных вещей. Так что о идеях написания ботов, хаков, читов и прочего мусора лучше забыть.
  • Хотите Вы того или нет, Ваш аддон будет opensource, потому что пишется на LUA+XML и распространяется в исходных кодах.


Из чего состоит аддон


1. TOC-файл (table of conteset). Это оглавление нашего аддона. Объясняет оболочке WoW кто мы такие, как называемся, кто автор, из чего состоим и пару других вещей. Вот, например, toc-файл моего аддона:
## Interface: 30000
## Title: Wnet Featurer 1.3
## Notes: Wnet Server Features List
## RequiredDeps: 
## OptionalDeps:
## LoadWith: Blizzard_TalentUI
## SavedVariables: 
Data.lua
WnetWarnings.lua
WnetFeaturer.xml


2. XML-файлы, описывающие изменения в интерфейсе, которые мы хотим внести (фреймы, кнопки, привязку событий в интерфейсе к определенным функциям в коде). Вот xml-файл моего аддона:
<Ui xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:schemaLocation="http://www.blizzard.com/wow/ui/" xmlns="http://www.blizzard.com/wow/ui/">
	<Script file="WnetFeaturer.lua" />
	<Frame name="WnetFeaturerFrame">
		<Scripts>
			<OnLoad>
				WnetFeaturer_OnLoad(self);
			</OnLoad>
			<OnEvent>
				WnetFeaturer_OnEvent(self, event, ...);
			</OnEvent>
		</Scripts>
	</Frame>
	<Frame name="GameTooltip" parent="GameTooltip">
		<Scripts>
			<OnUpdate>
        WnetChecker_GameTooltip_OnUpdate();
      </OnUpdate>
		</Scripts>
	</Frame>
</Ui>


Что тут написано? Мы объявляем привязку этого файла интерфейса к файлу кода WnetFeaturer.lua, далее декларируем два фрейма — один чисто для возможности повесить на события загрузки плагина своё событие, второй будет использоваться в игре для показа собственных тултипов (подсказок) к определенным объектам игрового мира.

3. Lua-файлы. Это код на языке Lua. Надо признать, что Lua в WoW несколько урезанный (детали можно почитать в WoWWiki), но для большинства задач его хватает. Вот один из файлов кода аддона (самый короткий).

--сохраняем процедуру отрисовки текста квестов (мы ее заменим на свою)
local before_wnet_featurer_old_QuestFrameDetailPanel_OnShow = QuestFrameDetailPanel_OnShow;
	
function WnetFeaturer_Log( text )
	SELECTED_CHAT_FRAME:AddMessage( text );
end

function WnetFeaturer_OnLoad(self)
	if ( not IsAddOnLoaded( "Blizzard_TalentUI" ) ) then
		_, reason = LoadAddOn( "Blizzard_TalentUI" );
	end

	if ( reason ) then
		return;
	end

	-- Hook the Talent Frame's Update function
	hooksecurefunc( "TalentFrame_Update", Planner_TalentFrame_AfterUpdate );
	
	-- Register for Loading Variables
	self:RegisterEvent( "PLAYER_ENTERING_WORLD" );
	self:RegisterEvent( "PLAYER_LEAVING_WORLD" );

	-- Locale/Gender-neutral class names FTW
	wnet_featurer_classes = { [1] = "DEATHKNIGHT",
	               [2] = "DRUID",
	               [3] = "HUNTER",
	               [4] = "MAGE",
	               [5] = "PALADIN",
	               [6] = "PRIEST",
	               [7] = "ROGUE",
	               [8] = "SHAMAN",
	               [9] = "WARLOCK",
	               [10] = "WARRIOR", };
end

function WnetFeaturer_OnEvent( self, event, ... )
	if ( event == "PLAYER_ENTERING_WORLD" ) then
		tp_name = UnitName( "player" ).." of "..GetRealmName();
		_, class = UnitClass( "player" );

		for k, v in ipairs( wnet_featurer_classes ) do
			if ( v == class ) then
				tp_class = k;
			end
			
		end			
	elseif ( tp_name ) then
		if ( event == "PLAYER_LEAVING_WORLD" ) then
			tp_name = nil;
			tp_class = nil;
		end
	end
end

function Planner_TalentFrame_AfterUpdate( frame )
	
	local page = PanelTemplates_GetSelectedTab( PlayerTalentFrame );
	local numTalents = GetNumTalents( page );
	local inspect = PlayerTalentFrame.inspect;
	local pet = PlayerTalentFrame.pet;
	local _, class = UnitClass( "player" );
	
	if ( pet or tp_standby or not tp_name or not tp_class or frame ~= PlayerTalentFrame or inspect ) then
		return;
	end
	
	for id = 1, numTalents, 1 do
		local name, texture, row, col, rank, maxRank = GetTalentInfo( page, id );
		local color = nil;
		
		if( wnet_talents[tp_class][page][name] ) then
			color = wnet_talents[tp_class][page][name];
		else 
			color = NORMAL_COLOR;			
		end
		
		if ( color ) then
			getglobal( "PlayerTalentFrameTalent"..id.."Slot" ):SetVertexColor( color.r, color.g, color.b );
			getglobal( "PlayerTalentFrameTalent"..id.."RankBorder" ):Show();
			getglobal( "PlayerTalentFrameTalent"..id.."Rank" ):SetText( rank.."/"..maxRank.." " );
			getglobal( "PlayerTalentFrameTalent"..id.."Rank" ):Show();
		else
			getglobal( "PlayerTalentFrameTalent"..id.."RankBorder" ):Hide();
			getglobal( "PlayerTalentFrameTalent"..id.."Rank" ):Hide();
		end
	end
end

function checkIsItemValid( text )
	if ( wnet_items[text] ) then
		return 1;
	else
		return nil;
	end
end

function checkIsNPCValid( text )
	if ( wnet_npc[text] ) then
		return 1;
	else
		return nil;
	end
end

WnetChecker_GameTooltip_OnUpdate=function()
	local errorString = nil;
	for i=1, GameTooltip:NumLines(), 1 do
		local currentTooltipStr = getglobal("GameTooltipTextLeft"..i):GetText();
		
		--проверка на баговых NPC и вещи
		if ( i == 1 ) then --имя NPC или вещи находится всегда в первой строке тултипа. Искать во всех незачем.
			if( checkIsNPCValid( currentTooltipStr ) ) then
				errorString = "Wnet: этот NPC может работать неверно!";
			elseif( checkIsItemValid( currentTooltipStr ) ) then
				errorString = "Wnet: эта вещь может работать неверно!";
			end
		end
		if strfind( currentTooltipStr, "Unique(.)Equipped" ) then
			errorString = "Wnet: Одевать 2 таких вещи нельзя!";
		elseif strfind( currentTooltipStr, "Target Dummy" ) then
			errorString = "Wnet: Использовать 2 таких вещи нельзя!";
		elseif( strfind( currentTooltipStr, "Chance on hit(.) Stuns target for" ) or strfind( currentTooltipStr, "Chance on hit(.) Оглушает цель на" ) ) then
			errorString = "Wnet: Использовать эту вещь нельзя!";
		elseif( strfind( currentTooltipStr, "Chance on hit(.) Knocks target silly for" ) ) then
			errorString = "Wnet: Использовать эту вещь нельзя!";
		elseif( strfind( currentTooltipStr, "Blackblade of Shahram" ) ) then
			errorString = "Wnet: Использовать эту вещь нельзя!";
		elseif( strfind( currentTooltipStr, "Seduction" ) ) then
			errorString = "Wnet: этот спелл запрещен!";
		elseif( strfind( currentTooltipStr, "Ursius" ) or strfind( currentTooltipStr, "Avian Darkhawk" ) or strfind( currentTooltipStr, "Avian Ripper" ) or strfind( currentTooltipStr, "Windroc Matriarch" ) ) then
			errorString = "Wnet: петом брать нельзя!";
		elseif( strfind( currentTooltipStr, "Wnet:") ) then
			errorString = nil;
		end
	end
			
	if errorString then
		GameTooltip:AddLine( errorString, 1, 0, 0 );
		local i;
		local s=10;
		for i=1, GameTooltip:NumLines(), 1 do
			s=s+getglobal("GameTooltipTextLeft"..i):GetHeight()+2;
		end
		GameTooltip:SetHeight(s+10);
		GameTooltip:SetWidth( max( GameTooltip:GetWidth(), 300 ) );
		end
end

function checkIsQuestNotValid( questTitle )
	if ( wnet_quests[questTitle] ) then
		return 1;
	else
		return nil;
	end
end

QuestFrameDetailPanel_OnShow=function()
	before_wnet_featurer_old_QuestFrameDetailPanel_OnShow();
	if( checkIsQuestNotValid( GetTitleText() ) ) then
		QuestDescription:SetText("|c00ff0000 Wnet: Этот квест может не работать!|r|n"..GetQuestText());
	end
end

Этот код выполняет следующий задачи:
  • WnetFeaturer_Log — функция вывода в чат игроку всяких логов
  • WnetFeaturer_OnLoad — вызывается при загрузке аддона (помните, в хмл-файле выше мы привязывали вызов этой функции к событию загрузки аддона). Проверяет необходимые зависимости, регистрирует пару хуков, подписывается на пару событий (например, событие входа игрока в игровой мир).
  • WnetFeaturer_OnEvent — вызывается при возникновении некоторых событий, на которые мы подписались ранее. Внутри — обработчики каждого события.
  • Planner_TalentFrame_AfterUpdate — самая сложная функция в этом файле. Нужна для отрисовки в дереве талантов персонажа зеленых, желтых и красных квадратиков поверх работающих, частично работающих и полностью сломанных талантов.

  • checkIsItemValid, checkIsNPCValid, checkIsQuestNotValid — проверка того, является ли вещь или NPC валидно работающими на данном сервере. Эти функции вызываются из других мест кода, которые потом возле невалидных вещей рисуют вот такие картинки:
  • WnetChecker_GameTooltip_OnUpdate — это хук на функцию показа тултипа. Здесь мы анализируем текст тултипа и, если в первой строке находим упоминание некоторого объекта, который работает неверно, мы изменяем текст тултипа.


В проекте есть еще пару файлов кода, но они, в большей своей части, являются просто базой невалидных объектов и подписей к ним. Интереса не вызывают.
Полные исходники можно посмотреть тут: http://code.google.com/p/wnet-featurer/downloads/list
А вот тема на форуме, где я более детально описывал фичи аддона.

Буду рад, если кому-нибудь это поможет. Спрашивайте, если что-то непонятно.
Tags:
Hubs:
Total votes 94: ↑78 and ↓16 +62
Views 79K
Comments Comments 21