Skip to content

Applications and Examples

szapp edited this page Apr 8, 2021 · 23 revisions

This page gives brief insight into some applications and examples of how to implement different ideas into patches.

Add New NPC

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(Ninja_[PatchName]_Npc1)) {
        Wld_InsertNpc(Ninja_[PatchName]_Npc1, MEM_GetAnyWP());
    };

    // Set geographical position by daily routine
    Npc_ExchangeRoutine(Ninja_[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.

Set AI Variables

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 Ninja_[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 Ninja_[PatchName]_InitSomeAIVars(var C_Npc slf) {
    Ninja_[PatchName]_SetAIVarSafe(slf, "AIV_IgnoresArmor",       TRUE);
    Ninja_[PatchName]_SetAIVarSafe(slf, "AIV_IgnoresFakeGuild",   TRUE);
    Ninja_[PatchName]_SetAIVarSafe(slf, "AIV_Ignore_Murder",      TRUE);
    Ninja_[PatchName]_SetAIVarSafe(slf, "AIV_Ignore_Theft",       TRUE);
    Ninja_[PatchName]_SetAIVarSafe(slf, "AIV_Ignore_Sheepkiller", TRUE);
    Ninja_[PatchName]_SetAIVarSafe(slf, "AIV_NoFightParker",      TRUE);
    Ninja_[PatchName]_SetAIVarSafe(slf, "AIV_NewsOverride",       TRUE);
    Ninja_[PatchName]_SetAIVarSafe(slf, "AIV_EnemyOverride",      TRUE);
    Ninja_[PatchName]_SetAIVarSafe(slf, "AIV_DropDeadAndKill",    TRUE);
};

Similarly, checking for AI variables should be done analogously.

func int Ninja_[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  Ninja_[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;
    };
};

Add New Dialogs

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.

Add New Spells

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 Ninja_[PatchName]_EnlargeStatStringArr(var int symbPtr, var int numNewTotal) {
    const int zCPar_Symbol___zCPar_Symbol_G1 = 7306624; //0x6F7D80
    const int zCPar_Symbol___zCPar_Symbol_G2 = 8001264; //0x7A16F0
    const int zCPar_Symbol__AllocSpace_G1    = 7306832; //0x6F7E50
    const int zCPar_Symbol__AllocSpace_G2    = 8001472; //0x7A17C0

    // 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), MEMINT_SwitchG1G2(zCPar_Symbol___zCPar_Symbol_G1,
                                                      zCPar_Symbol___zCPar_Symbol_G2));
        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 & ~4194304; // 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), MEMINT_SwitchG1G2(zCPar_Symbol__AllocSpace_G1, zCPar_Symbol__AllocSpace_G2));
        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 Ninja_FirstMageKit_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 Ninja_FirstMageKit_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 Ninja_[PatchName]_SetSpell(var int spellID, var string spellFxInst, var string spellFxAniLetter,
                                    var string spellTxt) {
    // Set static arrays
    MEM_WriteStatStringArr(spellFxInstanceNames, spellID, spellFxInst);
    MEM_WriteStatStringArr(spellFxAniLetters,    spellID, spellFxAniLetter);
    MEM_WriteStatStringArr(TXT_SPELLS,           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 = Ninja_[PatchName]_GetMaxSpell();

// Enlarge static arrays
Ninja_[PatchName]_EnlargeStatStringArr(MEM_GetSymbol("spellFxInstanceNames"), MAX_SPELL + NumNewSpells);
Ninja_[PatchName]_EnlargeStatStringArr(MEM_GetSymbol("spellFxAniLetters"),    MAX_SPELL + NumNewSpells);
Ninja_[PatchName]_EnlargeStatStringArr(MEM_GetSymbol("TXT_SPELLS"),           MAX_SPELL + NumNewSpells);

// Assign new spell ID
SPL_SpellA = MAX_SPELL;
SPL_SpellB = MAX_SPELL + 1;
Ninja_FirstMageKit_SetMaxSpell(MAX_SPELL + NumNewSpells);

// Add spells (also increments MAX_SPELL)
//                         Spell ID    spellFXInstanceNames   spellFxAniLetters   TXT_SPELLS
Ninja_[PatchName]_SetSpell(SPL_SpellA, "SpellA",              "FIB",              NAME_SPL_SpellA);
Ninja_[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",
                  "Ninja_[PatchName]_Spell_ProcessMana");

// Also mana processing release (only if they are release spells)
HookDaedalusFuncS("Spell_ProcessMana_Release",
                  "Ninja_[PatchName]_Spell_ProcessMana_Release");

These hooks should look something like the following.

Click to show code

/*
 * Additions to the mana processing functions
 */
func int Ninja_[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 Ninja_[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.

Add New World

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 Ninja_[PatchName]_ChangeWorld(var string level, var string waypoint) {
    const int oCGame__TriggerChangeLevel_G1 = 6542464; //0x63D480
    const int oCGame__TriggerChangeLevel_G2 = 7109360; //0x6C7AF0

    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), MEMINT_SwitchG1G2(oCGame__TriggerChangeLevel_G1,
                                                      oCGame__TriggerChangeLevel_G2));
        call = CALL_End();
    };
};

To actually traverse back and forth between the worlds the following wrapper functions are very useful. There, the variable Ninja_[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 Ninja_[PatchName]_ReturnWld;
var string Ninja_[PatchName]_ReturnWP;
var int Ninja_[PatchName]_NewWorld; // Boolean

/*
 * Enter the unknown world
 * Source: https://github.com/szapp/Ninja/wiki/Applications-and-Examples#add-new-world
 */
func void Ninja_[PatchName]_EnterWorld() {
    // Bind strings to make them save/load persistent
    PM_BindString(Ninja_[PatchName]_ReturnWld);
    PM_BindString(Ninja_[PatchName]_ReturnWP);

    // Remember where we came from
    Ninja_[PatchName]_ReturnWld = MEM_World.worldFilename;

    // Remember waypoint to return to
    Ninja_[PatchName]_ReturnWP = Npc_GetNearestWP(hero);

    // Enter the new world
    Ninja_[PatchName]_ChangeWorld("NINJA[PATCHNAME].ZEN", "SOMEWAYPOINT");
    Ninja_[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 Ninja_[PatchName]_LeaveWorld() {
    // Return to the previous world and waypoint
    Ninja_[PatchName]_ChangeWorld(Ninja_[PatchName]_ReturnWld, Ninja_[PatchName]_ReturnWP);
    Ninja_[PatchName]_NewWorld = FALSE; // Remember where we are currently
};

Translation Patch

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 ExportierenAlle 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
    Batch Script
    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_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

Technical Details

Applications and Examples
    Add New NPC
    Set AI Variables
    Add New Dialogs
    Add New Spells
    Add New World
    Translation Patch

Debugging
    Console
    Logging

Installation
    Requirements
    Instructions

Troubleshooting
    Is Ninja Active
    Is Patch Loaded
    Error Messages

Download

Checksums
    Setup
    In-Game

Changelog

Support this project  

Acknowledgements

Contact and Discussion

Clone this wiki locally