Эта статья подразумевает что у вас имеется пред настроенное окружение. В прошлой статье мы сделали первые шаги в создании своего мода для KOTOR, очень здорово, что нашелся читатель, который постарался разобраться в написанном и прошел мануал. В ходе обсуждения статьи мы пришли к выводу, что модификация не законченна и требует доработки.
Выявленные проблемы при смене компаньона в слот 7:
теряется опыт компаньона
теряется экипировка.
В этом руководстве мы рассмотрим этот частный случай и доработаем наш мод до приемлемого состояния.
Перед началом работы локализуем проблемный участок нашей логики. Проблема возникает в том, что, когда мы создаём компаньона, используя пред настройки из файла availnpc7_t3.utc, там у нас фиксированы экипировка и опыт. Да, у нас есть возможность менять этот файл и подсовывать свой результат в игру, но это решение плохо для живой игры.
Перед началом работы, для вашего удобства я подготовил учебный каталог, вы можете скачать его выше. В этом руководстве будет использован такой путь размещения
D:\KOTOR_HANDMADE2
Этот путь влияет на синтаксис команд для CMD которые мы будем использовать в этом руководстве.
Первый шаг
Создадим пустышки Вимы и Т3 UTC. Нам нужны чистые персонажи, чтоб мы исключили дублирование экипировки при переключениях. Возьмём из Override Steam-клиента два файла: availnpc7_t3.utc и p_vimasunrider.utc. Это файлы, в которых хранятся профили персонажей, которые мы позже вызовем нашим скриптом. Скопируйте файлы availnpc7_t3.utc и p_vimasunrider.utc и разместите их в 04_build.
Дальше мы Python-скриптом чистим эти файлы и подготавливаем заготовки для нашего мода.
Открываем CMD и вводим туда
cd /d D:\KOTOR_HANDMADE2
copy /y 04_build\availnpc7_t3.utc 05_release\availnpc7_t3.utc
copy /y 04_build\p_vimasunrider.utc 05_release\p_vimasunrider.utc
python tools\python\clear_utc_inventory_lists.py 05_release\availnpc7_t3.utc 05_release\p_vimasunrider.utc
можете проверить результат нашей утилитой.
python tools\python\kotor_inspect.py gff 05_release\availnpc7_t3.utc --field Tag --field Experience --field Equip_ItemList --field ItemList
python tools\python\kotor_inspect.py gff 05_release\p_vimasunrider.utc --field Tag --field Experience --field Equip_ItemList --field ItemList
Ожидаемый вывод на скриншоте.
Заготовки готовы!
Второй шаг
Подготовим диалоги. Мы уже это сделали в статье (часть 1), так что можем пропустить этот шаг. Для этого патча изменений в k_htmd_dialog.dlg и party_vima.dlg не требуется. Можете скопировать k_htmd_dialog.dlg и party_vima.dlg в 05_release из 01_original, а можете ничего не делать. Этот шаг контрольный для отчётности, чтоб не терять связь в последовательности редактируемых файлов — разработка под KOTOR путём реверсинга требует дисциплины на каждом шаге.
Третий шаг
Самый важный шаг - пишем nss to ncs. В этот раз я опишу его подробно, не так сжато, как в статье(часть1).
Возьмем наши заготовки из прошлой статьи и декомпилируем их.
Открываем CMD и вводим инструкции для терминала:
открыли рабочий каталог
cd /d D:\KOTOR_HANDMADE2
декомпилим vima_call_t3.ncs в vima_call_t3_old.nss
tools\nwnnsscomp\nwnnsscomp.exe -d 01_original\vima_call_t3.ncs -o 02_decompiled\vima_call_t3_old.nss
декомпилим t3_call_vima.ncs в t3_call_vima_old.nss
tools\nwnnsscomp\nwnnsscomp.exe -d 01_original\t3_call_vima.ncs -o 02_decompiled\t3_call_vima_old.nss
Почитаем полученный байткод.
Ну как? Что-нибудь понятно?
Не пробуйте редактировать полученный .nss. Если вы внимательно читали статью(часть1) – там я уже говорил, это можно только читать. Прочитайте следующее и запомните!
.nss бывает авторским исходником и восстановленным текстом после декомпиляции .ncs. Расширение одинаковое, но качество разное. Авторский .nss - это нормальный NWScript-код. Декомпилированный .nss - это скорее карта того, что делает байткод: по нему удобно понять вызовы RemoveAvailableNPC, AddAvailableNPCByTemplate, SpawnAvailableNPC, но новый скрипт лучше писать заново нормальным человеческим кодом.
Зная и понимая вышеописанное, вы официально посвящаетесь в модеры KOTOR. Больше у вас нет проблем с пониманием, осталось только нафармить опыта в C-подобных языках, и вы можете реализовать всё, к чему предрасположен KOTOR Odyssey-движок.
Прочитав NSS в виде ассемблероподобного байткода, я предлагаю обратиться к исходникам: ведь они у нас есть, и не будем усложнять жизнь.
Рассмотрим кодинг-часть и напишем NSS.
Это исходник
void SpawnSlot7(location lSpot) {
SpawnAvailableNPC(7, lSpot);
}
void main() {
object oCurrent = OBJECT_SELF;
object oHost = GetFirstPC();
location lSpot = GetLocation(oCurrent);
object oTempT3 = GetObjectByTag("x_t3m4", 0);
object oRealT3 = GetObjectByTag("t3m4", 0);
RemoveAvailableNPC(7);
AddAvailableNPCByTemplate(7, "availnpc7_t3");
AssignCommand(oHost, SetGlobalFadeOut(0.0, 0.35, 0.0, 0.0, 0.0));
if (GetIsObjectValid(oTempT3)) {
AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oTempT3)));
}
if (GetIsObjectValid(oRealT3)) {
AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oRealT3)));
}
AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oCurrent)));
AssignCommand(oHost, DelayCommand(0.45, SpawnSlot7(lSpot)));
AssignCommand(oHost, DelayCommand(0.80, SetGlobalFadeIn(0.0, 0.35, 0.0, 0.0, 0.0)));
}
Создадим 03_scripts\vima_call_t3.nss и наполним его C содержимым:
void SaveSlot7State()
{
SaveNPCState(7);
}
void TransferSlotItem(object oCreature, object oReceiver, int nSlot)
{
object oItem = GetItemInSlot(nSlot, oCreature);
if (GetIsObjectValid(oItem))
{
GiveItem(oItem, oReceiver);
}
}
void TransferAllEquipment(object oCreature, object oReceiver)
{
if (GetIsObjectValid(oCreature) && GetIsObjectValid(oReceiver))
{
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_HEAD);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_BODY);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_HANDS);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_RIGHTWEAPON);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_LEFTWEAPON);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_LEFTARM);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_RIGHTARM);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_IMPLANT);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_BELT);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_CWEAPON_L);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_CWEAPON_R);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_CWEAPON_B);
TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_CARMOUR);
}
}
void SpawnSlot7WithXP(location lSpot, int nInheritedXP)
{
object oSpawned = SpawnAvailableNPC(7, lSpot);
if (GetIsObjectValid(oSpawned))
{
SetXP(oSpawned, nInheritedXP);
DelayCommand(0.1, SaveSlot7State());
}
}
void ReplaceVimaWithT3(object oHost, object oCurrent, object oTempT3, object oRealT3, location lSpot, int nInheritedXP)
{
RemoveAvailableNPC(7);
AddAvailableNPCByTemplate(7, "availnpc7_t3");
AssignCommand(oHost, SetGlobalFadeOut(0.0, 0.35, 0.0, 0.0, 0.0));
if (GetIsObjectValid(oTempT3))
{
AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oTempT3)));
}
if (GetIsObjectValid(oRealT3))
{
AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oRealT3)));
}
AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oCurrent)));
AssignCommand(oHost, DelayCommand(0.45, SpawnSlot7WithXP(lSpot, nInheritedXP)));
AssignCommand(oHost, DelayCommand(0.80, SetGlobalFadeIn(0.0, 0.35, 0.0, 0.0, 0.0)));
}
void main()
{
object oCurrent = OBJECT_SELF;
object oHost = GetFirstPC();
int nInheritedXP = GetXP(oHost);
location lSpot = GetLocation(oCurrent);
object oTempT3 = GetObjectByTag("x_t3m4", 0);
object oRealT3 = GetObjectByTag("t3m4", 0);
TransferAllEquipment(oCurrent, oHost);
AssignCommand(oHost, DelayCommand(0.30, ReplaceVimaWithT3(oHost, oCurrent, oTempT3, oRealT3, lSpot, nInheritedXP)));
}
Объясняю, что мы изменили:
-void SpawnSlot7(location lSpot) {
-SpawnAvailableNPC(7, lSpot);
+void SaveSlot7State()
+{
+ SaveNPCState(7);
}
+
+void TransferSlotItem(object oCreature, object oReceiver, int nSlot)
+{
+ object oItem = GetItemInSlot(nSlot, oCreature);
+
+ if (GetIsObjectValid(oItem))
+ {
+ GiveItem(oItem, oReceiver);
+ }
+}
+
+void TransferAllEquipment(object oCreature, object oReceiver)
+{
+ if (GetIsObjectValid(oCreature) && GetIsObjectValid(oReceiver))
+ {
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_HEAD);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_BODY);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_HANDS);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_RIGHTWEAPON);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_LEFTWEAPON);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_LEFTARM);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_RIGHTARM);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_IMPLANT);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_BELT);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_CWEAPON_L);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_CWEAPON_R);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_CWEAPON_B);
+ TransferSlotItem(oCreature, oReceiver, INVENTORY_SLOT_CARMOUR);
+ }
+}
+
+void SpawnSlot7WithXP(location lSpot, int nInheritedXP)
+{
+ object oSpawned = SpawnAvailableNPC(7, lSpot);
+
+ if (GetIsObjectValid(oSpawned))
+ {
+ SetXP(oSpawned, nInheritedXP);
+ DelayCommand(0.1, SaveSlot7State());
+ }
+}
+
+void ReplaceVimaWithT3(object oHost, object oCurrent, object oTempT3, object oRealT3, location lSpot, int nInheritedXP)
+{
+ RemoveAvailableNPC(7);
+ AddAvailableNPCByTemplate(7, "availnpc7_t3");
+
+ AssignCommand(oHost, SetGlobalFadeOut(0.0, 0.35, 0.0, 0.0, 0.0));
+
+ if (GetIsObjectValid(oTempT3))
+ {
+ AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oTempT3)));
+ }
+
+ if (GetIsObjectValid(oRealT3))
+ {
+ AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oRealT3)));
+ }
+
+ AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oCurrent)));
+ AssignCommand(oHost, DelayCommand(0.45, SpawnSlot7WithXP(lSpot, nInheritedXP)));
+ AssignCommand(oHost, DelayCommand(0.80, SetGlobalFadeIn(0.0, 0.35, 0.0, 0.0, 0.0)));
+}
+
void main() {
object oCurrent = OBJECT_SELF;
object oHost = GetFirstPC();
+int nInheritedXP = GetXP(oHost);
+
location lSpot = GetLocation(oCurrent);
+
object oTempT3 = GetObjectByTag("x_t3m4", 0);
object oRealT3 = GetObjectByTag("t3m4", 0);
-RemoveAvailableNPC(7);
-AddAvailableNPCByTemplate(7, "availnpc7_t3");
-AssignCommand(oHost, SetGlobalFadeOut(0.0, 0.35, 0.0, 0.0, 0.0));
-if (GetIsObjectValid(oTempT3)) {
-AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oTempT3)));
-}
-if (GetIsObjectValid(oRealT3)) {
-AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oRealT3)));
-}
-AssignCommand(oHost, DelayCommand(0.40, DestroyObject(oCurrent)));
-AssignCommand(oHost, DelayCommand(0.45, SpawnSlot7(lSpot)));
-AssignCommand(oHost, DelayCommand(0.80, SetGlobalFadeIn(0.0, 0.35, 0.0, 0.0, 0.0)));
+
+TransferAllEquipment(oCurrent, oHost);
+
+AssignCommand(oHost, DelayCommand(0.30, ReplaceVimaWithT3(oHost, oCurrent, oTempT3, oRealT3, lSpot, nInheritedXP)));
}
`SpawnSlot7(lSpot)` заменили на: SpawnSlot7WithXP(lSpot, nInheritedXP);
Потому что теперь после спавна надо получить объект нового Т3 и вызвать:
SetXP(oSpawned, nInheritedXP);
В начало main() добавили:
int nInheritedXP = GetXP(oHost);
Перед заменой добавили:
TransferAllEquipment(oCurrent, oHost);
А старую большую цепочку замены вынесли из main() в отдельную функцию:
ReplaceVimaWithT3(...);
Это сделано не ради красоты, а из-за задержки:
DelayCommand(0.30, ReplaceVimaWithT3(...));
То есть Сначала переносим предметы через GiveItem, даем игре короткий момент обработать передачу, и только потом уничтожаем старую Виму и спавним Т3.
Старый скрипт уже показал функции переключения NPC. А функции для опыта и предметов были найдены в nwscript.nss: GetXP, SetXP, GetItemInSlot, GiveItem, SaveNPCState. После этого новая логика стала простой: до замены забираем данные и вещи у старого состояния, после спавна применяем данные к новому объекту.
Теперь скомпилируем скрипт в игровые ресурсы:
cd /d D:\KOTOR_HANDMADE2
tools\nwnnsscomp\nwnnsscomp.exe -c 03_scripts\vima_call_t3.nss -o 05_release\vima_call_t3.ncs
Статья получилась достаточно большой. И в ней я рассказал достаточно сложный материал.
Фаил t3_call_vima.nss как создать я описывать не буду, в статье написано достаточно чтоб желающий разобраться смог сделать это сам. Готовые файлы прикреплены к модификации и доступны для загрузки тут.
Важная техническая деталь: на каждой планете существуют разные копии Ebon Hawk. Все изменения, которые мы делали выше, автор делал для копии Ebon Hawk, которая находится на Слехероне и является точкой входа на планету.
Комментарии приветствуются.