Сегодня будет перевод-пересказ первой части замечательного руководства An Idiots Guide to Editing the DLL. Руководство действительно хорошее, так что пересказывать буду во первых чтобы в одном месте все было. Во вторых оно с двумя ошибками из-за которых пример с деревушками нерабочий. Ошибки будут исправлены в пересказе, автор оригинала пару мелочей пропустил.
На этом уроке мы скопируем уже булевский тег <bNoBadGoodies> из CIV4UnitInfos.xml в CIV4PromotionInfos.xml. Тег этот отвечает за результаты получаемые из деревушек. По умолчанию идет у исследователей и скаутов. Если этот параметр включен жители деревушки не нападут на вас, как варвары.
Schema & XML
Начинаем мы с XML уровня, а конкретно с файла CIV4PromotionInfos.xml. Открываем его и видим в самой первой строке.
Код:
<Civ4PromotionInfos xmlns="x-schema:CIV4UnitSchema.xml">
А значит это, что он он построен по схеме под названием CIV4UnitSchema.xml. Идем туда и видим множество записей наподобие.
Код:
<ElementType name="_________" content="textOnly" dt:type="int"/>
Код:
<ElementType name="_________" content="textOnly" dt:type="boolean"/>
Они определяют содержание тегов, которые имеются в XML-файлах. Дополнения вроде dt:type="int" например обозначает, что содержимое тега может быть только целочисленным(2 12 34). С другой стороны dt:type="boolean" означает, что содержимое может быть только булевским (ИСТИНА/ЛОЖЬ или как обычно пишется 1 или 0).
Если же строка имеет вид без дополнения, это значит, что содержимое может быть только текстовым. Например PROMOTION_COMBAT1.
Код:
<ElementType name="_________" content="textOnly"/>
В некоторых, достаточно редких случаях она может иметь вид
Код:
<ElementType name="_________" content="eltOnly">
Это означает, что она содержит внутри себя набор других тегов, вместо как например.
Код:
<ElementType name="FeaturePassableTechs" content="eltOnly">
Это важно. Все эти <ElementType name="_______" присутствует в единственном экземпляре, для каждого уникального названия тега. Поэтому если добавляете новую строку проверяйте нет ли у неё дубликатов.
В таких строках имеются вложенные указания для тегов. Вот такие вот.
Код:
<element type="_______" minOccurs="0"/>
minOccurs="0" отвечает за минимальное число его использований в xml-файле.
Если стоит "0", то тег становится опциональным и его можно не вписывать в каждый объект в XML файле. Для мелких введений это очень удобно, но для крупного мода будет лучше продублировать их в каждом объекте файла, где-нибудь в удобном месте вам месте. Например в конце, или после выбранного вам тега. И вводить свои теги уже выстраивая их в алфавитном порядке.
В наборах тегов встречается и maxOccurs="" это ограничение максимального числа использования тега. Но пока нам это не потребуется, да и вообще используется не так часто.
Утка в зайце, яйцо в утке.
Отправимся в этот самый файл Schema где указаны правила для прокачек. Из файла CIV4PromotionInfos можно понять что каждая прокачка представляет собой набор тегов под названием PromotionInfo. Все они в свою очередь находятся в наборе тегов PromotionInfos, который в свою очередь входит в файл Civ4PromotionInfos.
Как мы видим в схеме.
Код:
<ElementType name="Civ4PromotionInfos" content="eltOnly">
<element type="PromotionInfos" minOccurs="0" maxOccurs="*"/>
</ElementType>
Идем глубже и глубже.
Код:
<ElementType name="PromotionInfos" content="eltOnly">
<element type="PromotionInfo" maxOccurs="*"/>
</ElementType>
А нужное нам яйцо, сиречь PromotionInfo в свою очередь и содержит весь набор нужных нам тегов. Они идут очень длинным списком, который мы сделаем ещё длиннее. Находим последнюю запись.
Код:
<element type="iOrderPriority" minOccurs="0"/>
Ставим свои комментарии после неё. Комментарии в XML делаются вот так.
<!-- Cansei new tags(Название мода) 25/08/2013(Дата) Cansei(Автор) -->
Все что в скобках писать не надо, если что. Все остальное, что внутри <!-- --> на ваш выбор, мой пример просто образец.
У нас получается
Код:
<element type="iOrderPriority" minOccurs="0"/>
<!-- Cansei new tags 25/08/2013 Cansei -->
<element type="bNoBadGoodies" minOccurs="0"/>
<!-- Cansei new tags 25/08/2013 Cansei -->
</ElementType>
Так как мы копируем уже существующий тег, то на этом с файлом Schema мы заканчиваем. Ведь
Код:
<ElementType name="bNoBadGoodies" content="textOnly" dt:type="boolean"/>
у нас уже есть.
Переходим в Civ4PromotionInfos, где добавляем в конце нужной прокачки наш новый тег.
Код:
<Button>,Art/Interface/Buttons/Promotions/Combat3.dds,Art/Interface/Buttons/Promotions_Atlas.dds,2,6</Button>
<!-- Cansei new tags 25/08/2013 Cansei -->
<bNoBadGoodies>1</bNoBadGoodies>
<!-- Cansei new tags 25/08/2013 Cansei -->
</PromotionInfo>
Вверху вместо iOrderPriority значится <Button>, это потому что iOrderPriority пропущен, minOccurs="0" позволяет это.
Сохраняем наши изменения и запускаем мод. Если мы сделали все правильно, он нормально запустится и наша прокачка будет делать что ей положено… кроме того что мы сейчас добавили. Так и должно быть, ведь игра ещё не знает что ей делать с нашим тегом.
CvInfos
Переходим к работе с SDK, первым на очереди будут файлы CvInfos.cpp и CvInfos.h
В них мы добавим возможность игре прочитать наш новый тег, чтобы она смогла с ним что-то сделать. Открываем файл CvInfos.cpp и начинаем поиск bNoBadGoodies. Находим семь совпадений.
Первым из них идет переменная m_bNoBadGoodies(false), что в функции CvUnitInfo::CvUnitInfo() В этой функции указываются значения переменной по умолчанию. Поэтому там где в XML не указано свое значение, используется именно это. Название переменной отличается от той, что в XML. Это сделано для удобства. Префикс m_b означает, что это переменная классовая, а не локальная а стало быть доступна и вне класса, а b, потому что она булевская. Ничего сложного тут нет, но о префиксах я ещё сделаю заметку, как нибудь.
Следующий результат поиска, это функция bool CvUnitInfo::isNoBadGoodies() const. Единственное что она делает, это возвращает текущее значение переменной m_bNoBadGoodies. CvUnitInfo:: перед именем функции означают, что эта функция принадлежит классу CvUnitInfo. Но об этом я ещё расскажу подробнее.
Код:
bool CvUnitInfo::isNoBadGoodies() const
{
return m_bNoBadGoodies;
}
Далее идет два длинных списка, в которых присутствует наша переменная.
Код:
stream->Read(&m_bNoBadGoodies);
Код:
stream->Write(m_bNoBadGoodies);
Эти две строки отвечают за запись переменной в файл сохранения и загрузку из него. Они должны быть размещены В ОДНОМ И ТОМ ЖЕ ПОРЯДКЕ ПО ОТНОШЕНИЮ К СОСЕДНИМ. Потому что иначе сделанное сохранения будет испорчено и загрузить его будет невозможно. Поэтому будьте очень внимательны, делая изменения в этих функциях. Это нетрудно, просто будьте внимательнее.
Ах да, не каждое изменение в XML должно прописываться в stream->Read и stream->Write. Они нужны прежде всего для игры с опцией "Lock Modified Assets”. Эта игровая опция следит за тем, чтобы текущие значения XML-тегов совпадали с теми, чтобы были в момент сохранения игры. Но мы все равно добавим их сюда, для порядка.
И последние два результата, это
Код:
pXML->GetChildXmlValByName(&m_bNoBadGoodies, "bNoBadGoodies");
В этой строке считывается значение переменной из XML. И значение m_bNoBadGoodies становится равно bNoBadGoodies. Имя тега bNoBadGoodies должно быть в точности таким же, как оно прописано в XML.
На этом с просмотром CvInfos.cpp все и мы переходим к CvInfos.h
Там у нас два поиск выдал два результата.
Первый из них.
Код:
bool isNoBadGoodies() const; // Exposed to Python
Это прототип функции возвращающей значение переменной. Рассматриваю подробнее.
bool означает, что возвращаемый результат булевский (вкл/выкл)
isNoBadGoodies это имя нашей функции, обязательно должно совпадать с её же именем из CvInfos.cpp где она и находится по сути.
() потому что это функция, не буду вдаваться в подробности.
const потому что она не должна ничего изменять, ставится для верности.
; Потому что прототип.
// Exposed to Python Ничего не делает, просто комментарий для удобства, поясняющий, что значение этой переменной можно передать в Python.
Ну и второй результат, это bool m_bNoBadGoodies; Собственно тут и заявляется что будет использоваться такая-то переменная для класса.
С обзором нужного нам в этих файлах пока все, теперь пришла пора вносить нужные нам изменения.
Все что мы рассматривали до этого, относилось к классу, который разработчики определили как CvUnitInfo и дали нам вот такую подсказку.
Код:
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//
// class : CvUnitInfo
//
// DESC:
//
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Теперь же наша задача, аккуратно скопировать все нужное в аналогичные места в
Код:
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//
// class : CvPromotionInfo
//
// DESC:
//
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Начнет с .h файла. В удобное вам место копируем прототип нужной нам функции. Например после строки
bool isImmuneToFirstStrikes() const; // Exposed to Python
Код:
bool isImmuneToFirstStrikes() const; // Exposed to Python
//Cansei New tags 25.08.2013 Start
bool isNoBadGoodies() const; // Exposed to Python
//Cansei New tags 25.08.2013 End
Только то, что между закомментированными строками требуется скопировать. Ну и сами комментарии. Это очень удобно, вернее без них ОЧЕНЬ неудобно. Ставите свой ник, дату и задачу которую должен решить ваш код.
Далее скопируем объявление нашей переменной в набор переменных класса.
Код:
bool m_bImmuneToFirstStrikes;
//Cansei New tags 25.08.2013 Start
bool m_bNoBadGoodies; // Exposed to Python
//Cansei New tags 25.08.2013 End
Возвращаемся в .cpp
Вписываем в конструктор значение переменной по умолчанию. Так что получится.
Код:
m_bImmuneToFirstStrikes(false),
//Cansei New tags 25.08.2013 Start
m_bNoBadGoodies(false),
//Cansei New tags 25.08.2013 End
Копируем нужную нам функцию.
Код:
bool CvPromotionInfo::isImmuneToFirstStrikes() const
{
return m_bImmuneToFirstStrikes;
}
//Cansei New tags 25.08.2013 Start
bool CvPromotionInfo::isNoBadGoodies() const
{
return m_bNoBadGoodies;
}
//Cansei New tags 25.08.2013 End
Прописываем переменную на запись и чтения в файл сохранений.
Код:
stream->Read(&m_bImmuneToFirstStrikes);
//Cansei New tags 25.08.2013 Start
stream->Read(&m_bNoBadGoodies);
//Cansei New tags 25.08.2013 End
Код:
stream->Write(m_bImmuneToFirstStrikes);
//Cansei New tags 25.08.2013 Start
stream->Write(m_bNoBadGoodies);
//Cansei New tags 25.08.2013 End
Ну и завершаем копированием строки ответственной за считывание из XML.
Код:
pXML->GetChildXmlValByName(&m_bImmuneToFirstStrikes, "bImmuneToFirstStrikes");
//Cansei New tags 25.08.2013 Start
pXML->GetChildXmlValByName(&m_bNoBadGoodies, "bNoBadGoodies");
//Cansei New tags 25.08.2013 End
Компилируем и запускаем игру. Если мы все сделали правильно, dll успешно скомпилируется, а игра будет работать как надо, но все ещё без нашей новой прокачки. Игра теперь знает, что есть такая прокачка, но что с ней делать, она ещё не в курсе.
CvGameTextMgr
Теперь пришла пора научить игру отображать новую прокачку в подсказках при наведении на юнит и в Цивилопедии. Поиск по файлу CvGameTextMgr.cpp выдает нам:
Код:
if (GC.getUnitInfo(eUnit).isNoBadGoodies())
{
szBuffer.append(NEWLINE); szBuffer.append(gDLL->getText("TXT_KEY_UNIT_NO_BAD_GOODIES"));
}
Этот код отвечает, за подсказку при строительстве юнита и при просмотре Скаутов в Цивилопедии. Но для нас важно прежде всего то, что текст подсказки уже имеется и его не надо будет вводить заново.
В CvGameTextMgr.cpp несложно запутаться, поэтому сразу укажу нужные функции. И хорошо ещё, что нам редко потребуется лазить в CvGameTextMgr.h.
CvGameTextMgr:arsePromotionHelp(CvWStringBuffer &szBuffer, PromotionTypes ePromotion, const wchar* pcNewline).
Она отвечает за подсказки к прокачкам, как очевидно. Запрашивает информацию о прокачках и выводит её. ePromotion означет, что информация о прокачке берется из основной базы. Префикс e вообще означает, что речь идет о пока ещё теоретическом объекте. Например он используется при строительство нового юнита, или при просмотре Цивилопедии.
Поэтому добавляем по аналогии. Выбираем удобное место например.
Код:
if (GC.getPromotionInfo(ePromotion).getKamikazePercent() != 0)
{
szBuffer.append(pcNewline);
szBuffer.append(gDLL->getText("TXT_KEY_PROMOTION_KAMIKAZE_TEXT", GC.getPromotionInfo(ePromotion).getKamikazePercent()));
}
//Cansei New tags 25.08.2013 Start
if (GC.getPromotionInfo(ePromotion).isNoBadGoodies())
{
szBuffer.append(pcNewline);
szBuffer.append(gDLL->getText("TXT_KEY_UNIT_NO_BAD_GOODIES"));
}
//Cansei New tags 25.08.2013 End
Компилируем. Если все сделали правильно, теперь в цивилопедии для подсказки будет высвечиваться нужная подсказка. Обратите внимание, что вместо NEWLINE идет pcNewline. Принципиальной разницы между ними нет, но не забывайте следить за такими мелочами ориентируясь на соседние записи, меньше проблем будет.
Но нам нужно не только это, а ещё и чтобы при наведении на юнит всплывала эта же подсказка, не лазить же в описание каждой прокачки.
Идем в функцию отвечающую за эту подсказку.
void CvGameTextMgr::setUnitHelp(CvWStringBuffer &szString, const CvUnit* pUnit, bool bOneLine, bool bShort)
CvWStringBuffer &szString означает, что функция работает со строками.
const CvUnit* pUnit это константный указатель на текущий юнит. Для нас это значит что функция ничего не может случайно изменить в этом юните, для этого и стоит const. Про указатели в C++ почитайте в интернете. Для нас это значит, что выводится информация для конкретного реального юнита.
bool bOneLine если правда, то игра пытается вывести информацию о юните одной строкой, например меню F5.
bool bShort отвечает за сокрытие лишней информации при взгляде на вражеский юнит. Сделано не для секретности, а просто чтобы окно с информацией не загораживало то что надо.
Вставляем в эту функцию код в подходящее место, например по соседству от nukeRange().
//Cansei New tags 25.08.2013 Start
if (pUnit->isNoBadGoodies())
{
szString.append(NEWLINE); szString.append(gDLL->getText("TXT_KEY_UNIT_NO_BAD_GOODIES"));
}
//Cansei New tags 25.08.2013 End
if (pUnit->nukeRange() >= 0)
{
szString.append(NEWLINE);
szString.append(gDLL->getText("TXT_KEY_UNIT_CAN_NUKE"));
}
Внимание, обычно на этом этапе компилироваться игра перестает, ведь мы запрашиваем ещё не созданную функцию в CvUnit.cpp
Но нам повезло, ведь у нас уже есть такая функция. Мы же копируем уже существующий тег.
На этом с текстовой частью все и мы переходим к тому, чтобы игра знала, что ей делать с новым тегом.
Функционал.
Открываем CvUnit.cpp и ищем NoBadGoodies.
Код:
bool CvUnit::isNoBadGoodies() const
{
return (m_pUnitInfo->isNoBadGoodies()) ;
}
Сия функция запрашивает информацию из инфокласса, что определяли в CvInfos, общую для всех юнитов данного класса.
Наша задача добавить альтернативный способ получения этой информации. Мы создадим новую int переменную iNoBadGoodiesCount которая и будет отвечать за альтернативный вариант. Переменная будет int, потому что с ними меньше мороки.
Начинаем. В CvUnit.h находим
Код:
bool isNoBadGoodies() const;
//Cansei New tags 25.08.2013 Start
void changeNoBadGoodiesCount (int iChange);
//Cansei New tags 25.08.2013 End
А следом объявляем переменную в переменных класса, взяв за основу
int m_iKamikazePercent;
Получится у нас.
Код:
int m_iKamikazePercent;
//Cansei New tags 25.08.2013 Start
int m_iNoBadGoodiesCount;
//Cansei New tags 25.08.2013 End
Сохраняем и возвращаемся в CvUnit.cpp. Идем по следам и аналогии с m_iKamikazePercent;
В void CvUnit::reset устанавливаем значение по умолчанию.
Код:
m_iKamikazePercent = 0;
//Cansei New tags 25.08.2013 Start
m_iNoBadGoodiesCount = 0;
//Cansei New tags 25.08.2013 End
Меняем изначальную функцию и добавляем свою.
Код:
bool CvUnit::isNoBadGoodies() const
{
return (m_pUnitInfo->isNoBadGoodies() || m_iNoBadGoodiesCount > 0) ;
}
//Cansei New tags 25.08.2013 Start
void CvUnit::changeNoBadGoodiesCount(int iChange)
{
m_iNoBadGoodiesCount += iChange;
FAssert(m_iNoBadGoodiesCount >= 0);
}
//Cansei New tags 25.08.2013 End
Здесь мы делаем по аналогии с переменной m_iBlitzCount, то есть добавили проверку на то, что счетчик выше нуля. Добавили функцию на изменение счетчика. В которую ещё вписали и проверку FAssert(m_iNoBadGoodiesCount >= 0);
FAssert это проверка на то, что значение переменной находится в нужных пределах. Проверка эта, делается только если dll был скомпилирован в debug режиме.
Далее в функции void CvUnit::setHasPromotion(PromotionTypes eIndex, bool bNewValue) мы добавляем.
Код:
changeKamikazePercent((GC.getPromotionInfo(eIndex).getKamikazePercent()) * iChange);
//Cansei New tags 25.08.2013 Start
changeNoBadGoodiesCount((GC.getPromotionInfo(eIndex).isNoBadGoodies()) ? iChange : 0);
//Cansei New tags 25.08.2013 End
Сиречь, если isNoBadGoodies() возвращает true и юнит и так не получает плохих результатов из деревень, то лишняя прокачка ничего не изменит. А если false, то она добавит ему это умение.
Далее прописываем переменную на сохранение и загрузку.
Код:
pStream->Read(&m_iKamikazePercent);
//Cansei New tags 25.08.2013 Start
pStream->Read(&m_iNoBadGoodiesCount);
//Cansei New tags 25.08.2013 End
Код:
pStream->Write(m_iKamikazePercent);
//Cansei New tags 25.08.2013 Start
pStream->Write(m_iNoBadGoodiesCount);
//Cansei New tags 25.08.2013 End
На этом с функционалом почти все.
Открываем CvUnitAI.cpp и находим функцию int CvUnitAI::AI_promotionValue(PromotionTypes ePromotion)
Там вписываем советы по текущей прокачке юнитам, которыми управляет AI.
Примерно вот так.
Код:
if (GC.getPromotionInfo(ePromotion).isLeader())
{
// Don't consume the leader as a regular promotion
return 0;
}
//Cansei New tags 25.08.2013 Start
if (GC.getPromotionInfo(ePromotion).isNoBadGoodies() && !isNoBadGoodies())
{
if (AI_getUnitAIType() == UNITAI_EXPLORE)
{
iValue += 50;
}
else
{
iValue += 5;
}
}
//Cansei New tags 25.08.2013 End
if (GC.getPromotionInfo(ePromotion).isBlitz())
{
if ((AI_getUnitAIType() == UNITAI_RESERVE && baseMoves() > 1) ||
AI_getUnitAIType() == UNITAI_PARADROP)
{
Теперь если перед юнитом встает вопрос какую прокачку выбрать и у него тем умения NoBadGoodies, то в случае если у юнита действует модель поведения исследователя - AI_getUnitAIType() == UNITAI_EXPLORE он получит +50 к значению этой прокачки для выбора. Иными словами почти гарантированно выберет именно её. Если же этот юнит следует иной модели поведения, он вряд ли станет брать эту прокачку, если он не дает дополнительных бонусов.
Примечание. Выбирая следующую прокачку AI оценивает каждую из них, после чего добавляет к каждой из них случайное число от 0 до 15. Таким образом он может выбрать прокачку с более низким уровнем ценности, если разница между ней и более высокой менее 15.
И напоследок, добавление в python.
Открываем CyInfoInterface1.cpp
Находим там
Код:
python::class_<CvPromotionInfo, python::bases<CvInfoBase> >("CvPromotionInfo")
Вписываем нашу прокачку между двумя уже существующими. Название в кавычках, показывает, как она будет известно в python.
Код:
.def("isLeader", &CvPromotionInfo::isLeader, "bool ()")
.def("isNoBadGoodies", &CvPromotionInfo::isNoBadGoodies, "bool ()")
.def("isBlitz", &CvPromotionInfo::isBlitz, "bool ()")
Ну и напоследок, как же работает прокачка.
В CvGameInterface имеется проверка, если она успешна при выборе скаута появляются синии круги на деревнях, ничего важного, но приятно.
Код:
if (pHeadSelectedUnit->isNoBadGoodies())
{
if (pLoopPlot->isRevealedGoody(pHeadSelectedUnit->getTeam()))
{
gDLL->getEngineIFace()->addColoredPlot(pLoopPlot->getX_INLINE(), pLoopPlot->getY_INLINE(), GC.getColorInfo((ColorTypes)GC.getInfoTypeForString("COLOR_HIGHLIGHT_TEXT")).getColor(), PLOT_STYLE_CIRCLE, PLOT_LANDSCAPE_LAYER_RECOMMENDED_PLOTS);
}
}
В CvPlayer же и происходит самое главное. Если у юнита есть наше умение, он не получит плохого результата от деревни.
Код:
if (GC.getGoodyInfo(eGoody).isBad())
{
if ((pUnit == NULL) || pUnit->isNoBadGoodies())
{
return false;
}
}
В CvPlayerAI же есть такая проверка.
Код:
case UNITAI_EXPLORE:
iValue += (iCombatValue / 2);
iValue += (GC.getUnitInfo(eUnit).getMoves() * 200);
if (GC.getUnitInfo(eUnit).isNoBadGoodies())
{
iValue += 100;
}
break;
Когда AI выбирает кого построить для разведки, то если у юнита есть умение на безопасные деревни, то шансы, что AI его построит куда выше.
Фух. На этом кажется все. Компилируем и смотрим результат.
P.S. На самом деле, как можно заметить если прочитать инструкцию, почти 90% описанной работы обычная рутина, которая почти для всех аналогичных случаев одинакова. И при наличие опыта и возможности перемещаться по собственным комментариям в моем случае это //Cansei New tags она одолевается минут за десять.
Можно ещё поискать варианты с автоматической вставкой нужного текста. Но это уже вопрос про текстовые редакторы.
Если что не так пишите. Изменю урок там где ошибся.