-
-
Notifications
You must be signed in to change notification settings - Fork 0
Applications and Examples
This page gives brief insight into some applications and examples of how to implement different ideas into patches.
As mentioned in Inserting NPC, NPC that are inserted by a patch are by Ninja-default not persistent across saving and loading. As described, they can either forcibly be made persistent, or - if continuity allows - re-inserted at every initialization. On initialization after Init_Global, the NPC can be inserted as follows:
func void Ninja_[PatchName]_Init() {
// Requires initialization of Ikarus
MEM_InitAll();
// ...
// Insert the NPC if non-existent
if (!Hlp_IsValidNpc(Patch_[PatchName]_Npc1)) {
Wld_InsertNpc(Patch_[PatchName]_Npc1, MEM_GetAnyWP());
};
// Set geographical position by daily routine
Npc_ExchangeRoutine(Patch_[PatchName]_Npc1, "SOMEROUTINE");
// ...
};
We use the function MEM_GetAnyWP
from LeGo to obtain a valid way point (remember: we cannot make any assumptions about existing way points in any mod). Afterwards the actual geographical position of the NPC is determined by applying its routine "SOMEROUTINE".
One could remember the last known position of the NPC by saving the nearest waypoint as a string. However, string variables are not persistent across saving and loading. This can be facilitated (since Ninja 2.2.02) with LeGo's PermMem. An example of storing strings across saving and loading is giving in the code accompanying Add New World below.
When making the NPC persistent in game saves instead (which seems to be the easier option), it should be kept in mind that the NPC will vanish from the game save if the patch is unloaded. Thus, checking if the NPC still exists on initialization is inevitable in either approach.
Additionally, it is important to not presuppose any items, or AI variables a new NPC might have. The underlying mods might not have them.
While adding a new NPC to the game, setting certain AI variables may be necessary. However, mods may rename, replace or delete them, such that the simple line
aivar[AIV_IgnoresArmor] = TRUE;
may cause a crash (actually parsing error) for any mod that may have removed the constant AIV_IgnoresArmor
.
In order to approach this in a more secure way, here is a function that checks for the existence of an AI variable before setting it. If the constant of the AI variable does not exist, nothing happens.
func void Patch_[PatchName]_SetAIVarSafe(var C_Npc slf, var string AIVarName, var int value) {
var int symb; symb = MEM_GetParserSymbol(AIVarName);
if (symb) {
var int idx; idx = MEM_ReadInt(symb+zCParSymbol_content_offset);
MEM_WriteStatArr(slf.aivar, idx, value);
};
};
The recommended approach is to remove any AI variables from the NPC instance script and instead set them afterwards in a separate function. Here is an example:
func void Patch_[PatchName]_InitSomeAIVars(var C_Npc slf) {
Patch_[PatchName]_SetAIVarSafe(slf, "AIV_IgnoresArmor", TRUE);
Patch_[PatchName]_SetAIVarSafe(slf, "AIV_IgnoresFakeGuild", TRUE);
Patch_[PatchName]_SetAIVarSafe(slf, "AIV_Ignore_Murder", TRUE);
Patch_[PatchName]_SetAIVarSafe(slf, "AIV_Ignore_Theft", TRUE);
Patch_[PatchName]_SetAIVarSafe(slf, "AIV_Ignore_Sheepkiller", TRUE);
Patch_[PatchName]_SetAIVarSafe(slf, "AIV_NoFightParker", TRUE);
Patch_[PatchName]_SetAIVarSafe(slf, "AIV_NewsOverride", TRUE);
Patch_[PatchName]_SetAIVarSafe(slf, "AIV_EnemyOverride", TRUE);
Patch_[PatchName]_SetAIVarSafe(slf, "AIV_DropDeadAndKill", TRUE);
};
Similarly, checking for AI variables should be done analogously.
func int Patch_[PatchName]_GetAIVarSafe(var C_Npc slf, var string AIVarName, var int dflt) {
var int symb; symb = MEM_GetParserSymbol(AIVarName);
if (symb) {
var int idx; idx = MEM_ReadInt(symb+zCParSymbol_content_offset);
return MEM_ReadStatArr(slf.aivar, idx);
} else {
return dflt; // Return default value, e.g. 0
};
};
The same holds for checking properties like the NPC guild. All these constants can never be presupposed. Here is an example of how to check for the guild membership of an NPC.
func int Patch_[PatchName]_GetNpcGuild(var C_Npc slf, var string GIL_NAME) {
var int symb; symb = MEM_GetParserSymbol(GIL_NAME);
if (symb) {
var int idx; idx = MEM_ReadInt(symb+zCParSymbol_content_offset);
return slf.guild == idx;
} else {
return FALSE;
};
};
Adding new dialogs does not differ from the conventional way. A dialog script is created - of course with unique names following the Naming Conventions. Ninja will add the new dialog instances to the cutscene info library. The output units can be extracted from the new script with the Gothic Spacer or tools like Redefix. The resulting output unit file is also added to the patch and the new dialog is fully integrated into patch.
Across saving and loading Gothic will remember which dialogs were told and which weren't. However, if the patch is unloaded this information will be lost when saving the game.
First and foremost, the idea of adding new spells to the game is controversial. From the annual Gothic modding meeting in April 2019 it became apparent that mod developers have different opinions on adding spells to a mod that was not intended to provide them. Aside from balancing issues on mana usage and combat, new spells may inherently break the entire game.
Keeping that in mind, it is also very difficult to add new spells on a technical level. This is due to the various static Daedalus arrays that spells, their animations and effects are registered in. These static arrays (spellFxInstanceNames
, spellFxAniLetters
and TXT_SPELLS
) cannot be enlarged by Ninja without completely rewriting their content. This is of course not an option, because mods will have different spells in these arrays.
To still be able to incorporate new spells into the game with a patch, these static arrays can be enlarged "after the fact", that is, dynamically with Daedalus using Ikarus at initialization of the patch. The scripts below were developed for the FirstMageKit patch, but are kindly provided here for re-use (mentioning this page as the source is highly encouraged).
Click to show code
/*
* Enlarging stating arrays is tricky
* Source: https://github.com/szapp/Ninja/wiki/Applications-and-Examples
*/
func void Patch_[PatchName]_EnlargeStatStringArr(var int symbPtr, var int numNewTotal) { // Adjust name
const int zCPar_Symbol___zCPar_Symbol[4] = {/*G1*/7306624, /*G1A*/7544544, /*G2*/7609520, /*G2A*/8001264};
const int zCPar_Symbol__AllocSpace[4] = {/*G1*/7306832, /*G1A*/7544784, /*G2*/7609728, /*G2A*/8001472};
// First: Backup all the relevant information of the symbol
var zCPar_Symbol symb; symb = _^(symbPtr);
var string name; name = symb.name;
var int bitfield; bitfield = symb.bitfield;
var int numEle; numEle = bitfield & zCPar_Symbol_bitfield_ele;
// I refuse to make it smaller
if (numNewTotal <= numEle) {
return;
};
// The string content we'll have to backup this way (one string at a time, deep copy)
var int buffer; buffer = MEM_Alloc(numEle * sizeof_zSTRING);
repeat(i, numEle); var int i;
MEM_WriteStringArray(buffer, i, MEM_ReadStringArray(symb.content, i));
end;
// Free the content of the symbol
const int call = 0;
if (CALL_Begin(call)) {
CALL__thiscall(_@(symbPtr), zCPar_Symbol___zCPar_Symbol[IDX_EXE]);
call = CALL_End();
};
// Reset the properties how we want them - mind the increase in elements
symb.name = name;
symb.bitfield = (bitfield & ~zCPar_Symbol_bitfield_ele) | numNewTotal;
symb.bitfield = symb.bitfield & ~zCPar_Symbol_bitfield_space; // Set 'allocated' to false
// Have Gothic allocate the space for the content (we cannot do this ourselves, because it's tied to a pool)
const int call2 = 0;
if (CALL_Begin(call2)) {
CALL__thiscall(_@(symbPtr), zCPar_Symbol__AllocSpace[IDX_EXE]);
call2 = CALL_End();
};
// Restore the content - again one by one
repeat(i, numEle);
MEM_WriteStringArray(symb.content, i, MEM_ReadStringArray(buffer, i));
end;
MEM_Free(buffer);
};
/*
* Obtain number of spells in a safe way
*/
func int Patch_[PatchName]_GetMaxSpell() {
var int symbPtr; var zCPar_Symbol symb;
var int ret;
// Get MAX_SPELL if exists
symbPtr = MEM_GetSymbol("MAX_SPELL");
if (symbPtr) {
symb = _^(symbPtr);
ret = symb.content;
};
// Get number of elements in spellFxInstanceNames
symbPtr = MEM_GetSymbol("spellFxInstanceNames");
if (symbPtr) {
symb = _^(symbPtr);
if (ret < (symb.bitfield & zCPar_Symbol_bitfield_ele)) {
ret = (symb.bitfield & zCPar_Symbol_bitfield_ele);
};
} else {
// That should be near impossible
MEM_SendToSpy(zERR_TYPE_FATAL, "Symbol 'spellFxInstanceNames' not found.");
return -1;
};
// Get number of elements in spellFxAniLetters
symbPtr = MEM_GetSymbol("spellFxAniLetters");
if (symbPtr) {
symb = _^(symbPtr);
if (ret < (symb.bitfield & zCPar_Symbol_bitfield_ele)) {
ret = (symb.bitfield & zCPar_Symbol_bitfield_ele);
};
} else {
// That should be near impossible
MEM_SendToSpy(zERR_TYPE_FATAL, "Symbol 'spellFxAniLetters' not found.");
return -1;
};
// Return the most number of elements
return ret;
};
/*
* Set MAX_SPELL if the symbol exists
*/
func void Patch_[PatchName]_SetMaxSpell(var int value) {
var int symbPtr; symbPtr = MEM_GetSymbol("MAX_SPELL");
if (symbPtr) {
var zCPar_Symbol symb; symb = _^(symbPtr);
symb.content = value;
};
};
/*
* Add a new spell at "runtime" (kind of). Expects the static arrays to be already enlarged (see above)
*/
func void Patch_[PatchName]_SetSpell(var int spellID, var string spellFxInst, var string spellFxAniLetter,
var string spellTxt) {
// Set static arrays
var int symbPtr; var zCPar_Symbol symb;
// MEM_WriteStatStringArr(spellFxInstanceNames, spellID, spellFxInst);
symbPtr = MEM_GetSymbol("spellFxInstanceNames");
if (symbPtr) {
symb = _^(symbPtr);
MEM_WriteStringArray(symb.content, spellID, spellFxInst);
};
// MEM_WriteStatStringArr(spellFxAniLetters, spellID, spellFxAniLetter);
symbPtr = MEM_GetSymbol("spellFxAniLetters");
if (symbPtr) {
symb = _^(symbPtr);
MEM_WriteStringArray(symb.content, spellID, spellFxAniLetter);
};
// MEM_WriteStatStringArr(TXT_SPELLS, spellID, spellTxt);
symbPtr = MEM_GetSymbol("TXT_SPELLS");
if (symbPtr) {
symb = _^(symbPtr);
MEM_WriteStringArray(symb.content, spellID, spellTxt);
};
};
The first function relocates and enlarges a static array. This is safe as the address to each array element is always resolved dynamically from the symbol. The second function can be used subsequently to add the new entries to the three enlarged static arrays related to spells.
The usage is straight forward as shown below. The script should be called once from the Content Initialization Functions.
// ...
const int NumNewSpells = 2;
// Get MAX_SPELL (this constant might not exist in the mod, e.g. sometimes missing in translated scripts)
var int MAX_SPELL; MAX_SPELL = Patch_[PatchName]_GetMaxSpell();
// Enlarge static arrays
Patch_[PatchName]_EnlargeStatStringArr(MEM_GetSymbol("spellFxInstanceNames"), MAX_SPELL + NumNewSpells);
Patch_[PatchName]_EnlargeStatStringArr(MEM_GetSymbol("spellFxAniLetters"), MAX_SPELL + NumNewSpells);
Patch_[PatchName]_EnlargeStatStringArr(MEM_GetSymbol("TXT_SPELLS"), MAX_SPELL + NumNewSpells);
// Assign new spell ID
SPL_SpellA = MAX_SPELL;
SPL_SpellB = MAX_SPELL + 1;
Patch_[PatchName]_SetMaxSpell(MAX_SPELL + NumNewSpells);
// Add spells (also increments MAX_SPELL)
// Spell ID spellFXInstanceNames spellFxAniLetters TXT_SPELLS
Patch_[PatchName]_SetSpell(SPL_SpellA, "SpellA", "FIB", NAME_SPL_SpellA);
Patch_[PatchName]_SetSpell(SPL_SpellB, "SpellB", "SLE", NAME_SPL_SpellB);
// More spells ...
Additionally, the mana processing functions need to be hooked and extended with the new spells.
// Add mana processing calls
HookDaedalusFuncS("Spell_ProcessMana", "Patch_[PatchName]_Spell_ProcessMana");
// Also mana processing release (only if they are release spells)
HookDaedalusFuncS("Spell_ProcessMana_Release", "Patch_[PatchName]_Spell_ProcessMana_Release");
These hooks should look something like the following.
Click to show code
/*
* Additions to the mana processing functions
*/
func int Patch_[PatchName]_Spell_ProcessMana(var int manaInvested) {
var int activeSpell; activeSpell = Npc_GetActiveSpell(self);
if (activeSpell == SPL_SpellA) { return Spell_Logic_SpellA(manaInvested); };
if (activeSpell == SPL_SpellB) { return Spell_Logic_SpellB(manaInvested); };
// More spells ...
PassArgumentI(manaInvested);
ContinueCall();
};
func int Patch_[PatchName]_Spell_ProcessMana_Release(var int manaInvested) {
var int activeSpell; activeSpell = Npc_GetActiveSpell(self);
if (activeSpell == SPL_SpellA) { return SPL_SENDCAST; };
if (activeSpell == SPL_SpellB) { return SPL_SENDCAST; };
// More spells ...
PassArgumentI(manaInvested);
ContinueCall();
};
Following the Localization example, the spell names and descriptions can be made auto-adjustable on the mod language.
To get a complete picture, it is recommended to view the source code of the FirstMageKit patch.
Adding a new world with a patch usual presupposes a new story/quest line. Whether a patch is the best place to add new story elements is questionable, difficult to implement (conceptually and technically), requires a lot of technical foresight, but is definitely possible.
This implementation necessitates to Disallow Saving. Alternatively, as described, the player can be educated that a game save will depend on the patch once it has been installed and that the patch can no longer be removed.
The new world will require a world-specific initialization function as done usually as well. The guild attitudes should be set as well. However, it is not given that the function B_InitMonsterAttitudes
still exists all mods, as they may have renamed or deleted it. Therefore such an approach is advisable:
if (MEM_GetSymbol("B_InitMonsterAttitudes")) {
MEM_CallByString("B_InitMonsterAttitudes");
};
Alternatively, the guild attitudes can be set manually (Wld_SetGuildAttitude
). This sould work easily, since the monsters/NPC in the added world are known to the patch creator.
To actually change the world from within the game, the following function will become handy.
Click to show code
func void Patch_[PatchName]_ChangeWorld(var string level, var string waypoint) {
const int oCGame__TriggerChangeLevel[4] = { /*G1*/6542464, /*G1A*/6701312, /*G2*/6729088, /*G2A*/7109360 };
MEM_InitGlobalInst();
var int waypointPtr; waypointPtr = _@s(waypoint);
var int levelPtr; levelPtr = _@s(level);
var int gamePtr; gamePtr = _@(MEM_Game);
const int call = 0;
if (CALL_Begin(call)) {
CALL_PtrParam(_@(waypointPtr));
CALL_PtrParam(_@(levelPtr));
CALL__thiscall(_@(gamePtr), oCGame__TriggerChangeLevel[IDX_EXE]);
call = CALL_End();
};
};
To actually traverse back and forth between the worlds the following wrapper functions are very useful. There, the variable Patch_[PathName]_NewWorld
allows to check if the player is currently in the patch specific world. The last world as well as the nearest waypoint is stored before entering the new world. This allows to return exactly to where the player left the previous world.
However, string variables are not persistent across saving and loading. This can be facilitated (since Ninja 2.2.02) with LeGo's PermMem (included in the code below). The package LeGo_PermMem
will have to be initialized, see Initializing LeGo.
var string Patch_[PatchName]_ReturnWld;
var string Patch_[PatchName]_ReturnWP;
var int Patch_[PatchName]_NewWorld; // Boolean
/*
* Enter the unknown world
* Source: https://github.com/szapp/Ninja/wiki/Applications-and-Examples#add-new-world
*/
func void Patch_[PatchName]_EnterWorld() {
// Bind strings to make them save/load persistent
PM_BindString(Patch_[PatchName]_ReturnWld);
PM_BindString(Patch_[PatchName]_ReturnWP);
// Remember where we came from
Patch_[PatchName]_ReturnWld = MEM_World.worldFilename;
// Remember waypoint to return to
Patch_[PatchName]_ReturnWP = Npc_GetNearestWP(hero);
// Enter the new world
Patch_[PatchName]_ChangeWorld("NINJA[PATCHNAME].ZEN", "SOMEWAYPOINT");
Patch_[PatchName]_NewWorld = TRUE; // Remember where we are currently
};
/*
* There is no place like ~
* Source: https://github.com/szapp/Ninja/wiki/Applications-and-Examples#add-new-world
*/
func void Patch_[PatchName]_LeaveWorld() {
// Return to the previous world and waypoint
Patch_[PatchName]_ChangeWorld(Patch_[PatchName]_ReturnWld, Patch_[PatchName]_ReturnWP);
Patch_[PatchName]_NewWorld = FALSE; // Remember where we are currently
};
Ninja allows a very convenient way to translate mods. By replacing localized Daedalus strings of the content and menu scripts as well as the output units, the mod remains untouched, does not need to be recompiled and redistributed. This has the advantage of stability as de- and re-compiling is prone to cause bugs or subtle problems.
Nevertheless, a huge disadvantage of the approach with a patch is that the symbol names of the inline strings (e.g. ÿ10023
) change if the scripts change. Thus, if the mod was ever updated after it has been translated, the translation has to be redone or the symbol names at least confirmed and rearranged.
Extracting a list of all strings (including the auto-named inline strings, e.g. ÿ10023
) from a mod can be done with a modified version of the tool DecDat. This tool was originally developed by the World of Players user Gottfried. For proof of concept this program was only quickly extended to specifically extract the strings, but this modified version is not stable. Correctness and stability cannot be guaranteed and its usage is not recommended. To extract all string symbols, choose Type as the option and filter for "const string". Then click Exportieren → Alle gefilterten Symbole... and save the results to a D file. This file now contains all constant string symbol definitions including the inline strings.
To edit the output units, they should be available in CSL format, as the binary counterpart (BIN format) is not easy to edit. Here as well, only as proof of concept, a python script (right click, save as) was written to convert BIN files to CSL files. The script is not very optimized and very slow. Correctness and stability cannot be guaranteed and its usage is not recommended.
Effectively, such a translation patch consists of three files (here exemplary snippets for the German mod Legend of Ahssûn translated to French).
\Ninja\[PatchName]\Translation\Content.d
const string print_newlogentry = "Nouvelle entrée de journal";
const string topic_cortezmisenakapone = "UV: Un nouveau départ";
const string info_stadt = "La ville d'Ahssûn";
const string ÿ26291 = "Entrée de journal en ";
const string ÿ26299 = "LoA Trucs & astuces: ";
const string topic_tipps = "LoA: Trucs et astuces";
const string info_questbezeichner = "LoA: Trucs et astuces";
const string kapwechsel_1 = "Chapitre 1";
const string kapwechsel_1_loa_text = "Pas un bon début";
const string dialog_ende = "Je vais aller ... (FIN)";
const string dialog_back = "(retour)";
const string ÿ76380 = "Comment je vais voir le prince?";
const string ÿ76390 = "Que dois-je savoir sur cet endroit?";
const string ÿ76398 = "Pourquoi parlez-vous de la ville 'actuelle'?";
const string ÿ76404 = "Où puis-je me procurer de nouveaux vêtements?";
const string ÿ82532 = "Comment ai-je fini ici?";
const string ÿ71379 = "Peux-tu m'apprendre quelque chose?";
// ...
\Ninja\[PatchName]\Translation\Menu.d
const string diff_easy_label = "Simple";
const string diff_medium_label = "Normal";
const string diff_hard_label = "Difficile";
const string diff_challenge_label = "Défi";
const string diff_not_set_label = "Non disponible";
const string ÿ10023 = "Commencer l'aventure sur Ahssûn";
const string ÿ10024 = "Commencer une nouvelle aventure.";
const string ÿ10026 = "Charger le jeu";
const string ÿ10027 = "Charger une partie sauvegardée.";
const string ÿ10029 = "Enregistrer le jeu";
const string ÿ10030 = "Enregistrer la partie en cours.";
const string ÿ10032 = "Continuer à jouer";
const string ÿ10033 = "Continuer la partie en cours.";
const string ÿ10034 = "Paramètres";
const string ÿ10035 = "Ajustez le jeu, la vidéo, l'audio et le clavier.";
const string ÿ10037 = "Paramètres de LoA";
const string ÿ10038 = "Nouveaux paramètres (viser libre, réalisations).";
const string ÿ10040 = "Jouer l'intro";
const string ÿ10041 = "Jouer à nouveau la séquence d'introduction.";
const string ÿ10042 = "LoA Credits";
const string ÿ10043 = "Jouer les crédits de la modification.";
const string ÿ10044 = "Quitter LoA";
const string ÿ10045 = "Quitter le monde de LoA.";
// ...
\Ninja\[PatchName]\OU_G2.CSL
ZenGin Archive
ver 1
zCArchiverGeneric
ASCII
saveGame 0
date 6/3/2019 9:16:00 PM
user Ninja
END
objects 118
END
[% zCCSLib 0 0]
NumOfItems=int:39
[% zCCSBlock 0 1]
blockName=string:DIA_ST_K1_SQ_ANEWBEGINNING_1_CORTEZ_EINLEITUNG_00
numOfBlocks=int:1
subBlock0=float:0
[% zCCSAtomicBlock 0 2]
[% oCMsgConversation:oCNpcMessage:zCEventMessage 0 3]
subType=enum:0
text=string:Un autre de l'arène....
name=string:DIA_ST_K1_SQ_ANEWBEGINNING_1_CORTEZ_EINLEITUNG_00.WAV
[]
[]
[]
[% zCCSBlock 0 4]
blockName=string:DIA_ST_K1_SQ_ANEWBEGINNING_1_CORTEZ_EINLEITUNG_01
numOfBlocks=int:1
subBlock0=float:0
[% zCCSAtomicBlock 0 5]
[% oCMsgConversation:oCNpcMessage:zCEventMessage 0 6]
subType=enum:0
text=string:Servito et ses voyous ne font pas exception.
name=string:DIA_ST_K1_SQ_ANEWBEGINNING_1_CORTEZ_EINLEITUNG_01.WAV
[]
[]
[]
[% zCCSBlock 0 7]
blockName=string:DIA_ST_K1_SQ_ANEWBEGINNING_1_CORTEZ_EINLEITUNG_02
numOfBlocks=int:1
subBlock0=float:0
[% zCCSAtomicBlock 0 8]
[% oCMsgConversation:oCNpcMessage:zCEventMessage 0 9]
subType=enum:0
text=string:Ils ne traînent personne dans l'arène et le battent jusqu'à ce qu'il ait l'air de toi.
name=string:DIA_ST_K1_SQ_ANEWBEGINNING_1_CORTEZ_EINLEITUNG_02.WAV
[]
[]
[]
[% zCCSBlock 0 10]
blockName=string:DIA_ST_K1_SQ_ANEWBEGINNING_1_CORTEZ_EINLEITUNG_03
numOfBlocks=int:1
subBlock0=float:0
[% zCCSAtomicBlock 0 11]
[% oCMsgConversation:oCNpcMessage:zCEventMessage 0 12]
subType=enum:0
text=string:Je ne suis ni une mauviette ni personne! Quelque chose a mal tourné ici, mais personne ne m'écoute!
name=string:DIA_ST_K1_SQ_ANEWBEGINNING_1_CORTEZ_EINLEITUNG_03.WAV
[]
[]
[]
...
Additionally, the respective source files are necessary to complete this patch.
\Ninja\[PatchName]\Content_G2.src
Translation\Content.d
\Ninja\[PatchName]\Menu_G2.src
Translation\Menu.d
Because of overwriting inline strings, this patch is almost guaranteed to crash with any other mod. Thus, it is highly recommended to specify the name of the mod within the patch name, e.g. LoAFrench
.
Depending on the alphabet, the respective font files will also have to be included at their usual paths in the \_work\Data\Textures\
directory.
Introduction
Virtual Disk File System
Formats
Single File Formats
Collected File Formats
Limitations to Overcome
Scripts
Animations
Output Units
Solution
Implementation
Patch Structure
VDF File Tree
VDF Header
Patch Template
Patch Validator
Inter-Game Compatibility
Inject Changes
Daedalus Scripts
Overwriting Symbols
Naming Conventions
Preserved Symbols
Initialization Functions
Init_Global
Menu Creation
Ikarus and LeGo
Initializing LeGo
Modifications to LeGo
PermMem and Handles
Daedalus Hooks
Inserting NPC
Disallow Saving
Helper Symbols
NINJA_VERSION
NINJA_MODNAME
NINJA_PATCHES
NINJA_ID_PATCHNAME
NINJA_SYMBOLS_START
NINJA_SYMBOLS…PATCHNAME
Common Symbols
Localization
Animations and Armor
Output Units
Other Mechanics
Remove Invalid NPC
Safety Checks in Externals
Preserve Integer Variables
Detect zSpy
Incompatibility List for Mods
Applications and Examples
Add New NPC
Set AI Variables
Add New Dialogs
Add New Spells
Add New World
Translation Patch
Installation
Requirements
Instructions
Troubleshooting
Is Ninja Active
Is Patch Loaded
Error Messages